Agent skill
Nium Webhooks Skill
Receive and authenticate Nium webhooks. Use when setting up a Nium webhook endpoint, checking the static client-specified header (such as x-partner-key), debugging 401s, or handling events like CUSTOMER_STATUS_WEBHOOK, REMIT_TRANSACTION_PAID_WEBHOOK, FX_CONVERSION_COMPLETED_WEBHOOK, CARD_POS_APPROVED_WEBHOOK and RFI requests.
Install this skill
npx skills add hookdeck/webhook-skills --skill nium-webhooks
Nium is a cross-border payments platform (payouts, wallets, FX, card issuing, onboarding). It sends a JSON POST to the endpoint you register whenever something happens: a customer's status changes, a payout is paid, an FX conversion completes, a card transaction is approved, an RFI is raised. The event is named in the body's template field.
Nium webhooks are not signed. There is no HMAC and no timestamp. During client setup you agree a static header with Nium (the docs use x-partner-key as an example name, but the name is your choice), and Nium adds it to every webhook. Your endpoint authenticates the request by comparing that header's value with the secret you chose.
A static shared credential authenticates the sender, not the body, and gives no replay protection (no signature, no timestamp). Anyone who learns the value can send forged events, and a captured request can be replayed. Serve the endpoint over HTTPS only, rotate the credential with Nium if it leaks, and treat webhook bodies as hints: fetch current state from the Nium API before acting on money movement.
When to Use This Skill
- How do I receive Nium webhooks?
- How do I authenticate Nium webhook requests? Does Nium sign webhooks? (No: static header)
- How do I configure the
x-partner-keyheader for Nium webhooks? - How do I handle
REMIT_,TRANSACTION_ PAID_ WEBHOOK FX_orCONVERSION_ COMPLETED_ WEBHOOK CUSTOMER_STATUS_WEBHOOK? - How do I handle Nium RFI webhooks (
RFI_REQUESTED,RFI_RESPONDED,RFI_CLOSED)? - Why does my Nium webhook endpoint return 401?
Verification (core)
const crypto = require('crypto');
// Hash both sides: equal-length buffers, so timingSafeEqual never throws.
function safeEqual(a, b) {
const ha = crypto.createHash('sha256').update(String(a)).digest();
const hb = crypto.createHash('sha256').update(String(b)).digest();
return crypto.timingSafeEqual(ha, hb);
}
function verifyNiumRequest(
headers,
secret = process.env.NIUM_WEBHOOK_SECRET,
headerName = process.env.NIUM_WEBHOOK_HEADER || 'x-partner-key'
) {
if (!secret) return false; // never accept when unconfigured
const received = headers[headerName.toLowerCase()]; // Node lowercases header names
return Boolean(received) && safeEqual(received, secret); // compare the WHOLE value
}
- Header name: whatever you agreed with Nium (
NIUM_WEBHOOK_HEADER, defaultx-partner-key), looked up case-insensitively. - Value: the exact, whole header value (
NIUM_WEBHOOK_SECRET), including any prefix you typed such asBearer. Do not strip or parse aBearerprefix; compare it whole. - Return
401on a missing header or mismatch,400for a body that isn't a JSON object,200once handled. Nium treats any non-200 as a failed delivery. - Read the body raw (
express.raw({ type: '*/*' })) and authenticate before parsing it. - Python:
hmac.compare_. Encode both sides to bytes; passingdigest(received.encode(), secret.encode()) strraisesTypeErroron non-ASCII values.
For complete handlers with tests, see examples/express/, examples/nextjs/, examples/fastapi/.
Event Types
Dispatch on template. RFI events have no template: use rfiDetails.status.
| Event | Sent When |
|---|---|
CUSTOMER_STATUS_WEBHOOK | A customer's status or subStatus changes (onboarding, RFI, suspension) |
CUSTOMER_ | A customer's ongoing due diligence status changes |
CARD_ | The client's KYB compliance status changes |
REMIT_ | A payout is initiated |
REMIT_ | A payout is paid |
REMIT_ | A payout is rejected |
REMIT_ | A payout is returned |
FX_ | An FX conversion settles |
FX_ | An FX conversion fails |
FX_ | An FX conversion is cancelled |
CARD_ | A point-of-sale card transaction is approved |
CARD_ | An ATM card transaction is approved |
CARD_ | A card wallet receives funds |
VIRTUAL_ | A virtual account is assigned |
INCOMING_FUNDS_WEBHOOK | Funds arrive for a wallet |
RFI_REQUESTED (rfiDetails.status) | Nium raises a Request for Information |
RFI_RESPONDED (rfiDetails.status) | A response to an RFI is submitted |
RFI_CLOSED (rfiDetails.status) | Nium cancels an RFI before it is resolved |
Nium defines many more templates (cards, wallets, direct debit, settlement); see references/overview.md.
Environment Variables
NIUM_WEBHOOK_HEADER=x-partner-key # header name agreed with Nium (default x-partner-key)
NIUM_WEBHOOK_SECRET=your-shared-secret # exact, whole header value, including any 'Bearer ' prefix
Delivery and Duplicates
By default Nium delivers each event at most once: a timeout or non-200 response is not retried. Retries (up to 20, one every 5 minutes) must be enabled per event with your Nium representative. Every delivery carries an x-request-id header, and a redelivery reuses the original value, so use it to drop duplicates.
Local Development
Nium can't reach localhost. Tunnel with the Hookdeck CLI (no account required):
npx hookdeck-cli listen 3000 nium --path /webhooks/nium
Give the printed URL to Nium as your webhook URL.
Resources
- references/overview.md - What Nium webhooks are, events and payload shape
- references/setup.md - Register the endpoint and the static header with Nium
- references/verification.md - Header check details, limits, gotchas, debugging 401s
- examples/ - Runnable Express, Next.js and FastAPI handlers with tests