Developer Docs

← Back to Console

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

  1. Subscribe to webhook events via POST /api/webhooks
  2. Equos sends HTTP POST requests to your endpoint when events occur
  3. Each webhook includes a signature header for verification: X-EQUOS-Signature
  4. Your endpoint should respond with 2xx status code to acknowledge receipt
  5. Failed deliveries are retried with exponential backoff

GET/api/webhooksList webhooks
🔒 Bearer (Firebase token)

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
🔒 Bearer (Firebase token)

Subscribe to webhook events. Returns a secret that will be used to sign webhook payloads.

Request Body
FieldTypeRequiredDescription
urlstringRequiredHTTPS endpoint to receive webhook POST requests
eventsarrayRequiredArray of event types to subscribe to (min 1)
descriptionstringOptionalHuman-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
🔒 Bearer (Firebase token)

Update webhook configuration. All fields are optional.

Request Body
FieldTypeDescription
urlstringUpdate endpoint URL
eventsarrayReplace subscribed event types
isActivebooleanEnable or disable the webhook
descriptionstringUpdate 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
🔒 Bearer (Firebase token)

Delete a webhook subscription.

{"ok": true}
{"error": "webhook not found"}
POST/api/webhooks/:id/testTest webhook
🔒 Bearer (Firebase token)

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"}
Test Payload

The test webhook sends the following payload:

{
  "event": "test",
  "data": {
    "message": "Test webhook from EQUOS",
    "timestamp": "2024-01-21T12:00:00Z"
  }
}

Available Events

EventDescription
order.createdOrder created — fires for every origin (app, portals, API, connectors)
order.updatedIntegrator-relevant order fields changed (status, customer, references, dates, notes, totals)
order.status_changedOrder lifecycle transition (finalize / commit / void, including bulk and manufacturing)
order.deletedOrder moved to the recycle bin
inventory.adjustedStock quantity adjusted (manual adjustment, external sync, stocktake, warehouse operations)
inventory.low_stockAvailable quantity crossed below the item's warning level
inventory.transferStock moved between bins / locations
inventory.level_changedAn item's stock level at one warehouse changed (same values as GET /e9v3/inventory/levels; de-duplicate and order on data.seq)
contact.createdCustomer or supplier contact created — fires for every origin
contact.updatedContact details changed (name, company, email, phone, address, status)
shipment.createdFreight booked for an order
shipment.status_changedCarrier tracking status transition on a consignment linked to an order
shipment.deliveredCarrier tracking reports the consignment delivered
job.completedA job your API key enqueued settled completed or partial (for example a partial bulk fulfil)
job.failedA job your API key enqueued settled failed — data.errorMessage says why
payment.status_changedA one-off payment transitioned status (created, pending, succeeded, failed, refunded)
catalog.publishedThe public catalog feed was republished — re-pull GET /e9v3/catalog
prices.publishedA 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

EventWhen FiredKey Fields in Data
inventory.transferInventory moved between locationsbusinessId, operation, products
shipment.createdNew shipment bookedbusinessId, orderId, operation
shipment.status_changedShipment status updates (cancelled, edited, manifested, tracking)businessId, orderId, status, operation
shipment.deliveredShipment marked as deliveredbusinessId, 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>
HeaderDescription
X-EQUOS-SignatureHMAC-SHA256 of the request body, prefixed with sha256=
X-EQUOS-EventThe event type (e.g., order.status_changed)
X-EQUOS-DeliveryUnique 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}, 200

Verifying 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-Signature header 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 lastTriggeredAt and failureCount fields
  • Alert on failures: Set up alerts if a webhook fails repeatedly
  • Test regularly: Use the test endpoint to verify your webhook is working