Developer guide
VEXPay siteAPI reference
Browse documentation

Convert your VES or COP balance to USDT

Turn the bolívares or Colombian pesos you collect with VEXPay into USDT — from the portal or with the API, on demand or automatically for each COP payment. Quote a rate, accept it, and receive the USDT in your VEXPay USDT balance.

How it works

Every payment you collect in bolívares adds to your available VES balance (GET /v1/balance). A conversion moves part of that balance into your USDT balance (GET /v1/crypto/balance) at a rate you see and accept first. No bank transfer is involved and you never send VEXPay anything: the VES is already in your VEXPay balance.

  1. Have USDT enabled

    Conversions are on automatically for every account that has USDT enabled. Without USDT every conversion endpoint answers 403 conversions_not_enabled.

  2. Quote

    Ask for a quote with the VES you want to spend or the USDT you want to receive. The quote shows the exact rate and both amounts, and is valid for 60 seconds.

  3. Convert

    Accept the quote. The VES leaves your balance immediately — conversions may use bolívares that are still under the 24h hold; the USDT bought with them stays held until that hold ends. The conversion is often COMPLETED in the same response; otherwise it is PENDING until VEXPay delivers the USDT. The amounts are fixed — later market moves never change them.

  4. Receive the USDT

    VEXPay delivers the USDT into your USDT balance, the conversion becomes COMPLETED and you receive the conversion.completed webhook.

  5. Use it

    The USDT stays in your VEXPay USDT balance. Send it to any wallet with a USDT payout (POST /v1/crypto/payouts) whenever you want.

Statuses and your balances

StatusWhat it meansYour VESYour USDTWebhook
PENDINGYou accepted a quote; VEXPay is delivering the USDT (skipped when it completes in the same request)Already debitedNot yet credited— (the POST response)
COMPLETEDThe USDT was delivered (final)DebitedCredited with targetAmountUsdtconversion.completed
CANCELEDYou canceled it, or VEXPay could not fulfil it (final)Returned in fullUnchangedconversion.canceled

Example with a 2% spread and a market rate of 979.2450 (rate 998.8299), converting 10,000.00 VES:

MomentavailableVesconvertedVesUSDT available
Before25,000.000.000.00
After POST /v1/conversions (PENDING)15,000.0010,000.000.00
After conversion.completed15,000.0010,000.0010.01
…or after conversion.canceled25,000.000.000.00

convertedVes in GET /v1/balance is the net VES you have converted (canceled conversions are subtracted back). Conversions and VES payouts draw on the same available balance and are debited atomically, so the two can never spend the same bolívares.

The rate and what it costs

  • rate is VES per 1 USDT: the current market USDT/VES rate (marketRate) plus your spread (spreadPercent). It is not the BCV official rate.
  • The spread is the whole price of the conversion — there is no separate fee. What the quote shows is exactly what you pay and receive.
  • USDT you receive is rounded down to the cent; VES you pay is rounded up to the cent.
  • If no current market rate is available, quoting answers 503 rate_unavailable — retry in a few minutes.
  • Your account may have a minimum per conversion (amount_below_minimum) and a daily VES limit (daily_limit_exceeded, counted from 00:00 Caracas time).

Example: market 979.2450 + 2% → rate 998.8299. 10,000.00 VES → 10.01 USDT. 50.00 USDT → 49,941.50 VES.

Convert COP to USDT

The pesos you collect with Bre-B, Nequi and Daviplata sit in your COP balance (GET /v1/cop/balance). Convert them with the same endpoints: add "sourceCurrency": "COP" and send sourceAmount (whole pesos) or targetAmountUsdt. Your account needs USDT and COP enabled.

curlbash
curl -X POST "${API_URL}/v1/conversions/quotes" \
  --header "x-api-key: ${VEXPAY_API_KEY}" \
  --header "content-type: application/json" \
  --data '{ "sourceCurrency": "COP", "sourceAmount": "1000000" }'
