Agent skill

Ziina Webhooks Skill

Receive and verify Ziina webhooks. Use when setting up Ziina webhook handlers, debugging X-Hmac-Signature verification, or handling payment_intent.status.updated and refund.status.updated events from the Ziina payments API.

Install this skill

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


Ziina is a UAE payments app with a payment API. Each account has one webhook URL, set with POST /webhook. Ziina sends a POST to it whenever a payment intent or refund changes status.

When to Use This Skill

  • How do I receive Ziina webhooks?
  • How do I verify the Ziina X-Hmac-Signature header?
  • How do I handle payment_intent.status.updated and refund.status.updated events?
  • Why is my Ziina webhook signature verification failing, or why are deliveries unsigned?

Setup note

Set a secret when you register the webhook through the Ziina API. Without one, deliveries are unsigned and fail verification.

Verification (core)

When a secret was given to POST /webhook, Ziina sends X-Hmac-Signature: the bare lowercase hex HMAC-SHA256 of the raw request body, keyed with that secret's UTF-8 bytes. No prefix, and no timestamp or id is signed. This is not Standard Webhooks.

const crypto = require('crypto');

function verifyZiinaWebhook(rawBody, signatureHeader, secret) {
  // Validate format first: exactly 64 hex characters
  if (!secret || typeof signatureHeader !== 'string' || !/^[0-9a-fA-F]{64}$/.test(signatureHeader)) {
    return false;
  }
  const expected = crypto.createHmac('sha256', secret).update(rawBody).digest(); // rawBody is a Buffer
  const received = Buffer.from(signatureHeader, 'hex');
  return received.length === expected.length && crypto.timingSafeEqual(received, expected);
}

Reject a missing, empty, malformed or mismatching header with 401. Parse the JSON only after the check.

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

Event Types

Dispatch on the top-level event field. The new state is in data.status; the event name does not change with the status.

EventTriggered When
payment_intent.status.updatedA payment intent changes status (e.g. completed)
refund.status.updatedA refund changes status

Ziina retries a non-OK response 3 times; no timeout or spacing is documented. Return 200 quickly and handle duplicates idempotently.

Environment Variables

ZIINA_WEBHOOK_SECRET=your_chosen_secret   # The `secret` you passed to POST /webhook

Local Development

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

Reference Materials


Repository

hookdeck/webhook-skills

v0.1.0 · MIT · Updated Nov 13, 2026

View on GitHub →