Recipe: accept payments in an online store
Sell to Venezuelan buyers with one hosted checkout: bolívares, stablecoins and pesos, confirmed by webhook and withdrawn to your bank.
What you will build
A store where each order opens a hosted checkout, the order is marked paid from a verified webhook, and the money is withdrawn to your own bank account. No payment screens to build or certify.
- Buyer clicks Pay
Your server creates a checkout session for the order and redirects the buyer.
- Buyer pays on VEXPay
The buyer picks Pago Móvil, C2P, card, USDT/USDC or pesos and completes it there.
- You fulfill
payment.completed reaches your webhook; you mark the order paid.
- You get paid
After the 24h hold, you withdraw your bolívares to your bank account.
Step 01
Create a session per order
app.post('/checkout/:orderId', async (req, res) => {
const order = await orders.get(req.params.orderId);
const session = await vexpay.checkout.sessions.create(
{
amountUsd: order.totalUsd,
description: `Order #${order.number}`,
reference: order.id,
metadata: { orderNumber: String(order.number) },
successUrl: `https://shop.example/orders/${order.id}/thanks`,
cancelUrl: 'https://shop.example/cart',
},
{ idempotencyKey: `checkout-${order.id}` },
);
await orders.update(order.id, { checkoutSessionId: session.id });
res.redirect(303, session.url);
});- Price in USD. Bolívar methods convert at your Bolívar rate; pesos at VEX Pay’s USDT/COP rate.
- Leave
methodsout to offer everything your account accepts, or pass e.g.["pago_movil", "usdt"]. - The idempotency key makes a double click open the same session, not two.
Step 02
Mark the order paid 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 (await seen(req.headers['vexpay-event-id'])) return res.sendStatus(200);
const ref = event.data.checkoutSession?.reference;
if (event.event === 'payment.completed' && ref) await orders.markPaid(ref, event.data.paymentId);
if (event.event === 'payment.reversed' && ref) await orders.flagReversed(ref);
res.sendStatus(200);
});Step 03
Catch anything you missed
Webhooks are delivered once. Every few minutes, look up sessions still waiting on your side and settle them from their real status.
for (const order of await orders.awaitingPayment({ olderThanMinutes: 5 })) {
const s = await vexpay.checkout.sessions.retrieve(order.checkoutSessionId);
if (s.status === 'paid') await orders.markPaid(order.id, s.paymentId);
if (s.status === 'expired' || s.status === 'canceled') await orders.release(order.id);
}Step 04
Withdraw to your bank
- Verify your payout account once
Save your bank, cédula/RIF and phone with PUT /v1/tenant/payout-account, then confirm the two micro-deposits (Portal → Payouts does both).
- Wait for the hold
Each sale becomes available 24 hours after it completes. GET /v1/balance shows availableVes and nextReleaseAt.
- Withdraw
POST /v1/payouts/instant with the amount, or press Withdraw in the portal. The bolívar percentage applies to the amount withdrawn.
Crypto sales stay in your USDT balance until you send them with POST /v1/crypto/payouts. Peso sales stay in your COP balance.