Guide to Nomod Webhooks: Features and Best Practices
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.
| Aspect | Value |
|---|---|
| Method | POST, JSON body (Nomod prints no Content-Type, so read the raw bytes whatever it is) |
| Signature headers | svix-id, svix-timestamp, svix-signature (Standard Webhooks) |
| Success | 200 OK, returned as quickly as possible. A slow response counts as a failure and is retried |
| Deduplication | eventId, to ignore repeated deliveries |
| Event logs | Kept 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
| Feature | Details |
|---|---|
| Configuration | Dashboard, Owners and Admins only |
| Scope | One integration type per webhook, with chosen events |
| Signature | Standard Webhooks HMAC-SHA256 |
| Signature header | svix-signature |
| Secret | Per webhook, whsec_... |
| Rotation | Old secret stays valid for 24 hours |
| Body | JSON |
| Event routing | Body field type |
| Retries | Automatic, schedule not published |
| Event logs | 90 days |
For complete handlers covering all eight events, see the Nomod skill examples.
Events and payloads
Every event carries the full charge:
| Field | Meaning |
|---|---|
type | The event name. Branch on this |
eventId | Id of this event. Use it to detect and ignore repeated deliveries |
objectId | The charge the event is about (ch_...) |
data | The 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:
| Event | Triggered When | Common Use Cases |
|---|---|---|
charge.created | A payment object has been created and is awaiting customer action | Track pending payments |
charge.authorised | Funds are reserved on the customer's card and awaiting capture | Hold stock, capture later |
charge.completed | Funds are captured and settled to your balance | Fulfil the order, reconcile |
charge.failed | The payment was declined at authorisation or capture | Read data.error, notify the customer |
charge.cancelled | The authorisation was voided before capture | Release held stock |
charge.refunded | A full refund has been issued | Refund bookkeeping |
charge.partially_ | A partial refund has been issued; the remaining amount stays settled | Adjust the order total |
charge.dispute.created | A chargeback or dispute was opened by the customer | Gather 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.
- In the Nomod dashboard, go to Settings > Tools & customisations > Apps & APIs.
- Select Webhooks > Create webhook.
- Choose Nomod API or Nomod Hosted Checkout and tap Continue.
- Select the events and tap Continue. An event can be added to more than one webhook.
- 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
200as 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.
Five-minute window in the recommended library
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.