This guide covers the full flow for sending crypto from a Yativo account to an external wallet: estimating gas fees, executing the transfer with an idempotency key, and tracking the transaction to completion.
Test this flow in the Sandbox at https://crypto-sandbox.yativo.com/api/v1/. Sandbox transactions are simulated — no real funds move.
Get a gas fee estimate
Before sending, retrieve the current gas fee estimate for the target chain. This lets you show users an accurate fee breakdown and choose a priority level.curl -X POST https://crypto-api.yativo.com/api/v1/transactions/get-gas-price \
-H "Authorization: Bearer eyJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJ1c3JfMDFIWVo..." \
-H "Content-Type: application/json" \
-d '{
"chainType": "ethereum",
"priority": "medium",
"token_symbol": "USDC"
}'
import { YativoCrypto } from "@yativo/crypto-sdk";
const client = new YativoCrypto({
apiKey: process.env.YATIVO_API_KEY!,
baseUrl: "https://crypto-api.yativo.com/api/v1/",
});
const gasEstimate = await client.transactions.getGasPrice({
chain: "ethereum",
asset: "USDC",
});
console.log(gasEstimate);
/*
{
chain: "ethereum",
slow: { gasPrice: "18.2", estimatedFeeUsd: "0.82", estimatedTimeSeconds: 120 },
medium: { gasPrice: "22.5", estimatedFeeUsd: "1.02", estimatedTimeSeconds: 30 },
fast: { gasPrice: "31.0", estimatedFeeUsd: "1.40", estimatedTimeSeconds: 10 },
nativeAsset: "ETH",
updatedAt: "2026-03-26T11:00:00Z"
}
*/
from yativo_crypto import YativoCrypto
client = YativoCrypto(
api_key=os.environ["YATIVO_API_KEY"],
base_url="https://crypto-api.yativo.com/api/v1/",
)
gas_estimate = client.transactions.get_gas_price(
chain="ethereum",
asset="USDC",
)
print(gas_estimate.medium.estimated_fee_usd) # "1.02"
Response:{
"chain": "ethereum",
"slow": {
"gasPrice": "18.2",
"estimatedFeeUsd": "0.82",
"estimatedTimeSeconds": 120
},
"medium": {
"gasPrice": "22.5",
"estimatedFeeUsd": "1.02",
"estimatedTimeSeconds": 30
},
"fast": {
"gasPrice": "31.0",
"estimatedFeeUsd": "1.40",
"estimatedTimeSeconds": 10
},
"nativeAsset": "ETH",
"updatedAt": "2026-03-26T11:00:00Z"
}
Priority levels
| Priority | Speed | Use case |
|---|
slow | 1–3 min | Non-urgent payouts, batch processing |
medium | 15–30 sec | Default for most use cases |
fast | 5–15 sec | Time-sensitive transactions |
Send funds with an idempotency key
Use POST /transactions/send-funds to initiate the transfer. Always include an Idempotency-Key header to safely retry without double-spending.curl -X POST https://crypto-api.yativo.com/api/v1/transactions/send-funds \
-H "Authorization: Bearer eyJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJ1c3JfMDFIWVo..." \
-H "Content-Type: application/json" \
-H "Idempotency-Key: payout_01J2KG9MNPQ3R4S5T6UPAY001" \
-d '{
"account": "64b1f9e2a3c4d5e6f7a8b9c0",
"assets": "64b1f9e2a3c4d5e6f7a8b9d1",
"receiving_address": "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045",
"amount": 100,
"type": "USDC",
"chain": "ethereum",
"category": "payment",
"priority": "medium",
"description": "Payout for order #ORD-9821"
}'
import { v4 as uuidv4 } from "uuid";
// Generate a stable idempotency key tied to your payout record
const idempotencyKey = `payout_${payoutRecord.id}`;
const transaction = await client.transactions.sendFunds(
{
account: "64b1f9e2a3c4d5e6f7a8b9c0",
assets: "64b1f9e2a3c4d5e6f7a8b9d1",
receiving_address: "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045",
amount: 100,
type: "USDC",
chain: "ethereum",
category: "payment",
priority: "medium",
description: "Payout for order #ORD-9821",
},
{ idempotencyKey }
);
console.log(transaction.data.transaction_id); // txn_01HX9KZMB3F7VNQP8R2WDGT4E5
console.log(transaction.data.transaction_hash); // 0x9f3e2d...
transaction = client.transactions.send_funds(
account="64b1f9e2a3c4d5e6f7a8b9c0",
assets="64b1f9e2a3c4d5e6f7a8b9d1",
receiving_address="0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045",
amount=100,
type="USDC",
chain="ethereum",
category="payment",
priority="medium",
description="Payout for order #ORD-9821",
idempotency_key=f"payout_{payout_record.id}",
)
print(transaction.data.transaction_id) # txn_01HX9KZMB3F7VNQP8R2WDGT4E5
print(transaction.data.transaction_hash) # 0x9f3e2d...
Response:{
"status": true,
"message": "Transaction Created Successfully",
"data": {
"transaction_id": "txn_01HX9KZMB3F7VNQP8R2WDGT4E5",
"transaction_hash": "0x9f3e2d1c4b7a5e8f2c9b3d6a1e4c7f2b5d8e1c4a",
"gas_tx_hash": null,
"platform_fee_tx_hash": null,
"gas_amount": "0",
"platform_fee": "0.50000000",
"gas_funding_markup": "0.12000000",
"total_fee": "0.62000000",
"fee_breakdown": "N/A"
}
}
Idempotency keys
An idempotency key is a unique string you attach to a request. If the request fails or times out, you can safely retry with the same key — Yativo will return the original response instead of creating a duplicate transaction. If no key is supplied, no deduplication is applied.Best practices:
- Derive the key from your internal record ID (e.g.,
payout_${payoutId})
- Keys must be unique per operation — reusing a key for a different transfer will return the original transaction
- Keys expire after 24 hours
Poll for transaction status
You can poll for the current transaction status using POST /transactions/get-transactions, filtering by transaction_id.curl -X POST https://crypto-api.yativo.com/api/v1/transactions/get-transactions \
-H "Authorization: Bearer eyJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJ1c3JfMDFIWVo..." \
-H "Content-Type: application/json" \
-d '{
"search": "txn_01J2KH3MNPQ7R4S5T6U7VSND99"
}'
// Poll with exponential backoff
async function waitForTransaction(transactionId: string): Promise<any> {
const maxAttempts = 20;
let delay = 2000; // start at 2 seconds
for (let attempt = 0; attempt < maxAttempts; attempt++) {
const result = await client.transactions.getTransactions({ search: transactionId });
const txn = result.data?.[0];
if (!txn) throw new Error('Transaction not found');
if (txn.status === 'completed') return txn;
if (txn.status === 'failed') throw new Error(`Transaction failed: ${txn.error_message}`);
await new Promise((resolve) => setTimeout(resolve, delay));
delay = Math.min(delay * 1.5, 30000); // cap at 30s
}
throw new Error('Transaction did not complete within timeout');
}
const completed = await waitForTransaction('txn_01J2KH3MNPQ7R4S5T6U7VSND99');
console.log(completed.transaction_hash); // 0x9f3e2d1c4b7a5e8f2c9b3d6a1e4c7f2b5d8e1c4a...
import time
def wait_for_transaction(transaction_id: str) -> dict:
max_attempts = 20
delay = 2.0
for attempt in range(max_attempts):
result = client.transactions.get_transactions(search=transaction_id)
txn = result.data[0] if result.data else None
if not txn:
raise Exception('Transaction not found')
if txn.status == 'completed':
return txn
if txn.status == 'failed':
raise Exception(f'Transaction failed: {txn.error_message}')
time.sleep(delay)
delay = min(delay * 1.5, 30.0)
raise TimeoutError('Transaction did not complete within timeout')
completed = wait_for_transaction('txn_01J2KH3MNPQ7R4S5T6U7VSND99')
print(completed.transaction_hash)
Response (completed):{
"status": true,
"message": "Transactions fetched successfully",
"data": [
{
"transaction_id": "txn_01J2KH3MNPQ7R4S5T6U7VSND99",
"transaction_hash": "0x9f3e2d1c4b7a5e8f2c9b3d6a1e4c7f2b5d8e1c4a7f6e3d2b1a8c5f4e3d2c1b",
"receiving_address": "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045",
"amount": "100",
"type": "USDC",
"chain": "ethereum",
"status": "completed",
"blockchain_status": "confirmed",
"block_number": 19847345,
"confirmations": 12,
"fee_paid": "0.62000000",
"fee_currency": "USDC",
"category": "payment",
"description": "Payout for order #ORD-9821",
"createdAt": "2026-03-26T11:05:42.000Z"
}
]
}
Transaction statuses
| Status | Description |
|---|
pending | Submitted, waiting to be broadcast |
completed | Included in a block with sufficient confirmations |
failed | Transaction failed (see error_message) |
Handle completion via webhook (recommended)
Rather than polling, register a webhook for transaction.{type}.completed and transaction.failed events to receive push notifications when transactions settle.TypeScript (webhook handler)
case "transaction.withdrawal.completed": {
const { transactionId, toAddress, amount, asset, txHash } = event.data;
await db.payouts.markCompleted(transactionId, {
txHash,
confirmedAt: event.created_at,
});
await notifyUserPayoutSent({ toAddress, amount, asset });
break;
}
case "transaction.failed": {
const { transactionId, failureReason } = event.data;
await db.payouts.markFailed(transactionId, failureReason);
// Refund or retry logic here
break;
}
Common Errors
| Error | Cause | Fix |
|---|
Assets required | assets body field missing | Include the asset ObjectId in the request body |
Receiving address required | receiving_address missing | Include the destination address |
Valid amount required | amount missing, zero, or negative | Provide a positive numeric amount |
Insufficient balance | Asset balance too low | Check balance before sending |
Withdrawal amount must be greater than total fees | Amount too small to cover platform fee + gas markup | Increase amount or check fee estimates first |
Self funding is only available for native token transactions | use_self_funding: true on a token asset | Remove use_self_funding for token withdrawals |
Transaction blocked due to high-risk receiving address | Chainalysis/Circle screening blocked the address | Do not send to flagged addresses |
DUPLICATE_REQUEST_IN_PROGRESS | Same Idempotency-Key submitted while first request is still processing | Wait for the first request to complete before retrying |
Next Steps
- See Webhook Integration for the full
transaction.withdrawal.completed and transaction.failed event payloads.
- Use Swap Tokens to exchange assets before sending.