Node SDKjavascript
const quote = await vexpay.conversions.quotes.create({ sourceCurrency: 'COP', sourceAmount: '1000000' });
const conversion = await vexpay.conversions.create({ quoteId: quote.id }, { idempotencyKey: 'convert-cop-001' });
Python SDKpython
quote = vexpay.conversions.quotes.create({"sourceCurrency": "COP", "sourceAmount": "1000000"})
PHP SDKphp
$quote = $vexpay->conversions->quotes->create(['sourceCurrency' => 'COP', 'sourceAmount' => '1000000']);
Response (201)json
{
  "id": "0c7d3f2e-8a1b-4c5d-9e6f-1a2b3c4d5e6f",
  "object": "conversion_quote",
  "rate": "4037.6000",
  "marketRate": "3920.0000",
  "spreadPercent": "3.0000",
  "rateSource": "market",
  "sourceCurrency": "COP",
  "sourceAmount": "1000000",
  "sourceAmountVes": null,
  "targetAmountUsdt": "247.67",
  "expiresAt": "2026-10-09T15:01:00.000Z",
  "createdAt": "2026-10-09T15:00:00.000Z"
}
  • rate is COP per 1 USDT: the market USDT/COP rate plus your COP spread (it can differ from your VES spread).
  • COP you pay is rounded up to the whole peso; USDT you receive is rounded down to the cent. Example: market 3,920 + 3% → rate 4037.6000; 1,000,000 COP → 247.67 USDT; 50.00 USDT → 201,880 COP.
  • Accepting debits your available COP at once (GET /v1/cop/balance drops). Cancel a pending conversion and the COP comes back. The daily COP limit counts from 00:00 Bogotá time.
  • sourceAmountVes is null on COP quotes and conversions; read sourceCurrency and sourceAmount instead. GET /v1/conversions?sourceCurrency=COP lists only COP conversions.
  • A COP refund still needs enough available COP. Pesos you already converted are not available for refunds, so keep some COP if you expect refunds.

Auto-convert COP payments

Instead of quoting yourself, let VEXPay convert a share of every COP payment as it completes. Set a whole-number percentage from 0 (off, the default) to 100. When a COP payment completes, VEXPay converts that share of the net amount (after the COP fee) at the current rate plus your spread.

curlbash
curl -X PATCH "${API_URL}/v1/conversions/settings" \
  --header "x-api-key: ${VEXPAY_API_KEY}" \
  --header "content-type: application/json" \
  --data '{ "autoConvert": { "COP": { "percent": 50 } } }'
Node SDKjavascript
await vexpay.conversions.settings.update({ autoConvert: { COP: { percent: 50 } } });
const settings = await vexpay.conversions.settings.retrieve();
Python SDKpython
vexpay.conversions.settings.update({"autoConvert": {"COP": {"percent": 50}}})
PHP SDKphp
$vexpay->conversions->settings->update(['autoConvert' => ['COP' => ['percent' => 50]]]);
  • Example: a 100,000 COP payment with the 4.5% + COP 1,500 fee nets 94,000 COP. At 50%, 47,000 COP is converted (11.64 USDT at a rate of 4037.6000) and 47,000 COP stays in your COP balance.
  • You receive payment.completed first, then conversion.created with origin: "auto" and the paymentId, usually within a minute. Then conversion.completed, as for any conversion. Auto-convert never delays or changes the payment.
  • It applies only to payments completed after you turn it on. A payment whose share is below your USDT minimum, or that would go over your daily COP limit, is not converted: the pesos stay in COP.
  • If no market rate is available, VEXPay retries for up to 24 hours. After that, the pesos stay in COP.
  • You can cancel a pending auto-conversion like any other conversion.
  • GET /v1/conversions/settings also shows your spreads, minimum, daily limits and which currencies you can convert. Only autoConvert can be changed through the API; contact VEXPay for the rest.
  • In the portal, open COP: the "Auto-convert COP payments" card sets the same percentage.

From the portal (no code)

  1. Open USDT (or COP)

    In the VEXPay portal, open USDT. When USDT is enabled you see the "Convert VES → USDT" card with your available VES and your minimum. For pesos, open COP: the "Convert COP → USDT" card works the same way.

  2. Enter an amount

    Choose "Spend VES" or "Receive USDT", type the amount and click Get quote.

  3. Confirm within 60 seconds

    Check the rate and both amounts, then click Confirm conversion. If the countdown runs out, click Refresh quote.

  4. Follow it

    The conversion appears in the history below the card as pending, then completed (the USDT shows in your USDT balance) or canceled (the VES is back). You can cancel it from there while it is pending.

Step 01

Quote

Send exactly one of sourceAmountVes (VES to spend) or targetAmountUsdt (USDT to receive), as a string with up to 2 decimals.

curlbash
curl -X POST "${API_URL}/v1/conversions/quotes" \
  --header "x-api-key: ${VEXPAY_API_KEY}" \
  --header "content-type: application/json" \
  --data '{ "sourceAmountVes": "10000.00" }'
Node SDKjavascript
const quote = await vexpay.conversions.quotes.create({ sourceAmountVes: '10000.00' });
// or: { targetAmountUsdt: '50.00' }
Python SDKpython
quote = vexpay.conversions.quotes.create({"sourceAmountVes": "10000.00"})
PHP SDKphp
$quote = $vexpay->conversions->quotes->create(['sourceAmountVes' => '10000.00']);
Response (201)json
{
  "id": "6f1c2b0e-4c1d-4a8e-9a51-0f4a1e9a0001",
  "object": "conversion_quote",
  "rate": "998.8299",
  "marketRate": "979.2450",
  "spreadPercent": "2.0000",
  "rateSource": "market",
  "sourceAmountVes": "10000.00",
  "targetAmountUsdt": "10.01",
  "expiresAt": "2026-10-03T15:01:00.000Z",
  "createdAt": "2026-10-03T15:00:00.000Z"
}

