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-Signature header?
  • Why is my NMI webhook signature verification failing?
  • How do I handle transaction.sale.success, transaction.auth.success, transaction.refund.success, or transaction.void.success events?
  • 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:

  1. t is 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 parsing t as a Unix time.
  2. 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 the s value. 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.

EventFires WhenCommon Use Cases
transaction.sale.successA sale (auth + capture) is approvedFulfil order, send receipt
transaction.sale.failureA sale is declinedNotify customer, retry/dunning
transaction.auth.successAn authorization is approvedReserve funds, hold order
transaction.capture.successA prior auth is capturedMark order paid, fulfil
transaction.void.successA transaction is voided before settlementRelease hold, cancel order
transaction.refund.successA settled transaction is refundedReverse fulfilment, notify
transaction.credit.successAn unreferenced credit is issuedPayout/adjustment bookkeeping
transaction.validate.successA card validation succeedsSave 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


Repository

hookdeck/webhook-skills

v0.1.0 · MIT · Updated Aug 4, 2026

View on GitHub →