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.
- Have USDT enabled
Conversions are on automatically for every account that has USDT enabled. Without USDT every conversion endpoint answers 403
conversions_not_enabled. - 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.
- 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
COMPLETEDin the same response; otherwise it isPENDINGuntil VEXPay delivers the USDT. The amounts are fixed — later market moves never change them. - Receive the USDT
VEXPay delivers the USDT into your USDT balance, the conversion becomes
COMPLETEDand you receive theconversion.completedwebhook. - 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
| Status | What it means | Your VES | Your USDT | Webhook |
|---|---|---|---|---|
| PENDING | You accepted a quote; VEXPay is delivering the USDT (skipped when it completes in the same request) | Already debited | Not yet credited | — (the POST response) |
| COMPLETED | The USDT was delivered (final) | Debited | Credited with targetAmountUsdt | conversion.completed |
| CANCELED | You canceled it, or VEXPay could not fulfil it (final) | Returned in full | Unchanged | conversion.canceled |
Example with a 2% spread and a market rate of 979.2450 (rate 998.8299), converting 10,000.00 VES:
| Moment | availableVes | convertedVes | USDT available |
|---|---|---|---|
| Before | 25,000.00 | 0.00 | 0.00 |
| After POST /v1/conversions (PENDING) | 15,000.00 | 10,000.00 | 0.00 |
| After conversion.completed | 15,000.00 | 10,000.00 | 10.01 |
| …or after conversion.canceled | 25,000.00 | 0.00 | 0.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
rateis 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.
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" }'const quote = await vexpay.conversions.quotes.create({ sourceCurrency: 'COP', sourceAmount: '1000000' });
const conversion = await vexpay.conversions.create({ quoteId: quote.id }, { idempotencyKey: 'convert-cop-001' });quote = vexpay.conversions.quotes.create({"sourceCurrency": "COP", "sourceAmount": "1000000"})$quote = $vexpay->conversions->quotes->create(['sourceCurrency' => 'COP', 'sourceAmount' => '1000000']);{
"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"
}rateis 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/balancedrops). Cancel a pending conversion and the COP comes back. The daily COP limit counts from 00:00 Bogotá time. sourceAmountVesisnullon COP quotes and conversions; readsourceCurrencyandsourceAmountinstead.GET /v1/conversions?sourceCurrency=COPlists 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.
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 } } }'await vexpay.conversions.settings.update({ autoConvert: { COP: { percent: 50 } } });
const settings = await vexpay.conversions.settings.retrieve();vexpay.conversions.settings.update({"autoConvert": {"COP": {"percent": 50}}})$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.completedfirst, thenconversion.createdwithorigin: "auto"and thepaymentId, usually within a minute. Thenconversion.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/settingsalso shows your spreads, minimum, daily limits and which currencies you can convert. OnlyautoConvertcan 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)
- 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.
- Enter an amount
Choose "Spend VES" or "Receive USDT", type the amount and click Get quote.
- Confirm within 60 seconds
Check the rate and both amounts, then click Confirm conversion. If the countdown runs out, click Refresh quote.
- 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.
curl -X POST "${API_URL}/v1/conversions/quotes" \
--header "x-api-key: ${VEXPAY_API_KEY}" \
--header "content-type: application/json" \
--data '{ "sourceAmountVes": "10000.00" }'const quote = await vexpay.conversions.quotes.create({ sourceAmountVes: '10000.00' });
// or: { targetAmountUsdt: '50.00' }quote = vexpay.conversions.quotes.create({"sourceAmountVes": "10000.00"})$quote = $vexpay->conversions->quotes->create(['sourceAmountVes' => '10000.00']);{
"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.
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" }'const conversion = await vexpay.conversions.create(
{ quoteId: quote.id, reference: 'treasury-2026-10-03' },
{ idempotencyKey: 'convert-2026-10-03-001' },
);conversion = vexpay.conversions.create(
{"quoteId": quote.id, "reference": "treasury-2026-10-03"},
idempotency_key="convert-2026-10-03-001",
)$conversion = $vexpay->conversions->create(
['quoteId' => $quote->id, 'reference' => 'treasury-2026-10-03'],
['idempotency_key' => 'convert-2026-10-03-001'],
);{
"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.
{
"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"
}| Event | What to do |
|---|---|
| conversion.completed | Mark the conversion done in your system. targetAmountUsdt is now in your USDT balance and can be paid out. |
| conversion.created | A conversion started: you accepted a quote, or auto-convert converted part of a COP payment (origin: "auto", paymentId). The source is already debited. |
| conversion.canceled | Mark 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.
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 });
}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, readavailableVesin 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
referencemeaningful — it is the easiest way to match conversions to your books.
Errors
| Status | error | Meaning |
|---|---|---|
| 400 | invalid_amount | Send exactly one positive amount with at most 2 decimals (COP: whole pesos). |
| 400 | invalid_data | Unknown sourceCurrency, or a settings field you cannot change. |
| 403 | conversions_not_enabled | Conversions (or USDT, or COP for COP conversions) are not enabled for your account. |
| 403 | live_mode_not_activated | Your live account is not approved yet; use test mode. |
| 404 | not_found | Unknown quote or conversion (or it belongs to another account). |
| 409 | quote_used | That quote was already accepted. |
| 409 | conversion_not_cancelable | Only a PENDING conversion can be canceled. |
| 410 | quote_expired | More than 60 seconds passed — request a new quote. |
| 422 | amount_below_minimum | Below your minimum USDT per conversion. |
| 422 | insufficient_balance | Your available VES does not cover the amount. |
| 422 | daily_limit_exceeded | The conversion would exceed your daily VES limit. |
| 503 | rate_unavailable | No 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
| Question | Answer |
|---|---|
| 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. |