A quote is a price, not a reservation: it does not hold your VES. Show it to the person deciding, and accept it only when they confirm.

Step 02

Convert

Accept the quote before expiresAt. Always send an Idempotency-Key header (the SDKs send one per call for you): if your request times out, retry with the same key and you get the same conversion back instead of converting twice.

curlbash
curl -X POST "${API_URL}/v1/conversions" \
  --header "x-api-key: ${VEXPAY_API_KEY}" \
  --header "Idempotency-Key: convert-2026-10-03-001" \
  --header "content-type: application/json" \
  --data '{ "quoteId": "6f1c2b0e-4c1d-4a8e-9a51-0f4a1e9a0001", "reference": "treasury-2026-10-03" }'
Node SDKjavascript
const conversion = await vexpay.conversions.create(
  { quoteId: quote.id, reference: 'treasury-2026-10-03' },
  { idempotencyKey: 'convert-2026-10-03-001' },
);
Python SDKpython
conversion = vexpay.conversions.create(
    {"quoteId": quote.id, "reference": "treasury-2026-10-03"},
    idempotency_key="convert-2026-10-03-001",
)
PHP SDKphp
$conversion = $vexpay->conversions->create(
    ['quoteId' => $quote->id, 'reference' => 'treasury-2026-10-03'],
    ['idempotency_key' => 'convert-2026-10-03-001'],
);
Response (201)json
{
  "id": "b7e3a9d2-5f60-4c2b-8f0e-2d1c3b4a5e6f",
  "object": "conversion",
  "status": "PENDING",
  "reference": "treasury-2026-10-03",
  "rate": "998.8299",
  "marketRate": "979.2450",
  "spreadPercent": "2.0000",
  "rateSource": "market",
  "sourceAmountVes": "10000.00",
  "targetAmountUsdt": "10.01",
  "createdAt": "2026-10-03T15:00:20.000Z",
  "completedAt": null,
  "canceledAt": null,
  "cancelReason": null
}

reference is optional and yours (up to 128 characters): use it to tie the conversion to your own record. It comes back on every read and webhook.

Step 03

Handle the outcome

Subscribe your webhook endpoint to conversion.completed and conversion.canceled (POST /v1/webhooks or the portal), and to conversion.created if you use auto-convert. Each carries the conversion in data (with sourceCurrency, sourceAmount, origin and paymentId), plus livemode, and is signed like every other VEXPay webhook.

conversion.completedjson
{
  "event": "conversion.completed",
  "data": {
    "id": "b7e3a9d2-5f60-4c2b-8f0e-2d1c3b4a5e6f",
    "object": "conversion",
    "status": "COMPLETED",
    "reference": "treasury-2026-10-03",
    "rate": "998.8299",
    "sourceAmountVes": "10000.00",
    "targetAmountUsdt": "10.01",
    "completedAt": "2026-10-03T16:12:05.000Z",
    "livemode": true
  },
  "timestamp": "2026-10-03T16:12:05.000Z"
}
EventWhat to do
conversion.completedMark the conversion done in your system. targetAmountUsdt is now in your USDT balance and can be paid out.
conversion.createdA conversion started: you accepted a quote, or auto-convert converted part of a COP payment (origin: "auto", paymentId). The source is already debited.
conversion.canceledMark it canceled. sourceAmount is back in your available VES or COP. cancelReason is canceled_by_tenant when you canceled it, otherwise the reason VEXPay could not fulfil it. Quote again if you still want USDT.

Each event is delivered once and is not retried, so also reconcile: list your pending conversions (GET /v1/conversions?status=PENDING) on a schedule and read any you have not seen finish (GET /v1/conversions/{id}). Process events idempotently by conversion id.

Changed your mind? Cancel a pending conversion with POST /v1/conversions/{id}/cancel — the VES returns immediately. Once it is COMPLETED or CANCELED it can no longer be canceled (409 conversion_not_cancelable).

Complete example

Convert a fixed share of today's VES, store the conversion, and finish it from the webhook.

Node (@vexpay/node)javascript
import VexPay from '@vexpay/node';

const vexpay = new VexPay(process.env.VEXPAY_API_KEY);

