Skip to content
DocsAPI reference

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

EventMeaning
checkout.completedCheckout completed successfully
checkout.cancelledThe checkout was cancelled
checkout.failedThe checkout failed, including expiry of an open session

Example delivery:

json
{
  "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:

text
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:

js
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.

Source reviewed Checked against kernel fe5c475.

Found something unclear? Let us know

Start exploring

NavigateOpenEscClose