Gareth Wilson Gareth Wilson

Guide to Nomod Webhooks: Features and Best Practices

Published


Nomod lets small and medium businesses in the UAE take card payments through payment links, a hosted checkout and an API. Card payments go through several states, from created to authorised, captured, refunded or disputed, and Nomod's webhooks report each change on a charge so you can fulfil orders, reconcile your balance and handle refunds without polling.

What are Nomod webhooks?

Each Nomod webhook is tied to one integration type: Nomod API, for transactions you process through the API, or Nomod Hosted Checkout, for those that go through Nomod's hosted payment page. If you use both, create a webhook for each. Delivery and signing are handled by Svix, and Nomod's verification guide recommends the Svix library.

AspectValue
MethodPOST, JSON body (Nomod prints no Content-Type, so read the raw bytes whatever it is)
Signature headerssvix-id, svix-timestamp, svix-signature (Standard Webhooks)
Success200 OK, returned as quickly as possible. A slow response counts as a failure and is retried
DeduplicationeventId, to ignore repeated deliveries
Event logsKept for 90 days

Nomod's docs don't give a retry schedule or a response timeout, so don't build either into your code.

Nomod webhook features

FeatureDetails
ConfigurationDashboard, Owners and Admins only
ScopeOne integration type per webhook, with chosen events
SignatureStandard Webhooks HMAC-SHA256
Signature headersvix-signature
SecretPer webhook, whsec_...
RotationOld secret stays valid for 24 hours
BodyJSON
Event routingBody field type
RetriesAutomatic, schedule not published
Event logs90 days

For complete handlers covering all eight events, see the Nomod skill examples.

Events and payloads

Every event carries the full charge:

FieldMeaning
typeThe event name. Branch on this
eventIdId of this event. Use it to detect and ignore repeated deliveries
objectIdThe charge the event is about (ch_...)
dataThe full charge: total, currency, status, customer, items, fee, net, paymentMethod, refundTotal, refunds, referenceId, error and more

data.events[].type, for example completed, is the charge's own history, not the webhook event type. Amounts are decimal strings, so parse them with a decimal type rather than floating point. Nomod's payload example shows every field.

Nomod documents eight events, and you choose which ones each webhook receives:

EventTriggered WhenCommon Use Cases
charge.createdA payment object has been created and is awaiting customer actionTrack pending payments
charge.authorisedFunds are reserved on the customer's card and awaiting captureHold stock, capture later
charge.completedFunds are captured and settled to your balanceFulfil the order, reconcile
charge.failedThe payment was declined at authorisation or captureRead data.error, notify the customer
charge.cancelledThe authorisation was voided before captureRelease held stock
charge.refundedA full refund has been issuedRefund bookkeeping
charge.partially_refundedA partial refund has been issued; the remaining amount stays settledAdjust the order total
charge.dispute.createdA chargeback or dispute was opened by the customerGather evidence, flag the order

Only charge.completed has a published example. The envelope above applies to all eight, but Nomod doesn't show the other seven, so log real deliveries before relying on fields such as error or refunds.

Setting up Nomod webhooks

You need to be an Owner or Admin on the Nomod account; Managers and Staff don't see the Webhooks option.

  1. In the Nomod dashboard, go to Settings > Tools & customisations > Apps & APIs.
  2. Select Webhooks > Create webhook.
  3. Choose Nomod API or Nomod Hosted Checkout and tap Continue.
  4. Select the events and tap Continue. An event can be added to more than one webhook.
  5. Enter a name and the endpoint URL, then tap Create webhook. Delivery starts straight away.

Then open the webhook and copy the masked Signing secret into NOMOD_WEBHOOK_SECRET, keeping the whsec_ prefix:

NOMOD_WEBHOOK_SECRET=whsec_xxxxxxxxxxxxxxxxxxxxxxxx

To put Hookdeck in front, create a source with the Nomod type, paste the secret, and enter the source URL as the webhook's URL. During development, the Hookdeck CLI forwards it to your machine:

npx hookdeck-cli listen 3000 nomod --path /webhooks/nomod

To rotate the secret, select the webhook, tap the rotate icon next to Signing secret, and confirm with Generate. The new secret applies immediately and the old one keeps working for 24 hours, so svix-signature may carry two v1, entries during that time. Update your stored secret within the window.

Securing Nomod webhooks

Nomod's verification page spells out the construction: signedContent = svix-id + "." + svix-timestamp + "." + rawBody, HMAC-SHA256 with the base64 portion of the secret after whsec_, base64-encoded and compared with each space-delimited v1, entry. This function does that and also reads webhook-* header names:

const crypto = require('node:crypto');

function decodeBase64(value) {
  if (typeof value !== 'string' || !/^[A-Za-z0-9+/]+={0,2}$/.test(value)) return null;
  const bytes = Buffer.from(value, 'base64');
  return bytes.length > 0 && bytes.toString('base64').replace(/=+$/, '') === value.replace(/=+$/, '')
    ? bytes : null;
}

