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-key header for Nium webhooks?
  • How do I handle REMIT_TRANSACTION_PAID_WEBHOOK, FX_CONVERSION_COMPLETED_WEBHOOK or 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, default x-partner-key), looked up case-insensitively.
  • Value: the exact, whole header value (NIUM_WEBHOOK_SECRET), including any prefix you typed such as Bearer . Do not strip or parse a Bearer prefix; compare it whole.
  • Return 401 on a missing header or mismatch, 400 for a body that isn't a JSON object, 200 once 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_digest(received.encode(), secret.encode()). Encode both sides to bytes; passing str raises TypeError on 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.

EventSent When
CUSTOMER_STATUS_WEBHOOKA customer's status or subStatus changes (onboarding, RFI, suspension)
CUSTOMER_ODD_STATUS_WEBHOOKA customer's ongoing due diligence status changes
CARD_CLIENT_KYB_STATUS_WEBHOOKThe client's KYB compliance status changes
REMIT_TRANSACTION_INITIATED_WEBHOOKA payout is initiated
REMIT_TRANSACTION_PAID_WEBHOOKA payout is paid
REMIT_TRANSACTION_REJECTED_WEBHOOKA payout is rejected
REMIT_TRANSACTION_RETURNED_WEBHOOKA payout is returned
FX_CONVERSION_COMPLETED_WEBHOOKAn FX conversion settles
FX_CONVERSION_FAILED_WEBHOOKAn FX conversion fails
FX_CONVERSION_CANCELLED_WEBHOOKAn FX conversion is cancelled
CARD_POS_APPROVED_WEBHOOKA point-of-sale card transaction is approved
CARD_ATM_APPROVED_WEBHOOKAn ATM card transaction is approved
CARD_WALLET_FUNDING_WEBHOOKA card wallet receives funds
VIRTUAL_ACCOUNT_ASSIGNED_WEBHOOKA virtual account is assigned
INCOMING_FUNDS_WEBHOOKFunds 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


Repository

hookdeck/webhook-skills

v0.1.0 · MIT · Updated Nov 3, 2026

View on GitHub →