> ## Documentation Index
> Fetch the complete documentation index at: https://docs.yativo.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Webhook Integration

> Receive real-time events from Yativo Crypto

Webhooks let Yativo push real-time event notifications to your server the moment something happens — a deposit confirms, a card transaction clears, a swap completes. This guide covers registering webhooks, verifying signatures, handling events correctly, and understanding the retry policy.

<Note>
  Test webhooks against the Sandbox at `https://crypto-sandbox.yativo.com/api/v1/`. Use a tool like [ngrok](https://ngrok.com) or [Hookdeck](https://hookdeck.com) to expose your local server during development.
</Note>

<Steps>
  <Step title="Create a webhook endpoint in your app">
    Your webhook handler must:

    * Accept `POST` requests at a public HTTPS URL
    * Parse the JSON body and verify the `X-Yativo-Signature` + `X-Yativo-Timestamp` headers before processing
    * Return `HTTP 200` within **5 seconds**
    * Process event logic asynchronously after returning 200

    <Tabs>
      <Tab title="TypeScript (Express)">
        ```typescript TypeScript theme={null}
        import express from "express";
        import crypto from "crypto";

        const app = express();

        // IMPORTANT: Use raw body middleware — not json() — for the webhook route
        app.post(
          "/webhooks/yativo",
          express.json(),
          async (req, res) => {
            // Verify signature (see Step 3)
            const signature = req.headers["x-yativo-signature"] as string;
            const timestamp = req.headers["x-yativo-timestamp"] as string;
            const isValid = verifyWebhookSignature(req.body, signature, timestamp, process.env.YATIVO_WEBHOOK_SECRET!);

            if (!isValid) {
              return res.status(401).json({ error: "Invalid signature" });
            }

            // Respond immediately
            res.sendStatus(200);

            // Process asynchronously
            setImmediate(() => handleEvent(req.body).catch(console.error));
          }
        );
        ```
      </Tab>

      <Tab title="Python (Flask)">
        ```python Python theme={null}
        from flask import Flask, request, jsonify
        import hmac
        import hashlib
        import json
        import threading
        import time

        app = Flask(__name__)

        @app.route("/webhooks/yativo", methods=["POST"])
        def webhook():
            signature = request.headers.get("X-Yativo-Signature", "")
            timestamp = request.headers.get("X-Yativo-Timestamp", "")
            payload = request.get_json(force=True)

            if not verify_webhook_signature(payload, signature, timestamp, os.environ["YATIVO_WEBHOOK_SECRET"]):
                return jsonify({"error": "Invalid signature"}), 401

            # Respond immediately, process in background
            thread = threading.Thread(target=handle_event, args=(payload,))
            thread.daemon = True
            thread.start()

            return "", 200
        ```
      </Tab>

      <Tab title="PHP">
        ```php PHP theme={null}
        <?php

        $payload = json_decode(file_get_contents("php://input"), true);
        $signature = $_SERVER["HTTP_X_YATIVO_SIGNATURE"] ?? "";
        $timestamp = $_SERVER["HTTP_X_YATIVO_TIMESTAMP"] ?? "";
        $secret = getenv("YATIVO_WEBHOOK_SECRET");

        if (!verifyWebhookSignature($payload, $signature, $timestamp, $secret)) {
            http_response_code(401);
            echo json_encode(["error" => "Invalid signature"]);
            exit;
        }

        // Acknowledge immediately
        http_response_code(200);
        echo "OK";

        // Flush output and continue processing
        if (function_exists("fastcgi_finish_request")) {
            fastcgi_finish_request();
        }

        handleEvent($payload);
        ```
      </Tab>
    </Tabs>
  </Step>

  <Step title="Register the webhook with Yativo">
    Register your endpoint and select which event types you want to receive.

    <CodeGroup>
      ```bash cURL theme={null}
      curl -X POST https://crypto-api.yativo.com/api/v1/webhook/create-webhook \
        -H "Authorization: Bearer eyJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJ1c3JfMDFIWVo..." \
        -H "Content-Type: application/json" \
        -d '{
          "url": "https://api.yourapp.com/webhooks/yativo",
          "events": [
            "deposit.detected",
            "deposit.confirmed",
            "transaction.failed",
            "transaction.authorized",
            "transaction.settled",
            "customer.funded"
          ],
          "description": "Production webhook"
        }'
      ```

      ```typescript TypeScript theme={null}
      import { YativoCrypto } from "@yativo/crypto-sdk";

      const client = new YativoCrypto({
        apiKey: process.env.YATIVO_API_KEY!,
        baseUrl: "https://crypto-api.yativo.com/api/v1/",
      });

      const webhook = await client.webhooks.create({
        url: "https://api.yourapp.com/webhooks/yativo",
        events: [
          "deposit.detected",
          "deposit.confirmed",
          "transaction.failed",
          "transaction.authorized",
          "transaction.settled",
          "customer.funded",
        ],
        description: "Production webhook",
      });

      console.log(webhook.data.secret); // whsec_a8f3c2e1d4b7f9e2c5a3b6d8f1e4c7a2
      ```

      ```python Python theme={null}
      webhook = client.webhooks.create(
          url="https://api.yourapp.com/webhooks/yativo",
          events=[
              "deposit.detected",
              "deposit.confirmed",
              "transaction.failed",
              "transaction.authorized",
              "transaction.settled",
              "customer.funded",
          ],
          description="Production webhook",
      )

      print(webhook["data"]["secret"])  # whsec_a8f3c2e1d4b7f9e2c5a3b6d8f1e4c7a2
      ```
    </CodeGroup>

    **Response:**

    ```json theme={null}
    {
      "status": true,
      "message": "Webhook created successfully",
      "data": {
        "webhook_id": "664abc123def456789000001",
        "url": "https://api.yourapp.com/webhooks/yativo",
        "events": ["deposit.detected", "deposit.confirmed", "transaction.failed", "transaction.authorized", "transaction.settled", "customer.funded"],
        "secret": "whsec_a8f3c2e1d4b7f9e2c5a3b6d8f1e4c7a2b5d8e1f4",
        "created_at": "2026-03-26T15:00:00Z"
      }
    }
    ```

    <Warning>
      Store the `secret` securely in an environment variable or secrets manager. It can be retrieved later via `GET /v1/yativo-card/webhooks/:webhookId`, but treat it like a password.
    </Warning>
  </Step>

  <Step title="Verify webhook signatures">
    Every delivery includes two headers:

    | Header               | Value                    |
    | -------------------- | ------------------------ |
    | `X-Yativo-Signature` | `sha256=<hmac>`          |
    | `X-Yativo-Timestamp` | Unix timestamp (seconds) |

    The HMAC is computed over `"${timestamp}.${JSON.stringify(payload)}"`. **Always verify this signature before processing the event.**

    <Tabs>
      <Tab title="TypeScript">
        ```typescript TypeScript theme={null}
        import crypto from "crypto";

        function verifyWebhookSignature(
          payload: object,
          signatureHeader: string,
          timestamp: string,
          secret: string
        ): boolean {
          if (!signatureHeader || !timestamp) return false;

          // Reject replays older than 5 minutes
          const ts = parseInt(timestamp, 10);
          if (Math.abs(Date.now() / 1000 - ts) > 300) return false;

          const signedPayload = `${timestamp}.${JSON.stringify(payload)}`;
          const expected = "sha256=" + crypto
            .createHmac("sha256", secret)
            .update(signedPayload)
            .digest("hex");

          try {
            return crypto.timingSafeEqual(
              Buffer.from(signatureHeader),
              Buffer.from(expected)
            );
          } catch {
            return false;
          }
        }
        ```
      </Tab>

      <Tab title="Python">
        ```python Python theme={null}
        import hmac
        import hashlib
        import json
        import time

        def verify_webhook_signature(payload: dict, signature_header: str, timestamp: str, secret: str) -> bool:
            if not signature_header or not timestamp:
                return False

            # Reject replays older than 5 minutes
            if abs(time.time() - int(timestamp)) > 300:
                return False

            signed_payload = f"{timestamp}.{json.dumps(payload, separators=(',', ':'))}"
            expected = "sha256=" + hmac.new(
                secret.encode("utf-8"),
                signed_payload.encode("utf-8"),
                hashlib.sha256,
            ).hexdigest()

            return hmac.compare_digest(signature_header, expected)
        ```
      </Tab>

      <Tab title="PHP">
        ```php PHP theme={null}
        function verifyWebhookSignature(array $payload, string $signatureHeader, string $timestamp, string $secret): bool {
            if (empty($signatureHeader) || empty($timestamp)) {
                return false;
            }

            // Reject replays older than 5 minutes
            if (abs(time() - intval($timestamp)) > 300) {
                return false;
            }

            $signedPayload = $timestamp . '.' . json_encode($payload);
            $expected = 'sha256=' . hash_hmac('sha256', $signedPayload, $secret);

            return hash_equals($expected, $signatureHeader);
        }
        ```
      </Tab>
    </Tabs>

    <Warning>
      Always use a timing-safe comparison function (`timingSafeEqual`, `hmac.compare_digest`, `hash_equals`). Using a regular string equality check (`===`) exposes you to timing attacks.
    </Warning>
  </Step>

  <Step title="Handle events idempotently">
    The same event may be delivered more than once (see Retry Policy below). Your handler must be idempotent — processing the same event twice should produce the same result as processing it once.

    ```typescript TypeScript theme={null}
    async function handleEvent(event: WebhookEvent): Promise<void> {
      // 1. Check if already processed
      const processed = await db.webhookEvents.findById(event.id);
      if (processed) {
        console.log(`Event ${event.id} already processed — skipping`);
        return;
      }

      // 2. Process the event
      switch (event.type) {
        case "deposit.confirmed":
          await handleDepositConfirmed(event.data);
          break;
        case "transaction.failed":
          await handleTransactionFailed(event.data);
          break;
        case "transaction.authorized":
          await handleCardAuthorized(event.data);
          break;
        case "transaction.settled":
          await handleCardSettled(event.data);
          break;
        case "customer.funded":
          await handleCustomerFunded(event.data);
          break;
        default:
          console.log(`Unhandled event type: ${event.type}`);
      }

      // 3. Mark as processed
      await db.webhookEvents.insert({
        id: event.id,
        type: event.type,
        processedAt: new Date(),
      });
    }
    ```

    <Tip>
      Store processed event IDs in a database table with a unique index on `id`. Use the insert as an upsert or check-then-insert within a transaction to prevent race conditions when events arrive in parallel.
    </Tip>
  </Step>

  <Step title="Return 200 quickly and process asynchronously">
    Yativo waits up to **5 seconds** for an HTTP 200 response. If your server does not respond in time, the delivery is treated as failed and will be retried.

    **Pattern:** Respond 200 first, then process.

    ```typescript TypeScript theme={null}
    app.post("/webhooks/yativo", express.json(), async (req, res) => {
      // Signature verification is fast — do it synchronously
      const sig = req.headers["x-yativo-signature"] as string;
      const ts = req.headers["x-yativo-timestamp"] as string;
      if (!verifyWebhookSignature(req.body, sig, ts, secret)) {
        return res.status(401).send();
      }

      // Acknowledge receipt immediately
      res.sendStatus(200);

      // Enqueue for async processing (use a job queue in production)
      await jobQueue.enqueue("process_webhook_event", req.body);
    });
    ```

    In production, use a job queue (e.g., BullMQ, Celery, SQS) rather than `setImmediate` so events survive server restarts.
  </Step>

  <Step title="Understand the retry policy">
    If your endpoint returns a non-2xx status code, times out, or is unreachable, Yativo retries the delivery with exponential backoff:

    | Attempt     | Delay after previous |
    | ----------- | -------------------- |
    | 1 (initial) | —                    |
    | 2           | 1 minute             |
    | 3           | 5 minutes            |
    | 4           | 30 minutes           |
    | 5           | 2 hours              |
    | 6           | 6 hours              |
    | 7           | 24 hours             |

    After 7 failed attempts, the event is marked as **permanently failed** and no further retries are made. You can manually replay failed deliveries via `POST /v1/yativo-card/webhooks/deliveries/:deliveryId/retry`.

    <Note>
      Because retries are possible, your handlers must be idempotent (see Step 4). A 200 response stops retries — never return 200 if you have not processed (or enqueued) the event.
    </Note>
  </Step>
</Steps>

## Event Types Reference

### Deposit Events

| Event               | Description                                           |
| ------------------- | ----------------------------------------------------- |
| `deposit.detected`  | A deposit transaction was seen on-chain (unconfirmed) |
| `deposit.confirmed` | Deposit has sufficient confirmations — safe to credit |

### Transaction Events

| Event                          | Description                                                                         |
| ------------------------------ | ----------------------------------------------------------------------------------- |
| `transaction.initiated`        | Outbound transaction created and queued                                             |
| `transaction.submitted`        | Transaction broadcast to the network                                                |
| `transaction.failed`           | Transaction failed; funds returned to account                                       |
| `transaction.{type}.completed` | A transaction of the given type completed (e.g. `transaction.withdrawal.completed`) |

### Card Events

Card issuer events and general crypto events share the same webhook service. Register at `POST /v1/webhook/create-webhook` and include the relevant card event slugs in your `events` array.

| Event                           | Description                                     |
| ------------------------------- | ----------------------------------------------- |
| `card.created`                  | A card was issued                               |
| `card.activated`                | A card was activated (physical cards)           |
| `card.frozen`                   | A card was frozen                               |
| `card.unfrozen`                 | A frozen card was re-enabled                    |
| `card.lost`                     | A card was marked lost                          |
| `card.stolen`                   | A card was reported stolen                      |
| `card.voided`                   | A card was voided                               |
| `card.cancelled`                | A card was cancelled                            |
| `card.deactivated`              | A card was permanently deactivated              |
| `transaction.authorized`        | A card transaction was authorized               |
| `transaction.declined`          | A card transaction was declined                 |
| `transaction.settled`           | A card transaction settled                      |
| `transaction.reversed`          | A card transaction was reversed                 |
| `transaction.refund.created`    | A refund was initiated                          |
| `customer.funded`               | A customer's card wallet was credited           |
| `customer.funding.failed`       | A funding transfer failed                       |
| `customer.balance.updated`      | A customer's card balance changed               |
| `master_wallet.deposit`         | Funds arrived in the card program master wallet |
| `master_wallet.swap`            | A swap executed in the master wallet            |
| `master_wallet.customer_funded` | Master wallet funds allocated to a customer     |

See [Card Issuer Webhook Events](/api-reference/issuer/webhooks) for full payload examples.

### IBAN Events

| Event                    | Description                                 |
| ------------------------ | ------------------------------------------- |
| `iban.activated`         | IBAN account is active and ready to receive |
| `iban.transfer.received` | Incoming bank transfer detected             |

### KYC Events

| Event                | Description                   |
| -------------------- | ----------------------------- |
| `kyc.submitted`      | A KYC submission was received |
| `kyc.approved`       | KYC was approved              |
| `kyc.rejected`       | KYC was rejected              |
| `kyc.pending_review` | KYC is awaiting manual review |

## Webhook Event Envelope

All events share a common envelope. The request body is JSON; delivery metadata is carried in HTTP headers.

**Request body:**

```json theme={null}
{
  "id": "evt_1716652800_abc123xyz",
  "type": "deposit.confirmed",
  "created_at": "2026-03-26T15:30:00Z",
  "data": {
    // Event-specific payload
  }
}
```

**Request headers:**

| Header                 | Value                                             |
| ---------------------- | ------------------------------------------------- |
| `X-Yativo-Signature`   | `sha256=<hmac>` — use this to verify authenticity |
| `X-Yativo-Timestamp`   | Unix timestamp (seconds) — included in the HMAC   |
| `X-Yativo-Event`       | Event type string (e.g. `deposit.confirmed`)      |
| `X-Yativo-Delivery-Id` | Unique delivery ID for this attempt               |

**Body fields:**

| Field        | Type     | Description                           |
| ------------ | -------- | ------------------------------------- |
| `id`         | string   | Unique event ID — use for idempotency |
| `type`       | string   | Event type                            |
| `created_at` | ISO 8601 | When the event was generated          |
| `data`       | object   | Event-specific payload                |

## Managing Webhooks

```bash theme={null}
# List all webhooks
GET /v1/yativo-card/webhooks

# Get a single webhook (includes secret)
GET /v1/yativo-card/webhooks/:webhookId

# Update events or URL
PUT /v1/yativo-card/webhooks/:webhookId
# body: { "events": ["deposit.confirmed", "transaction.failed"], "enabled": true }

# Pause delivery without deleting
PUT /v1/yativo-card/webhooks/:webhookId
# body: { "enabled": false }

# Delete a webhook
DELETE /v1/yativo-card/webhooks/:webhookId

# Rotate the signing secret
POST /v1/yativo-card/webhooks/:webhookId/rotate-secret

# View delivery history
GET /v1/yativo-card/webhooks/:webhookId/deliveries?limit=20&status=failed

# Retry a failed delivery
POST /v1/yativo-card/webhooks/deliveries/:deliveryId/retry
```
