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

# Card Issuer Webhooks

> Receive real-time event notifications across the full card issuer lifecycle — from master wallet deposits to card transactions

Card issuer webhooks let Yativo push event notifications to your server the moment something happens in your program — a deposit settles, a customer's card is funded, a transaction is authorized, or a card is frozen. This guide covers every step from registering a URL to handling the complete event lifecycle.

<Note>
  Card issuer events and general crypto events (deposits, swaps, transactions) are delivered through the same webhook service. Register once at `POST /v1/webhook/create-webhook` and subscribe to any combination of event types.
</Note>

***

## How It Works

Your server exposes a public HTTPS URL. Yativo sends a signed `POST` request to that URL each time an event occurs in your issuer program. Your server verifies the signature, acknowledges receipt with HTTP `200`, and processes the event.

```
Yativo event occurs
       │
       ▼
POST https://your-app.com/webhooks/yativo
       │
       ▼
Verify X-Yativo-Signature header
       │
       ▼
Return HTTP 200 immediately
       │
       ▼
Process event asynchronously
```

***

## Step 1 — Build Your Webhook Endpoint

Your endpoint must:

* Accept `POST` requests at a public HTTPS URL
* Capture the **raw request body** before JSON-parsing it (required for signature verification)
* Return HTTP `2xx` within **30 seconds**
* Process event logic asynchronously after returning 200

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

    const app = express();

    // Use express.raw() — not express.json() — so you have the raw body for signature verification
    app.post(
      "/webhooks/yativo/card-issuer",
      express.raw({ type: "application/json" }),
      async (req, res) => {
        const signature = req.headers["x-yativo-signature"] as string;
        const timestamp = req.headers["x-yativo-timestamp"] as string;
        const secret = process.env.YATIVO_WEBHOOK_SECRET!;

        if (!verifySignature(req.body, signature, timestamp, secret)) {
          return res.status(401).json({ error: "Invalid signature" });
        }

        // Acknowledge immediately — Yativo waits up to 30 seconds
        res.sendStatus(200);

        // Parse and dispatch asynchronously
        const event = JSON.parse(req.body.toString());
        setImmediate(() => handleCardIssuerEvent(event).catch(console.error));
      }
    );

    function verifySignature(
      rawBody: Buffer,
      signature: string,
      timestamp: string,
      secret: string
    ): boolean {
      if (!signature || !timestamp) return false;
      const signed = `${timestamp}.${rawBody.toString("utf8")}`;
      const expected =
        "sha256=" +
        crypto.createHmac("sha256", secret).update(signed).digest("hex");
      try {
        return crypto.timingSafeEqual(
          Buffer.from(signature),
          Buffer.from(expected)
        );
      } catch {
        return false;
      }
    }

    async function handleCardIssuerEvent(event: any) {
      // Use event.id as an idempotency key — the same event may be delivered more than once
      const alreadyProcessed = await db.webhookEvents.findById(event.id);
      if (alreadyProcessed) return;

      switch (event.type) {
        case "master_wallet.deposit":
          if (event.data.status === "settled") {
            // withdrawal_id present → this credit is from a card withdrawal settling
            await handleMasterWalletDeposit(event.data);
          }
          break;
        case "customer.funded":
          await handleCustomerFunded(event.data);
          break;
        case "customer.funding.failed":
          await handleCustomerFundingFailed(event.data);
          break;
        case "transaction.authorized":
          await handleTransactionAuthorized(event.data);
          break;
        case "transaction.settled":
          await handleTransactionSettled(event.data);
          break;
        case "transaction.declined":
          await handleTransactionDeclined(event.data);
          break;
        case "card.frozen":
        case "card.unfrozen":
        case "card.voided":
          await handleCardStatusChange(event.type, event.data);
          break;
        default:
          console.log(`Unhandled event type: ${event.type}`);
      }

      await db.webhookEvents.insert({ id: event.id, type: event.type, processedAt: new Date() });
    }
    ```
  </Tab>

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

    app = Flask(__name__)

    @app.route("/webhooks/yativo/card-issuer", methods=["POST"])
    def card_issuer_webhook():
        raw_body = request.get_data()
        signature = request.headers.get("X-Yativo-Signature", "")
        timestamp = request.headers.get("X-Yativo-Timestamp", "")
        secret = os.environ["YATIVO_WEBHOOK_SECRET"]

        if not verify_signature(raw_body, signature, timestamp, secret):
            return jsonify({"error": "Invalid signature"}), 401

        event = request.get_json(force=True)

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

        return "", 200


    def verify_signature(raw_body: bytes, signature: str, timestamp: str, secret: str) -> bool:
        if not signature or not timestamp:
            return False
        signed = f"{timestamp}.{raw_body.decode('utf-8')}"
        expected = "sha256=" + hmac.new(
            secret.encode("utf-8"), signed.encode("utf-8"), hashlib.sha256
        ).hexdigest()
        return hmac.compare_digest(signature, expected)
    ```
  </Tab>

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

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

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

    // Acknowledge immediately
    http_response_code(200);
    echo "OK";
    if (function_exists("fastcgi_finish_request")) {
        fastcgi_finish_request();
    }

    $event = json_decode($rawBody, true);
    handleEvent($event);


    function verifySignature(string $rawBody, string $signature, string $timestamp, string $secret): bool {
        if (empty($signature) || empty($timestamp)) return false;
        $signed   = "{$timestamp}.{$rawBody}";
        $expected = "sha256=" . hash_hmac("sha256", $signed, $secret);
        return hash_equals($expected, $signature);
    }
    ```
  </Tab>
</Tabs>

<Warning>
  Always use a timing-safe comparison (`timingSafeEqual`, `hmac.compare_digest`, `hash_equals`). A regular `===` check is vulnerable to timing attacks.
</Warning>

***

## Step 2 — Register Your Webhook

Register your endpoint at the unified webhook endpoint. You can subscribe to card issuer events, general crypto events, or any mix.

<CodeGroup>
  ```bash cURL theme={null}
  curl -X POST 'https://crypto-api.yativo.com/api/v1/webhook/create-webhook' \
    -H 'Authorization: Bearer YOUR_TOKEN' \
    -H 'Content-Type: application/json' \
    -d '{
      "url": "https://api.yourapp.com/webhooks/yativo",
      "events": [
        "master_wallet.deposit",
        "master_wallet.swap",
        "master_wallet.customer_funded",
        "master_wallet.withdrawal",
        "customer.funded",
        "customer.funding.failed",
        "customer.balance.updated",
        "wallet.deposit.confirmed",
        "card.created",
        "card.activated",
        "card.frozen",
        "card.unfrozen",
        "card.voided",
        "card.lost",
        "card.stolen",
        "card.cancelled",
        "card.deactivated",
        "transaction.authorized",
        "transaction.settled",
        "transaction.declined",
        "transaction.reversed",
        "transaction.refund.created"
      ],
      "description": "Production card issuer webhook"
    }'
  ```

  ```typescript TypeScript theme={null}
  const response = await fetch(
    "https://crypto-api.yativo.com/api/v1/webhook/create-webhook",
    {
      method: "POST",
      headers: {
        Authorization: `Bearer ${process.env.YATIVO_TOKEN}`,
        "Content-Type": "application/json",
      },
      body: JSON.stringify({
        url: "https://api.yourapp.com/webhooks/yativo",
        events: ["*"], // Subscribe to all events
        description: "Production card issuer webhook",
      }),
    }
  );

  const { data } = await response.json();

  console.log("Webhook ID:", data.webhook_id);
  console.log("Secret:", data.secret); // Store securely — also retrievable via GET /v1/yativo-card/webhooks/:webhookId
  ```
</CodeGroup>

```json Response theme={null}
{
  "status": true,
  "message": "Webhook created successfully",
  "data": {
    "webhook_id": "68241abc2f3d4e5f6a7b8c9d",
    "url": "https://api.yourapp.com/webhooks/yativo",
    "events": ["master_wallet.deposit", "customer.funded", "..."],
    "secret": "whsec_a8f3c2e1d4b7f9e2c5a3b6d8f1e4c7a2b5d8e1f4c7a2b5d8",
    "created_at": "2026-05-12T14:00:00.000Z"
  }
}
```

<Warning>
  The `secret` is included in the creation response and is also retrievable later via `GET /v1/yativo-card/webhooks/:webhookId`. Store it in a secure environment variable or secrets manager (e.g., AWS Secrets Manager, HashiCorp Vault). If you need to invalidate a compromised secret, rotate it via `POST /v1/yativo-card/webhooks/:webhookId/rotate-secret`.
</Warning>

Pass `"events": ["*"]` to receive every event type. You can narrow the list later via `PUT /v1/yativo-card/webhooks/:webhookId`.

***

## Step 3 — Verify Signatures

Every webhook POST includes four security headers:

| Header                 | Description                                              |
| ---------------------- | -------------------------------------------------------- |
| `X-Yativo-Signature`   | `sha256=<HMAC-SHA256 hex>` of the signed string          |
| `X-Yativo-Timestamp`   | Unix timestamp (seconds) when the event was dispatched   |
| `X-Yativo-Event`       | The event type (e.g. `customer.funded`)                  |
| `X-Yativo-Delivery-Id` | Unique delivery ID — used to retry individual deliveries |

The signature is computed as:

```
HMAC-SHA256(secret, "<timestamp>.<raw JSON body>")
```

The `<raw JSON body>` is the exact bytes received — not re-serialized. This is why you must capture the raw body before JSON-parsing.

**Replay attack protection:** Also check that the timestamp is within 5 minutes of your server clock. Discard requests that are older.

```typescript theme={null}
function verifySignature(
  rawBody: Buffer,
  signature: string,
  timestamp: string,
  secret: string
): boolean {
  if (!signature || !timestamp) return false;

  // Guard against replay attacks
  const age = Math.abs(Date.now() / 1000 - parseInt(timestamp, 10));
  if (age > 300) return false; // older than 5 minutes

  const signed = `${timestamp}.${rawBody.toString("utf8")}`;
  const expected =
    "sha256=" +
    crypto.createHmac("sha256", secret).update(signed).digest("hex");

  try {
    return crypto.timingSafeEqual(
      Buffer.from(signature),
      Buffer.from(expected)
    );
  } catch {
    return false;
  }
}
```

***

## The Event Lifecycle

The following diagram shows the full lifecycle of a card issuer program, and where webhook events fire at each stage.

### Master wallet deposit → settled

When you send funds to your master wallet deposit address, Yativo fires `master_wallet.deposit` events as the deposit progresses. The number of events depends on the deposit path:

* **Two-step deposits** (funds routed through an intermediate settlement): a `processing` event fires first when the deposit is detected, followed by a `settled` event when the funds land in your spendable balance.
* **One-step deposits** (funds that settle directly): only a single `settled` event fires — there is no preceding `processing` event.

```
You send funds to your deposit address
           │
           ▼  (on detection — two-step deposits only)
  ┌────────────────────────────────────┐
  │  master_wallet.deposit             │
  │  status: "processing"             │
  └────────────────────────────────────┘
           │
           ▼  (once funds are spendable)
  ┌────────────────────────────────────┐
  │  master_wallet.deposit             │
  │  status: "settled"                │
  │  settled_amount: 499.75           │
  └────────────────────────────────────┘
           │
           ▼
  Master wallet balance updated
