Webhooks events
Niftipay can send webhook events to your server whenever an order changes status (crypto invoices, fiat card orders, refunds, payouts, etc.). You can register one or multiple webhook URLs. Niftipay will POST JSON to each URL.Scoping webhooks to integrations
Webhook URLs live in two places:- Settings → Webhooks (global list,
GET/POST /api/user/webhooks). Each row can be bound to a specific fiat integration, or left on All integrations (wildcard). - Integrations → Webhook URL (
fiatIntegration.merchantWebhookUrl). Edited per-integration; always scoped to that integration’s orders.
- the integration’s own
merchantWebhookUrl(if any), plus - every global webhook whose
integrationIdis either the originating integration ornull(wildcard).
What you need to build on your side
You must expose an HTTPS endpoint that accepts:- Method:
POST - Content-Type:
application/json - Path:
/niftipay/webhook(required)
Important: when you register a webhook URL in Niftipay, we force the path toYour endpoint should:/niftipay/webhook. So if you typehttps://example.com/hooks, the stored URL becomes:https://example.com/hooks/niftipay/webhook
- Verify the signature (recommended, see below)
- Parse the JSON body
- Return HTTP 2xx quickly (recommended: within 2–3 seconds)
- Process the event asynchronously (queue/job) to avoid timeouts and retries
Managing your webhook URLs (merchant API)
All webhook management is done via the authenticated merchant API.List webhooks
GET /api/user/webhooks
Returns your configured webhooks. We do not return secrets here.
Response
integrationId is null for wildcard webhooks (receive events from every integration and every integration-less event) or the id of a specific fiatIntegration.
Create a webhook
POST /api/user/webhooks
Body
integrationId is optional. Omit it (or send null) for a wildcard webhook. When provided, the id must belong to the calling user’s fiat integrations.
Response (secret returned only once)
webhookSecret securely. You will not see it again unless you rotate.
Update a webhook URL
PUT /api/user/webhooks/:id
Body
integrationId is optional; omit it to leave the current binding unchanged. Send null (or an empty string) to clear the binding (wildcard), or a specific fiat integration id to bind the webhook.
Response
integrationId appears in the response only when the field was included in the request body. Omit it from the body to leave the current binding untouched (and keep the response field absent).
Delete a webhook
DELETE /api/user/webhooks/:id
Response
Rotate a webhook secret
POST /api/user/webhooks/:id/secret
Rotates the secret and returns the new one once.
Response
Test your webhook receiver
Before going live, send a test payload to verify your endpoint works.Endpoint
POST /api/user/webhooks/:id/test
Sends a signed test webhook with event type "test" and sample order data to your URL.
Example request
Example response
success: truemeans your endpoint returned HTTP 2xxsigned: truemeans the payload included HMAC signature headers- Your endpoint should handle the
"test"event gracefully (return 200 and ignore)
Webhook payload format (common)
Niftipay sends a JSON object shaped like:Event names
You may receive theseevent values:
pendingpaidunderpaidcancelledexpired— sent when an order is force-cancelled past a hard timeout. Currently emitted for Banxa onramp orders that exceed the 72h KYC hard cap (payload includesreason: "ramping_kyc_timeout"). See Ramping webhooks.refundedpayout_upcomingpayout_sentchargeback— a chargeback has been filed against one of your ordersrisk_alert— an IP address has been flagged for repeated chargebacks
Most merchants only need to handle:paid,cancelled,refunded(+ optionallyunderpaid,chargeback).
Signing & verification (recommended)
When a webhook has a secret, Niftipay signs the payload and includes:x-webhook-id: webhook id (orlegacy:<userId>for older single-webhook setups)x-timestamp: unix seconds (string)x-signature:v1=<hex_hmac_sha256>
payload = raw JSON string(exact bytes as sent)ts = x-timestampsigned = HMAC_SHA256(secret, ts + "." + payload)x-signature = "v1=" + hex(signed)
Node.js verification example
Python verification example
Replay protection (required for production)
Reject webhooks with timestamps older than 5 minutes to prevent replay attacks:(event, order.id, txId) as idempotency keys (see below).
Delivery behavior, timeouts, and retries
Timeouts
Webhook delivery uses a short timeout (default ~6 seconds). If your endpoint is slow, the request may time out and be retried.Retries
Failed webhook deliveries may be retried (best-effort):- Retry interval: ~15 minutes
- Max attempts: 3
408,409,425,429- any
5xx
400,401,403,404,405,410,415,422
200 immediately.
Idempotency & processing recommendations
Webhook requests can be delivered more than once (network retries, timeouts, upstream duplicates). Best practice:- Deduplicate by a stable key:
- Crypto:
(event, order.id, txId)or justtxIdfor payments - Fiat:
(event, order.id, nopayn.order_id)and/or NoPayn refund id when present
- Crypto:
- Store “processed” markers in your DB
- Keep the webhook handler stateless and fast
- Always verify signatures in production
Crypto webhooks (orders paid on-chain)
Crypto payments are detected via the Tatum inbound webhook (/api/tatum/webhook), then Niftipay emits a merchant webhook event.
Crypto order events you’ll commonly see
pending
A crypto order was created and is waiting for payment.
paid
Payment was received and accepted (including under-payment within tolerance, if enabled on the platform).
underpaid
Payment was received but not enough to consider the order paid. You may receive multiple underpaid events as more funds arrive.
cancelled / expired
Order was cancelled or expired before completion.
refunded
Refund was executed from the deposit address to a refund address (for cancelled orders).
Example payloads (crypto)
paid example
underpaid example
refunded example (crypto refund endpoint)
Fiat webhooks
Fiat card payments are different than crypto payments so the shape will be different Niftipay emits a merchant webhook to your configured URL(s) with the sameevent model as crypto (pending, paid, cancelled, expired, refunded).
Fiat status mapping (simplified)
NoPayn order status → merchant webhook event:new/processing→pendingcompleted→paidcancelled→cancelledexpired→expired- refunded (refund detected) →
refunded
Your merchant webhook receives a normalized event.
Example payloads (fiat)
pending example
paid example
refunded example (fiat)
Note: fiat webhook payloads intentionally avoid exposing internal orderKey to merchants.
chargeback example
Sent when a chargeback is filed against one of your orders.
risk_alert example
Sent when an IP address is flagged for repeated chargebacks across your orders. This indicates a potential fraud pattern.
Tip: When you receive achargebackorrisk_alertevent, check the Chargeback Cases API to see if the IP or email has been automatically blocked.
Ramping webhooks (onramp lifecycle)
Onramp orders (e.g. Banxa fiat → crypto) emit a separate stream of events on the same merchant webhook URL as crypto/fiat events. Same HMAC signing scheme (x-timestamp / x-signature: v1=...), same retry behavior, same replay-protection guidance — only the event value differs.
These events let you track the onramp provider’s own lifecycle (KYC, fiat capture, status transitions) in addition to the underlying crypto invoice (pending / paid / cancelled / expired).
Envelope difference: ramping events wrap the payload in a top-leveldataobject, e.g.{ "event": "ramping.status_changed", "data": { ... } }. Crypto and fiat events useorderinstead.
Ramping events you may receive
ramping.status_changed
The provider reported a status change for the ramping order (e.g. pendingPayment → waitingPayment → paymentReceived → complete). Fires every time the upstream provider’s status changes. The status field on the payload mirrors Banxa’s canonical status (see Banxa upstream statuses below).
ramping.kyc_pending
The ramping order has been in a non-terminal upstream state long enough that the underlying crypto invoice was about to expire (default 12h), and Niftipay has automatically extended the invoice’s expiresAt so the customer journey can complete.
- Trigger is age-based, not status-specific: the event fires whenever the linked Banxa order is still in any non-terminal status at the 10h mark. The most common upstream statuses at that point are
extraVerification(Banxa requires additional ID/address documents),pendingPayment(KYC accepted, awaiting fiat),waitingPayment(payment in transit), andpaymentReceived(fiat captured, crypto delivery pending). - Fires once per ramping order, approximately 10 hours after the ramping order was created.
- The crypto invoice’s
expiresAtis rolled forward torampingOrder.createdAt + 72 hours(the hard cap). - If the order does not reach a successful terminal status (
complete/completed) by the 72h cap, the invoice is force-cancelled and a coreexpiredevent fires (see below).
ramping.createdandramping.deposit_instructionsare reserved event names that may be enabled in a future release. Today onlyramping.status_changedandramping.kyc_pendingare emitted.
Banxa upstream statuses
Thestatus field passed through on ramping.status_changed payloads (and the banxaStatus field on ramping.kyc_pending) is Banxa’s canonical order status. The full list and how Niftipay treats each:
| Banxa status | Terminal? | Niftipay treatment |
|---|---|---|
pendingPayment | no | non-terminal — invoice stays pending, eligible for 72h extension |
waitingPayment | no | non-terminal — invoice stays pending, eligible for 72h extension |
paymentReceived | no | non-terminal — invoice stays pending, eligible for 72h extension |
extraVerification | no | non-terminal — explicit KYC-needs-more-docs state, eligible for 72h extension |
inProgress | no | non-terminal — Banxa has marked this status for deprecation |
coinTransferred / cryptoTransferred | no | non-terminal — crypto on-chain, awaiting confirmations |
complete (sent in webhook payloads) | yes ✓ | success — invoice marked appropriately when on-chain delivery is confirmed by Tatum |
completed (returned by Banxa Get-Order endpoint) | yes ✓ | same as complete |
cancelled | yes ✗ | failure — invoice cancelled |
declined | yes ✗ | failure — invoice cancelled |
expired | yes ✗ | failure — invoice cancelled |
refunded | yes ✗ | failure — invoice cancelled |
Banxa usesFor the upstream definitions and the full state machine, see Banxa’s Order Status reference.completein webhook payloads butcompletedin the Get-Order REST response. Niftipay treats both as the same successful terminal state.
Banxa KYC timeout behavior
If Banxa fails to clear KYC within 72 hours of the ramping order creation, the underlying crypto invoice is force-cancelled and you will receive a coreexpired event (not a ramping event):
ramping.status_changed event around the same time with reason: "kyc_timeout_72h" for symmetry on the ramping channel.
Example payloads (ramping)
ramping.status_changed example
ramping.kyc_pending example
banxaStatushere may be any non-terminal Banxa status — most commonlyextraVerification,pendingPayment,waitingPayment, orpaymentReceived. See the status reference table.
Recommended handling
- Treat
ramping.kyc_pendingas informational — no action required, but you may want to surface a “verification in progress” notice to the customer. - Continue to rely on the core
paidevent for final fulfilment confirmation. Crypto delivery is detected on-chain by the same Tatum pipeline used for direct crypto orders, regardless of the ramping path. - If you receive
expiredwithreason: "ramping_kyc_timeout", the customer’s funds (if any) remain with Banxa — Banxa handles the refund directly.
Recommended webhook implementation checklist
Security
- ✅ Use HTTPS only
- ✅ Verify
x-signature(HMAC) using your storedwebhookSecret - ✅ Reject old timestamps (suggested ±5 minutes)
- ✅ Keep secrets out of logs
Reliability
- ✅ Respond
200 OKquickly (do not do heavy work in-request) - ✅ Process events in a queue (Redis, SQS, database jobs, etc.)
- ✅ Deduplicate events with an idempotency key
- ✅ Make your handler idempotent (safe to run twice)
Correctness
- ✅ Treat webhooks as the source of truth for status transitions
- ✅ Use
paidto fulfill orders, andrefundedto reverse fulfillment - ✅ Handle
underpaidif you allow partial top-ups from customers - ✅ Handle
chargebackto flag disputed orders and pause fulfillment - ✅ Handle
risk_alertto review orders from flagged IPs before shipping
Monitoring
- ✅ Log: event, order id, timestamp, webhook id, and processing outcome
- ✅ Alert on repeated failures or signature mismatches
- ✅ Store raw webhook payloads for short-term debugging (redact secrets)
Common mistakes
- Returning non-2xx while you “already accepted” the event → causes retries
- Doing heavy DB work inside the HTTP request → timeouts & duplicate deliveries
- Not deduplicating → double-fulfillment
- Not verifying signatures → anyone can spoof events to your endpoint
- Registering a URL that doesn’t implement
/niftipay/webhook→ you’ll never receive events