Agent skill
NMI Webhooks Skill
Receive and verify NMI (Network Merchants) webhooks. Use when setting up NMI webhook handlers, debugging Webhook-Signature verification, or handling transaction events like transaction.sale.success, transaction.auth.success, transaction.refund.success, and transaction.void.success. Note: NMI does NOT use Standard Webhooks — the Webhook-Signature header is "t=<nonce>,s=<sig>" (comma-separated) where t is a NONCE (not a Unix timestamp), and the signature is HMAC-SHA256 over "<nonce>.<raw_body>", lowercase hex.
Install this skill
npx skills add hookdeck/webhook-skills --skill nmi-webhooks
When to Use This Skill
- How do I receive NMI (Network Merchants) webhooks?
- How do I verify the NMI
Webhook-Signatureheader? - Why is my NMI webhook signature verification failing?
- How do I handle
transaction.sale.success,transaction.auth.success,transaction.refund.success, ortransaction.void.successevents? - What is the
t=value in the NMI signature header — is it a timestamp?
How NMI Webhooks Work (Read This First)
NMI does not use the Standard Webhooks spec. Each delivery carries a single custom header:
Webhook-Signature: t=f3c1e9a2b7d84c15,s=9b7c...e10a
Two facts drive everything below:
tis a NONCE, not a timestamp. It is a random value NMI generates per delivery and includes in the signed content. Because it is not a timestamp, NMI documents no replay/timestamp tolerance window — do not try to reject "old" deliveries by parsingtas a Unix time.- The signature signs
"<nonce>.<raw_body>". You verify by computing HMAC-SHA256 over the nonce, a literal., and the raw, unparsed request body, keyed with your signing key, hex-encoding it, and comparing (timing -safe) to thesvalue. Re-serializing the JSON breaks the HMAC.
NMI ──POST body + "Webhook-Signature: t=<nonce>,s=<hex>"──▶ your endpoint
│ parse t + s
│ hmac_sha256(key, t + "." + rawBody)
▼
timing-safe compare hex == s → 200
The payload envelope is { "event_id", "event_type", "event_body" }. The event_type is a dotted lowercase string like transaction.sale.success.
Verification (core)
const crypto = require('crypto');
// Header: "Webhook-Signature: t=<nonce>,s=<lowercase-hex-hmac>"
// t is a NONCE (not a timestamp); the signed content is `<nonce>.<rawBody>`.
function verifyNmiWebhook(rawBody, signatureHeader, signingKey) {
const parts = {};
for (const seg of String(signatureHeader || '').split(',')) {
const i = seg.indexOf('=');
if (i !== -1) parts[seg.slice(0, i).trim()] = seg.slice(i + 1).trim();
}
const { t: nonce, s: signature } = parts;
if (!nonce || !signature || !signingKey) return false;
const body = Buffer.isBuffer(rawBody) ? rawBody.toString('utf8') : rawBody;
const expected = crypto
.createHmac('sha256', signingKey)
.update(`${nonce}.${body}`)
.digest('hex');
try {
return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signature));
} catch {
return false; // length mismatch = invalid
}
}
There is no official NMI SDK, so verification is manual in every language. Always verify against the raw body — parse JSON only after the signature checks out.
For complete handlers with route wiring, event dispatch, and tests, see:
Common Event Types
Event names are dotted lowercase transaction.<action>.<result>, where action is one of sale, auth, capture, void, refund, credit, or validate, and result is success, failure, or unknown.
| Event | Fires When | Common Use Cases |
|---|---|---|
transaction.sale.success | A sale (auth + capture) is approved | Fulfil order, send receipt |
transaction.sale.failure | A sale is declined | Notify customer, retry/dunning |
transaction.auth.success | An authorization is approved | Reserve funds, hold order |
transaction.capture.success | A prior auth is captured | Mark order paid, fulfil |
transaction.void.success | A transaction is voided before settlement | Release hold, cancel order |
transaction.refund.success | A settled transaction is refunded | Reverse fulfilment, notify |
transaction.credit.success | An unreferenced credit is issued | Payout/adjustment bookkeeping |
transaction.validate.success | A card validation succeeds | Save card on file |
The .failure and .unknown result variants exist for every action. See references/overview.md for the full matrix and the event_body payload structure.
Environment Variables
NMI_SIGNING_KEY=your_webhook_signing_key # Merchant Control Panel → Settings → Webhooks
The signing key is generated in the NMI Merchant Control Panel under Settings → Webhooks. It is distinct from your gateway API/security key.
Local Development
# Start a tunnel (no account needed) — forwards to your local handler
npx hookdeck-cli listen 3000 nmi --path /webhooks/nmi
Register the printed public URL as the endpoint URL under Settings → Webhooks in the Merchant Control Panel, then run a test transaction to see a delivery.
Reference Materials
- references/overview.md - Event types, the
transaction.<action>.<result>matrix, payload structure - references/setup.md - Merchant Control Panel configuration, getting the signing key
- references/verification.md - Webhook-Signature verification in depth and gotchas