Webhooks
Receive and verify signed notifications from the hosted checkout.
Checkout notifications tell your shop when a session completes, is cancelled, or fails. They are delivered by the background job queue to the tenant's configured checkout webhook URL.
Configure delivery
The tenant settings are tenant_<id>.checkout.webhook_url and tenant_<id>.checkout.webhook_secret. Configure them through the supported checkout settings for your deployment. Delivery requires an HTTPS URL reachable by the server. The delivery worker does not follow redirects.
Events
| Event | Meaning |
|---|---|
checkout.completed | Checkout completed successfully |
checkout.cancelled | The checkout was cancelled |
checkout.failed | The checkout failed, including expiry of an open session |
Example delivery:
{
"id": "cwh_example",
"type": "checkout.completed",
"created_at": "2026-10-17T12:00:00.000Z",
"data": {
"session_id": "cs_example",
"order_id": "ord_example",
"status": "paid",
"amount_cents": 2490,
"currency": "EUR",
"reason": null
}
}
The request includes X-Sellforge-Event-Id and X-Sellforge-Signature headers.
Verify the signature
The signature header has this format:
t=<unix-timestamp>,v1=<hex-hmac>
The HMAC uses SHA-256, the checkout webhook secret, and the exact string timestamp.rawBody. Preserve the original request bytes before parsing JSON.
The following Node.js example also applies a five-minute replay window. That window is a receiver-side choice:
import { createHmac, timingSafeEqual } from 'node:crypto'
export function verifyCheckoutWebhook(rawBody, signature, secret) {
if (typeof signature !== 'string' || !secret) return false
const match = /^t=(\d+),v1=([a-f0-9]{64})$/.exec(signature)
if (!match) return false
const [, timestamp, digest] = match
const age = Math.abs(Date.now() / 1000 - Number(timestamp))
if (!Number.isFinite(age) || age > 300) return false
const expected = createHmac('sha256', secret)
.update(`${timestamp}.`)
.update(rawBody)
.digest()
const received = Buffer.from(digest, 'hex')
return received.length === expected.length
&& timingSafeEqual(received, expected)
}
Handle retries
New checkout delivery jobs allow up to five attempts. The event ID remains the same between retries; the delivery timestamp and signature can change.
Persist the event ID alongside your business action and make repeated deliveries safe to acknowledge. Return a successful HTTP response after accepting the event. A non-success response causes the job handler to fail and enter the queue's retry handling.
Other event mechanisms
These HTTP notifications are different from the in-process plugin hook bus. Incoming Stripe and shipping-provider callbacks are handled by the webhooks backend and follow those providers' verification rules.
Found something unclear? Let us know