Webhooks
Subscribe to real-time events from your Equos account
Overview
Webhooks allow your integration to receive real-time event notifications when orders, inventory, or other resources change. Your application can subscribe to specific event types and receive HTTP POST notifications.
Authentication: All webhook endpoints require Firebase authentication (for admin users only).
How Webhooks Work
- Subscribe to webhook events via
POST /api/webhooks - Equos sends HTTP POST requests to your endpoint when events occur
- Each webhook includes a signature header for verification:
X-EQUOS-Signature - Your endpoint should respond with 2xx status code to acknowledge receipt
- Failed deliveries are retried with exponential backoff
GET/api/webhooksList webhooks
List all webhook subscriptions for your business.
[
{
"id": "webhook-uuid-1",
"url": "https://example.com/webhooks/orders",
"events": ["order.created", "order.status_changed"],
"isActive": true,
"description": "Order event webhook",
"createdAt": "2024-01-15T10:00:00Z",
"updatedAt": "2024-01-15T10:00:00Z",
"lastTriggeredAt": "2024-01-20T14:30:00Z",
"failureCount": 0
}
]{"error": "no businessId in user claims"}POST/api/webhooksCreate webhook
Subscribe to webhook events. Returns a secret that will be used to sign webhook payloads.
| Field | Type | Required | Description |
|---|---|---|---|
url | string | Required | HTTPS endpoint to receive webhook POST requests |
events | array | Required | Array of event types to subscribe to (min 1) |
description | string | Optional | Human-readable description |
{
"id": "webhook-uuid-2",
"url": "https://example.com/webhooks/inventory",
"events": ["inventory.adjusted", "inventory.low_stock"],
"secret": "d5daca96fa06d179f8eeded2b009c44e95c3d8820035e8538241969df16ee8bc",
"isActive": true,
"description": "Inventory webhook",
"createdAt": "2024-01-21T12:00:00Z",
"updatedAt": "2024-01-21T12:00:00Z",
"lastTriggeredAt": null,
"failureCount": 0
}{"error": "url must use the https:// scheme"}{"error": "no businessId in user claims"}PATCH/api/webhooks/:idUpdate webhook
Update webhook configuration. All fields are optional.
| Field | Type | Description |
|---|---|---|
url | string | Update endpoint URL |
events | array | Replace subscribed event types |
isActive | boolean | Enable or disable the webhook |
description | string | Update description |
{
"id": "webhook-uuid-2",
"url": "https://example.com/webhooks/inventory",
"events": ["inventory.adjusted", "inventory.low_stock"],
"isActive": true,
"description": "Inventory webhook",
"createdAt": "2024-01-21T12:00:00Z",
"updatedAt": "2024-01-21T12:00:00Z",
"lastTriggeredAt": null,
"failureCount": 0
}{"error": "webhook not found"}{"error": "no fields to update"}DELETE/api/webhooks/:idDelete webhook
Delete a webhook subscription.
{"ok": true}{"error": "webhook not found"}POST/api/webhooks/:id/testTest webhook
Send a test webhook to verify your endpoint is working.
{
"success": true,
"statusCode": 200,
"responseBody": "Webhook received successfully"
}{"success": false, "statusCode": 0, "error": "connection refused"}{"error": "webhook not found"}The test webhook sends the following payload:
{
"event": "test",
"data": {
"message": "Test webhook from EQUOS",
"timestamp": "2024-01-21T12:00:00Z"
}
}Available Events
| Event | Description |
|---|---|
order.created | Order created — fires for every origin (app, portals, API, connectors) |
order.updated | Integrator-relevant order fields changed (status, customer, references, dates, notes, totals) |
order.status_changed | Order lifecycle transition (finalize / commit / void, including bulk and manufacturing) |
order.deleted | Order moved to the recycle bin |
inventory.adjusted | Stock quantity adjusted (manual adjustment, external sync, stocktake, warehouse operations) |
inventory.low_stock | Available quantity crossed below the item's warning level |
inventory.transfer | Stock moved between bins / locations |
inventory.level_changed | An item's stock level at one warehouse changed (same values as GET /e9v3/inventory/levels; de-duplicate and order on data.seq) |
contact.created | Customer or supplier contact created — fires for every origin |
contact.updated | Contact details changed (name, company, email, phone, address, status) |
shipment.created | Freight booked for an order |
shipment.status_changed | Carrier tracking status transition on a consignment linked to an order |
shipment.delivered | Carrier tracking reports the consignment delivered |
job.completed | A job your API key enqueued settled completed or partial (for example a partial bulk fulfil) |
job.failed | A job your API key enqueued settled failed — data.errorMessage says why |
payment.status_changed | A one-off payment transitioned status (created, pending, succeeded, failed, refunded) |
catalog.published | The public catalog feed was republished — re-pull GET /e9v3/catalog |
prices.published | A feed price tier was republished — re-pull GET /e9v3/catalog |
Every event listed above is emitted by the platform. Order and contact lifecycle events fire regardless of where the change originated — the EQUOS apps, client portals, the API, or a connector.
Event Payload Examples
Each webhook is delivered as a JSON POST with the following envelope structure:
Order Status Changed
Fired when an order status changes (e.g., finalized, committed, voided):
{
"event": "order.status_changed",
"data": {
"businessId": "biz_abc123",
"orderId": "ord_xyz789",
"orderType": "send",
"status": "completed",
"operation": "Finalize",
"result": {}
},
"timestamp": "2024-01-21T12:00:00Z",
"deliveryId": "uuid-v4-string"
}Operations: Finalize, Commit, Void, Unfinalize, Uncommit (and Bulk variants: BulkFinalize, BulkCommit, BulkVoid)
Inventory Adjusted
Fired when inventory quantities change:
{
"event": "inventory.adjusted",
"data": {
"businessId": "biz_abc123",
"warehouseId": "wh_456",
"locationId": "loc_789",
"operation": "StockAdjust",
"products": [
{
"sku": "WIDGET-A",
"quantity": 10,
"previousQuantity": 8
}
],
"result": {}
},
"timestamp": "2024-01-21T12:00:00Z",
"deliveryId": "uuid-v4-string"
}Operations: StockAdjust, StockAdd, StockDecrement, WarehouseStockAdjust
Job Completed / Job Failed
Every write that returns a jobId (for example POST /e9v3/orders/send-matched) can report its outcome by webhook instead of being polled. job.completed fires when a job your API key enqueued settles completed or partial; job.failedfires when it settles failed. An API-key subscription receives these only for jobs its own key enqueued, unless it has webhooks:manage (then it receives all of the business's).data carries the same fields as GET /e9v3/jobs/{jobId} plussourceRecordId and the order identity. Nullable fields are null, never omitted.
{
"event": "job.completed",
"data": {
"jobId": "send_matched_5f2c9e0d4b7a1c3e",
"businessId": "biz_123",
"processType": "InboundCustomerOrderCreate",
"status": "completed",
"errorMessage": null,
"sourceRecordId": "27-15313-03998",
"orderId": "ord_7c1e9a3f5b",
"orderNumber": null,
"orderType": "send",
"result": { "orderId": "ord_7c1e9a3f5b", "customerId": "cus_42", "supplierId": "cus_42" },
"completedAt": "2026-10-27T01:02:03Z"
},
"timestamp": "2026-10-27T01:02:04Z",
"deliveryId": "a1b2c3d4-..."
}Scopes to subscribe: webhooks:manage, orders:read ororders:write. deliveryId is deterministic per settlement: a re-sent event keeps its id, and a retried submission that settles again gets a new one, so de-duplicate on it.job.failed covers send-matched and receive failures, read-only module denials, failed bulk fulfils and trigger-doc failures of API-key jobs; other failures show onGET /e9v3/jobs/{jobId} only. Prefer polling?GET /e9v3/jobs/{jobId}?wait=25 holds the request until the job settles.
Other Emitted Events
| Event | When Fired | Key Fields in Data |
|---|---|---|
inventory.transfer | Inventory moved between locations | businessId, operation, products |
shipment.created | New shipment booked | businessId, orderId, operation |
shipment.status_changed | Shipment status updates (cancelled, edited, manifested, tracking) | businessId, orderId, status, operation |
shipment.delivered | Shipment marked as delivered | businessId, orderId, status |
Delivery Details
HTTP Headers
Each webhook request includes the following headers:
Content-Type: application/json
X-EQUOS-Signature: sha256=<hmac-sha256-hex>
X-EQUOS-Event: order.status_changed
X-EQUOS-Delivery: <uuid-v4>| Header | Description |
|---|---|
X-EQUOS-Signature | HMAC-SHA256 of the request body, prefixed with sha256= |
X-EQUOS-Event | The event type (e.g., order.status_changed) |
X-EQUOS-Delivery | Unique delivery ID for this webhook attempt (for idempotency) |
Circuit Breaker
If a webhook endpoint fails to respond with a 2xx status code, the system will retry with exponential backoff. After 10 consecutive failures, the webhook subscription is automatically disabled (isActive: false).
To re-enable the webhook after circuit breaker activation, use the PATCH endpoint:
PATCH /api/webhooks/:id
Content-Type: application/json
{
"isActive": true
}Monitor the failureCount field in webhook responses to track delivery health.
Verifying Webhook Signatures
Always verify the signature before processing the webhook. The signature is computed using HMAC-SHA256 of the request body with your webhook secret as the key.
Verifying Signatures in Python
import hmac
import hashlib
def verify_webhook_signature(body, signature, secret):
expected_signature = hmac.new(
secret.encode(),
body.encode() if isinstance(body, str) else body,
hashlib.sha256
).hexdigest()
provided_signature = signature.replace('sha256=', '')
return hmac.compare_digest(expected_signature, provided_signature)
# In your Flask endpoint:
@app.route('/webhook', methods=['POST'])
def handle_webhook():
signature = request.headers.get('X-EQUOS-Signature', '')
event = request.headers.get('X-EQUOS-Event')
body = request.get_data()
if not verify_webhook_signature(body, signature, WEBHOOK_SECRET):
return {'error': 'Invalid signature'}, 401
data = request.json
# Process webhook...
return {'ok': True}, 200Verifying Signatures in JavaScript
const crypto = require('crypto');
function verifyWebhookSignature(body, signature, secret) {
const expectedSignature = crypto
.createHmac('sha256', secret)
.update(body)
.digest('hex');
const providedSignature = signature.replace('sha256=', '');
return crypto.timingSafeEqual(
Buffer.from(expectedSignature),
Buffer.from(providedSignature)
);
}
// In your Express endpoint:
app.post('/webhook', (req, res) => {
const signature = req.headers['x-equos-signature'] || '';
const event = req.headers['x-equos-event'];
const body = req.rawBody; // Store raw body before parsing
if (!verifyWebhookSignature(body, signature, process.env.WEBHOOK_SECRET)) {
return res.status(401).json({ error: 'Invalid signature' });
}
const data = req.body;
// Process webhook...
res.json({ ok: true });
});Best Practices
Handling Webhook Payloads
- Always verify signatures: Check the
X-EQUOS-Signatureheader to ensure the request came from Equos - Respond quickly: Return a 2xx status immediately; process the event asynchronously
- Idempotency: Design your handler to be idempotent (safe to process the same event twice)
- Log all events: Keep detailed logs for debugging and audit purposes
Endpoint Requirements
- HTTPS only: Webhooks can only be sent to HTTPS endpoints
- Timeout: Your endpoint must respond within 30 seconds
- Retry-friendly: If you return non-2xx status, the webhook will be retried with exponential backoff
Monitoring
- Track delivery success: Monitor the
lastTriggeredAtandfailureCountfields - Alert on failures: Set up alerts if a webhook fails repeatedly
- Test regularly: Use the test endpoint to verify your webhook is working