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.

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.
Test webhooks against the Sandbox at https://crypto-sandbox.yativo.com/api/v1/. Use a tool like ngrok or Hookdeck to expose your local server during development.
1

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
TypeScript
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));
  }
);
2

Register the webhook with Yativo

Register your endpoint and select which event types you want to receive.
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"
  }'
Response:
{
  "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"
  }
}
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.
3

Verify webhook signatures

Every delivery includes two headers:
HeaderValue
X-Yativo-Signaturesha256=<hmac>
X-Yativo-TimestampUnix timestamp (seconds)
The HMAC is computed over "${timestamp}.${JSON.stringify(payload)}". Always verify this signature before processing the event.
TypeScript
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;
  }
}
Always use a timing-safe comparison function (timingSafeEqual, hmac.compare_digest, hash_equals). Using a regular string equality check (===) exposes you to timing attacks.
4

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
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(),
  });
}
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.
5

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

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

Event Types Reference

Deposit Events

EventDescription
deposit.detectedA deposit transaction was seen on-chain (unconfirmed)
deposit.confirmedDeposit has sufficient confirmations — safe to credit

Transaction Events

EventDescription
transaction.initiatedOutbound transaction created and queued
transaction.submittedTransaction broadcast to the network
transaction.failedTransaction failed; funds returned to account
transaction.{type}.completedA 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.
EventDescription
card.createdA card was issued
card.activatedA card was activated (physical cards)
card.frozenA card was frozen
card.unfrozenA frozen card was re-enabled
card.lostA card was marked lost
card.stolenA card was reported stolen
card.voidedA card was voided
card.cancelledA card was cancelled
card.deactivatedA card was permanently deactivated
transaction.authorizedA card transaction was authorized
transaction.declinedA card transaction was declined
transaction.settledA card transaction settled
transaction.reversedA card transaction was reversed
transaction.refund.createdA refund was initiated
customer.fundedA customer’s card wallet was credited
customer.funding.failedA funding transfer failed
customer.balance.updatedA customer’s card balance changed
master_wallet.depositFunds arrived in the card program master wallet
master_wallet.swapA swap executed in the master wallet
master_wallet.customer_fundedMaster wallet funds allocated to a customer
See Card Issuer Webhook Events for full payload examples.

IBAN Events

EventDescription
iban.activatedIBAN account is active and ready to receive
iban.transfer.receivedIncoming bank transfer detected

KYC Events

EventDescription
kyc.submittedA KYC submission was received
kyc.approvedKYC was approved
kyc.rejectedKYC was rejected
kyc.pending_reviewKYC 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:
{
  "id": "evt_1716652800_abc123xyz",
  "type": "deposit.confirmed",
  "created_at": "2026-03-26T15:30:00Z",
  "data": {
    // Event-specific payload
  }
}
Request headers:
HeaderValue
X-Yativo-Signaturesha256=<hmac> — use this to verify authenticity
X-Yativo-TimestampUnix timestamp (seconds) — included in the HMAC
X-Yativo-EventEvent type string (e.g. deposit.confirmed)
X-Yativo-Delivery-IdUnique delivery ID for this attempt
Body fields:
FieldTypeDescription
idstringUnique event ID — use for idempotency
typestringEvent type
created_atISO 8601When the event was generated
dataobjectEvent-specific payload

Managing Webhooks

# 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