Developer guide
VEXPay siteAPI reference
Browse documentation

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.

StepYou doDone when
1. KeysStore your Test key and API URL on your serverYour server can read both from its environment
2. First callCall GET /v1/banksYou get a 200 with the bank list
3. Choose a flowPick hosted checkout or a direct methodYou know which endpoint creates your payments
4. First paymentCreate a test payment and complete itGET shows the payment COMPLETED
5. WebhooksRegister an endpoint and verify signaturespayment.completed reaches your server and passes verification
6. Safe retriesAdd idempotency and recovery by referenceA retried request never creates a second payment
7. BalancesRead your balance and the 24h holdYou can explain available, held and reserve
8. PayoutsWithdraw or pay out from available fundsA test payout completes and payout.completed arrives
9. Go liveRequest review, then switch keysYour 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.

.env (server only)bash
API_URL=https://api.pay.vexwallet.co
VEXPAY_API_KEY=vk_test_replace_me
VEXPAY_WEBHOOK_SECRET=whsec_replace_me

Checkpoint: 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.

curlbash
curl "${API_URL}/v1/banks" \
  --header "x-api-key: ${VEXPAY_API_KEY}"
Node SDKjavascript
import VexPay from '@vexpay/node';

const vexpay = new VexPay(process.env.VEXPAY_API_KEY);
const banks = await vexpay.banks.list();
Python SDKpython
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.

OptionBest whenCreate with
Hosted checkout (recommended)You want every method with no payment UI to buildPOST /v1/checkout/sessions
Payment linksYou sell without code (WhatsApp, social media)Portal → Payment links
C2PYour own screen; the buyer authorizes with a bank tokenPOST /v1/payments/c2p/request, then POST /v1/payments/c2p
Pago Móvil verifyThe buyer already transferred and gives you the referencePOST /v1/payments/pago-movil/verify
Card (VPOS)Card payments on your own form (PCI scope applies)POST /v1/payments/vpos
USDT / USDCStablecoin deposits per customerPOST /v1/crypto/deposit-addresses
Colombian pesosBuyers 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.

curlbash
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"
  }'
Node SDKjavascript
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
Responsejson
{
  "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
}
  1. 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.

  2. Check the result

    GET /v1/checkout/sessions/:id shows status: "paid" and the paymentId. GET /v1/payments/:id shows the payment COMPLETED.

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.

Register (curl)bash
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"]
  }'
Verify (Node)javascript
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 secret from the create response — it is never shown again.
  • Deduplicate on the VexPay-Event-Id header: 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/test to 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

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

  2. Store the ids you get back

    Keep the session id, paymentId and payoutId with your order. For payouts and merchants, externalRef is itself the idempotency key.

  3. Recover by reference

    If a response or a webhook never arrived, look the payment up: GET /v1/payments/by-ref/:externalRef or GET /v1/checkout/sessions/:id. Decide from its status, never from a timeout.

  4. Run a reconcile job

    Once a day (or hourly), walk GET /v1/balance/transactions and 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.

GET /v1/balance (excerpt)json
{
  "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 goesEndpointBefore the first payout
Your own bank account (Bs)POST /v1/payouts/instantSave and verify your payout account (two micro-deposits)
A seller or supplier (Bs)POST /v1/payoutsRegister the merchant and verify its Pago Móvil destination
A crypto wallet (USDT / USDC)POST /v1/crypto/payoutsHave a USDT or USDC balance
Withdraw to your account (curl)bash
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.reversed and payout.failed, not only the happy path.
  • Your reconcile job runs and alerts you when totals disagree.
  1. 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.

  2. Register live webhooks

    Webhook endpoints belong to each key: register your production URL with the Live key and store its new secret.

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