```

<Note>
  Always act on `status: "settled"` — this is the authoritative confirmation that funds are available. If you receive only a `settled` event with no prior `processing` event, that is expected for one-step deposit paths.
</Note>

### Fund customer card

When you call `POST /v1/card-issuer/fund-customer`, two events fire in tandem: `master_wallet.customer_funded` (your master wallet was debited) and `customer.funded` (the customer's card wallet was credited).

```
POST /v1/card-issuer/fund-customer
           │
           ▼
  ┌────────────────────────────────────┐
  │  master_wallet.customer_funded     │
  │  Your master wallet debited       │
  └────────────────────────────────────┘
           │
  ┌────────────────────────────────────┐
  │  customer.funded                   │
  │  Customer card wallet credited    │
  └────────────────────────────────────┘
           │
  ┌────────────────────────────────────┐
  │  customer.balance.updated          │
  │  New card balance snapshot        │
  └────────────────────────────────────┘
```

If the funding transfer fails (e.g. network error or insufficient master wallet balance), only `customer.funding.failed` fires.

### Card transaction lifecycle

When your customer uses their card, events fire at each stage of the authorization → settlement cycle.

```
Customer taps card at merchant
           │
           ▼
  ┌────────────────────────────────────┐
  │  transaction.authorized            │
  │  Funds reserved (hold placed)     │
  └────────────────────────────────────┘
           │
  ┌────────────────────────────────────┐
  │  customer.balance.updated          │
  │  available_balance reduced        │
  └────────────────────────────────────┘
           │
           ▼  (merchant batch clearing, typically same day)
  ┌────────────────────────────────────┐
  │  transaction.settled               │
  │  Hold released, charge finalized  │
  └────────────────────────────────────┘
           │
  ┌────────────────────────────────────┐
  │  customer.balance.updated          │
  │  ledger_balance reduced           │
  └────────────────────────────────────┘
