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.
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 (Express)
Python (Flask)
PHP
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));
}
);
from flask import Flask, request, jsonify
import hmac
import hashlib
import json
import threading
import time
app = Flask(__name__)
@app.route("/webhooks/yativo", methods=["POST"])
def webhook():
signature = request.headers.get("X-Yativo-Signature", "")
timestamp = request.headers.get("X-Yativo-Timestamp", "")
payload = request.get_json(force=True)
if not verify_webhook_signature(payload, signature, timestamp, os.environ["YATIVO_WEBHOOK_SECRET"]):
return jsonify({"error": "Invalid signature"}), 401
# Respond immediately, process in background
thread = threading.Thread(target=handle_event, args=(payload,))
thread.daemon = True
thread.start()
return "", 200
<?php
$payload = json_decode(file_get_contents("php://input"), true);
$signature = $_SERVER["HTTP_X_YATIVO_SIGNATURE"] ?? "";
$timestamp = $_SERVER["HTTP_X_YATIVO_TIMESTAMP"] ?? "";
$secret = getenv("YATIVO_WEBHOOK_SECRET");
if (!verifyWebhookSignature($payload, $signature, $timestamp, $secret)) {
http_response_code(401);
echo json_encode(["error" => "Invalid signature"]);
exit;
}
// Acknowledge immediately
http_response_code(200);
echo "OK";
// Flush output and continue processing
if (function_exists("fastcgi_finish_request")) {
fastcgi_finish_request();
}
handleEvent($payload);
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.
Verify webhook signatures
Every delivery includes two headers:| Header | Value |
|---|
X-Yativo-Signature | sha256=<hmac> |
X-Yativo-Timestamp | Unix timestamp (seconds) |
The HMAC is computed over "${timestamp}.${JSON.stringify(payload)}". Always verify this signature before processing the event.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;
}
}
import hmac
import hashlib
import json
import time
def verify_webhook_signature(payload: dict, signature_header: str, timestamp: str, secret: str) -> bool:
if not signature_header or not timestamp:
return False
# Reject replays older than 5 minutes
if abs(time.time() - int(timestamp)) > 300:
return False
signed_payload = f"{timestamp}.{json.dumps(payload, separators=(',', ':'))}"
expected = "sha256=" + hmac.new(
secret.encode("utf-8"),
signed_payload.encode("utf-8"),
hashlib.sha256,
).hexdigest()
return hmac.compare_digest(signature_header, expected)
function verifyWebhookSignature(array $payload, string $signatureHeader, string $timestamp, string $secret): bool {
if (empty($signatureHeader) || empty($timestamp)) {
return false;
}
// Reject replays older than 5 minutes
if (abs(time() - intval($timestamp)) > 300) {
return false;
}
$signedPayload = $timestamp . '.' . json_encode($payload);
$expected = 'sha256=' . hash_hmac('sha256', $signedPayload, $secret);
return hash_equals($expected, $signatureHeader);
}
Always use a timing-safe comparison function (timingSafeEqual, hmac.compare_digest, hash_equals). Using a regular string equality check (===) exposes you to timing attacks.
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.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.
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.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. Understand the retry policy
If your endpoint returns a non-2xx status code, times out, or is unreachable, Yativo retries the delivery with exponential backoff:| Attempt | Delay after previous |
|---|
| 1 (initial) | — |
| 2 | 1 minute |
| 3 | 5 minutes |
| 4 | 30 minutes |
| 5 | 2 hours |
| 6 | 6 hours |
| 7 | 24 hours |
After 7 failed attempts, the event is marked as permanently failed and no further retries are made. You can manually replay failed deliveries via POST /v1/yativo-card/webhooks/deliveries/:deliveryId/retry.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
| Event | Description |
|---|
deposit.detected | A deposit transaction was seen on-chain (unconfirmed) |
deposit.confirmed | Deposit has sufficient confirmations — safe to credit |
Transaction Events
| Event | Description |
|---|
transaction.initiated | Outbound transaction created and queued |
transaction.submitted | Transaction broadcast to the network |
transaction.failed | Transaction failed; funds returned to account |
transaction.{type}.completed | A transaction of the given type completed (e.g. transaction.withdrawal.completed) |
Card Events
Card issuer events and general crypto events share the same webhook service. Register at POST /v1/webhook/create-webhook and include the relevant card event slugs in your events array.
| Event | Description |
|---|
card.created | A card was issued |
card.activated | A card was activated (physical cards) |
card.frozen | A card was frozen |
card.unfrozen | A frozen card was re-enabled |
card.lost | A card was marked lost |
card.stolen | A card was reported stolen |
card.voided | A card was voided |
card.cancelled | A card was cancelled |
card.deactivated | A card was permanently deactivated |
transaction.authorized | A card transaction was authorized |
transaction.declined | A card transaction was declined |
transaction.settled | A card transaction settled |
transaction.reversed | A card transaction was reversed |
transaction.refund.created | A refund was initiated |
customer.funded | A customer’s card wallet was credited |
customer.funding.failed | A funding transfer failed |
customer.balance.updated | A customer’s card balance changed |
master_wallet.deposit | Funds arrived in the card program master wallet |
master_wallet.swap | A swap executed in the master wallet |
master_wallet.customer_funded | Master wallet funds allocated to a customer |
See Card Issuer Webhook Events for full payload examples.
IBAN Events
| Event | Description |
|---|
iban.activated | IBAN account is active and ready to receive |
iban.transfer.received | Incoming bank transfer detected |
KYC Events
| Event | Description |
|---|
kyc.submitted | A KYC submission was received |
kyc.approved | KYC was approved |
kyc.rejected | KYC was rejected |
kyc.pending_review | KYC is awaiting manual review |
Webhook Event Envelope
All events share a common envelope. The request body is JSON; delivery metadata is carried in HTTP headers.
Request body:
{
"id": "evt_1716652800_abc123xyz",
"type": "deposit.confirmed",
"created_at": "2026-03-26T15:30:00Z",
"data": {
// Event-specific payload
}
}
Request headers:
| Header | Value |
|---|
X-Yativo-Signature | sha256=<hmac> — use this to verify authenticity |
X-Yativo-Timestamp | Unix timestamp (seconds) — included in the HMAC |
X-Yativo-Event | Event type string (e.g. deposit.confirmed) |
X-Yativo-Delivery-Id | Unique delivery ID for this attempt |
Body fields:
| Field | Type | Description |
|---|
id | string | Unique event ID — use for idempotency |
type | string | Event type |
created_at | ISO 8601 | When the event was generated |
data | object | Event-specific payload |
Managing Webhooks
# 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