Webhooks
A webhook endpoint is a URL you register for an organization. When a subscribed event happens, the worker POSTs a signed JSON body to it. Endpoints are managed under Settings → Webhooks or via POST /admin/webhooks (see API reference).
Registering an endpoint
Give an Endpoint URL (HTTPS), an optional description, and the events you want — leave the list empty to receive everything. Creating the endpoint reveals its signing secret once; copy it. Rotate signing secret issues a new one and stops the old immediately.
The delivery
POST /your/endpoint HTTP/1.1
Content-Type: application/json
X-Webhook-Event: order.placed
X-Webhook-Signature: 3f9c…e1 (hex HMAC-SHA256 of the raw body, keyed with the endpoint secret)
{
"event": "order.placed",
"data": { …event payload… },
"sent_at": "2026-09-05T09:12:44Z"
}
X-Sokisoko-Event and X-Sokisoko-Signature are sent as well for older consumers; they are deprecated in favour of the neutral names. The request times out after 15 seconds.
Respond with any 2xx quickly and do your work afterwards. A non-2xx response or a timeout marks the attempt failed and the job queue retries with backoff; every attempt is recorded with its attempt number, response status and error, and shows in the endpoint's Delivery log in the admin console. Replay (POST /admin/webhooks/deliveries/{id}/replay) re-queues a delivery on demand — useful after you fix a bug on your side.
Deliveries are at-least-once. Make your handler idempotent on the event's identifying fields (for example the order public_id plus the new status).
Verifying the signature
Compute HMAC-SHA256 over the raw request body bytes with your endpoint secret, hex-encode it, and compare in constant time with X-Webhook-Signature.
import { createHmac, timingSafeEqual } from 'node:crypto'
export function verify(rawBody, header, secret) {
const expected = createHmac('sha256', secret).update(rawBody).digest('hex')
return expected.length === header.length &&
timingSafeEqual(Buffer.from(expected), Buffer.from(header))
}
import hmac, hashlib
def verify(raw_body: bytes, header: str, secret: str) -> bool:
expected = hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, header)
Parse the JSON only after the signature checks out, and read the body as bytes before any framework re-serialises it.
Event catalogue
| Event | When |
|---|---|
rfq.submitted | A buyer submitted a request for quote |
sourcing.quoted | A supplier store on the platform sent a quote for one of your sourcing requests |
quote.sent | A quote (or a new version) was sent to the buyer |
quote.countered | The buyer proposed changes |
quote.accepted / quote.declined | The buyer decided |
order.placed | An order was created — checkout, quote acceptance, or a rep placing it on behalf of a customer |
order.status_changed | Any order transition (confirmed, processing, shipped, delivered, closed, cancelled, on hold) |
order.backordered | A confirmed order could not fully reserve stock |
approval.requested | An order went on hold for the buyer company's approver |
order.approval_decided | The approver released or rejected it |
shipment.status_changed | A shipment moved (pending, shipped, delivered, returned) |
invoice.paid | An invoice was settled in full |
invoice.undunnable | The dunning sweep found an overdue invoice whose customer has no contact email |
payment.refunded | A payment was refunded |
order.refund_due | A cancellation left money to return |
return.requested / return.decided | A return was requested; approved, received or rejected |
lead.created | A storefront enquiry, popup submission or manual lead |
record_message.posted | A message on an order, quote or invoice thread |
product.submitted | A vendor listing entered moderation |
vendor.application_submitted | Someone applied to sell |
vendor_order.status_changed / vendor_order.cancelled | A marketplace sub-order moved or was cancelled |
subscription.run_failed | A recurring order could not be placed |
Payloads carry the entity's public_id and the fields relevant to the event; fetch the full record from the API when you need more. The same events drive the built-in automation rules, so anything you can automate in the console you can also react to externally.
Related topics
- Authentication
- ERP and accounting sync — a purpose-built alternative for orders and invoices
- Admin guide — Webhooks