Skip to main content
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/. 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
  • Read the raw request body (before JSON parsing) for signature verification
  • 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.raw({ type: "application/json" }),
  async (req, res) => {
    // Verify signature (see Step 3)
    const signature = req.headers["x-webhook-signature"] as string;
    const isValid = verifySignature(req.body, signature, process.env.YATIVO_WEBHOOK_SECRET!);

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

    // Respond immediately
    res.sendStatus(200);

    // Process asynchronously
    const event = JSON.parse(req.body.toString());
    setImmediate(() => handleEvent(event).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/webhook/create \
  -H "Authorization: Bearer eyJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJ1c3JfMDFIWVo..." \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://api.yourapp.com/webhooks/yativo",
    "events": [
      "deposit.detected",
      "deposit.confirmed",
      "transaction.confirmed",
      "transaction.failed",
      "swap.completed",
      "swap.failed"
    ],
    "description": "Production webhook"
  }'
Response:
{
  "webhookId": "wh_01J2KY3MNPQ7R4S5T6U7VWEBHK",
  "url": "https://api.yourapp.com/webhooks/yativo",
  "events": ["deposit.detected", "deposit.confirmed", "transaction.confirmed", "transaction.failed", "swap.completed", "swap.failed"],
  "status": "active",
  "secret": "whsec_a8f3c2e1d4b7f9e2c5a3b6d8f1e4c7a2b5d8e1f4",
  "createdAt": "2026-03-26T15:00:00Z"
}
The secret is returned only once at creation time. Store it immediately in a secure environment variable or secrets manager. If you lose it, delete the webhook and create a new one.
3

Verify webhook signatures

Every webhook request includes an X-Webhook-Signature header containing an HMAC-SHA256 hex digest of the raw request body, signed with your webhook secret.Always verify this signature before processing the event.
TypeScript
import crypto from "crypto";

function verifySignature(
  rawBody: Buffer,
  signatureHeader: string,
  secret: string
): boolean {
  if (!signatureHeader) return false;

  const expected = crypto
    .createHmac("sha256", secret)
    .update(rawBody)
    .digest("hex");

  // Use timing-safe comparison to prevent timing attacks
  try {
    return crypto.timingSafeEqual(
      Buffer.from(signatureHeader, "hex"),
      Buffer.from(expected, "hex")
    );
  } 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.eventId);
  if (processed) {
    console.log(`Event ${event.eventId} already processed — skipping`);
    return;
  }

  // 2. Process the event
  switch (event.type) {
    case "deposit.confirmed":
      await handleDepositConfirmed(event.data);
      break;
    case "transaction.confirmed":
      await handleTransactionConfirmed(event.data);
      break;
    case "transaction.failed":
      await handleTransactionFailed(event.data);
      break;
    case "swap.completed":
      await handleSwapCompleted(event.data);
      break;
    case "swap.failed":
      await handleSwapFailed(event.data);
      break;
    case "card.transaction.approved":
      await handleCardTransaction(event.data);
      break;
    default:
      console.log(`Unhandled event type: ${event.type}`);
  }

  // 3. Mark as processed
  await db.webhookEvents.insert({
    id: event.eventId,
    type: event.type,
    processedAt: new Date(),
  });
}
Store processed event IDs in a database table with a unique index on eventId. 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.raw({ type: "application/json" }), async (req, res) => {
  // Signature verification is fast — do it synchronously
  if (!verifySignature(req.body, req.headers["x-webhook-signature"] as string, secret)) {
    return res.status(401).send();
  }

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

  // Enqueue for async processing (use a job queue in production)
  const event = JSON.parse(req.body.toString());
  await jobQueue.enqueue("process_webhook_event", event);
});
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 events from the Dashboard or via POST /webhook/{webhookId}/replay.
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

Wallet & Deposit Events

EventDescription
deposit.detectedA deposit transaction was seen on-chain (unconfirmed)
deposit.confirmedDeposit has sufficient confirmations — safe to credit
deposit.failedA detected deposit was not confirmed (e.g., dropped from mempool)

Transaction Events

EventDescription
transaction.pendingOutbound transaction created and queued
transaction.broadcastingBeing submitted to the blockchain
transaction.confirmedTransaction confirmed on-chain
transaction.failedTransaction failed; funds returned to account

Swap Events

EventDescription
swap.initiatedSwap execution started
swap.completedDestination asset arrived in account
swap.failedSwap failed; see failureReason
swap.refundedSwap failed after source funds left; original asset returned

Card Events

EventDescription
card.transaction.approvedAuthorization approved
card.transaction.declinedAuthorization declined (includes declineReason)
card.transaction.completedSettlement cleared
card.transaction.reversedTransaction reversed or refunded
card.funding.completedCard top-up completed
card.spending_limit.reachedCard spending limit hit

KYC & Customer Events

EventDescription
kyc.status_changedKYC status updated (includes new status)
customer.kyc.approvedCustomer KYC approved (issuer program)
customer.kyc.rejectedCustomer KYC rejected

IBAN Events

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

Issuer Program Events

EventDescription
card_issuer.application_approvedIssuer program application approved
card_issuer.application_rejectedIssuer program application rejected
card_issuer.limit_warningApproaching program volume limits

Webhook Event Envelope

All events share a common envelope:
{
  "eventId": "evt_01J2KZ3MNPQ7R4S5T6U7VEVT99",
  "type": "deposit.confirmed",
  "webhookId": "wh_01J2KY3MNPQ7R4S5T6U7VWEBHK",
  "createdAt": "2026-03-26T15:30:00Z",
  "attempt": 1,
  "data": {
    // Event-specific payload
  }
}
FieldTypeDescription
eventIdstringUnique event ID — use for idempotency
typestringEvent type
webhookIdstringID of the registered webhook
createdAtISO 8601When the event was generated
attemptintegerDelivery attempt number (starts at 1)
dataobjectEvent-specific payload

Managing Webhooks

TypeScript
// List all registered webhooks
const webhooks = await client.webhooks.list();

// Update a webhook's events or URL
await client.webhooks.update("wh_01J2KY3MNPQ7R4S5T6U7VWEBHK", {
  events: ["deposit.confirmed", "transaction.confirmed"],
});

// Disable a webhook temporarily
await client.webhooks.setStatus("wh_01J2KY3MNPQ7R4S5T6U7VWEBHK", "inactive");

// Delete a webhook
await client.webhooks.delete("wh_01J2KY3MNPQ7R4S5T6U7VWEBHK");

// Replay a failed event
await client.webhooks.replayEvent("wh_01J2KY3MNPQ7R4S5T6U7VWEBHK", "evt_01J2KZ3MNPQ7R4S5T6U7VEVT99");