Guide to Nium Webhooks: Features and Best Practices
Nium webhooks tell your platform what is happening to the money and cards you run through Nium: a payout was sent to the beneficiary's bank, a wallet was funded, a card transaction was approved or declined, an FX conversion completed, or Nium needs more information about a customer. If you build cross-border payments, payroll or card products on Nium, webhooks are how those status changes reach your systems without polling the API.
This guide covers how Nium delivers webhooks, how to identify event types across Nium's irregular naming, how to authenticate requests that Nium doesn't sign, the at-most-once delivery default and how to change it, and the best practices for a production receiver.
What are Nium webhooks?
Nium sends webhooks from the Nium One platform to a single endpoint you give them, in the form https://<customerHost:Port>/webhook. Every event goes to that URL as a POST with a JSON body. Setup isn't self-serve: you provide the URL to Nium, and the delivery options are configured as part of your client setup.
The event type is in the body. For almost every event, it's the template field, for example CARD_. Request for Information (RFI) webhooks are the exception: they have no template, and their type is rfiDetails.status.
Each body is a flat object with fields specific to the event, and Nium says it "generally carries just enough details about the event". You're expected to call Nium's APIs for anything more.
Nium doesn't sign webhooks. Its documented authentication is a static identifier header, for example x-partner-key, which you agree with Nium during client setup and which Nium then includes on every webhook. Your receiver compares that header with the value you stored.
Nium webhook features
| Feature | Details |
|---|---|
| Configuration | One client webhook URL, provided to Nium and set up as part of client configuration |
| Method | POST with content-type: application/json |
| Event type | template in the body, or rfiDetails.status for RFI webhooks |
| Documented events | 79 template values and 3 RFI statuses |
| Delivery ID | x-request-id header, unique per event and repeated on redelivery |
| Authentication | A static identifier header such as x-partner-key, configured at client setup |
| Signature | None |
| Success | HTTP 200. Any other status, or a timeout, is a failure |
| Default delivery | At most once. Failures are not retried |
| Optional retries | Enabled per event type with Nium: up to 20 retries, one every 5 minutes |
| Source IPs | Not published for webhooks |
Common events
Nium groups its webhooks into platform, payout, payin, issuing and card, and RFI events. A selection:
| Event | Fires when |
|---|---|
REMIT_ | A transfer is created |
REMIT_ | A payout is sent to a partner bank |
REMIT_ | A remittance transaction is paid |
REMIT_ | A payout request made through the Transfers API fails |
REMIT_ | A remittance transaction is returned |
BENEFICIARY_ | Nium has verified a beneficiary's bank account details |
CARD_ | A wallet is funded |
FX_ | An FX conversion between two currencies completes |
CUSTOMER_STATUS_WEBHOOK | A customer's status or sub-status changes |
CARD_ | A point-of-sale card transaction is approved |
CARD_ | A card transaction is declined for insufficient funds |
RFI_REQUESTED | Nium raises a Request for Information and is waiting for a response |
Don't assume a naming pattern. Most template values end in _WEBHOOK, but CUSTOMER_, BENEFICIARY_ and the DIRECT_DEBIT_* funding instrument and micro-deposit events don't, and CARD_ is mixed case. Treat each value as an opaque string and match it exactly.
A payout status update looks like this (trimmed):
{
"clientHashId": "0498f10f-1968-494f-9f7a-454ed23942a0",
"customerHashId": "0de1e512-e0d7-4eca-ad41-dd39325facc2",
"walletHashId": "e83cca77-8b63-4a25-b580-1d872380ef29",
"transactionCurrency": "INR",
"transactionAmount": "5418.1700",
"systemReferenceNumber": "RT1343085439",
"billingCurrency": "SGD",
"billingAmount": "100.0000",
"payoutMethod": "LOCAL",
"template": "REMIT_TRANSACTION_SENT_TO_BANK_WEBHOOK",
"externalId": "Custom1245"
}
Amounts are strings, so parse them with a decimal type rather than a float. RFI webhooks share one shape instead: clientHashId, customerHashId and an rfiDetails object with rfiId, status, query, rfiExpiry and related fields.
Nium's own examples aren't always reliable. Several, including the overview's CARD_ example, are missing commas or contain placeholders, so they aren't valid JSON. A few event pages, such as VTS Token, Remit Transaction Sub-status Update and Card Details Updated, show no usable template value at all. Build parsers from real deliveries where you can.
See Nium webhook payloads in action. Inspect and replay sample Nium webhook payloads in Hookdeck Inspect, with no account or setup required.
Setting up Nium webhooks
Because Nium configures webhooks during client setup, setup is a conversation with your Nium representative rather than a dashboard form.
- Generate a long random value for the identifier header (
openssl rand -hex 32works) and store it in your app's environment. - Give Nium your HTTPS endpoint URL and ask them to include your identifier header, for example
x-partner-key, on every webhook. - Decide which events need retries and ask Nium to enable them. Without this, every event is delivered at most once.
- Confirm the exact header name and value Nium will send, since your receiver must match both.
For local development, use the Hookdeck CLI: hookdeck listen 3000 nium --path /webhook gives you a public HTTPS URL that forwards to your local server, plus a web UI for inspecting and replaying deliveries, with no account required. Use it with a sandbox client.
With Hookdeck. Create a Nium source in Hookdeck, enter the Header Name you agreed with Nium (for example x-partner-key) and the Header Value, including any prefix, and give Nium the source URL as your webhook endpoint.
Securing Nium webhooks
Compare the identifier header with your stored value. Look the header up case-insensitively, compare the whole value in constant time, and check lengths first, because crypto.timingSafeEqual throws on buffers of different lengths. Return exactly 200 on success: Nium treats any other status as a failure.
const crypto = require('crypto');
const express = require('express');
const app = express();
function safeEqual(a, b) {
const x = Buffer.from(String(a), 'utf8');
const y = Buffer.from(String(b), 'utf8');
return x.length === y.length && crypto.timingSafeEqual(x, y);
}
function verifyNiumRequest(headers) {
const secret = process.env.NIUM_WEBHOOK_SECRET;
if (!secret) return false; // fail closed when unconfigured
const name = (process.env.NIUM_WEBHOOK_HEADER || 'x-partner-key').toLowerCase();
const received = headers[name];
return typeof received === 'string' && safeEqual(received, secret);
}
function eventType(body) {
if (typeof body.template === 'string') return body.template;
if (body.rfiDetails && typeof body.rfiDetails.status === 'string') return body.rfiDetails.status;
return 'UNKNOWN';
}
app.post('/webhook', express.raw({ type: '*/*' }), async (req, res) => {
if (!verifyNiumRequest(req.headers)) {
return res.sendStatus(401);
}
let body;
try {
body = JSON.parse(req.body.toString('utf8'));
} catch {
return res.sendStatus(400);
}
const requestId = req.headers['x-request-id'];
// Persist before acknowledging: by default Nium never retries
await saveEvent(requestId, eventType(body), body);
res.sendStatus(200); // exactly 200, not 201 or 204
});
The same check in Python:
import hmac
import os
def verify_nium_request(headers) -> bool:
secret = os.environ.get("NIUM_WEBHOOK_SECRET")
if not secret:
return False
name = os.environ.get("NIUM_WEBHOOK_HEADER", "x-partner-key")
received = headers.get(name) # case-insensitive in Starlette and Flask
if not received:
return False
return hmac.compare_digest(received.encode("utf-8"), secret.encode("utf-8"))
A static header proves the sender knows the value. It doesn't protect the body from modification, and nothing prevents a captured request from being replayed, because Nium sends no signature or timestamp. For a payments integration, that matters: treat each webhook as a notification and confirm amounts and statuses through Nium's API before you move money or release goods. Keep the endpoint on HTTPS and agree a rotation process with Nium in case the value leaks.
Make Nium webhooks production-ready. Hookdeck Event Gateway checks your identifier header on every request, answers Nium with a
200as soon as the event is stored, and retries delivery to your service, so the at-most-once default stops costing you events.
Nium webhook limitations and pain points
At most once by default
The Problem: If your endpoint times out or returns anything other than 200, the event is gone. A deploy, a crash or a 204 response loses a payout or card status update.
Why It Happens: Nium's default is to "deliver every webhook event at most once", with no retry on a timeout or a non-200 status. Retries exist, but only for the event types Nium enables for you, and they stop after 20 attempts five minutes apart, about 100 minutes in all.
Workarounds:
- Ask Nium to enable retries for every event you depend on.
- Return exactly
200, quickly, after storing the event. - Reconcile transaction and payout status from the Nium API on a schedule.
How Hookdeck Can Help: Hookdeck answers Nium with a 200 as soon as the request is stored, then delivers to your service with automatic retries on a schedule you control. Issues alert you when deliveries fail, and you can replay any stored event.
A static header and no signature
The Problem: A request with the right header value is indistinguishable from a real one, and its body can't be integrity-checked.
Why It Happens: Nium's documented authentication is the static identifier header configured at client setup.
Workarounds:
- Use a long random value and compare it in constant time.
- Verify important state through the Nium API before acting.
- Keep the value out of logs and agree a rotation process with Nium.
How Hookdeck Can Help: Hookdeck's Nium source compares the header you name against the exact value you enter. A request without a matching header still gets a 200 response, but Hookdeck marks it as failed verification and creates no event, so it never reaches your destination. Every request is stored with its headers and body.
Two type fields and irregular names
The Problem: A handler that only reads template misses every RFI webhook, and pattern matching on _WEBHOOK misses several real events.
Why It Happens: RFI webhooks carry their type in rfiDetails.status, and template values don't follow one convention.
Workarounds:
- Read
templatefirst, then fall back torfiDetails.status. - Match exact strings from Nium's event pages, and log anything unrecognised.
How Hookdeck Can Help: Filters can match on template or rfiDetails.status to route payout, card and compliance events to different destinations.
Duplicates from retries
The Problem: With retries enabled, an event your service processed but didn't acknowledge in time arrives again.
Why It Happens: Every redelivery repeats the original attempt, with the same x-request-id.
Workarounds:
- Store
x-request-idand skip any value you've already processed, as Nium recommends.
How Hookdeck Can Help: Hookdeck deduplication can drop repeated deliveries based on the x-request-id header before they reach your handler.
Best practices
Store, then return exactly 200
Nium counts any status other than 200 as a failure. Persist the event, return 200, then process it in the background. See why to process webhooks asynchronously.
Deduplicate on x-request-id
It's unique per event and identical on every redelivery, which makes it the natural idempotency key. See our guide to webhook idempotency.
Treat events as notifications
The body carries "just enough details". Fetch the transaction, wallet or card from the API before taking any irreversible action, especially for payouts and refunds.
Use decimal types for amounts
Amounts and balances arrive as strings, sometimes with four decimal places. Parse them with a decimal library and keep the currency alongside.
Handle unknown events gracefully
Nium documents more than 80 event types, and new ones can appear. Store and acknowledge events you don't recognise rather than rejecting them, so a new event type never turns into a lost one.
Conclusion
Nium sends unsigned JSON POST requests to one client URL, identified by template or, for RFI webhooks, rfiDetails.status, and authenticated by a static header agreed during client setup. Delivery is at most once unless Nium enables retries for an event, success means exactly 200, and x-request-id is your deduplication key.
Hookdeck Event Gateway checks the identifier header, stores every event on arrival and retries delivery to your service, which turns Nium's at-most-once default into delivery you can rely on.
Get started with Hookdeck for free and handle Nium webhooks reliably in minutes.