```

A declined authorization fires `transaction.declined` instead of `transaction.authorized`. A reversal fires `transaction.reversed`.

### Card status changes

Card lifecycle events fire whenever a card's status changes — whether triggered by your API, by the customer, or by a compliance action.

```
card.created        → Virtual card issued to a customer
card.activated      → Physical card activated (physical cards only)
card.frozen         → Spending temporarily paused
card.unfrozen       → Spending re-enabled (upstream status: "active")
card.voided         → Card permanently deactivated
card.lost           → Card reported lost (new card required)
card.stolen         → Card reported stolen (new card required)
card.cancelled      → Card cancelled
card.deactivated    → Card deactivated
```

***

## Event Reference

<Note>
  **Amount fields** — Every monetary amount is provided in two formats:

  * `amount` (float) — human-readable decimal, e.g. `12.50`
  * `amount_minor` (integer) — minor currency units (cents), e.g. `1250`

  Use floats for display. Use minor units for precise arithmetic, storing in integer columns, or comparing amounts without floating-point error. Both are always present when the amount is known; both are `null` when the amount is unavailable.
</Note>

### Envelope

All events share this structure. The `data` object is event-specific.

```json theme={null}
{
  "id": "evt_1747058400000_abc123",
  "type": "customer.funded",
  "created_at": "2026-05-12T14:10:00.000Z",
  "data": { }
}
```

| Field        | Type     | Description                              |
| ------------ | -------- | ---------------------------------------- |
| `id`         | string   | Unique event ID — use as idempotency key |
| `type`       | string   | Event type                               |
| `created_at` | ISO 8601 | When the event was generated             |
| `data`       | object   | Event-specific payload (see below)       |

### `master_wallet.deposit`

Fires when a deposit is detected or settles into your spendable balance. One-step deposits fire only `settled`. Two-step deposits fire `processing` first, then `settled`.

All amount fields are provided as both a decimal float and an integer in minor units (cents). For example, `500.00 USD` → `amount: 500.00`, `amount_minor: 50000`.

```json Processing theme={null}
{
  "id": "evt_1747058400000_abc123",
  "type": "master_wallet.deposit",
  "created_at": "2026-05-12T14:00:00.000Z",
  "data": {
    "settlement_id": "bridge_1745123456_2ea87af00de1",
    "source_currency": "USDC_SOL",
    "source_amount": 500.00,
    "source_amount_minor": 50000,
    "tx_hash": "5KtP3MzXYZABCDE1234567890abcdef12345678901234567890abcdef1234567",
    "status": "processing"
  }
}
```

```json Settled — regular deposit theme={null}
{
  "id": "evt_1747058520000_def456",
  "type": "master_wallet.deposit",
  "created_at": "2026-05-12T14:02:00.000Z",
  "data": {
    "settlement_id": "bridge_1745123456_2ea87af00de1",
    "deposit_amount": 499.75,
    "deposit_amount_minor": 49975,
    "wallet_balance": 999.50,
    "wallet_balance_minor": 99950,
    "currency": "USD",
    "tx_hash": "0xa8739436c44460738a4147055530e450f8b55694e2448875e72cd25eb29cd21f",
    "solana_tx_hash": "5KtP3MzXYZABCDE1234567890abcdef12345678901234567890abcdef1234567",
    "status": "settled"
  }
}
```

```json Settled — withdrawal credit theme={null}
{
  "id": "evt_1747058520000_ghi012",
  "type": "master_wallet.deposit",
  "created_at": "2026-05-12T14:02:00.000Z",
  "data": {
    "settlement_id": "6a294844e71b09a046f28103",
    "withdrawal_id": "cwd_1748012345678_x7k2qp",
    "yativo_card_id": "yativo_card_customer_...",
    "deposit_amount": 50.0,
    "deposit_amount_minor": 5000,
    "wallet_balance": 550.0,
    "wallet_balance_minor": 55000,
    "currency": "USD",
    "tx_hash": "0xa8739436c44460738a4147055530e450f8b55694e2448875e72cd25eb29cd21f",
    "solana_tx_hash": null,
    "status": "settled"
  }
}
```

<Note>
  `master_wallet.deposit` fires for two distinct scenarios: (1) a new deposit credited to your master wallet, and (2) funds returning to your master wallet when a card withdrawal settles. In the withdrawal case the payload includes `withdrawal_id` and `yativo_card_id` so you can correlate it with the originating `card.withdrawal.settled` and `master_wallet.withdrawal` events. When `withdrawal_id` is absent the event is a regular deposit.
</Note>

| Field                  | Type            | Description                                                                                                                                                            |
| ---------------------- | --------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `source_currency`      | string          | Currency/chain of the incoming deposit (e.g. `USDC_SOL`, `USDC_XDC`). Present on `processing` events only.                                                             |
| `source_amount`        | float           | Amount sent by the depositor, in `source_currency` units. Present on `processing` events.                                                                              |
| `source_amount_minor`  | integer         | `source_amount` in minor units (cents).                                                                                                                                |
| `deposit_amount`       | float           | Amount that arrived in your wallet after settlement. Present on `settled` events.                                                                                      |
| `deposit_amount_minor` | integer         | `deposit_amount` in minor units (cents).                                                                                                                               |
| `wallet_balance`       | float \| null   | Total wallet balance after this credit. Present on `settled` events.                                                                                                   |
| `wallet_balance_minor` | integer \| null | `wallet_balance` in minor units (cents).                                                                                                                               |
| `currency`             | string          | Wallet currency (e.g. `USD`, `EUR`, `GBP`). Present on `settled` events.                                                                                               |
| `settlement_id`        | string \| null  | Internal settlement or bridge transaction reference.                                                                                                                   |
| `tx_hash`              | string \| null  | On-chain transaction hash of the final settlement.                                                                                                                     |
| `solana_tx_hash`       | string \| null  | Original Solana deposit transaction signature. Present for `USDC_SOL` deposits; `null` for all other paths including withdrawal credits.                               |
| `withdrawal_id`        | string \| null  | Present only when this event is a withdrawal credit. Matches the `withdrawal_id` on the corresponding `card.withdrawal.settled` and `master_wallet.withdrawal` events. |
| `yativo_card_id`       | string \| null  | Present only when this event is a withdrawal credit. Identifies the card the funds were withdrawn from.                                                                |

### `master_wallet.swap`

Fired when a currency swap is submitted from your master wallet (e.g. USD → EUR).

```json theme={null}
{
  "id": "evt_1747058700000_ghi789",
  "type": "master_wallet.swap",
  "created_at": "2026-05-12T14:05:00.000Z",
  "data": {
    "swap_id": "0x4a1b2c3d...",
    "from_token": "USD",
    "to_token": "EUR",
    "amount": 1000.00,
    "amount_minor": 100000,
    "expected_output": 921.50,
    "expected_output_minor": 92150,
    "status": "submitted"
  }
}
```

### `master_wallet.customer_funded`

Fired alongside `customer.funded` whenever your master wallet is debited. Includes `remaining_balance` when available and `tx_hash` with the on-chain transaction hash of the funding transfer.

```json theme={null}
{
  "id": "evt_1747059001000_mwf01",
  "type": "master_wallet.customer_funded",
  "created_at": "2026-05-12T14:10:00.000Z",
  "data": {
    "transfer_id": "tx_1745128800_9fc12ab3e4d5",
    "customer_id": "69f0bdf29a84752db9cc8ff9",
    "amount": 50.00,
    "amount_minor": 5000,
    "currency": "USD",
    "tx_hash": "0xa8739436c44460738a4147055530e450f8b55694e2448875e72cd25eb29cd21f",
    "status": "completed",
    "remaining_balance": 114.98,
    "remaining_balance_minor": 11498
  }
}
```

### `customer.funded`

Fired when a customer's card wallet has been successfully credited.

```json theme={null}
{
  "id": "evt_1747059000000_ghi789",
  "type": "customer.funded",
  "created_at": "2026-05-12T14:10:00.000Z",
  "data": {
    "transfer_id": "tx_1745128800_9fc12ab3e4d5",
    "customer_id": "69f0bdf29a84752db9cc8ff9",
    "amount": 50.00,
    "amount_minor": 5000,
    "currency": "USD",
    "tx_hash": "0xa8739436c44460738a4147055530e450f8b55694e2448875e72cd25eb29cd21f",
    "status": "completed"
  }
}
```

### `customer.funding.failed`

Fired when a funding transfer to a customer's card wallet fails.

```json theme={null}
{
  "id": "evt_1747059000000_fail01",
  "type": "customer.funding.failed",
  "created_at": "2026-05-12T14:10:00.000Z",
  "data": {
    "customer_id": "69f0bdf29a84752db9cc8ff9",
    "yativo_card_id": "yativo_card_customer_8f9a...",
    "amount": 50.00,
    "amount_minor": 5000,
    "currency": "USD"
  }
}
```

### `wallet.deposit.confirmed`

Fired when a customer's account wallet balance increases — i.e. a deposit has arrived. This is the primary event for detecting incoming funds to a customer card wallet.

```json theme={null}
{
  "id": "evt_1747060100000_dep01",
  "type": "wallet.deposit.confirmed",
  "created_at": "2026-05-12T14:29:45.000Z",
  "data": {
    "yativo_card_id": "yativo_card_customer_8f9a...",
    "amount": 50.00,
    "amount_minor": 5000,
    "new_balance": 145.50,
    "new_balance_minor": 14550,
    "currency": "EUR",
    "timestamp": "2026-05-12T14:29:45.000Z"
  }
}
```

| Field               | Type           | Description                                    |
| ------------------- | -------------- | ---------------------------------------------- |
| `amount`            | float          | Amount deposited (balance delta)               |
| `amount_minor`      | integer        | `amount` in minor units (cents)                |
| `new_balance`       | float          | Customer's new total balance after the deposit |
| `new_balance_minor` | integer        | `new_balance` in minor units (cents)           |
| `currency`          | string \| null | Card currency (`USD`, `EUR`, `GBP`)            |

<Note>
  A `customer.balance.updated` event fires for the same balance change. `wallet.deposit.confirmed` fires only when the balance increased; `customer.balance.updated` fires for all balance changes including spend and reversal.
</Note>

***

### `customer.balance.updated`

Fired after any card balance change — top-up, purchase authorization, settlement, reversal, or periodic reconciliation.

```json theme={null}
{
  "id": "evt_1747060200000_stu901",
  "type": "customer.balance.updated",
  "created_at": "2026-05-12T14:30:00.000Z",
  "data": {
    "yativo_card_id": "yativo_card_customer_8f9a...",
    "ledger_balance": 45.23,
    "ledger_balance_minor": 4523,
    "available_balance": 38.50,
    "available_balance_minor": 3850,
    "pending_balance": 6.73,
    "pending_balance_minor": 673,
    "currency": "EUR",
    "balance_source": "live",
    "trigger": "funding",
    "transfer_id": "bridge_1779218694580_c22259aa6951",
    "tx_hash": "0x3fbd8b6c6a1e2e6f0a1b8f6a5b4c3d2e1f0a1b8f6a5b4c3d2e1f0a1b8f6a5b4c",
    "timestamp": "2026-05-12T14:30:00.000Z"
  }
}
```

| Field                     | Type            | Description                                                                                                                                                                                                                                                                                                                                                                  |
| ------------------------- | --------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `ledger_balance`          | float           | Total balance in the customer's account wallet                                                                                                                                                                                                                                                                                                                               |
| `ledger_balance_minor`    | integer         | `ledger_balance` in minor units (cents)                                                                                                                                                                                                                                                                                                                                      |
| `available_balance`       | float           | Spendable balance after pending authorizations                                                                                                                                                                                                                                                                                                                               |
| `available_balance_minor` | integer         | `available_balance` in minor units (cents)                                                                                                                                                                                                                                                                                                                                   |
| `pending_balance`         | float \| null   | Held for authorized but not yet settled transactions. `null` when no pending authorizations                                                                                                                                                                                                                                                                                  |
| `pending_balance_minor`   | integer \| null | `pending_balance` in minor units (cents)                                                                                                                                                                                                                                                                                                                                     |
| `currency`                | string \| null  | `USD`, `EUR`, or `GBP`                                                                                                                                                                                                                                                                                                                                                       |
| `balance_source`          | string \| null  | `"reconciliation"` when fired by the background reconciliation job; absent or `null` for live card events                                                                                                                                                                                                                                                                    |
| `trigger`                 | string \| null  | What we determined caused this balance change: `"funding"`, `"withdraw"`, or `"transaction"` (a card purchase). `null` if we couldn't match it to a known operation within a 10-minute window.                                                                                                                                                                               |
| `transfer_id`             | string \| null  | Our own reference for the underlying operation — matches the `transfer_id` you already received on `customer.funded`/`master_wallet.customer_funded` (funding), the `withdrawal_id` from the withdrawal endpoints (withdrawal), or the transaction reference (card purchase). Use this to reconcile a `customer.balance.updated` event back to the operation that caused it. |
| `tx_hash`                 | string \| null  | On-chain hash for the underlying operation, when one exists (funding/withdrawal). `null` for card purchases, which don't have a dedicated on-chain leg at this stage.                                                                                                                                                                                                        |

<Note>
  `pending_balance` / `pending_balance_minor` are always `null` when `balance_source` is `"reconciliation"` — the reconciliation job reads the on-chain settled balance and cannot determine pending authorizations. Use `available_balance` for spendable funds.
</Note>

<Note>
  A single top-up typically produces **two** `customer.balance.updated` events a couple of minutes apart — the first when the ledger reflects the incoming funds, the second when they become spendable (`available_balance` catches up). Both carry the same `trigger`/`transfer_id`, since both stem from the same underlying operation — use `transfer_id` to recognize them as the same event rather than treating the second as a new, unrelated top-up.

  `trigger`/`transfer_id`/`tx_hash` are inferred by matching this balance change against our own records of your recent funding/withdrawal/transaction activity for this card, not supplied directly by the card network (which doesn't include a correlator on this event type). If nothing matched within the 10-minute window, all three come back `null`.
</Note>

### `card.created`

Fired when a new virtual card is issued to a customer. `card_id` is the card token identifier. `type` and `status` reflect values from the upstream card provider and may be `null` depending on the event payload received.

```json theme={null}
{
  "id": "evt_1747057800000_abc001",
  "type": "card.created",
  "created_at": "2026-05-12T13:50:00.000Z",
  "data": {
    "yativo_card_id": "yativo_card_customer_8f9a...",
    "card_id": "card_token_abc123",
    "type": null,
    "status": null,
    "timestamp": "2026-05-12T13:50:00.000Z"
  }
}
```

### `card.activated`

Fired when a physical card is activated by the cardholder. Virtual cards do not fire this event.

```json theme={null}
{
  "id": "evt_1747057900000_act01",
  "type": "card.activated",
  "created_at": "2026-05-12T13:52:00.000Z",
  "data": {
    "yativo_card_id": "yativo_card_customer_8f9a...",
    "card_id": "card_token_abc123",
    "timestamp": "2026-05-12T13:52:00.000Z"
  }
}
```

### `card.frozen` / `card.unfrozen`

For `card.frozen` the `status` field reflects the upstream frozen status string. For `card.unfrozen` the `status` field is `"active"` — the upstream provider reports the card status returning to active when a freeze is lifted.

```json card.frozen theme={null}
{
  "id": "evt_1747059600000_mno345",
  "type": "card.frozen",
  "created_at": "2026-05-12T14:20:00.000Z",
  "data": {
    "yativo_card_id": "yativo_card_customer_8f9a...",
    "card_id": "card_token_abc123",
    "status": "Frozen",
    "timestamp": "2026-05-12T14:20:00.000Z"
  }
}
```

```json card.unfrozen theme={null}
{
  "id": "evt_1747059700000_unf01",
  "type": "card.unfrozen",
  "created_at": "2026-05-12T14:21:00.000Z",
  "data": {
    "yativo_card_id": "yativo_card_customer_8f9a...",
    "card_id": "card_token_abc123",
    "status": "active",
    "timestamp": "2026-05-12T14:21:00.000Z"
  }
}
```

<Note>
  The `card_id` in webhook events is the card token identifier and may differ from the `card_id` returned by the Get Customer API. Use `yativo_card_id` as the stable cross-reference between events and API responses.
</Note>

### `card.lost` / `card.stolen` / `card.voided` / `card.cancelled` / `card.deactivated`

Same shape as `card.frozen` — the envelope `type` identifies the event and `data.status` reflects the upstream status string for that change.

```json theme={null}
{
  "id": "evt_1747059900000_pqr678",
  "type": "card.lost",
  "created_at": "2026-05-12T14:25:00.000Z",
  "data": {
    "yativo_card_id": "yativo_card_customer_8f9a...",
    "card_id": "card_token_abc123",
    "status": "Lost",
    "timestamp": "2026-05-12T14:25:00.000Z"
  }
}
```

### Transaction event field reference

All `transaction.*` events share the same `data` shape. Fields are populated from card network data and some may be `null` depending on the transaction type or network.

| Field                  | Type            | Notes                                                                                                                                                                                                                                                                                                                                                                             |
| ---------------------- | --------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `yativo_card_id`       | string          | Stable card identifier — use this as the cross-reference key                                                                                                                                                                                                                                                                                                                      |
| `card_id`              | string \| null  | Card token from the card network                                                                                                                                                                                                                                                                                                                                                  |
| `transaction_id`       | string \| null  | Network transaction ID. Use this as the key for deduplication. Falls back to the webhook event `id` when not provided by the card network.                                                                                                                                                                                                                                        |
| `blockchain_tx_hash`   | string \| null  | Hash of the on-chain settlement transaction that debited the card's wallet. Populated on `transaction.authorized` (we wait for this to be available before dispatching — see note below). May be `null` on other event types where no on-chain leg exists yet (e.g. `transaction.declined`, or a reversal that never settled on-chain).                                           |
| `type`                 | string \| null  | Transaction type as reported by the card network (e.g. `"Payment"`, `"Refund"`)                                                                                                                                                                                                                                                                                                   |
| `amount`               | float \| null   | Transaction amount as a decimal number, in the merchant's local currency                                                                                                                                                                                                                                                                                                          |
| `amount_minor`         | integer \| null | `amount` in minor units (cents)                                                                                                                                                                                                                                                                                                                                                   |
| `currency`             | string \| null  | ISO 4217 alpha-3 currency code of `amount` — the merchant's local currency (e.g. `"EUR"`, `"USD"`, `"BRL"`)                                                                                                                                                                                                                                                                       |
| `billing_amount`       | float \| null   | The amount actually debited from the card's own settlement currency. **Present on every transaction**, not only foreign-currency ones — on a same-currency purchase it simply equals `amount`.                                                                                                                                                                                    |
| `billing_amount_minor` | integer \| null | `billing_amount` in minor units (cents)                                                                                                                                                                                                                                                                                                                                           |
| `billing_currency`     | string \| null  | ISO 4217 alpha-3 currency code of `billing_amount` — the card's own currency. Equals `currency` when the purchase was made in the card's native currency.                                                                                                                                                                                                                         |
| `fx_rate`              | float \| null   | Local-currency units per 1 unit of billing currency: `fx_rate = amount / billing_amount`. Reads as "1 {billing_currency} = {fx_rate} {currency}" — e.g. `924.3827` means `1 USD = 924.3827 CLP`. Equals `1` on a same-currency purchase. Provided so you don't have to compute it, but if you ever need to derive `billing_amount` yourself: `billing_amount = amount / fx_rate`. |
| `merchant`             | string \| null  | Merchant name — `null` for contactless/tap or when not provided by the network                                                                                                                                                                                                                                                                                                    |
| `merchant_category`    | string \| null  | Merchant category code (MCC)                                                                                                                                                                                                                                                                                                                                                      |
| `merchant_city`        | string \| null  | Merchant city — `null` when not provided                                                                                                                                                                                                                                                                                                                                          |
| `merchant_country`     | string \| null  | ISO 3166 alpha-2 country code (e.g. `"FR"`, `"BR"`) — `null` when not provided                                                                                                                                                                                                                                                                                                    |
| `status`               | string \| null  | Raw status string from the card network — values vary (e.g. `"Approved"`, `"Declined"`)                                                                                                                                                                                                                                                                                           |
| `timestamp`            | ISO 8601        | When the card network event occurred                                                                                                                                                                                                                                                                                                                                              |

<Warning>
  `status` and `type` values are passed through directly from the card network and are not normalized. Do not hard-code comparisons against specific strings — check for membership in an expected set and handle unknowns gracefully.
</Warning>

### `transaction.authorized`

Fired once per card purchase, when the authorization clears (not at the earlier point-of-sale authorization moment — we intentionally wait for clearing so this event always carries both `transaction_id` and `blockchain_tx_hash` together, rather than firing an incomplete notification first). A `customer.balance.updated` event follows immediately.

<Note>
  Use `data.transaction_id` (not `event.id`) as your reconciliation key. If `transaction_id` is `null`, fall back to `event.id`.
</Note>

```json theme={null}
{
  "id": "evt_1747059300000_jkl012",
  "type": "transaction.authorized",
  "created_at": "2026-05-12T14:15:00.000Z",
  "data": {
    "yativo_card_id": "yativo_card_customer_8f9a...",
    "card_id": "544599049",
    "transaction_id": "tx_9d8e7f6a5b4c3d2e1f",
    "blockchain_tx_hash": "0xedbca669196b14ddc4b5a8b6fd6c313a1400795cdabe686b3d4b908d8e8279bb",
    "type": "Payment",
    "amount": 12.50,
    "amount_minor": 1250,
    "currency": "EUR",
    "billing_amount": 12.50,
    "billing_amount_minor": 1250,
    "billing_currency": "EUR",
    "fx_rate": 1,
    "merchant": "Starbucks",
    "merchant_category": "5812",
    "merchant_city": "Paris",
    "merchant_country": "FR",
    "status": "Approved",
    "timestamp": "2026-05-12T14:15:00.000Z"
  }
}
```

<Note>
  This card settles in EUR and the purchase was also in EUR, so `billing_amount`/`billing_currency` simply match `amount`/`currency` and `fx_rate` is `1` — no conversion took place. See the `transaction.settled` example below for a foreign-currency purchase, where they differ.
</Note>

### `transaction.settled`

Fired when the merchant batch clears and the authorization is finalized.

```json theme={null}
{
  "id": "evt_1747063800000_uvw234",
  "type": "transaction.settled",
  "created_at": "2026-05-12T15:30:00.000Z",
  "data": {
    "yativo_card_id": "yativo_card_customer_8f9a...",
    "card_id": "544599049",
    "transaction_id": "tx_9d8e7f6a5b4c3d2e1f",
    "blockchain_tx_hash": null,
    "type": "Payment",
    "amount": 101,
    "amount_minor": 10100,
    "currency": "BRL",
    "billing_amount": 20.15,
    "billing_amount_minor": 2015,
    "billing_currency": "USD",
    "fx_rate": 5.0124,
    "merchant": "MERCADOPAGO*STORE",
    "merchant_category": "7298",
    "merchant_city": null,
    "merchant_country": "BR",
    "status": "Approved",
    "timestamp": "2026-05-12T15:30:00.000Z"
  }
}
```

<Note>
  This example shows a foreign-currency purchase: a USD-denominated card used at a Brazilian merchant. `amount`/`currency` are the merchant's local currency (BRL, what was charged at the point of sale); `billing_amount`/`billing_currency` are what was actually debited from the card (USD). `fx_rate` (`5.0124`) reads as "1 USD = 5.0124 BRL" — check: `101 / 5.0124 ≈ 20.15`, which matches `billing_amount`.

  `blockchain_tx_hash` on `transaction.settled` is not guaranteed the way it is on `transaction.authorized` — treat it as best-effort here.

  `merchant` and `merchant_city` may come back `null` on some networks/transaction stages even when populated on the corresponding `transaction.authorized` event for the same `transaction_id` — treat them as best-effort, not guaranteed on every event.
</Note>

### `transaction.declined`

Fired when a card transaction is declined.

```json theme={null}
{
  "id": "evt_1747059400000_dec01",
  "type": "transaction.declined",
  "created_at": "2026-05-12T14:16:00.000Z",
  "data": {
    "yativo_card_id": "yativo_card_customer_8f9a...",
    "card_id": "544599049",
    "transaction_id": "tx_9d8e7f6a5b4c3d2e1f",
    "blockchain_tx_hash": null,
    "type": "Payment",
    "amount": 200.00,
    "amount_minor": 20000,
    "currency": "EUR",
    "merchant": null,
    "merchant_category": "5411",
    "merchant_city": null,
    "merchant_country": "DE",
    "status": "Declined",
    "timestamp": "2026-05-12T14:16:00.000Z"
  }
}
```

### `transaction.reversed`

Fired when an authorization is reversed (e.g. a cancelled hold before settlement).

```json theme={null}
{
  "id": "evt_1747060000000_rev01",
  "type": "transaction.reversed",
  "created_at": "2026-05-12T14:26:00.000Z",
  "data": {
    "yativo_card_id": "yativo_card_customer_8f9a...",
    "card_id": "card_token_abc123",
    "transaction_id": "tx_9d8e7f...",
    "amount": 12.50,
    "amount_minor": 1250,
    "currency": "EUR",
    "status": "REVERSED",
    "timestamp": "2026-05-12T14:26:00.000Z"
  }
}
```

### `transaction.refund.created`

Fired when a merchant issues a refund.

```json theme={null}
{
  "id": "evt_1747064000000_ref01",
  "type": "transaction.refund.created",
  "created_at": "2026-05-12T15:32:00.000Z",
  "data": {
    "yativo_card_id": "yativo_card_customer_8f9a...",
    "card_id": "card_token_abc123",
    "transaction_id": "tx_9d8e7f...",
    "amount": 12.50,
    "amount_minor": 1250,
    "currency": "EUR",
    "status": "REFUNDED",
    "timestamp": "2026-05-12T15:32:00.000Z"
  }
}
```

***

## Managing Webhooks

### List all subscriptions

```bash theme={null}
GET /v1/yativo-card/webhooks
Authorization: Bearer YOUR_TOKEN
```

### Get one subscription

```bash theme={null}
GET /v1/yativo-card/webhooks/:webhookId
Authorization: Bearer YOUR_TOKEN
```

### Update URL, events, or enabled state

```bash theme={null}
curl -X PUT 'https://crypto-api.yativo.com/api/v1/yativo-card/webhooks/68241abc...' \
  -H 'Authorization: Bearer YOUR_TOKEN' \
  -H 'Content-Type: application/json' \
  -d '{
    "url": "https://api.yourapp.com/webhooks/yativo/v2",
    "events": ["customer.funded", "transaction.authorized", "transaction.declined"],
    "enabled": true
  }'
