Skip to main content

Webhooks

Receive events (transaction status changes, payout updates) at your endpoint. Configure Endpoint URL and Secret in the dashboard: Organization → API & Webhooks. We send a signed JSON body and headers you can verify.

Automatic delivery — Events are sent when status changes. Manual resend — You can resend a transaction.status_changed for any payment from Payments → View → Resend webhook.

Checkout ↔ Payment link — Store checkout_id from the create response. The webhook includes order_id (same as checkout_id) so you can correlate payments with your stored checkouts.

Headers

Every webhook request includes:

HeaderDescription
Content-Typeapplication/json
X-Webhook-EventEvent type (e.g. transaction.status_changed, webhook.test)
X-Webhook-Delivery-IdUnique delivery ID (UUID)
X-Webhook-TimestampUnix timestamp (seconds)
X-Webhook-Signaturesha256=<hex_hmac> — see Verifying signatures

Verifying signatures

We sign the raw request body with HMAC-SHA256 using your webhook secret. Verify as follows:

  1. Read the raw body of the request (as bytes / string, before parsing JSON).
  2. Compute HMAC-SHA256(raw_body, your_webhook_secret) and get the hex digest.
  3. Compare it to the value in X-Webhook-Signature after the sha256= prefix.

Important: Use the exact raw body. Do not re-serialize parsed JSON (key order or formatting may differ).

Example (Node.js)

const crypto = require('crypto');

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

Example (Python)

import hmac
import hashlib

def verify_signature(raw_body: bytes, signature_header: str, secret: str) -> bool:
expected = 'sha256=' + hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(signature_header, expected)

Event: transaction.status_changed

Sent when a transaction’s status changes (payment success/failure, refund created/completed, cancel). Checkout canceled — When the customer cancels (before or after payment), we send status canceled; use order_id to match your checkout; transaction_id may be null if canceled before payment. Also: payment success/failure, checkout sync, refund created/completed.

Payload:

{
"event": "transaction.status_changed",
"transaction_id": "TXabc123",
"order_id": "org1-1234567890-abc123",
"amount": "97.52",
"gross_amount": "99.00",
"fee_percent": 1.5,
"status": "paid",
"currency": "USD",
"created_at": "2026-03-01T12:00:00Z",
"updated_at": "2026-03-01T12:01:00Z"
}
FieldTypeDescription
transaction_idstring | nullPublic ID (use with GET /api/transactions/). null when checkout was canceled before payment
order_idstring(when from checkout) Same as checkout_id from create. Use to link payment to your stored checkout.
amountstringNet amount — what the merchant receives after fees
gross_amountstringGross amount — total charged to the customer (before fee deduction)
fee_percentnumber(optional) Organization fee percentage applied (e.g. 1.5)
statusstringpaid | failed | pending | processing | canceled | refund_pending | refunded
currencystringUSD | EUR
created_at / updated_atstring | nullISO 8601 timestamps

Example (checkout canceled before payment): transaction_id is null; use order_id to match your checkout. amount and gross_amount equal product total (no fees):

{
"event": "transaction.status_changed",
"transaction_id": null,
"order_id": "org1-1234567890-abc123",
"amount": "99.00",
"gross_amount": "99.00",
"status": "canceled",
"currency": "USD",
"created_at": "2026-03-01T12:00:00Z",
"updated_at": "2026-03-01T12:05:00Z"
}

Event: payout.status_changed

Sent when a payout status changes (pending → completed or rejected).

{
"event": "payout.status_changed",
"payout_id": "POxyz789",
"amount": "500.00",
"status": "completed",
"currency": "USD",
"created_at": "2026-03-01T10:00:00Z",
"updated_at": "2026-03-02T14:30:00Z"
}
FieldTypeDescription
payout_idstringPublic payout ID
amountstringPayout amount
statusstringpending | completed | rejected
currencystringUSD
created_at / updated_atstring | nullISO 8601 timestamps

Event: webhook.test

Manual test event. Use Send test event in Organization → Webhooks to verify your endpoint and signature logic. Your endpoint should respond with 2xx to confirm delivery.

{
"event": "webhook.test",
"message": "Test delivery from ND8"
}

Manual resend

In Payments, open a transaction and click Resend webhook to manually send a transaction.status_changed for that payment. Same payload as automatic delivery. Requires webhook endpoint and secret configured.

Secret rotation

Rotate your webhook secret in Organization → API & Webhooks. After rotation, only the new secret is shown once; previous signatures will no longer verify. Update your endpoint to use the new secret immediately.