Hooks and filters
The six action hooks PuntoSuite fires today, their payloads, and how to register filters (the first core filter, sale:receiptTitle, is now live and host-re-validated).
PuntoSuite plugins react to the desktop POS through two mechanisms on the mediated BodegaAPI:
- Action hooks — named points in the app flow where your callback runs after something happens. Core fires six of them today.
- Filters — a transform pipeline where your callback can return a modified value. The first core filter,
sale:receiptTitle, is now live (fired when exporting a sale ticket PDF); the host re-validates whatever your callback returns before using it.
Both live under api.hooks and api.filters. For the complete API surface (config, db, ui, i18n), see API reference.
Action hooks
Register an action with api.hooks.addAction(name, callback, priority?). It returns a disposer — track it and call it in deactivate().
import type { BodegaAPI } from '@bodegasuite/plugin-sdk'
let disposers: Array<() => void> = []
export async function activate(api: BodegaAPI): Promise<void> {
const off = api.hooks.addAction('sale:afterComplete', async (sale) => {
api.ui.notify(`Sale ${sale.id} completed`, { type: 'success' })
})
disposers.push(off)
}
export async function deactivate(): Promise<void> {
for (const off of disposers) off()
disposers = []
}
priority?defaults to10. Lower numbers run earlier.- Callbacks may be async; they are awaited.
- A callback that throws ~5 consecutive times gets its plugin torn down (auto-disable). Validate your inputs and fail gracefully.
Hooks fired by core today
These are the only action hooks core emits. Payload type names are exported from @bodegasuite/api (and mirrored in the SDK types).
| Hook | When it runs | Payload type |
|---|---|---|
app:ready | Once, after the app and all plugins finish booting | AppReadyEvent |
sale:afterComplete | After a sale is persisted (sales:create) | SaleWithDetails |
sale:afterRefund | After a sale is cancelled/refunded (sales:refund); payload is the updated sale | SaleWithDetails |
inventory:afterAdjust | After a stock adjustment is applied | InventoryAdjustEvent |
inventory:lowStock | After an adjustment leaves stock at or below the product minimum | InventoryLowStockEvent |
cashClosure:afterSubmit | After a cash closure is submitted (cashClosure:submit) | CashClosureWithDetails |
Payload shapes
The small event types are defined alongside the hook catalog:
interface AppReadyEvent {
businessId: string
locale: string
hostVersion: string
}
interface InventoryAdjustEvent {
product: Product
log: InventoryLog
}
interface InventoryLowStockEvent {
product: Product
}
SaleWithDetails (for sale:afterComplete / sale:afterRefund) and CashClosureWithDetails (for cashClosure:afterSubmit) are the same detailed read models the host uses internally. Import the names from @bodegasuite/plugin-sdk to type your callbacks; do not hand-roll their fields from memory.
import type { BodegaAPI, SaleWithDetails, InventoryLowStockEvent } from '@bodegasuite/plugin-sdk'
export async function activate(api: BodegaAPI): Promise<void> {
api.hooks.addAction('sale:afterRefund', async (sale: SaleWithDetails) => {
// react to the refunded sale
})
api.hooks.addAction('inventory:lowStock', async (e: InventoryLowStockEvent) => {
api.ui.notify(`Low stock: ${e.product.name}`, { type: 'warning' })
})
}
Hook names are stable within an
apiVersion. New hooks may be added; a hook is only removed or renamed across anapiVersionbump, with a deprecation window first.
Filters
The filter API mirrors hooks: register with api.filters.addFilter(name, callback, priority?), which also returns a disposer. A filter callback receives a value (plus any extra context args) and returns the transformed value.
export async function activate(api: BodegaAPI): Promise<void> {
const off = api.filters.addFilter('sale:receiptTitle', (title) => {
// `title` is the localized default heading; return a replacement string.
// must RETURN the (possibly modified) value
return `${title} — Acme`
})
// remember to dispose in deactivate()
}
Filters fired by core today
| Filter | When it runs | Value in/out | Re-validation applied by the host |
|---|---|---|---|
sale:receiptTitle | When an operator exports a sale ticket PDF | string | Must be a non-empty string; reduced to one line (control chars/newlines stripped) and clamped to 120 chars; otherwise the localized default heading is used. |
The host is authoritative over filter returns.
applyFilteritself does not re-validate — each fire site re-validates the value your filter returns before using it, so a bad or malicious return can never corrupt host output. Filter names other thansale:receiptTitleare not wired yet: a filter you register under a different name will not fire until core adds that fire point.
Cleanup contract
Every addAction / addFilter call returns a disposer. activate runs on load and on reload, so always collect disposers and release them in deactivate() to avoid duplicate registrations. The host also tears down a plugin's hooks/filters when it is disabled or uninstalled.
Related
- API reference — full
BodegaAPIsurface (config, db, ui, i18n) - Lifecycle —
activate/deactivateand reload behaviour - Security — worker isolation, permissions, auto-disable
- Manifest — permissions and capabilities