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#
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) |
CatalogueItems#
| 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.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.repack.item |
An item is returned and repacked during a repack session |
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 |
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 |
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.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_details_updated |
A line item's details (description, notes, etc.) are updated |
rental.item_renamed |
A line item is renamed |
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 |
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_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 |
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) |
payment.refunded |
A refund is recorded against an invoice — payload embeds payment/refund fields (see Invoices API) |
credit_note.draft_created |
A draft credit note is created against an invoice |
credit_note.issued |
A credit note is issued against an invoice |
credit_note.applied |
A credit note is applied to an invoice |
credit_note.voided |
A credit note is voided |
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.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 |
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 |
EquipmentTests#
| 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 |
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 |
Scan Sessions#
| Event |
Trigger |
scan_session.completed |
A scanning session is completed |
scan_session.abandoned |
A scanning session is abandoned |
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": "jane@example.com"
}
}
}
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.
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 |
CatalogueItem 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 warehouses 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.