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 SettingsWebhooks 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": "[email protected]",
"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 WebhooksFailed 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.