Skip to main content

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

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
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":
      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() });
}
Always use a timing-safe comparison (timingSafeEqual, hmac.compare_digest, hash_equals). A regular === check is vulnerable to timing attacks.

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.
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"
  }'
Response
{
  "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"
  }
}
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.
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:
HeaderDescription
X-Yativo-Signaturesha256=<HMAC-SHA256 hex> of the signed string
X-Yativo-TimestampUnix timestamp (seconds) when the event was dispatched
X-Yativo-EventThe event type (e.g. customer.funded)
X-Yativo-Delivery-IdUnique 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.
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
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.

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

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.

Envelope

All events share this structure. The data object is event-specific.
{
  "id": "evt_1747058400000_abc123",
  "type": "customer.funded",
  "created_at": "2026-05-12T14:10:00.000Z",
  "data": { }
}
FieldTypeDescription
idstringUnique event ID — use as idempotency key
typestringEvent type
created_atISO 8601When the event was generated
dataobjectEvent-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 USDamount: 500.00, amount_minor: 50000.
Processing
{
  "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"
  }
}
Settled
{
  "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",
    "status": "settled"
  }
}
FieldTypeDescription
source_currencystringCurrency/chain of the incoming deposit (e.g. USDC_SOL, USDC_XDC). Present on processing events only.
source_amountfloatAmount sent by the depositor, in source_currency units. Present on processing events.
source_amount_minorintegersource_amount in minor units (cents).
deposit_amountfloatAmount that arrived in your wallet after settlement. Present on settled events.
deposit_amount_minorintegerdeposit_amount in minor units (cents).
wallet_balancefloat | nullTotal USD wallet balance after this deposit. Present on settled events fired from on-chain confirmation; null when fired from bridge completion.
wallet_balance_minorinteger | nullwallet_balance in minor units (cents).
currencystringWallet currency (e.g. USD, EUR, GBP). Present on settled events.
settlement_idstring | nullBridge or settlement transaction reference.
tx_hashstring | nullOn-chain blockchain transaction hash. Present on both processing and settled events when the underlying chain transaction is known. Use this for reconciliation and accounting.

master_wallet.swap

Fired when a currency swap is submitted from your master wallet (e.g. USD → EUR).
{
  "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.
{
  "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.
{
  "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.
{
  "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.
{
  "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"
  }
}
FieldTypeDescription
amountfloatAmount deposited (balance delta)
amount_minorintegeramount in minor units (cents)
new_balancefloatCustomer’s new total balance after the deposit
new_balance_minorintegernew_balance in minor units (cents)
currencystring | nullCard currency (USD, EUR, GBP)
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.

customer.balance.updated

Fired after any card balance change — top-up, purchase authorization, settlement, reversal, or periodic reconciliation.
{
  "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",
    "timestamp": "2026-05-12T14:30:00.000Z"
  }
}
FieldTypeDescription
ledger_balancefloatTotal balance in the customer’s account wallet
ledger_balance_minorintegerledger_balance in minor units (cents)
available_balancefloatSpendable balance after pending authorizations
available_balance_minorintegeravailable_balance in minor units (cents)
pending_balancefloat | nullHeld for authorized but not yet settled transactions. null when no pending authorizations
pending_balance_minorinteger | nullpending_balance in minor units (cents)
currencystring | nullUSD, EUR, or GBP
balance_sourcestring | null"reconciliation" when fired by the background reconciliation job; absent or null for live card events
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.

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.
{
  "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.
{
  "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.
card.frozen
{
  "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"
  }
}
card.unfrozen
{
  "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"
  }
}
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.

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.
{
  "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.
FieldTypeNotes
yativo_card_idstringStable card identifier — use this as the cross-reference key
card_idstring | nullCard token from the card network
transaction_idstring | nullNetwork 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_hashstring | nullOn-chain transaction hash when the card network includes one. null for most card-network transactions where settlement is off-chain.
typestring | nullTransaction type as reported by the card network (e.g. "Payment", "Refund")
amountfloat | nullTransaction amount as a decimal number
amount_minorinteger | nullamount in minor units (cents)
currencystring | nullISO 4217 alpha-3 currency code (e.g. "EUR", "USD", "BRL")
merchantstring | nullMerchant name — null for contactless/tap or when not provided by the network
merchant_categorystring | nullMerchant category code (MCC)
merchant_citystring | nullMerchant city — null when not provided
merchant_countrystring | nullISO 3166 alpha-2 country code (e.g. "FR", "BR") — null when not provided
statusstring | nullRaw status string from the card network — values vary (e.g. "Approved", "Declined")
timestampISO 8601When the card network event occurred
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.

transaction.authorized

Fired when a card purchase is authorized. A customer.balance.updated event follows immediately.
transaction.authorized can fire twice for the same card transaction — once when the authorization is created and again when it is cleared. Both events carry the same transaction_id. Use data.transaction_id (not event.id) as the key for transaction-level deduplication in your handler. If transaction_id is null, fall back to event.id.
{
  "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": null,
    "type": "Payment",
    "amount": 12.50,
    "amount_minor": 1250,
    "currency": "EUR",
    "merchant": "Starbucks",
    "merchant_category": "5812",
    "merchant_city": "Paris",
    "merchant_country": "FR",
    "status": "Approved",
    "timestamp": "2026-05-12T14:15:00.000Z"
  }
}

transaction.settled

Fired when the merchant batch clears and the authorization is finalized.
{
  "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": 12.50,
    "amount_minor": 1250,
    "currency": "EUR",
    "merchant": null,
    "merchant_category": "7298",
    "merchant_city": null,
    "merchant_country": "BR",
    "status": "Approved",
    "timestamp": "2026-05-12T15:30:00.000Z"
  }
}

transaction.declined

Fired when a card transaction is declined.
{
  "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).
{
  "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.
{
  "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

GET /v1/yativo-card/webhooks
Authorization: Bearer YOUR_TOKEN

Get one subscription

GET /v1/yativo-card/webhooks/:webhookId
Authorization: Bearer YOUR_TOKEN

Update URL, events, or enabled state

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

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

DELETE /v1/yativo-card/webhooks/:webhookId
Authorization: Bearer YOUR_TOKEN

Delivery history

# 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

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:
AttemptDelay after previous attempt
1 (initial)
21 minute
35 minutes
430 minutes
52 hours
612 hours
724 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

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") {
        // Always act on settled — this is the authoritative confirmation.
        // One-step deposits fire only settled; two-step deposits fire processing then settled.
        await updateMasterWalletBalance(event.data.settled_amount, event.data.currency);
      }
      // Ignore processing events until settled
      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 or Hookdeck during development
# 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"
  }'