Guide

Webhook delivery contract

Events, envelope, HMAC signature verification, the retry ladder, coalescing, auto deactivation and multi-tenant patterns. Everything a production consumer needs.

·12 min read ·Updated 3 Sep 2026

Stockpilot pushes events to an HTTPS endpoint you control. You register an endpoint with POST /webhooks/create, verify the signature on every request, and respond 2xx quickly. Delivery is at-least-once, so your handler must be idempotent.

Available events

EventFires when
orders.completedAn order reaches the completed state, either by transitioning to it or by arriving already completed, such as a bol.com import. Fires once per order.
inventory.stock_changedA product’s stock quantity changes in any warehouse.

GET /webhooks/events returns the same list at runtime, so an integration can discover new event types without a redeploy. Call it rather than hardcoding.

The envelope

{
  "webhook_id": "wh_7f3a91",
  "organization_id": 4812,
  "name": "erp stock sync",
  "event": "inventory.stock_changed",
  "event_triggered_at": "2026-09-03T09:14:22.481Z",
  "delivery_id": "dl_2c9b40e1",
  "data": { }
}

Headers

HeaderContents
X-Stockpilot-EventEvent name
X-Stockpilot-DeliveryUnique delivery ID, use it to dedupe
X-Stockpilot-WebhookThe webhook that produced this delivery
X-Stockpilot-OrganizationOrganization the event belongs to
X-Stockpilot-Signaturebase64(HMAC-SHA256(raw_request_body, webhook_secret))
User-AgentStockpilot-Webhooks/1.0

Verifying the signature

import base64, hashlib, hmac

def verify(raw_body: bytes, signature: str, secret: str) -> bool:
    digest = hmac.new(secret.encode(), raw_body, hashlib.sha256).digest()
    expected = base64.b64encode(digest).decode()
    return hmac.compare_digest(signature, expected)

Compute the HMAC over the raw body bytes, before any JSON parsing. Re-serialising a parsed body changes whitespace and key order and will not match.

Use hmac.compare_digest rather than == so the comparison is constant time.

Secrets

Supply your own secret at creation (minimum 16 characters) or omit it and Stockpilot generates one. Store it wherever you store your other secrets, and use the same one across every webhook you register if you want a single verification path.

Payload for inventory.stock_changed

{
  "product_id": 90211,
  "sku": "TSHIRT-BLK-M",
  "quantity": 60,
  "incoming": 120,
  "offered_stock": 38,
  "quantities": [
    { "warehouse_id": 1, "name": "Main",       "quantity": 44, "inbound": 120, "sums_onto_offered": true },
    { "warehouse_id": 7, "name": "Amazon FBA", "quantity": 16, "inbound": 0,   "sums_onto_offered": false }
  ]
}
FieldMeaning
quantityTotal physical units across all warehouses
incomingUnits on inbound purchase orders, at product level
offered_stockWhat Stockpilot actually offers to sales channels: the sum of warehouses with sums_onto_offered: true, minus buffer stock. This is the number most integrations want.
quantities[]Per warehouse breakdown

Do not sum inbound across warehouses. On the default warehouse, per warehouse inbound repeats the product level incoming, because purchase orders land there. Summing inbound across warehouses and comparing the result to incoming double counts.

There are no deltas

No before and after pair is sent. Deliveries are coalesced, so a previous and new pair would be misleading. Treat every payload as current state, and write setters rather than adjusters in your handler.

Warehouse scoping

Omit warehouse_id to receive changes from all warehouses; set it to filter. A scoped webhook still receives the full product payload. Scoping filters which changes trigger a delivery; it does not trim the body.

The consumer contract

Four obligations, and the behaviour you get in return.

Respond 2xx within 10 seconds

Do the real work in a queue. An ERP write, a marketplace call or anything touching a database under load will blow the budget.

Expect retries

A failed delivery is retried 15 times over roughly 17 hours:

10s · 30s · 1m · 2m · 5m · 10m · 20m · 30m · 1h · 1h · 2h · 2h · 3h · 3h · 3h

Expect auto deactivation

After five fully failed deliveries in 24 hours, the webhook is automatically deactivated. Bring it back with POST /webhooks/{webhook_id}/reactivate , and monitor for it:

for hook in client.get("/webhooks").json():
    if not hook["is_active"]:
        client.post(f"/webhooks/{hook['id']}/reactivate")
        alert(f"reactivated {hook['id']}")

Dedupe on delivery_id

Delivery is at-least-once. The same delivery_id can arrive more than once, and your handler must treat the second arrival as a no-op.

Coalescing

inventory.stock_changed is coalesced over roughly 5 seconds per product per warehouse. A burst of changes to one SKU in one warehouse produces one delivery carrying the final state. Coalescing does not merge across warehouses, so a product that moved in two warehouses can produce two deliveries.

Endpoint requirements

  • HTTPS only, and publicly resolvable. Private, loopback, link local and cloud metadata addresses are rejected.
  • Redirects are not followed. Register the final URL.

Multi-tenant integrations

If you are a platform receiving webhooks for many Stockpilot organizations:

  1. Register one webhook per customer organization, using your own shared secret.
  2. Store the returned webhook_id against that customer’s record.
  3. Verify with the constant secret, then route on X-Stockpilot-Webhook or X-Stockpilot-Organization.

A shared secret is visible to every organization admin who has it configured. If your customers need to be mutually distrustful, use a distinct secret per organization instead, look the customer up by X-Stockpilot-Webhook first, then verify with that customer’s secret.

Networking

Deliveries originate from Stockpilot’s worker infrastructure, not from the API gateway. Customers who IP allowlist inbound traffic need the worker egress ranges. Allowlisting the gateway’s addresses will not work.

Debugging

EndpointUse
POST /webhooks/{webhook_id}/testFire a delivery on demand, without changing data
GET /webhooks/{webhook_id}/deliveriesRecent deliveries with status codes and responses
GET /webhooks/{webhook_id}Current configuration and active state

All webhooks are scoped to your organization. A webhook ID belonging to another organization returns 404, the same as one that does not exist.

Walk through a working setup in Receive your first webhook.

A Stockpilot engineer talking through an integration with a developer

Stuck on something

Ask a person, not a search box

The API surface is wide, and some of it only makes sense once someone explains why it works that way. If a payload is not doing what you expect, or you are weighing two approaches, say so and we will look at it with you.