# 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.

```javascript
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/](https://github.com/hookdeck/webhook-skills/tree/main/skills/ziina-webhooks/examples/express/), [examples/nextjs/](https://github.com/hookdeck/webhook-skills/tree/main/skills/ziina-webhooks/examples/nextjs/), [examples/fastapi/](https://github.com/hookdeck/webhook-skills/tree/main/skills/ziina-webhooks/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.

| Event | Triggered When |
| --- | --- |
| `payment_intent.status.updated` | A payment intent changes status (e.g. `completed`) |
| `refund.status.updated` | A 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

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

```

## Local Development

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

```

## Reference Materials

* [references/overview.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/ziina-webhooks/references/overview.md) - What Ziina webhooks are, events, payload
* [references/setup.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/ziina-webhooks/references/setup.md) - Registering the webhook and secret
* [references/verification.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/ziina-webhooks/references/verification.md) - Signature verification details and gotchas