function verifySignature(rawBody, headers, secret) {
  if (!Buffer.isBuffer(rawBody) || typeof secret !== 'string' || !secret) return false;
  const prefix = headers['svix-id'] ? 'svix' : 'webhook';
  const id = headers[`${prefix}-id`];
  const timestamp = headers[`${prefix}-timestamp`];
  const signature = headers[`${prefix}-signature`];
  if (typeof id !== 'string' || !id || typeof timestamp !== 'string' ||
      !/^[0-9]+$/.test(timestamp) || typeof signature !== 'string') return false;
  const key = decodeBase64(secret.replace(/^whsec_/, ''));
  if (!key) return false;
  const message = Buffer.concat([Buffer.from(`${id}.${timestamp}.`, 'utf8'), rawBody]);
  const candidates = signature.split(/\s+/).filter((item) => item.startsWith('v1,')).map((item) => item.slice(3));
  const expected = crypto.createHmac('sha256', key).update(message).digest();
  return candidates.some((candidate) => {
    const received = decodeBase64(candidate);
    if (!received) return false;
    return received.length === expected.length && crypto.timingSafeEqual(received, expected);
  });
}

Nomod also tells receivers to reject requests more than five minutes old. Behind Hookdeck that rule would reject genuine payments, because Hookdeck has no timestamp tolerance and resends the original svix-timestamp when it retries or replays. The function above checks the signature only.

Nomod counts a slow response as a failure, so the receiver stores the charge event and returns at once:

const express = require('express');

// Returns the parsed body when it is a JSON object, otherwise null.
function parseJsonObject(rawBody) {
  try {
    const value = JSON.parse(rawBody.toString('utf8'));
    return value && typeof value === 'object' && !Array.isArray(value) ? value : null;
  } catch {
    return null;
  }
}

// chargeEvents.save(eventId, event) stores the event once and resolves when durable.
function createNomodReceiver(chargeEvents) {
  const app = express();
  app.post('/webhooks/nomod', express.raw({ type: '*/*' }), async (req, res) => {
    const secret = process.env.NOMOD_WEBHOOK_SECRET;
    if (!secret) return res.sendStatus(500);
    if (!verifySignature(req.body, req.headers, secret)) return res.sendStatus(401);

    const event = parseJsonObject(req.body);
    if (!event) return res.sendStatus(400); // signed, but not a JSON object
    try {
      await chargeEvents.save(event.eventId, event);
    } catch {
      return res.sendStatus(503);
    }
    return res.sendStatus(200);
  });
  return app;
}

When Nomod sends to a Hookdeck source, a request with a bad signature still gets 200. Hookdeck flags it as failed verification and doesn't forward it.

Make Nomod webhooks production-ready. Hookdeck Event Gateway verifies each charge event, keeps a searchable history beyond your own logs and retries your service on a schedule you can see.

Nomod webhook limitations and pain points

No published retry policy

The Problem: You can't tell how long Nomod will keep trying after an outage, or whether it ever gives up on an endpoint.

Why It Happens: Nomod says failed deliveries are retried automatically, but doesn't publish a schedule, a timeout or a disabling policy.

Workarounds:

  • Return 200 as soon as the event is stored.
  • Check Nomod's event logs, kept for 90 days, after an outage, and reconcile charges through the API.

How Hookdeck Can Help: Hookdeck acknowledges Nomod at once and retries your service on a policy you set and can see.

One integration type per webhook

The Problem: Hosted checkout payments don't reach the webhook you set up for API payments.

Why It Happens: Each webhook belongs to exactly one integration type.

Workarounds:

  • Create a webhook for each integration type you use.

How Hookdeck Can Help: Both webhooks can point at one Hookdeck source, so your service gets a single stream.

Seven events have no example

The Problem: Your charge.failed or charge.dispute.created handler is written blind.

Why It Happens: Nomod publishes a full example for charge.completed only.

Workarounds:

  • Log the first real delivery of each event type and check its fields before you depend on them.

How Hookdeck Can Help: Hookdeck stores every payload it receives, so you can inspect real charge.failed and dispute events as they happen.

The Problem: Replayed or delayed payment events fail verification.

Why It Happens: Nomod recommends the Svix library and a five-minute window, and the library enforces it.

Workarounds:

  • Use a signature-only check, like the one above, for anything that can be delayed or replayed.

How Hookdeck Can Help: Hookdeck checks the signature itself and has no timestamp window, so replays from its log reach your service.

Best practices

Fulfil on charge.completed

charge.authorised only reserves funds. Ship goods or grant access when funds are captured, which is charge.completed.

Release stock on cancel and failure

charge.cancelled means an authorisation was voided, and charge.failed a decline. Read data.error on failures before messaging the customer.

Act on disputes quickly

charge.dispute.created means a chargeback has been opened. Flag the order and start gathering evidence.

Deduplicate on eventId

Nomod tells you to use eventId to ignore repeated deliveries. Store it with the change it causes; see our walkthrough of webhook idempotency.

Conclusion

Nomod's webhooks follow each charge through capture, refund and dispute, with Svix signing and a 24-hour rotation window. Only one of the eight events has an example, so log real payloads early.

Hookdeck Event Gateway adds a retry policy you control and a record of every charge event. Get started with Hookdeck.


Gareth Wilson

Gareth Wilson

Product Marketing

Multi-time founding marketer, Gareth is PMM at Hookdeck and author of the newsletter, Community Inc.