Skip to content

Webhooks Reference

Swft sends signed webhooks to a URL you specify whenever notable events happen in a checkout — orders complete, payments fail, refunds issue. This page documents the event names, payload schema, and signature verification.

  1. In your Swft Dashboard, go to Settings → Webhooks and paste your endpoint URL (e.g. https://yourapp.com/swft-webhook).
  2. Click Generate secret. Copy the secret — you’ll use it to verify incoming requests.
  3. Optionally subscribe to specific events via the Subscriptions section; if you leave this empty, Swft delivers every event to your URL.

Swft retries delivery on transient failures with exponential backoff. Your endpoint should be idempotent — use the data.session_id as your deduplication key.

Every webhook is signed with an HMAC-SHA256 of the raw body using your webhook secret. The signature is in the X-Swft-Signature header, prefixed with sha256=:

X-Swft-Signature: sha256=4f5e6d...

Verify in your handler:

import crypto from 'crypto'
function verify(rawBody, signatureHeader, secret) {
const expected = 'sha256=' + crypto.createHmac('sha256', secret).update(rawBody).digest('hex')
return crypto.timingSafeEqual(Buffer.from(signatureHeader), Buffer.from(expected))
}

Reject any request where the signature doesn’t match. Use a timing-safe comparison (timingSafeEqual in Node, hmac.compare_digest in Python).

Every payload follows the same envelope:

{
"event": "order.completed",
"created_at": "2026-05-13T22:30:00.000Z",
"swft_version": "1.0",
"merchant_id": "11111111-2222-3333-4444-555555555555",
"data": {
"order_id": "wc-1234",
"session_id": "cs_abc123...",
"customer": {
"email": "shopper@example.com",
"first_name": "Jamie",
"last_name": "Doe",
"phone": "+44 7700 900123"
},
"billing_address": { "line1": "...", "city": "...", "postcode": "...", "country": "GB" },
"shipping_address": { "line1": "...", "city": "...", "postcode": "...", "country": "GB" },
"items": [
{
"product_id": "42",
"name": "Tote Bag",
"quantity": 1,
"price_pence": 1500,
"total_pence": 1500
}
],
"subtotal_pence": 1500,
"tax_pence": 300,
"shipping_pence": 500,
"discount_pence": 0,
"total_pence": 2300,
"currency": "GBP",
"payment_method": "card",
"coupon_code": "WELCOME10",
"b2b": {
"company_name": "Acme Ltd",
"vat_number": "GB123456789",
"po_number": "PO-2024-042"
}
}
}

Fields marked optional (phone, coupon_code, b2b) are omitted when not present.

The shopper has paid and the WooCommerce order has been created.

When it fires: On payment_intent.succeeded (Stripe), paypal_capture (PayPal), klyme_complete (Klyme), or nomupay-result returning success (NomuPay).

Payload: Full envelope above.

This is the event you almost certainly want — fulfilment, accounting sync, CRM updates, email-marketing identification all hook here.

A payment attempt failed for a final reason (declined card, fraud block, 3DS challenge rejected).

When it fires: On payment_intent.payment_failed (Stripe) or equivalent failure callbacks from other gateways.

Payload: Same envelope, with data.total_pence reflecting the attempted amount. The payment_method field is the method that failed.

Useful for routing to your support team or triggering a recovery email from outside the standard Dunning flow.

A merchant has refunded all or part of an order.

When it fires: On charge.refunded from Stripe. (For PayPal/Klyme/NomuPay, refunds happen in the merchant’s WooCommerce admin — the WC plugin handles them; Swft is not in the refund path.)

Payload: Same envelope, with data.total_pence set to the refunded amount (not the original order amount). data.order_id ties back to the original order.

A checkout session expired with items in the cart but no payment completed.

Status: Defined in the event enum, not yet implemented. If you build for it, expect it to ship in a future release.

By default, Swft sends every event to your webhook_url. If you only want a subset:

  1. In Settings → Webhooks, add Subscriptions.
  2. For each subscription, choose an event name and a delivery URL.

Subscriptions are stored in merchant_webhook_subscriptions. One merchant can have many subscriptions; they’re fanned out in parallel.

  • Each delivery times out after 5 seconds. Make sure your endpoint responds quickly — defer heavy work to a job queue.
  • A 2xx response is success. Anything else (including 3xx redirects) is treated as failure.
  • Failed deliveries are retried with exponential backoff: 30s, 2min, 10min, 30min, 2h, 6h (max 6 retries).
  • After 6 failed retries, the delivery is marked abandoned and won’t be retried again. You’ll see it in the Webhooks → Failed deliveries table in your dashboard.

Your webhook_url is checked at save time and at delivery time against a deny-list of internal / loopback / link-local addresses. URLs pointing at localhost, 127.0.0.0/8, 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, 169.254.0.0/16, or fc00::/7 are rejected.

If you need to deliver to an internal address for testing, use a service like ngrok or webhook.site.

The dashboard’s Webhooks page has a Send test event button that fires a synthetic order.completed to your URL. Use this to confirm your signature verification works before relying on live traffic.

  • Don’t trust data.merchant_id. Always verify the HMAC signature first; the body is only authentic after that.
  • Idempotency is your responsibility. Swft retries on failure; if your handler completes work but fails to return 2xx (network blip, gateway timeout), you’ll get the same event again. Dedupe on data.session_id.
  • One delivery URL doesn’t have to mean one event type. Send everything to one endpoint and dispatch internally on event, or split into multiple subscriptions per event — your call.
  • Versioning. The swft_version field is currently "1.0". We’ll add new fields without bumping the version; breaking changes will bump to "2.0" and we’ll dual-write during a transition window.