Agent skill

Nomod Webhooks Skill

Receive and verify Nomod webhooks (UAE payment links and card acceptance, delivered via Svix). Use when setting up Nomod webhook handlers, debugging svix-signature verification, or handling charge events like charge.completed, charge.authorised, charge.failed, charge.refunded, and charge.dispute.created.

Install this skill

npx skills add hookdeck/webhook-skills --skill nomod-webhooks


Nomod is a UAE payment-links and card-acceptance platform for SMEs. It sends charge lifecycle webhooks for the Nomod API and Nomod Hosted Checkout integration types, delivered through Svix, which implements Standard Webhooks. Every delivery is an HTTP POST with a JSON body and three signature headers: svix-id, svix-timestamp, and svix-signature. The body is {type, eventId, objectId, data}: type is the event name, eventId deduplicates repeated deliveries, objectId is the charge (ch_...) and data is the full charge.

When to Use This Skill

  • How do I receive Nomod webhooks?
  • How do I verify Nomod webhook signatures (svix-signature)?
  • How do I handle charge.completed, charge.failed or charge.refunded events?
  • How do I react to charge.authorised or charge.dispute.created?
  • Why is my Nomod webhook signature verification failing, or rejecting Hookdeck retries?

Verification (core)

Signed content is {svix-id}.{svix-timestamp}.{raw body}. Svix signs it with HMAC-SHA256 using the base64-decoded secret (the part after whsec_), base64-encodes the result, and sends one or more space-separated v1,<signature> entries. Use the official svix library, which compares in constant time. Read the svix-* headers, and when svix-id is absent fall back to the webhook-* headers (webhook-id, webhook-timestamp, webhook-signature), as Hookdeck does.

import { Webhook } from 'svix'; // svix 2.x is ESM-only

const prefix = req.headers['svix-id'] ? 'svix' : 'webhook';
const id = req.headers[`${prefix}-id`];
const timestamp = req.headers[`${prefix}-timestamp`];
const signature = req.headers[`${prefix}-signature`];
// rawBody: the raw Buffer/string, never re-serialized JSON
new Webhook(process.env.NOMOD_WEBHOOK_SECRET).verify(rawBody, {
  'svix-id': id, 'svix-timestamp': timestamp, 'svix-signature': signature,
  'webhook-id': id, 'webhook-timestamp': timestamp, 'webhook-signature': signature,
}); // throws on a bad signature or a timestamp outside the window
const event = JSON.parse(rawBody.toString('utf8')); // parse only after verify passes
from svix.webhooks import Webhook, WebhookVerificationError

wh = Webhook(os.environ["NOMOD_WEBHOOK_SECRET"])  # "whsec_..."
wh.verify(raw_body, {"svix-id": id, "svix-timestamp": ts, "svix-signature": sig,
                     "webhook-id": id, "webhook-timestamp": ts, "webhook-signature": sig})

For complete handlers with tests, see examples/express/, examples/nextjs/, examples/fastapi/.

The signing secret is per webhook, starts with whsec_, and is in the dashboard under Settings > Tools & customisations > Apps & APIs > Webhooks > your webhook > Signing secret. Keep the full whsec_... value in NOMOD_WEBHOOK_SECRET. Handlers fail closed: with the variable unset they return 500 instead of accepting anything. Missing headers or a bad signature get a 400. Nomod tells receivers to reject timestamps more than five minutes from now.

Hookdeck retries and replays hit the 5-minute window

The svix library always enforces a fixed 5-minute timestamp window, and it can't be configured. Hookdeck retries and replays resend the original svix-timestamp, so the library rejects any retry or replay that happens more than 5 minutes after the original delivery. If you verify behind Hookdeck (or replay old events), use the manual verifier in references/verification.md, where the window can be dropped or widened. Deduplicate on eventId (or svix-id) instead of relying on the timestamp.

Common Event Types

Eight events are documented (the list may not be exhaustive). The name is type in the JSON body (not data.events[].type, which is the charge's own history). The examples handle all of these:

EventTriggered When
charge.createdA payment object is created and awaiting customer action
charge.authorisedFunds are reserved on the customer's card, awaiting capture
charge.completedFunds are captured and settled to your balance
charge.failedThe payment was declined at authorisation or capture
charge.cancelledThe authorisation was voided before capture
charge.refundedA full refund was issued
charge.partially_refundedA partial refund was issued; the remaining amount stays settled
charge.dispute.createdA chargeback or dispute was opened by the customer

See references/overview.md for the payload and delivery rules.

Environment Variables

# Signing secret of the webhook (Svix format, starts with whsec_)
NOMOD_WEBHOOK_SECRET=whsec_xxxxx

Local Development

# Start tunnel (no account needed)
npx hookdeck-cli listen 3000 nomod --path /webhooks/nomod

Create the webhook in Settings > Tools & customisations > Apps & APIs > Webhooks (Owners and Admins only), pick an integration type and the events, and use the tunnel URL. Nomod sends no challenge. Copy the Signing secret into .env. See references/setup.md.

Reference Materials


Repository

hookdeck/webhook-skills

v0.1.0 · MIT · Updated Nov 12, 2026

View on GitHub →