Build your integration, step by step
From your first API call to live payments: nine steps, in order, with the exact requests and what to check before moving on.
The path at a glance
Follow the steps in order. Each one ends with a checkpoint: when it passes, move on. Everything up to step 8 runs with your Test key, so no real money moves until you go live.
| Step | You do | Done when |
|---|---|---|
| 1. Keys | Store your Test key and API URL on your server | Your server can read both from its environment |
| 2. First call | Call GET /v1/banks | You get a 200 with the bank list |
| 3. Choose a flow | Pick hosted checkout or a direct method | You know which endpoint creates your payments |
| 4. First payment | Create a test payment and complete it | GET shows the payment COMPLETED |
| 5. Webhooks | Register an endpoint and verify signatures | payment.completed reaches your server and passes verification |
| 6. Safe retries | Add idempotency and recovery by reference | A retried request never creates a second payment |
| 7. Balances | Read your balance and the 24h hold | You can explain available, held and reserve |
| 8. Payouts | Withdraw or pay out from available funds | A test payout completes and payout.completed arrives |
| 9. Go live | Request review, then switch keys | Your first live payment completes |
Step 01
Get your keys and set up your environment
Your account has a Live and a Test key. Use the Test key for everything until step 9 — it works immediately, runs on sandboxes, and never moves real money. Copy it from Portal → Credentials.
API_URL=https://api.pay.vexwallet.co
VEXPAY_API_KEY=vk_test_replace_me
VEXPAY_WEBHOOK_SECRET=whsec_replace_meCheckpoint: your server reads API_URL and VEXPAY_API_KEY from its environment. More detail: Authentication.
Step 02
Make your first call
Every request sends the key in the x-api-key header. The bank list is a safe first call: it reads only and needs no setup.
curl "${API_URL}/v1/banks" \
--header "x-api-key: ${VEXPAY_API_KEY}"import VexPay from '@vexpay/node';
const vexpay = new VexPay(process.env.VEXPAY_API_KEY);
const banks = await vexpay.banks.list();import os
from vexpay import VexPay
vexpay = VexPay(os.environ["VEXPAY_API_KEY"])
banks = vexpay.banks.list()Checkpoint: a 200 response with the bank list. A 401 means the key is missing or wrong. An official SDK (Node, Python, PHP, Laravel) saves you writing HTTP, retries and webhook verification yourself.
Step 03
Choose how your buyers pay
Start with the hosted checkout unless you need your own payment screens: one request gives you a page where the buyer picks bolívares, stablecoins or Colombian pesos, and VEXPay runs every step.
| Option | Best when | Create with |
|---|---|---|
| Hosted checkout (recommended) | You want every method with no payment UI to build | POST /v1/checkout/sessions |
| Payment links | You sell without code (WhatsApp, social media) | Portal → Payment links |
| C2P | Your own screen; the buyer authorizes with a bank token | POST /v1/payments/c2p/request, then POST /v1/payments/c2p |
| Pago Móvil verify | The buyer already transferred and gives you the reference | POST /v1/payments/pago-movil/verify |
| Card (VPOS) | Card payments on your own form (PCI scope applies) | POST /v1/payments/vpos |
| USDT / USDC | Stablecoin deposits per customer | POST /v1/crypto/deposit-addresses |
| Colombian pesos | Buyers in Colombia (Bre-B, Nequi, Daviplata) | POST /v1/cop/payments |
Checkpoint: you know which endpoint creates your payments. GET /v1/payment-methods tells you which bolívar methods your account can offer right now.
Step 04
Create and complete a test payment
Create a checkout session for an order and send the buyer to its url (or embed it with @vexpay/js using clientSecret). Keep the session id with your order.
curl --request POST "${API_URL}/v1/checkout/sessions" \
--header "content-type: application/json" \
--header "x-api-key: ${VEXPAY_API_KEY}" \
--data '{
"amountUsd": 25.00,
"description": "Order #1042",
"reference": "order-1042",
"successUrl": "https://shop.example/thanks",
"cancelUrl": "https://shop.example/cart"
}'const session = await vexpay.checkout.sessions.create({
amountUsd: 25,
description: 'Order #1042',
reference: 'order-1042',
successUrl: 'https://shop.example/thanks',
cancelUrl: 'https://shop.example/cart',
});
// Redirect the buyer to session.url{
"id": "cs_4f0c6f1e2b7d4c1a9e3f5a6b7c8d9e0f",
"object": "checkout.session",
"status": "open",
"url": "https://pay.vexwallet.co/pay/cs-9f2a4b7c1d3e",
"clientSecret": "cs-9f2a4b7c1d3e_secret_k8Jc…",
"amountUsd": "25.00",
"reference": "order-1042",
"livemode": false
}- Open the url
In test mode the hosted page shows test controls instead of real bank details: complete C2P with the code from Portal → Sandbox, press Simular pago recibido for Pago Móvil, or use the test buttons on the USDT page.
- Check the result
GET /v1/checkout/sessions/:idshowsstatus: "paid"and thepaymentId.GET /v1/payments/:idshows the paymentCOMPLETED.
Checkpoint: the session is paid and the payment COMPLETED, all with livemode: false.
Step 05
Fulfill orders from signed webhooks
Fulfill only from the server side: a verified payment.completed webhook (or a GET that shows the payment completed). Browser redirects and @vexpay/js events are hints, never proof.
curl --request POST "${API_URL}/v1/webhooks" \
--header "content-type: application/json" \
--header "x-api-key: ${VEXPAY_API_KEY}" \
--data '{
"url": "https://shop.example/webhooks/vexpay",
"events": ["payment.completed", "payment.failed", "payment.reversed", "payout.completed", "payout.failed"]
}'app.post('/webhooks/vexpay', express.raw({ type: 'application/json' }), async (req, res) => {
const event = vexpay.webhooks.constructEvent(
req.body, // raw bytes, not parsed JSON
req.headers['vexpay-signature'],
process.env.VEXPAY_WEBHOOK_SECRET,
);
if (event.event === 'payment.completed') {
// Checkout payments: match your order on data.checkoutSession.reference
await markOrderPaid(event.data.checkoutSession?.reference, event.data.paymentId);
}
res.sendStatus(200);
});- Save the
secretfrom the create response — it is never shown again. - Deduplicate on the
VexPay-Event-Idheader: the same event can reach you more than once. - Each event is sent once, with no automatic retry, and your endpoint has 10 seconds. Queue the work and answer 200 quickly; step 6 covers what you missed.
- Use
POST /v1/notifications/testto send yourself a test delivery.
Checkpoint: a test payment produces a payment.completed that passes signature verification and marks the order paid exactly once. More detail: Webhooks.
Step 06
Make retries safe and recover what you missed
- Send Idempotency-Key on every POST
Use your order id. The same key and body within 24h replays the first response (
Idempotent-Replayed: true) instead of creating a second payment. The SDKs send one automatically. - Store the ids you get back
Keep the session id,
paymentIdandpayoutIdwith your order. For payouts and merchants,externalRefis itself the idempotency key. - Recover by reference
If a response or a webhook never arrived, look the payment up:
GET /v1/payments/by-ref/:externalReforGET /v1/checkout/sessions/:id. Decide from its status, never from a timeout. - Run a reconcile job
Once a day (or hourly), walk
GET /v1/balance/transactionsand check every movement against your books. The sum always equals your ledger total, so nothing can hide.
Checkpoint: retrying the same create request twice gives the same payment, and a missed webhook is caught by the lookup or the reconcile job. More detail: Idempotency, Reconcile.
Step 07
Read your balance and the 24h hold
Every completed payment lands in your balance, but it is held for 24 hours before it can be paid out. GET /v1/balance shows both parts and when the next held amount becomes available.
{
"ledgerNetVes": "7342.50",
"reserveVes": "0.00",
"heldVes": "4342.50",
"nextReleaseAt": "2026-10-20T00:48:52.784Z",
"availableVes": "3000.00",
"balanceHoldHours": 24
}availableVes = ledgerNetVes − reserveVes − heldVes. Only availableVes can be paid out. The same applies to USDT (held, heldUsdt) and pesos (heldCop). Checkpoint: after a test payment you can point at the held amount and its release time. Full explanation: Balances, holds and payouts.
Step 08
Pay out
| Where the money goes | Endpoint | Before the first payout |
|---|---|---|
| Your own bank account (Bs) | POST /v1/payouts/instant | Save and verify your payout account (two micro-deposits) |
| A seller or supplier (Bs) | POST /v1/payouts | Register the merchant and verify its Pago Móvil destination |
| A crypto wallet (USDT / USDC) | POST /v1/crypto/payouts | Have a USDT or USDC balance |
curl --request POST "${API_URL}/v1/payouts/instant" \
--header "content-type: application/json" \
--header "x-api-key: ${VEXPAY_API_KEY}" \
--data '{ "monto": "1000.00", "concepto": "Retiro", "externalRef": "withdraw-2026-10-19" }'Checkpoint: a test payout completes and payout.completed reaches your endpoint. More detail: Merchants & payouts, USDT & USDC.
Step 09
Go live
- Steps 4–8 pass with the Test key, including at least one webhook delivery (Portal → Webhooks shows it).
- Your business profile and owner contact are complete in the portal.
- You handle
payment.failed,payment.reversedandpayout.failed, not only the happy path. - Your reconcile job runs and alerts you when totals disagree.
- Request go-live
Click Request go-live in the portal. The VEX Pay team reviews your account; until then live keys answer 403
live_mode_not_activated. - Register live webhooks
Webhook endpoints belong to each key: register your production URL with the Live key and store its new secret.
- Switch keys
When you receive
tenant.live_status_changed, create a live key in the portal and deploy it. Make one small real payment and follow it to your balance.