```

### Rotate signing secret

```bash theme={null}
POST /v1/yativo-card/webhooks/:webhookId/rotate-secret
Authorization: Bearer YOUR_TOKEN
```

The response returns the new secret — store it immediately, then update your environment variable.

### Delete a subscription

```bash theme={null}
DELETE /v1/yativo-card/webhooks/:webhookId
Authorization: Bearer YOUR_TOKEN
```

### Delivery history

```bash theme={null}
# Deliveries for one webhook
GET /v1/yativo-card/webhooks/:webhookId/deliveries

# All deliveries across webhooks
GET /v1/yativo-card/webhooks/deliveries/all
```

### Retry a failed delivery

```bash theme={null}
POST /v1/yativo-card/webhooks/deliveries/:deliveryId/retry
Authorization: Bearer YOUR_TOKEN
```

***

## Retry Policy

If your endpoint returns a non-2xx response, times out (> 30 s), or is unreachable, Yativo retries using exponential backoff:

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

After 7 failed attempts the delivery is marked **permanently failed** — no further automatic retries. Replay it manually via `POST /v1/yativo-card/webhooks/deliveries/:deliveryId/retry`.

If your endpoint fails **100 consecutive deliveries**, Yativo automatically disables the subscription to protect your program's event queue. Re-enable it with `PUT /v1/yativo-card/webhooks/:webhookId` and `{ "enabled": true }` after resolving the issue.

***

## Best Practices

**Return 200 first, process after.** Yativo waits up to 30 seconds for a response. Enqueue the event to a job queue (BullMQ, Celery, SQS) before returning — don't block on database writes or downstream calls.

**Deduplicate with `event.id`.** Yativo deduplicates deliveries server-side so the same upstream event is not queued twice, but network-level retries can still cause your endpoint to receive the same delivery more than once. Store processed event IDs in a database table with a unique constraint and skip duplicates.

**Do not return 200 speculatively.** Only acknowledge when you have successfully enqueued the event. If your queue is unavailable, return 500 to trigger a retry.

**Handle unknown event types gracefully.** Log and ignore event types you don't recognise rather than throwing an error. Yativo adds new event types over time — unknown types should not break your handler.

**Subscribe to the events you need.** Use a focused event list rather than `"*"` in production to reduce noise and make your handler logic explicit.

***

## Idempotent Handler Pattern

```typescript theme={null}
async function handleCardIssuerEvent(event: {
  id: string;
  type: string;
  data: any;
}) {
  // 1. Deduplicate
  const exists = await db.processedEvents.findUnique({ where: { id: event.id } });
  if (exists) return;

  // 2. Process
  switch (event.type) {
    case "master_wallet.deposit":
      if (event.data.status === "settled") {
        if (event.data.withdrawal_id) {
          // Withdrawal credit — funds returned from a card withdrawal.
          // withdrawal_id links this back to the originating card.withdrawal.settled event.
          await recordWithdrawalCredit(event.data.withdrawal_id, event.data.deposit_amount, event.data.tx_hash);
        } else {
          // Regular deposit — new funds arriving from an external source.
          await updateMasterWalletBalance(event.data.deposit_amount, event.data.currency);
        }
      }
      // Ignore processing events — act only on settled
      break;

    case "master_wallet.withdrawal":
      // Companion to card.withdrawal.settled — confirms master wallet was credited.
      // Use for master wallet reconciliation; withdrawal_id is the join key.
      await recordMasterWalletWithdrawalSettled(event.data.withdrawal_id, event.data.tx_hash);
      break;

    case "customer.funded":
      await creditCustomerLedger(event.data.customer_id, event.data.amount, event.data.currency);
      break;

    case "customer.funding.failed":
      await flagFailedFunding(event.data.customer_id, event.data.amount);
      await notifyOpsTeam(event);
      break;

    case "customer.balance.updated":
      await syncCustomerBalance(event.data.yativo_card_id, {
        ledger: event.data.ledger_balance,
        available: event.data.available_balance,
        pending: event.data.pending_balance,
      });
      break;

    case "transaction.authorized":
      await recordPendingTransaction(event.data);
      break;

    case "transaction.settled":
      await finalizeTransaction(event.data.transaction_id);
      break;

    case "transaction.declined":
      await notifyCustomerOfDecline(event.data.yativo_card_id);
      break;

    case "card.frozen":
    case "card.voided":
    case "card.lost":
    case "card.stolen":
      await updateCardStatus(event.data.yativo_card_id, event.data.status);
      break;
  }

  // 3. Mark processed
  await db.processedEvents.create({ data: { id: event.id, processedAt: new Date() } });
}
```

***

## Testing

Use the sandbox environment to test your webhook handler without real funds:

* **Sandbox base URL:** `https://crypto-sandbox.yativo.com/api/v1/`
* **Register webhooks** the same way as production — use the same endpoint `POST /v1/webhook/create-webhook`
* **Expose your local server** using [ngrok](https://ngrok.com) or [Hookdeck](https://hookdeck.com) during development

```bash theme={null}
# Register a sandbox webhook pointing at your local ngrok tunnel
curl -X POST 'https://crypto-sandbox.yativo.com/api/v1/webhook/create-webhook' \
  -H 'Authorization: Bearer YOUR_SANDBOX_TOKEN' \
  -H 'Content-Type: application/json' \
  -d '{
    "url": "https://abc123.ngrok-free.app/webhooks/yativo",
    "events": ["*"],
    "description": "Local dev webhook"
  }'
```

***

## Related

* [Card Issuer Program guide](/guides/b2b-card-issuer)
* [Webhook Events API reference](/api-reference/issuer/webhooks)
* [Fund Customer API](/api-reference/issuer/fund-customer)
* [Funding deposits](/api-reference/issuer/deposits)
* [General webhook integration](/guides/webhook-integration)
