This guide walks you through the full flow for accepting crypto deposits: creating a custodial account, generating wallet addresses per chain, registering webhooks, and crediting user accounts when deposits confirm.
Test this entire flow against the Sandbox at https://crypto-sandbox.yativo.com/api/ before going live. Sandbox deposits are simulated — no real funds are used.
Overview
The deposit flow has four components:
- Account — a logical container for assets belonging to one user or entity
- Wallet addresses — per-chain addresses derived from the account
- Webhooks — real-time notifications when deposits are detected and confirmed
- Business logic — your code credits the user after confirming the deposit
Create an account
Each user or entity in your system maps to a Yativo account. Create one at onboarding time and store the returned accountId.curl -X POST https://crypto-api.yativo.com/api/accounts/create-account \
-H "Authorization: Bearer eyJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJ1c3JfMDFIWVo..." \
-H "Content-Type: application/json" \
-d '{
"label": "alice_account",
"metadata": {
"userId": "usr_01HYZPQR3NMVK8F2GXB7T9D4CE",
"email": "alice@example.com"
}
}'
Response:{
"accountId": "acc_01J2K9MNPQ3R4S5T6U7V8W9X0Y",
"label": "alice_account",
"status": "active",
"metadata": {
"userId": "usr_01HYZPQR3NMVK8F2GXB7T9D4CE",
"email": "alice@example.com"
},
"createdAt": "2026-03-26T10:00:00Z"
}
Store accountId in your database alongside the user record. You will reference it for every wallet and transaction operation.
Create wallet addresses per chain
For each blockchain you want to support, add an asset to the account. This returns a deposit address on that chain.# Ethereum (ERC-20 / ETH)
curl -X POST https://crypto-api.yativo.com/api/assets/add-asset \
-H "Authorization: Bearer eyJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJ1c3JfMDFIWVo..." \
-H "Content-Type: application/json" \
-d '{
"accountId": "acc_01J2K9MNPQ3R4S5T6U7V8W9X0Y",
"chain": "ethereum",
"asset": "USDC"
}'
Response (Ethereum example):{
"assetId": "ast_01J2KBMNPQ7R4S5T6U7V8W9XEth",
"accountId": "acc_01J2K9MNPQ3R4S5T6U7V8W9X0Y",
"chain": "ethereum",
"asset": "USDC",
"address": "0x742d35Cc6634C0532925a3b8D4C9D5b9a1f4e8Bd",
"balance": "0.000000",
"createdAt": "2026-03-26T10:01:00Z"
}
Register webhooks for deposit events
Register a webhook endpoint to receive deposit.detected (immediate, unconfirmed) and deposit.confirmed (safe to credit) events.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"],
"description": "Crypto deposit notifications"
}'
Response:{
"webhookId": "wh_01J2KCMNPQ3R4S5T6U7VWEBHK",
"url": "https://api.yourapp.com/webhooks/yativo",
"events": ["deposit.detected", "deposit.confirmed"],
"status": "active",
"secret": "whsec_a8f3c2e1d4b7f9e2c5a3b6d8f1e4c7a2b5d8e1f4",
"createdAt": "2026-03-26T10:02:00Z"
}
Store secret securely (e.g., in an environment variable). It is only returned once and is used to verify webhook signatures. See Step 6 for verification details. Display wallet addresses to users
Fetch the wallet addresses for an account and present them in your UI. Users send crypto to these addresses.// In your backend — return wallet addresses to your frontend
app.get("/api/deposit-addresses/:userId", async (req, res) => {
const user = await db.users.findById(req.params.userId);
const assets = await client.assets.listByAccount(user.yativoAccountId);
const addresses = assets.map((a) => ({
chain: a.chain,
asset: a.asset,
address: a.address,
network: a.networkLabel, // "Ethereum Mainnet", "Polygon", etc.
}));
res.json({ addresses });
});
Show a QR code for each address in your UI — most crypto wallets can scan them directly.
Handle webhook events and credit user accounts
When a deposit confirms, your webhook handler receives a deposit.confirmed event. Verify the signature (next step), then credit the user.Here is a complete Express.js webhook handler:TypeScript (Express webhook handler)
import express from "express";
import crypto from "crypto";
const app = express();
// Use raw body for signature verification
app.use(
"/webhooks/yativo",
express.raw({ type: "application/json" }),
async (req, res) => {
// 1. Verify signature immediately
const signature = req.headers["x-webhook-signature"] as string;
const secret = process.env.YATIVO_WEBHOOK_SECRET!;
const expectedSig = crypto
.createHmac("sha256", secret)
.update(req.body)
.digest("hex");
if (
!signature ||
!crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expectedSig)
)
) {
return res.status(401).json({ error: "Invalid signature" });
}
// 2. Parse event
const event = JSON.parse(req.body.toString());
// 3. Return 200 immediately — process async
res.sendStatus(200);
// 4. Process asynchronously
setImmediate(async () => {
try {
await processEvent(event);
} catch (err) {
console.error("Failed to process event", event.eventId, err);
}
});
}
);
async function processEvent(event: any) {
// Idempotency: skip already-processed events
const alreadyProcessed = await db.events.exists(event.eventId);
if (alreadyProcessed) return;
switch (event.type) {
case "deposit.detected":
// Optionally notify user of pending deposit — don't credit yet
await notifyUserPendingDeposit(event.data);
break;
case "deposit.confirmed":
const { accountId, amount, asset, chain, txHash } = event.data;
// Look up the user by accountId
const user = await db.users.findByYativoAccountId(accountId);
if (!user) {
console.warn("No user found for accountId", accountId);
return;
}
// Credit the user
await db.balances.credit(user.id, asset, amount);
await db.transactions.record({
userId: user.id,
type: "deposit",
asset,
chain,
amount,
txHash,
yativoEventId: event.eventId,
});
await notifyUserDepositConfirmed(user, amount, asset);
break;
}
// Mark event as processed
await db.events.markProcessed(event.eventId);
}
Verify webhook signatures
All webhook requests from Yativo include an X-Webhook-Signature header containing an HMAC-SHA256 signature of the raw request body.function verifyWebhookSignature(
rawBody: Buffer,
signatureHeader: string,
secret: string
): boolean {
const expected = crypto
.createHmac("sha256", secret)
.update(rawBody)
.digest("hex");
// Use timing-safe comparison to prevent timing attacks
return crypto.timingSafeEqual(
Buffer.from(signatureHeader),
Buffer.from(expected)
);
}
Webhook Payload: deposit.confirmed
{
"eventId": "evt_01J2KF3MNPQ7R4S5T6U7VDPST1",
"type": "deposit.confirmed",
"createdAt": "2026-03-26T10:15:32Z",
"data": {
"depositId": "dep_01J2KF3MNPQ7R4S5T6U7VDEP42",
"accountId": "acc_01J2K9MNPQ3R4S5T6U7V8W9X0Y",
"assetId": "ast_01J2KBMNPQ7R4S5T6U7V8W9XEth",
"chain": "ethereum",
"asset": "USDC",
"amount": "250.000000",
"address": "0x742d35Cc6634C0532925a3b8D4C9D5b9a1f4e8Bd",
"txHash": "0x4a7d3f2e1c8b5a9e2f4d7c3b6a1e8d2f5c4b3a7e1d9f2c5b8a4e3d7c1b6a2f",
"blockNumber": 19847201,
"confirmations": 12,
"networkFee": "0.001823",
"metadata": {
"userId": "usr_01HYZPQR3NMVK8F2GXB7T9D4CE"
}
}
}
Next Steps
- See Webhook Integration for the full webhook reference including all event types and retry policy.
- Use Send Crypto to move funds back out on behalf of users.