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.
Quick start
Section titled “Quick start”- In your Swft Dashboard, go to Settings → Webhooks and paste your endpoint URL (e.g.
https://yourapp.com/swft-webhook). - Click Generate secret. Copy the secret — you’ll use it to verify incoming requests.
- 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.
Signature verification
Section titled “Signature verification”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).
Payload structure
Section titled “Payload structure”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": { "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.
Events
Section titled “Events”order.completed
Section titled “order.completed”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.
order.failed
Section titled “order.failed”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.
refund.issued
Section titled “refund.issued”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.
cart.abandoned
Section titled “cart.abandoned”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.
Subscribing to specific events
Section titled “Subscribing to specific events”By default, Swft sends every event to your webhook_url. If you only want a subset:
- In Settings → Webhooks, add Subscriptions.
- 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.
Delivery & retries
Section titled “Delivery & retries”- Each delivery times out after 5 seconds. Make sure your endpoint responds quickly — defer heavy work to a job queue.
- A
2xxresponse is success. Anything else (including3xxredirects) 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.
SSRF protection
Section titled “SSRF protection”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.
Testing
Section titled “Testing”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.
Gotchas
Section titled “Gotchas”- 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 ondata.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_versionfield 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.