Webhooks
Receive events (transaction status changes, refund 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 or when deposit attempt history updates (see Deposit attempts). 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:
| Header | Description |
|---|---|
Content-Type | application/json |
X-Webhook-Event | Event type (e.g. transaction.status_changed, refund.status_changed, webhook.test) |
X-Webhook-Delivery-Id | Unique delivery ID (UUID) |
X-Webhook-Timestamp | Unix timestamp (seconds) |
X-Webhook-Signature | sha256=<hex_hmac> — see Verifying signatures |
Verifying signatures
We sign the raw request body with HMAC-SHA256 using your webhook secret. Verify as follows:
- Read the raw body of the request (as bytes / string, before parsing JSON).
- Compute
HMAC-SHA256(raw_body, your_webhook_secret)and get the hex digest. - Compare it to the value in
X-Webhook-Signatureafter thesha256=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), or when deposit attempt history changes while the customer retries payment. 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.
Full guide: Deposit attempts.
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",
"depositAttempts": [
{
"status": "succeeded",
"paymentId": "pay_abc123",
"attemptedAt": "2026-06-16T23:56:01.145Z",
"errorMessage": null,
"paymentMethod": "card"
},
{
"status": "requires_payment_method",
"paymentId": "pay_abc123",
"attemptedAt": "2026-06-16T23:55:04.411Z",
"errorMessage": "Your card has insufficient funds.",
"paymentMethod": "card"
}
],
"created_at": "2026-03-01T12:00:00Z",
"updated_at": "2026-03-01T12:01:00Z"
}
| Field | Type | Description |
|---|---|---|
transaction_id | string | null | Public ID (use with Get payment by id: GET /api/transactions/<id>/). null when checkout was canceled before payment |
order_id | string | (when from checkout) Same as checkout_id from create. Use to link payment to your stored checkout. |
amount | string | Net amount — what the merchant receives after fees |
gross_amount | string | Gross amount — total charged to the customer (before fee deduction) |
fee_percent | number | (optional) Organization fee percentage applied (e.g. 1.5) |
status | string | paid | failed | pending | processing | canceled | refund_pending | refunded |
currency | string | USD | EUR |
depositAttempts | array | Deposit attempt history from the provider (newest first). See Deposit attempts. Empty array if none yet. |
created_at / updated_at | string | null | ISO 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: refund.status_changed
Sent when a refund’s status changes (processing, completed, or failed). Use this event to track refund lifecycle independently of transaction.status_changed (which still fires when the parent payment status updates).
When sent:
- After a successful refund request to the provider (
processing) - When Inflow sync confirms completion or failure (
completed/failed) - When a provider refund request fails immediately (
failed)
Payload:
{
"event": "refund.status_changed",
"refund_id": "RFabc123",
"transaction_id": "TXxyz789",
"amount": "40.00",
"currency": "USD",
"amount_usd": "39.20",
"amount_in_cents": 4000,
"status": "processing",
"reason": "Customer request",
"created_at": "2026-03-01T12:00:00Z",
"updated_at": "2026-03-01T12:00:05Z"
}
| Field | Type | Description |
|---|---|---|
refund_id | string | Public refund ID |
transaction_id | string | Parent payment public ID |
amount | string | Refund amount in charge currency (TTC) |
currency | string | USD | EUR |
amount_usd | string | (optional) Merchant wallet deduction in USD |
amount_in_cents | integer | (optional) Amount sent to Inflow (HT basis) |
status | string | pending | processing | completed | failed | rejected |
reason | string | Refund reason |
created_at / updated_at | string | null | ISO 8601 timestamps |
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"
}
| Field | Type | Description |
|---|---|---|
payout_id | string | Public payout ID |
amount | string | Payout amount |
status | string | pending | completed | rejected |
currency | string | USD |
created_at / updated_at | string | null | ISO 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.