Accept and send USDT and USDC
Give each customer a stablecoin deposit address, accept USDT or USDC at checkout, and send them to external wallets. Each coin settles in itself — it is never converted to bolívares or to the other coin.
How USDT works
- USDT has its own balance (
GET /v1/crypto/balance), separate from your VES balance. 1 USDT is treated as 1 USD. - USDT you receive is held for 24 hours before it can be paid out: the balance reports it as
held(heldUsdt) withnextReleaseAt, andavailableexcludes it. See Balances, holds and payouts. - Receive with a static deposit address per customer, or with the
usdtmethod on checkout sessions and payment links. - Send with
POST /v1/crypto/payouts. Payouts within your limits go out automatically; larger ones are reviewed first. - Every movement is reported with signed webhooks:
payment.completedfor money in,payout.completed/payout.failedfor money out. - Want USDT from the bolívares you collect? Convert your VES balance with
POST /v1/conversions— see Convert VES to USDT.
Pricing
| Movement | Fee |
|---|---|
| USDT received (deposits and checkout) | 2.5% + 0.50 USDT, deducted from the amount received (never more than the amount) |
| USDT payout to an external address | The network fee for the chosen network + 0.25 USDT (e.g. 1.00 USDT on BEP20, Polygon and Solana; see GET /v1/crypto/networks) |
| USDT payout to one of your own deposit addresses | Free — settled inside VEXPay, nothing is sent on-chain |
Example: a customer sends 40.00 USDT → fee 1.50 → 38.50 USDT credited.
Networks
List the enabled networks, their payout fee and whether a payout of a given amount can be sent right now. Prefer the cheaper networks (BEP20, Polygon) when your customer can use them; TRC20 is the most expensive to send.
curl "${API_URL}/v1/crypto/networks?amountUsdt=50.00" \
--header "x-api-key: ${VEXPAY_API_KEY}"| network | Chain |
|---|---|
| TRC20 | Tron |
| BEP20 | BNB Smart Chain |
| POLYGON | Polygon |
| SOL | Solana |
| TON | TON (needs a tag / memo) |
| ARB1 | Arbitrum One |
Step 01
Deposit addresses
customerRef is your id for the customer. Calling again for the same customer and network returns the same address, so you can call it every time you show the deposit screen.
curl -X POST "${API_URL}/v1/crypto/deposit-addresses" \
--header "x-api-key: ${VEXPAY_API_KEY}" \
--header "content-type: application/json" \
--data '{ "customerRef": "user_123", "network": "BEP20" }'const address = await vexpay.crypto.depositAddresses.create({
customerRef: 'user_123',
network: 'BEP20',
});
// address.address, address.tag (TON only)address = vexpay.crypto.deposit_addresses.create({"customerRef": "user_123", "network": "BEP20"})When USDT arrives you receive payment.completed with method: "USDT", customerRef, amountUsdt, feeUsdt, network and txHashes. Credit your customer amountUsdt − feeUsdt, once per paymentId.
Step 02
USDT at checkout
Add usdt to methods when you create a checkout session (or leave methods empty to offer everything your account accepts). The buyer picks a network on the hosted page and pays the exact amount; the session becomes paid when the payment arrives.
curl -X POST "${API_URL}/v1/checkout/sessions" \
--header "x-api-key: ${VEXPAY_API_KEY}" \
--header "content-type: application/json" \
--data '{ "amountUsd": 25.00, "methods": ["usdt", "c2p"], "reference": "order-1042" }'The payment.completed webhook for a session payment carries checkoutSession: { id, reference, metadata } — match your order on checkoutSession.reference (the payment's own externalRef belongs to the session). The VEXPay WooCommerce plugin already offers USDT this way whenever it is enabled on your account.
Step 03
USDT payouts
Amount + fee are reserved from your available USDT balance immediately (USDT still under the 24h hold cannot be paid out). idempotencyKey is required: repeating the same request returns the same payout (200); reusing the key with different details answers 409 idempotency_key_reused.
curl -X POST "${API_URL}/v1/crypto/payouts" \
--header "x-api-key: ${VEXPAY_API_KEY}" \
--header "content-type: application/json" \
--data '{
"network": "POLYGON",
"address": "0x9f3c4e1b2a7d6c5e4f3a2b1c0d9e8f7a6b5c4d3e",
"amountUsdt": "50.00",
"customerRef": "user_123",
"idempotencyKey": "withdrawal_8812"
}'const payout = await vexpay.crypto.payouts.create({
network: 'POLYGON',
address: '0x9f3c…',
amountUsdt: '50.00',
idempotencyKey: 'withdrawal_8812',
});| status | Meaning |
|---|---|
| pending | Reserved; waiting for review (over your limits) or for network capacity |
| processing | Sent to the network |
| completed | Delivered — txHash is set (internal: true when settled inside VEXPay) |
| failed | Not sent — amount + fee returned to your balance; see failureReason |
Errors: 422 insufficient_balance (with held, heldUsdt and nextReleaseAt when part of your balance is still on hold), 422 amount_below_minimum, 400 invalid_address, 400 unsupported_network.
Step 04
USDC
USDC works exactly like USDT, with the same endpoints, fees, webhooks and payout review. Pass currency: "USDC" (body) or ?currency=USDC (query); without it every endpoint keeps working in USDT, unchanged.
- Networks: POLYGON and BASE only. BASE carries USDC only; a USDT request on BASE answers 400
unsupported_network. - Separate balance: USDC has its own balance (
GET /v1/crypto/balance?currency=USDC). USDC payouts never draw on USDT and USDT payouts never draw on USDC.GET /v1/crypto/balancesreturns both. - Enabled separately: USDC must be enabled on your account on its own. Having USDT does not enable USDC; until it is enabled, USDC requests answer 403
method_not_allowed. - Same pay-in fee as USDT (2.5% + 0.50, charged in USDC). USDC payouts cost 0.75 USDC on BASE and Polygon (0.50 network fee + 0.25);
GET /v1/crypto/networks?currency=USDClists them. - Payout limits are shared: USDT and USDC payouts count together, 1:1 in USD, against the same per-payout and daily limits.
- Any other
currencyvalue answers 400unsupported_currency.
curl -X POST "${API_URL}/v1/crypto/deposit-addresses" \
--header "x-api-key: ${VEXPAY_API_KEY}" \
--header "content-type: application/json" \
--data '{ "customerRef": "user_123", "network": "POLYGON", "currency": "USDC" }'curl -X POST "${API_URL}/v1/crypto/payouts" \
--header "x-api-key: ${VEXPAY_API_KEY}" \
--header "content-type: application/json" \
--data '{
"currency": "USDC",
"network": "BASE",
"address": "0x9f3c4e1b2a7d6c5e4f3a2b1c0d9e8f7a6b5c4d3e",
"amount": "50.00",
"idempotencyKey": "withdrawal_8813"
}'const usdc = await vexpay.crypto.balance.retrieve({ currency: 'USDC' });
const all = await vexpay.crypto.balances.list();
await vexpay.crypto.payouts.create({
currency: 'USDC',
network: 'BASE',
address: '0x9f3c…',
amount: '50.00',
idempotencyKey: 'withdrawal_8813',
});Use amount for USDC payouts. amountUsdt is accepted only as an alias for USDT payouts; sending both with different values answers 400 invalid_data. Every crypto response now includes currency, amount and fee for both assets; the *Usdt fields are returned for USDT only, with their existing meaning. A payout's currency is part of its idempotency check: reusing an idempotencyKey with another currency answers 409 idempotency_key_reused.
At checkout, add usdc to methods (or leave methods empty). When both stablecoins are available the buyer picks the coin, then the network. A link or session limited to ["usdt"] never offers USDC.
Webhooks
| Event | When | Key fields |
|---|---|---|
| payment.completed | USDT or USDC received (deposit address or checkout) | method and currency ("USDT" / "USDC"), amount, fee, network, txHashes, customerRef, underpaid; USDT also amountUsdt, feeUsdt |
| payout.completed | Payout delivered | payoutId, currency, amount, fee, txHash, internal, idempotencyKey; USDT also amountUsdt, feeUsdt |
| payout.failed | Payout not sent; refunded | payoutId, currency, failureReason, idempotencyKey |
Testing
With a test key, the hosted USDT page shows test buttons (confirm / cancel / insufficient) so you can drive each outcome without real funds. Test payments never credit a live account. Test payouts complete right away with payout.completed (no txHash): nothing is sent on-chain and no limits or approval apply.