// 1. Convert (e.g. from a daily job)
export async function convertVes(amountVes, runId) {
  const quote = await vexpay.conversions.quotes.create({ sourceAmountVes: amountVes });
  console.log(`${quote.sourceAmountVes} VES → ${quote.targetAmountUsdt} USDT at ${quote.rate}`);

  const conversion = await vexpay.conversions.create(
    { quoteId: quote.id, reference: runId },
    { idempotencyKey: `convert-${runId}` },
  );
  await db.conversions.insert({ id: conversion.id, status: conversion.status, usdt: conversion.targetAmountUsdt });
}

// 2. Finish from the webhook
app.post('/webhooks/vexpay', express.raw({ type: 'application/json' }), async (req, res) => {
  const event = vexpay.webhooks.constructEvent(req.body, req.headers['vexpay-signature'], process.env.VEXPAY_WEBHOOK_SECRET);
  if (event.event === 'conversion.completed' || event.event === 'conversion.canceled') {
    await db.conversions.update(event.data.id, { status: event.data.status });
  }
  res.sendStatus(200);
});

// 3. Reconcile (e.g. every hour)
for await (const c of vexpay.conversions.list({ status: 'PENDING' })) {
  await db.conversions.update(c.id, { status: c.status });
}
Python (vexpay)python
from vexpay import VexPay, Webhook

vexpay = VexPay(os.environ["VEXPAY_API_KEY"])

def convert_ves(amount_ves: str, run_id: str):
    quote = vexpay.conversions.quotes.create({"sourceAmountVes": amount_ves})
    conversion = vexpay.conversions.create(
        {"quoteId": quote.id, "reference": run_id},
        idempotency_key=f"convert-{run_id}",
    )
    db.save_conversion(conversion.id, conversion.status, conversion.targetAmountUsdt)

def on_webhook(raw_body: bytes, signature: str):
    event = Webhook.construct_event(raw_body, signature, os.environ["VEXPAY_WEBHOOK_SECRET"])
    if event.event in ("conversion.completed", "conversion.canceled"):
        db.update_conversion(event.data["id"], event.data["status"])

After the conversion: using your USDT

  • The USDT is in your VEXPay USDT balance (GET /v1/crypto/balance → availableUsdt), together with any USDT you received from customers.
  • Send it to any wallet with POST /v1/crypto/payouts (network fee applies; see USDT & USDC). Payouts within your limits go out automatically; larger ones are reviewed first.
  • A conversion never sends USDT to a wallet by itself, and USDT is never converted back to VES.

Good practices

  • Quote right before you convert. On 410 quote_expired, request a new quote and show the new amounts — never accept a rate the user has not seen.
  • Use one Idempotency-Key per intended conversion (e.g. your run or order id) and reuse it on retries.
  • On 422 insufficient_balance, read availableVes in the error and quote a smaller amount; VES payouts and other conversions may have used the balance in between.
  • Treat the USDT as yours only after conversion.completed.
  • Keep your reference meaningful — it is the easiest way to match conversions to your books.

Errors

StatuserrorMeaning
400invalid_amountSend exactly one positive amount with at most 2 decimals (COP: whole pesos).
400invalid_dataUnknown sourceCurrency, or a settings field you cannot change.
403conversions_not_enabledConversions (or USDT, or COP for COP conversions) are not enabled for your account.
403live_mode_not_activatedYour live account is not approved yet; use test mode.
404not_foundUnknown quote or conversion (or it belongs to another account).
409quote_usedThat quote was already accepted.
409conversion_not_cancelableOnly a PENDING conversion can be canceled.
410quote_expiredMore than 60 seconds passed — request a new quote.
422amount_below_minimumBelow your minimum USDT per conversion.
422insufficient_balanceYour available VES does not cover the amount.
422daily_limit_exceededThe conversion would exceed your daily VES limit.
503rate_unavailableNo current market rate; retry shortly.

Testing

Conversions are on for your test account as long as USDT is enabled. With a test key, conversions complete immediately: the test VES is debited, your test USDT balance is credited and conversion.completed is sent, so you can build and test the whole flow — including your webhook handler — before going live. Test conversions use the real market rate but never move real money.

FAQ

QuestionAnswer
Is the rate the BCV rate?No. It is the current market USDT/VES rate plus your spread.
Can the amount change after I accept?No. Both amounts are fixed when you accept the quote.
Are there other fees?No. The spread is the price of the conversion. Sending USDT out later costs the network fee of the payout.
Can I convert USDT back to VES?No. Conversions only go from VES or COP to USDT; there is no USDT → VES off-ramp. Pago Móvil and bank payouts (POST /v1/payouts) are paid only from your VES balance, which comes from the bolívares you collect, so USDT cannot fund them. If you need an off-ramp, contact VEX Pay.
Can VEXPay send the USDT straight to my wallet?Not as part of the conversion. Once it is in your USDT balance, create a USDT payout.
How do I get it enabled or change my spread or limits?Contact VEX Pay.