Subscribe to real-time event notifications delivered to your server via HTTPS.
Overview#
Webhooks let you receive real-time notifications when events happen in Signals. Register an endpoint URL, choose which events to subscribe to, and Signals will send a signed HTTP POST to your server whenever those events occur.
Managing Webhooks#
Webhooks can be managed via the API (/api/v1/webhooks) or from the admin panel at Admin > Settings > Webhooks.
Events#
The event catalogue is owned by the shared EventRegistry. Existing wildcard (*) subscriptions expand automatically when a new webhook-visible core or plugin event is registered.
Additional Registry Events#
| Event |
Trigger |
credit_note.updated |
A credit note is updated |
dispatch.stage_advanced |
A dispatch advances to its next stage |
invoice_payment.updated |
An invoice payment is updated |
list_design.created |
A list is created |
list_design.deleted |
A list is deleted |
list_design.updated |
A list is updated |
list_entry.created |
A list entry is created |
list_entry.deleted |
A list entry is deleted |
list_entry.updated |
A list entry is updated |
company_tax_type.created |
An company tax type is created |
company_tax_type.deleted |
An company tax type is deleted |
company_tax_type.updated |
An company tax type is updated |
catalogue_item.flightcase_config.updated |
A catalogue item flightcase configuration is updated |
catalogue_item_tax_type.created |
A product tax type is created |
catalogue_item_tax_type.deleted |
A product tax type is deleted |
catalogue_item_tax_type.updated |
A product tax type is updated |
returns.closed |
A return is closed |
returns.disposition_recorded |
A return disposition is recorded |
returns.stage_advanced |
A return advances to its next stage |
scan_session.opened |
A scan session is opened |
scannable_identifier.deactivated |
A scannable identifier is deactivated |
scannable_identifier.registered |
A scannable identifier is registered |
stock_check.discrepancy_dismissed |
A stock-check discrepancy is dismissed |
stock_check.sign_off_cleared |
A stock-check sign-off is cleared |
stock_check.signed_off |
A stock check is signed off |
Accounts#
| Event |
Trigger |
account.created |
A new account is created |
account.updated |
A account's details are updated |
account.archived |
A account is archived |
account.restored |
An archived account is restored |
account.deleted |
A account is permanently deleted |
account.merged |
Two accounts are merged (secondary archived into primary) |
account.anonymised |
A account's personally identifiable information is erased |
account.vat_validated |
A account's VAT number is validated (VIES or manual confirmation) |
Catalogue items#
| Event |
Trigger |
catalogue_item.created |
A new catalogue item is created |
catalogue_item.updated |
A catalogue item's details are updated |
catalogue_item.archived |
A catalogue item is archived (soft-deleted, recoverable) |
catalogue_item.restored |
An archived catalogue item is restored |
catalogue_item.deleted |
A catalogue item is deleted |
catalogue_item.merged |
Two catalogue items are merged |
catalogue_item_group.created |
A new catalogue item group is created |
catalogue_item_group.updated |
A catalogue item group is updated |
catalogue_item_group.deleted |
A catalogue item group is deleted |
accessory.created |
An accessory link is added to a catalogue item |
accessory.updated |
An accessory link is updated |
accessory.deleted |
An accessory link is removed |
kit_component.created |
A kit bill-of-materials component is added |
kit_component.updated |
A kit bill-of-materials component is updated |
kit_component.deleted |
A kit bill-of-materials component is removed |
Stock#
| Event |
Trigger |
asset.created |
An asset record is created |
asset.updated |
An asset is updated |
asset.deleted |
An asset is deleted |
asset_transaction.created |
An asset transaction is recorded |
asset_transaction.deleted |
An asset transaction is deleted |
Flightcases#
| Event |
Trigger |
flightcase.created |
A flightcase is created (catalogue-item-backed or temporary) |
flightcase.updated |
Flightcase metadata or scan mode is updated |
flightcase.item.packed |
An item is packed into a flightcase |
flightcase.item.unpacked |
An item is unpacked from a flightcase |
flightcase.item.transferred |
An item is moved between flightcases in one step |
flightcase.nested |
A flightcase is packed inside another flightcase |
flightcase.sealed |
A flightcase passes the seal gate and is sealed |
flightcase.unsealed |
A sealed flightcase is reopened (manual or transfer-triggered) |
flightcase.dissolved |
A flightcase is manually or dispatch-dissolved |
flightcase.dispatched |
A kit/hybrid flightcase is dispatched as a persistent unit |
flightcase.returned |
A kit/hybrid flightcase is returned from a rental |
flightcase.repack.started |
Repack-on-return initiated for a transport flightcase housing |
flightcase.checkin_verified |
A flightcase's contents are verified during check-in |
Activities#
| Event |
Trigger |
activity.created |
A new activity is created |
activity.updated |
An activity is updated |
activity.deleted |
An activity is deleted |
activity.completed |
An activity is marked as completed |
Addresses#
| Event |
Trigger |
address.created |
An address is created |
address.updated |
An address is updated |
address.deleted |
An address is deleted |
Conversations#
| Event |
Trigger |
conversation.created |
A conversation note/email/system entry is created |
conversation.updated |
A conversation is updated |
conversation.deleted |
A conversation is deleted |
conversation.mentioned |
A user-type account is @mentioned in a conversation or comment |
conversation_comment.created |
A comment is added to a conversation |
conversation_comment.updated |
A conversation comment is edited |
conversation_comment.deleted |
A conversation comment is deleted |
Rate specifications#
| Event |
Trigger |
rate_specification.created |
A rate specification is created |
rate_specification.updated |
A rate specification is updated |
rate_specification.deleted |
A rate specification is deleted |
catalogue_item_rate.created |
A catalogue item rate is assigned |
catalogue_item_rate.updated |
A catalogue item rate is updated |
catalogue_item_rate.deleted |
A catalogue item rate is removed |
Tax#
| Event |
Trigger |
tax_rate.created |
A tax rate is created |
tax_rate.updated |
A tax rate is updated |
tax_rate.deleted |
A tax rate is deleted |
tax_rule.created |
A tax rule is created |
tax_rule.updated |
A tax rule is updated |
tax_rule.deleted |
A tax rule is deleted |
tax_zone.created |
A tax zone is created |
tax_zone.updated |
A tax zone is updated |
tax_zone.deleted |
A tax zone is deleted |
tax_category.created |
A tax category is created |
tax_category.updated |
A tax category is updated |
tax_category.deleted |
A tax category is deleted |
Pricing#
| Event |
Trigger |
discount_category.created |
A discount category is created |
discount_category.updated |
A discount category is updated |
discount_category.deleted |
A discount category is deleted |
discount_rule.created |
A discount rule is created |
discount_rule.updated |
A discount rule is updated |
discount_rule.deleted |
A discount rule is deleted |
surcharge.created |
A surcharge is created |
surcharge.updated |
A surcharge is updated |
surcharge.deleted |
A surcharge is deleted |
surcharge_group.created |
A surcharge group is created |
surcharge_group.updated |
A surcharge group is updated |
surcharge_group.deleted |
A surcharge group is deleted |
price_category.created |
A price category is created |
price_category.updated |
A price category is updated |
price_category.deleted |
A price category is deleted |
bundle_rule.created |
A bundle rule is created |
bundle_rule.updated |
A bundle rule is updated |
bundle_rule.deleted |
A bundle rule is deleted |
Warehouses#
| Event |
Trigger |
warehouse.created |
A warehouse location is created |
warehouse.updated |
A warehouse location is updated |
warehouse.deleted |
A warehouse location is deleted |
Currencies#
| Event |
Trigger |
currency.created |
A currency is created |
currency.updated |
A currency is updated |
currency.deleted |
A currency is deleted |
Exchange Rates#
| Event |
Trigger |
exchange_rate.created |
An exchange rate is created |
exchange_rate.updated |
An exchange rate is updated |
exchange_rate.deleted |
An exchange rate is deleted |
Users#
| Event |
Trigger |
user.created |
A new user is invited or created |
user.updated |
A user's profile or roles are updated |
user.deactivated |
A user account is deactivated |
user.deleted |
A user is deleted |
Roles#
| Event |
Trigger |
role.created |
A new role is created |
role.updated |
A role's name or permissions are modified |
role.deleted |
A role is deleted |
Settings#
| Event |
Trigger |
settings.updated |
Application settings are changed |
Rentals#
Rental events fire on every committed mutation of a rental, its line
items, costs, allocated assets, and quote versions. They are never re-delivered
when the event warehouse is rebuilt.
| Event |
Trigger |
rental.created |
A new rental (quote) is created |
rental.updated |
A rental's header fields are updated |
rental.quoted |
A rental is marked as quoted |
rental.converted_to_order |
A quote is converted to an order |
rental.reverted_to_quotation |
An order is reverted back to a quotation |
rental.reverted_to_enquiry |
A quotation is reverted back to a draft |
rental.reinstated |
A lost, dead, postponed, or cancelled rental is reinstated |
rental.reopened |
A completed order is re-opened back to an active order |
rental.locks_applied |
FX/tax locks are applied to an order |
rental.locks_released |
An order's FX/tax locks are released for re-pricing |
rental.status_changed |
A rental's status changes |
rental.status_promoted |
A rental's status is promoted along its workflow |
rental.cloned |
A rental is cloned into a new rental |
rental.deleted |
A rental is soft-deleted |
rental.restored |
A soft-deleted rental is restored |
rental.deal_price_set |
A negotiated deal total is set |
rental.deal_price_cleared |
A negotiated deal total is cleared |
rental.group_deal_price_set |
A negotiated deal total is set on a line-item group |
rental.group_deal_price_cleared |
A negotiated deal total is cleared from a line-item group |
rental.pricing_recalculated |
Rental pricing is recalculated (rates, tax, surcharges) |
rental.late_return |
An order still has equipment out past its scheduled end date |
rental.quote_expiring |
A quote is approaching its quote_invalid_at expiry |
rental.discount_reset |
Applied discounts are cleared from a rental |
rental.discount_category_applied |
A discount category is applied to a rental |
rental.discount_manually_overridden |
A discount category percentage is manually overridden |
rental.item_added |
A line item is added |
rental.item_removed |
A line item is removed |
rental.item_quantity_changed |
A line item's quantity changes |
rental.item_dates_changed |
A line item's hire dates change |
rental.item_discount_set |
A line item discount is set |
rental.item_deal_price_discount_set |
A line item deal-price-derived discount is set or cleared |
rental.item_optional_toggled |
A line item is toggled optional/required |
rental.item_price_overridden |
A line item unit price is overridden |
rental.item_substituted |
A line item catalogue item is substituted |
rental.item_surcharge_overridden |
A line item surcharge amount is manually overridden |
rental.items_restructured |
A rental's line-item tree is restructured |
rental.items_reordered |
A rental's line items are reordered |
rental.surcharge_group_toggled |
A surcharge group is enabled or disabled on a rental |
rental.cost_added |
An additional cost is added |
rental.cost_updated |
An additional cost is updated |
rental.cost_removed |
An additional cost is removed |
rental.version_created |
A new quote version is created |
rental.version_activated |
A quote version is made active |
rental.version_accepted |
A quote version is accepted by the customer |
rental.version_declined |
A quote version is declined |
rental.version_sent |
A quote version is sent to the customer |
rental.version_relabelled |
A quote version's label is changed |
rental.version_superseded |
A quote version is superseded by a newer one |
rental.version_deleted |
A quote version is deleted |
rental.asset_allocated |
A barcoded asset is allocated to a line item |
rental.asset_deallocated |
A barcoded asset is deallocated |
rental.asset_substituted |
An allocated asset is substituted for another |
rental.asset_prepared |
An asset is marked prepared for dispatch |
rental.asset_preparation_reverted |
An asset's prepared state is reverted |
rental.asset_checked |
An asset is scan-checked |
rental.asset_dispatched |
An asset is dispatched |
rental.asset_on_hire |
An asset is marked on-hire |
rental.asset_returned |
An asset is returned |
rental.asset_status_reverted |
An asset's lifecycle status is reverted |
rental.asset_flightcase_set |
An asset is placed in a flightcase |
rental.asset_flightcase_cleared |
An asset is removed from its flightcase |
rental.bulk_dispatched |
A bulk (non-barcoded) quantity is dispatched |
rental.bulk_returned |
A loose quantity is returned |
rental.bulk_quantity_adjusted |
A bulk dispatched/returned quantity is adjusted |
Communication Drafts#
Payloads are lean envelopes — id, status, and the title pointer. A draft's
body is customer prose that has not (yet) cleared the approval gate, so it is
never broadcast; re-read /api/v1/communication_drafts/{id} for the content.
| Event |
Trigger |
communication_draft.created |
A customer communication is drafted for approval |
communication_draft.approved |
A human approves a draft (optionally editing it first) |
communication_draft.discarded |
A draft is rejected and can never be sent |
communication_draft.sent |
An approved draft is handed to the delivery pipeline |
Documents#
| Event |
Trigger |
document.created |
A document is generated from a template |
document.pdf_generated |
A PDF is generated and stored for a document |
document.finalised |
A document is finalised (locked for sharing) |
document.voided |
A finalised document is voided |
document.emailed |
A document is emailed to a recipient (Notification Engine, P38) |
document.public_link_created |
A public sharing link is created for a document |
document.public_link_accessed |
A public sharing link is accessed for the first time |
document.public_link_revoked |
A public sharing link is revoked |
document.public_link_configured |
A public link's expiry or PIN settings are changed |
document.approval_requested |
A document is shared with the customer asking for approval |
document.portal_question |
A customer asks a question from the public document portal |
document.approved |
A customer approves a document from the public portal |
document.declined |
A customer declines a document from the public portal |
document_template.created |
A document template is created or duplicated |
document_template.updated |
A document template is saved |
document_template.deleted |
A custom document template is deleted |
document_template.restored |
A system template is restored to its shipped default |
document_template.set_as_default |
A template is marked as the default for its type |
document_sequence.reset |
A document numbering sequence rolls over to a new period |
document_sequence.updated |
A document numbering sequence is updated |
Communication Templates#
| Event |
Trigger |
communication_template.created |
A communication template is created |
communication_template.updated |
A communication template is updated |
communication_template.deleted |
A communication template is deleted |
Email Layouts & Templates#
| Event |
Trigger |
email_layout.created |
An email layout is created |
email_layout.updated |
An email layout is saved or restored from a version |
email_layout.deleted |
A custom email layout is deleted |
email_layout.set_as_default |
An email layout is promoted to the default |
email_template.created |
A custom email template is created in the admin UI |
email_template.updated |
An email template is saved or restored from a version |
email_template.deleted |
A custom email template is deleted in the admin UI |
Attachments#
| Event |
Trigger |
attachment.created |
A file is attached to an entity |
attachment.deleted |
An attachment is deleted from an entity |
Invoices#
| Event |
Trigger |
invoice.created |
A draft invoice is generated |
invoice.issued |
A draft invoice is issued to the customer |
invoice.voided |
An invoice is voided |
invoice.overdue |
An issued invoice passes its due date with an outstanding balance |
payment.recorded |
A payment is recorded against an invoice — payload embeds payment fields (see Invoices API) |
invoice.portal_payment_received |
A customer pays an invoice from the public portal |
payment.refunded |
A refund is recorded against an invoice — payload embeds payment/refund fields (see Invoices API) |
credit_note.issued |
A credit note is issued against an invoice |
credit_note.draft_created |
A draft credit note is created (no financial effect) |
credit_note.applied |
A draft credit note is applied and balances committed |
credit_note.voided |
A credit note is voided |
Billing Runs#
| Event |
Trigger |
billing_run.created |
A billing run is created |
billing_run.completed |
A billing-run issue batch finishes successfully |
Availability#
| Event |
Trigger |
availability.changed |
A catalogue-item/warehouse's availability read model has been recalculated |
Shortages#
| Event |
Trigger |
shortage.detected |
A shortage is detected for an order line |
shortage.cleared |
A previously-detected shortage no longer exists |
shortage.acknowledged |
A shortage is acknowledged at a confirmation gate |
shortage.resolution.created |
A shortage resolution is created |
shortage.resolution.confirmed |
A shortage resolution is confirmed |
shortage.resolution.in_progress |
A shortage resolution's fulfilment has begun |
shortage.resolution.fulfilled |
A shortage resolution completes and stock is available |
shortage.resolution.partially_fulfilled |
A shortage resolution is partially fulfilled |
shortage.resolution.failed |
A shortage resolution attempt fails |
shortage.resolution.cancelled |
A shortage resolution is cancelled |
shortage.waitlist.created |
A waitlist monitor is placed on a shortage |
shortage.waitlist.matched |
Monitored stock becomes available |
shortage.waitlist.expired |
A waitlist monitor expires without matching |
shortage.escalated |
An unresolved shortage passes the configured escalation threshold |
Purchase Orders#
| Event |
Trigger |
purchase_order.created |
A draft purchase order is created |
purchase_order.updated |
A draft purchase order is updated |
purchase_order.sent |
A purchase order is sent to the supplier |
purchase_order.received |
Goods are received against a purchase order |
purchase_order.cancelled |
A purchase order is cancelled |
purchase_order.deleted |
A draft purchase order is deleted |
purchase_order_document.created |
A document (e.g. supplier PDF) is generated for a purchase order |
Virtual Stock#
| Event |
Trigger |
virtual_stock_intake.created |
A draft virtual stock intake is created |
virtual_stock_intake.updated |
A draft virtual stock intake is updated |
virtual_stock_intake.confirmed |
A virtual stock intake is confirmed |
virtual_stock_intake.received |
Virtual stock is received into availability |
virtual_stock_intake.returned |
Virtual stock is returned after the hire window |
virtual_stock_intake.cancelled |
A virtual stock intake is cancelled |
virtual_stock_intake.deleted |
A draft virtual stock intake is deleted |
Vehicles#
| Event |
Trigger |
vehicle.created |
A vehicle is created |
vehicle.updated |
A vehicle is updated |
vehicle.deleted |
A vehicle is deleted |
vehicle.assigned |
A vehicle is assigned to a rental or job |
vehicle.assignment_updated |
A vehicle assignment's details are updated |
vehicle.unassigned |
A vehicle is unassigned |
Facilities#
| Event |
Trigger |
facility.created |
A bookable facility is created |
facility.updated |
A bookable facility is updated |
facility.deleted |
A bookable facility is deleted |
Services#
| Event |
Trigger |
service.created |
A labour catalogue service is created |
service.updated |
A labour catalogue service is updated |
service.deleted |
A labour catalogue service is deleted |
Resource assignments#
| Event |
Trigger |
resource_assignment.created |
A scheduling resource assignment is created |
resource_assignment.updated |
A scheduling resource assignment is updated |
resource_assignment.cancelled |
A scheduling resource assignment is cancelled |
resource_assignment.confirmed |
A scheduling resource assignment is confirmed |
resource_assignment.started |
A scheduling resource assignment is started |
resource_assignment.completed |
A scheduling resource assignment is completed |
resource_assignment.rescheduled |
A scheduling resource assignment window is changed |
resource_assignment.demand_unmet |
Cancelling an assignment leaves service demand unmet (rental_item_id + shortfall) |
Assignment route stops#
| Event |
Trigger |
assignment_route_stop.created |
A route stop is appended to a resource assignment |
assignment_route_stop.updated |
A route stop's location, type, times, or notes are updated |
assignment_route_stop.reordered |
Route stops on an assignment are reordered |
assignment_route_stop.deleted |
A route stop is removed from a resource assignment |
Scheduling conflicts#
| Event |
Trigger |
scheduling_conflict.detected |
A scheduling conflict is detected on assignment write |
scheduling_conflict.resolved |
A scheduling conflict is resolved |
Repair#
| Event |
Trigger |
repair.created |
A stock item is placed into repair |
repair.updated |
A repair record's details are updated |
repair.released |
A repaired item is released back into service |
Equipment tests#
| Event |
Trigger |
equipment_test.created |
An equipment_test record is created |
equipment_test.updated |
An equipment_test record is updated |
equipment_test.deleted |
An equipment_test record is deleted |
equipment_test.completed |
An equipment_test is completed |
equipment_test.failed |
An equipment_test is recorded as failed |
equipment_test_result.created |
A result line is recorded against an equipment_test |
equipment_test_result.deleted |
An equipment_test result line is deleted |
Asset Lifecycle#
| Event |
Trigger |
asset_cost.created |
A maintenance/asset cost is recorded |
asset_cost.updated |
An asset cost is updated |
asset_cost.deleted |
An asset cost is deleted |
asset_retirement.created |
An asset is retired (disposed) |
asset_retirement.voided |
A stock-check-sourced retirement is voided directly (not emitted on reinstate) |
asset_retirement.reinstated |
A retired asset is reinstated (returned to active stock) |
usage_reading.recorded |
A usage reading (e.g. odometer/hours) is recorded against a stock unit |
usage_reading.deleted |
A usage reading is deleted |
Stock Checks#
| Event |
Trigger |
stock_check.created |
A stock check is created |
stock_check.updated |
A stock check is updated |
stock_check.submitted |
A stock check is submitted for review |
stock_check.completed |
A stock check is completed and reconciled |
stock_check.reverted |
A completed stock check is reverted |
stock_check.cancelled |
A stock check is cancelled |
stock_check.discrepancy_resolved |
A stock check discrepancy is resolved (ledger/relocation applied) |
Warehouse Transfers#
| Event |
Trigger |
warehouse_transfer.created |
A warehouse transfer is created |
warehouse_transfer.updated |
A warehouse transfer's details are updated |
warehouse_transfer.dispatched |
A warehouse transfer is dispatched from the origin warehouse |
warehouse_transfer.received |
A warehouse transfer is received at the destination warehouse |
warehouse_transfer.cancelled |
A warehouse transfer is cancelled |
Rental Projects#
| Event |
Trigger |
rental_project.created |
A rental project is created |
rental_project.updated |
A rental project's details or status are updated |
rental_project.deleted |
A rental project is deleted (its rentals are detached) |
rental.assigned_to_project |
A rental is assigned to a rental project |
rental.detached_from_project |
A rental is detached from its rental project |
Supplier Costs#
| Event |
Trigger |
supplier_cost.created |
A supplier cost is recorded against a catalogue item |
supplier_cost.updated |
A supplier cost is updated |
supplier_cost.deleted |
A supplier cost is deleted |
Dispatch & Returns#
| Event |
Trigger |
dispatch.stage_advanced |
A rental dispatch is moved to a new dispatch stage |
returns.stage_advanced |
A rental return is moved to a new return stage |
returns.disposition_recorded |
A return disposition (damage, loss, write-off, …) is recorded |
returns.closed |
A rental's returns are closed |
Scannable Identifiers#
| Event |
Trigger |
scannable_identifier.registered |
A barcode/QR/RFID identifier is registered against a scannable entity |
scannable_identifier.deactivated |
A scannable identifier is deactivated |
Scan Sessions#
| Event |
Trigger |
scan_session.completed |
A scanning session is completed |
scan_session.abandoned |
A scanning session is abandoned |
scan_session.opened |
A scanning session is opened |
Reporting#
| Event |
Trigger |
report.generated |
A scheduled report finishes running and has been delivered to its subscribers |
Import / Export#
| Event |
Trigger |
export.completed |
An export job finishes successfully |
export.failed |
An export job fails |
export.deleted |
An export job and its generated file are deleted |
import.batch.parsing |
An import batch begins parsing the uploaded file |
import.batch.validated |
An import batch finishes validation |
import.batch.committed |
An import batch finishes committing rows |
import.batch.failed |
An import batch commit fails or pauses with errors |
import.plan.started |
A migration / import plan begins sequential execution |
import.plan.step_completed |
A plan step finishes committing its batch |
import.plan.completed |
All plan steps complete successfully |
import.plan.failed |
Plan execution pauses at a failing step (pause-not-rollback) |
Updates#
| Event |
Trigger |
update.started |
A self-hosted upgrade run begins |
update.completed |
A self-hosted upgrade run finishes successfully |
update.failed |
A self-hosted upgrade run stops at a failing step |
Event stability & deprecation#
Event names are a public contract. A subscription stores the names it wants
verbatim, so a renamed or removed name simply stops matching and deliveries end
silently — there is no error for the integration to see.
The policy is therefore:
- Additive only. New events may be added at any time. A released event name
is never renamed and never removed.
- Payload changes are additive. Fields may be added to an event payload;
existing fields are not renamed, retyped, or dropped. Subscribers should
ignore unknown fields.
- Deprecation, not deletion. An event that is no longer recommended is
documented as deprecated (and, in future, flagged as such in the catalogue
with a sunset window). It keeps firing until the sunset passes.
This is enforced in CI, not just by convention: ReleasedEventNames::NAMES
(tests/Support/Events/ReleasedEventNames.php) is a frozen snapshot of every
released event name, and the ratchet in
tests/Feature/Architecture/EventCatalogueRatchetTest.php fails if any of them
disappears from config/events.php. Adding an event is free — append the new
name to the snapshot.
Every webhook delivery sends a JSON POST request with the event name and associated data:
{
"event": "user.created",
"timestamp": "2026-01-15T14:30:00Z",
"data": {
"user": {
"id": 42,
"name": "Jane Smith",
"email": "[email protected]"
}
}
}
Actor attribution#
Every data object carries a reserved actor_type key naming who caused the event:
| Value |
Meaning |
user |
A signed-in person acting in the web UI, including via the built-in API Explorer |
api |
An external caller using an API token |
agent |
An AI agent acting over MCP |
system |
Scheduled work, queue jobs, artisan commands, or an internal process |
It is stamped once at the dispatch boundary, so it is present regardless of which part of the application produced the event.
actor_type answers who acted, which is not the same as how the call arrived. A
request replayed through the in-process kernel is classified by its caller, not by
the token it carries: MCP is an agent, the API Explorer is the user driving it,
and the CLI is the system — so a token-authenticated artisan run is never
reported as api.
Cross-entity reference (ref)#
Alongside actor_type, the data object carries an optional ref key naming the
title of the event as a single addressable string:
"ref": "rental:123"
The format is entity:id — the entity slug is the first segment of the event name,
and the id is the title's integer primary key. Primary keys elsewhere in the
payload are unchanged; ref is an additive convenience for consumers correlating
events across entities.
It is stamped at the dispatch boundary and is present when the title's id is
unambiguous:
- the payload nests the entity's own object (
data.rental.id for any
rental.* event), or
- the event name is a two-segment
entity.action and the payload carries a
top-level id (the lean_envelope shape).
ref is absent when the title cannot be identified without guessing — for
example sub-entity events such as account.address.created, whose top-level id
belongs to the address rather than the account, and ad_hoc payloads that carry no
id for the event's entity. Treat ref as optional and fall back to the ids in the
payload body.
A producer that sets ref explicitly keeps its own value.
Payload Schemas#
Most events embed the affected resource under a singular key inside data, using the same shape the REST API returns. Lifecycle events that only need to identify a record (deletions, archive/restore) send just an id.
Each event declares its payload shape in the event catalogue:
| Shape |
data contains |
full_dto |
the affected resource under a singular key, in the same shape the REST API returns |
lean_envelope |
id and action only — re-read the resource endpoint for detail |
ad_hoc |
a bespoke payload documented per event (change snapshots, progress counters) |
Tax Rate Events#
tax_rate.created and tax_rate.updated include the full tax rate object:
{
"event": "tax_rate.created",
"timestamp": "2026-01-15T14:30:00Z",
"data": {
"tax_rate": {
"id": 1,
"name": "UK Standard",
"description": "Standard rate VAT",
"rate": "20.0000",
"is_active": true,
"created_at": "2026-01-15T14:30:00Z",
"updated_at": "2026-01-15T14:30:00Z"
}
}
}
| Field |
Type |
Description |
id |
integer |
Tax rate identifier |
name |
string |
Display name (e.g. UK Standard) |
description |
string|null |
Optional description |
rate |
string |
Percentage as a decimal string (e.g. 20.0000) |
is_active |
boolean |
Whether the rate is active |
created_at / updated_at |
string |
ISO 8601 UTC timestamps |
tax_rate.deleted sends only the identifier:
{
"event": "tax_rate.deleted",
"timestamp": "2026-01-15T14:30:00Z",
"data": { "id": 1 }
}
Tax Rule Events#
tax_rule.created and tax_rule.updated include the full tax rule object:
{
"event": "tax_rule.created",
"timestamp": "2026-01-15T14:30:00Z",
"data": {
"tax_rule": {
"id": 5,
"company_tax_type_id": 1,
"catalogue_item_tax_type_id": 2,
"tax_rate_id": 1,
"priority": 10,
"is_active": true,
"created_at": "2026-01-15T14:30:00Z",
"updated_at": "2026-01-15T14:30:00Z"
}
}
}
| Field |
Type |
Description |
id |
integer |
Tax rule identifier |
company_tax_type_id |
integer |
Company tax type matched by the rule |
catalogue_item_tax_type_id |
integer |
Catalogue item tax type matched by the rule |
tax_rate_id |
integer |
Tax rate applied when the rule matches |
priority |
integer |
Higher priority rules win when several match |
is_active |
boolean |
Whether the rule is active |
created_at / updated_at |
string |
ISO 8601 UTC timestamps |
tax_rule.deleted sends only the identifier:
{
"event": "tax_rule.deleted",
"timestamp": "2026-01-15T14:30:00Z",
"data": { "id": 5 }
}
CatalogueItem Archive & Restore Events#
catalogue_item.archived and catalogue_item.restored are lifecycle transitions on an existing catalogue item and carry only its identifier. Fetch the current state from GET /api/v1/catalogue_items/{id} if you need the full record.
{
"event": "catalogue_item.archived",
"timestamp": "2026-01-15T14:30:00Z",
"data": { "id": 42 }
}
| Field |
Type |
Description |
id |
integer |
Catalogue item identifier |
Verifying Signatures#
Every delivery includes an X-Signals-Signature header containing an HMAC-SHA256 signature of the request body. Verify it against the secret you received when creating the webhook:
$signature = hash_hmac('sha256', $requestBody, $webhookSecret);
if (! hash_equals($signature, $request->header('X-Signals-Signature'))) {
abort(401, 'Invalid signature');
}
Retry Behaviour#
Failed deliveries (non-2xx response or connection timeout) are retried with exponential backoff up to 6 times over approximately 12 hours. After 3 consecutive days of failures, the webhook is automatically disabled.
Re-enable a disabled webhook from the admin panel or by updating it via the API with is_active: true.
Delivery Logs#
Every delivery attempt is logged. View logs via the API at GET /api/v1/webhooks/{id}/logs or from the API Console Webhooks page by clicking the logs icon on a webhook row. Deliveries also appear in the unified Request Log on the webhook_sent channel.
Outbound metering weights#
Each outbound event carries a price weight (0–4) declared in WebhookEventRegistry. Weights default to 1 unless overridden at registration time. On the first delivery attempt of each job run, DeliverWebhook records the weight on the webhook_logs row and increments the webhook_sent daily rollup by the event's weight.
Weight 0 events are still logged but contribute zero weighted units toward usage totals.
Inbound webhook receiver#
Signals can accept webhook POSTs from external systems via registered inbound sources. Manage sources from the API Console at /api/webhooks (Inbound tab) or programmatically when that surface is exposed.
Receive URL#
POST {your-app-url}/webhooks/receive/{slug}
Replace {slug} with the source's URL slug. The route is public (no Sanctum token) — authentication is HMAC signature verification only.
Signature verification#
Each source stores a secret, signature algorithm (default sha256), and header name (default X-Signature). The middleware computes:
$expected = hash_hmac($source->signature_algo, $requestBody, $source->secret);
$provided = $request->header($source->signature_header);
$valid = is_string($provided) && hash_equals($expected, $provided);
Send the HMAC digest as a plain string in the configured header (not prefixed with sha256=).
Responses#
| Status |
Condition |
Body |
200 |
Valid signature |
{"status":"received"} — fires InboundWebhookReceived |
401 |
Missing or invalid signature |
{"message":"Invalid signature"} — logged with weight 0 |
404 |
Unknown slug or inactive source |
Laravel 404 page |
Inbound metering weights#
Unlike outbound events (code-defined in WebhookEventRegistry), inbound weights are stored per source in inbound_webhook_sources.price_weight (0–4, editable in the console). Valid requests meter on the webhook_received channel; invalid signatures log detail rows but do not increment weighted units.
Inbound logs#
Receipts are stored in inbound_webhook_logs and surfaced in the Request Log (webhook_received channel) and the source's logs modal in the API Console.