# 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)

```javascript
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/](https://github.com/hookdeck/webhook-skills/tree/main/skills/nium-webhooks/examples/express/), [examples/nextjs/](https://github.com/hookdeck/webhook-skills/tree/main/skills/nium-webhooks/examples/nextjs/), [examples/fastapi/](https://github.com/hookdeck/webhook-skills/tree/main/skills/nium-webhooks/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_ODD_STATUS_WEBHOOK` | A customer's ongoing due diligence status changes |
| `CARD_CLIENT_KYB_STATUS_WEBHOOK` | The client's KYB compliance status changes |
| `REMIT_TRANSACTION_INITIATED_WEBHOOK` | A payout is initiated |
| `REMIT_TRANSACTION_PAID_WEBHOOK` | A payout is paid |
| `REMIT_TRANSACTION_REJECTED_WEBHOOK` | A payout is rejected |
| `REMIT_TRANSACTION_RETURNED_WEBHOOK` | A payout is returned |
| `FX_CONVERSION_COMPLETED_WEBHOOK` | An FX conversion settles |
| `FX_CONVERSION_FAILED_WEBHOOK` | An FX conversion fails |
| `FX_CONVERSION_CANCELLED_WEBHOOK` | An FX conversion is cancelled |
| `CARD_POS_APPROVED_WEBHOOK` | A point-of-sale card transaction is approved |
| `CARD_ATM_APPROVED_WEBHOOK` | An ATM card transaction is approved |
| `CARD_WALLET_FUNDING_WEBHOOK` | A card wallet receives funds |
| `VIRTUAL_ACCOUNT_ASSIGNED_WEBHOOK` | 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](https://github.com/hookdeck/webhook-skills/blob/main/skills/nium-webhooks/references/overview.md).

## Environment Variables

```bash
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):

```bash
npx hookdeck-cli listen 3000 nium --path /webhooks/nium

```

Give the printed URL to Nium as your webhook URL.

## Resources

* [references/overview.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/nium-webhooks/references/overview.md) - What Nium webhooks are, events and payload shape
* [references/setup.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/nium-webhooks/references/setup.md) - Register the endpoint and the static header with Nium
* [references/verification.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/nium-webhooks/references/verification.md) - Header check details, limits, gotchas, debugging 401s
* [examples/](https://github.com/hookdeck/webhook-skills/tree/main/skills/nium-webhooks/examples/) - Runnable Express, Next.js and FastAPI handlers with tests