Developer guide
VEXPay siteAPI reference
Browse documentation

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) with nextReleaseAt, and available excludes it. See Balances, holds and payouts.
  • Receive with a static deposit address per customer, or with the usdt method 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.completed for money in, payout.completed / payout.failed for money out.
  • Want USDT from the bolívares you collect? Convert your VES balance with POST /v1/conversions — see Convert VES to USDT.

Pricing

MovementFee
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 addressThe 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 addressesFree — 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.

curlbash
curl "${API_URL}/v1/crypto/networks?amountUsdt=50.00" \
  --header "x-api-key: ${VEXPAY_API_KEY}"
networkChain
TRC20Tron
BEP20BNB Smart Chain
POLYGONPolygon
SOLSolana
TONTON (needs a tag / memo)
ARB1Arbitrum 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.

curlbash
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" }'
Node SDKjavascript
const address = await vexpay.crypto.depositAddresses.create({
  customerRef: 'user_123',
  network: 'BEP20',
});
// address.address, address.tag (TON only)
Python SDKpython
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.

curlbash
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.

curlbash
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"
  }'
Node SDKjavascript
const payout = await vexpay.crypto.payouts.create({
  network: 'POLYGON',
  address: '0x9f3c…',
  amountUsdt: '50.00',
  idempotencyKey: 'withdrawal_8812',
});
statusMeaning
pendingReserved; waiting for review (over your limits) or for network capacity
processingSent to the network
completedDelivered — txHash is set (internal: true when settled inside VEXPay)
failedNot 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/balances returns 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=USDC lists 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 currency value answers 400 unsupported_currency.
Deposit addressbash
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" }'
Payoutbash
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"
  }'
Node SDKjavascript
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

EventWhenKey fields
payment.completedUSDT or USDC received (deposit address or checkout)method and currency ("USDT" / "USDC"), amount, fee, network, txHashes, customerRef, underpaid; USDT also amountUsdt, feeUsdt
payout.completedPayout deliveredpayoutId, currency, amount, fee, txHash, internal, idempotencyKey; USDT also amountUsdt, feeUsdt
payout.failedPayout not sent; refundedpayoutId, 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.