Developer guide
VEXPay siteAPI reference
Browse documentation

Collect Colombian pesos

Charge buyers in Colombia with Bre-B, Nequi or Daviplata. Payments settle in Colombian pesos in their own COP balance — never converted to bolívares or USD.

How COP works

  • Create a payment with POST /v1/cop/payments, choosing a channel: breb, nequi or daviplata. Amounts are whole pesos (amountCop), at least COP 20,000 per payment; less answers 400 amount_below_minimum with minimumCop.
  • The response says what the buyer does next (next.type). The payment completes asynchronously — listen for payment.completed / payment.failed, or read it with GET /v1/cop/payments/:id.
  • Completed payments are credited to your COP balance (GET /v1/cop/balance) net of the COP fee. COP is kept apart from your VES and stablecoin balances.
  • New pesos are held for 24 hours: GET /v1/cop/balance reports availableCop, heldCop and nextReleaseAt. See Balances, holds and payouts.
  • COP payouts are sent by bank transfer in Colombia; contact VEX Pay to request one. You receive payout.completed with currency: "COP" when it is paid.

The Node (vexpay.cop), Python (vexpay.cop), PHP ($vexpay->cop) and Laravel (VexPay::cop()) SDKs cover every COP endpoint: payments.create, retrieve, submitOtp, cancel, refund and balance.retrieve.

Channels

channelBuyer doesBuyer dataExpires
brebScans the QR or enters the transfer key in any Colombian bank appOptional~15 min
nequiApproves the push notification in the Nequi appbuyer required (email, phone, documentType, documentNumber)~5 min
daviplataReads you the code received by SMS; you submit itbuyer required; documentType CC, CE or TI~5 min

buyer.phone is a Colombian mobile number (10 digits starting with 3; +57 optional). documentType is one of CC, CE, TI, NIT, PP.

Pricing

MovementFee
COP payment completed4.5% of the amount + COP 1,500, in pesos (never more than the amount)
Full refundThe fee is returned with the refund

Example: a buyer pays 100.000 COP → fee 6.000 (4.500 + 1.500) → 94.000 COP credited. The minimum payment is 20.000 COP.

Hosted checkout

Payment links and checkout sessions can collect COP with no buyer UI on your side. The hosted page lets the buyer choose the currency (bolívares, crypto or Colombian pesos), shows the total in each, and runs the Bre-B, Nequi or Daviplata step itself. Add cop to methods when you create a session, or leave methods empty: COP is offered whenever it is enabled on your account.

curlbash
curl -X POST "${API_URL}/v1/checkout/sessions" \
  --header "x-api-key: ${VEXPAY_API_KEY}" \
  --header "content-type: application/json" \
  --data '{ "amountUsd": 25.00, "methods": ["c2p", "usdt", "cop"], "reference": "order-1042" }'
  • The session is priced in USD. The peso amount is the USD amount × VEX Pay's USDT/COP rate plus a 2.5% FX margin, rounded up to whole pesos, and is fixed when the buyer starts paying.
  • When no current rate is available, the page hides the pesos option; bolívares and crypto keep working.
  • Marketplace seller (merchant) links offer COP too: the payment is credited to that seller's COP balance, net of the COP fee, after the later of your seller hold and the 24h balance hold.
  • COP is hidden when the peso total is under the COP 20,000 minimum (on a link where the buyer chooses a quantity, it stays visible but disabled until the total reaches it).
  • The COP fee applies as for any COP payment. payment.* webhooks for checkout payments also carry amountUsd and copRate, plus the usual checkoutSession block (id, reference, metadata).

Step 01

Bre-B (QR or transfer key)

Show the QR image (next.qrPngBase64, a base64 PNG) and the transfer key (next.transferKey) as a copyable fallback. They are returned only in this response.

curlbash
curl -X POST "${API_URL}/v1/cop/payments" \
  --header "x-api-key: ${VEXPAY_API_KEY}" \
  --header "content-type: application/json" \
  --header "Idempotency-Key: order-1042" \
  --data '{ "amountCop": 100000, "channel": "breb", "reference": "order-1042" }'
Node SDKjavascript
const payment = await vexpay.cop.payments.create(
  { amountCop: 100000, channel: 'breb', reference: 'order-1042' },
  { idempotencyKey: 'order-1042' },
);
// payment.next.qrPngBase64, payment.next.transferKey
Python SDKpython
payment = vexpay.cop.payments.create(
    {"amountCop": 100000, "channel": "breb", "reference": "order-1042"},
    idempotency_key="order-1042",
)
PHP SDKphp
$payment = $vexpay->cop->payments->create(
    ['amountCop' => 100000, 'channel' => 'breb', 'reference' => 'order-1042'],
    ['idempotency_key' => 'order-1042'],
);
Responsejson
{
  "id": "6f1c…",
  "status": "pending",
  "method": "COP",
  "channel": "breb",
  "amountCop": 100000,
  "reference": "order-1042",
  "metadata": {},
  "expiresAt": "2026-10-07T20:15:00.000Z",
  "createdAt": "2026-10-07T20:00:00.000Z",
  "next": { "type": "show_qr", "qrPngBase64": "iVBORw0KGgo…", "transferKey": "BREB-…" }
}

Render the QR with <img src="data:image/png;base64,{qrPngBase64}">.

Step 02

Nequi (push)

curlbash
curl -X POST "${API_URL}/v1/cop/payments" \
  --header "x-api-key: ${VEXPAY_API_KEY}" \
  --header "content-type: application/json" \
  --data '{
    "amountCop": 50000,
    "channel": "nequi",
    "buyer": { "email": "juan@example.com", "phone": "3001234567", "documentType": "CC", "documentNumber": "1234567890" }
  }'
Node SDKjavascript
await vexpay.cop.payments.create({
  amountCop: 50000,
  channel: 'nequi',
  buyer: { email: 'juan@example.com', phone: '3001234567', documentType: 'CC', documentNumber: '1234567890' },
});
Python SDKpython
vexpay.cop.payments.create({
    "amountCop": 50000,
    "channel": "nequi",
    "buyer": {"email": "juan@example.com", "phone": "3001234567", "documentType": "CC", "documentNumber": "1234567890"},
})
PHP SDKphp
$vexpay->cop->payments->create([
    'amountCop' => 50000,
    'channel' => 'nequi',
    'buyer' => ['email' => 'juan@example.com', 'phone' => '3001234567', 'documentType' => 'CC', 'documentNumber' => '1234567890'],
]);

The response has next: { "type": "approve_in_app", "phoneMasked": "****567" }. Tell the buyer to open Nequi and approve.

Step 03

Daviplata (SMS code)

Create the payment with channel: "daviplata" and a buyer. The buyer receives a code by SMS; submit it to complete the payment. A wrong code answers 400 invalid_otp and the payment stays pending until it expires.

curlbash
curl -X POST "${API_URL}/v1/cop/payments/{id}/otp" \
  --header "x-api-key: ${VEXPAY_API_KEY}" \
  --header "content-type: application/json" \
  --data '{ "otp": "123456" }'
Node SDKjavascript
await vexpay.cop.payments.submitOtp(payment.id, { otp: '123456' });
Python SDKpython
vexpay.cop.payments.submit_otp(payment.id, {"otp": "123456"})
PHP SDKphp
$vexpay->cop->payments->submitOtp($payment->id, ['otp' => '123456']);

Statuses, cancel and refunds

statusMeaning
pendingWaiting for the buyer.
completedPaid and credited to your COP balance (feeCop is set).
failedfailureCode: "expired" (not paid in time) or "rejected" (buyer declined).
canceledYou canceled it with POST /v1/cop/payments/:id/cancel before it was paid.
refundedFully refunded with POST /v1/cop/payments/:id/refund.

Refunds are full only and allowed within 96 hours of completion (409 refund_window_expired after that). The amount credited to you is debited from your COP balance (a sale still under the 24h hold is taken from the hold) and the fee is returned; if available plus held pesos do not cover it the answer is 409 insufficient_balance. A refund can also answer 503 processor_unavailable (temporarily unavailable) shortly after the payment — nothing is debited; retry later.

Webhooks

COP payments send payment.completed, payment.failed, payment.canceled and payment.reversed (refund). The payload carries method: "COP", channel, amountCop, feeCop, reference and metadata.

payment.completedjson
{
  "event": "payment.completed",
  "data": {
    "paymentId": "6f1c…",
    "status": "completed",
    "method": "COP",
    "currency": "COP",
    "channel": "nequi",
    "amountCop": 100000,
    "feeCop": 6000,
    "reference": "order-1042",
    "metadata": {},
    "livemode": true
  }
}

Test mode

Test API keys (and test-mode tenants) send COP payments to the processor sandbox. Use these values:

ChannelBuyer dataWhat happens
brebNoneThe sandbox returns a QR and a transfer key. Confirm by polling GET /v1/cop/payments/:id or by webhook.
nequiAny valid data, e.g. phone 3001234567, CC 1234567890, email juan@example.comCompletes on its own about a minute after you create it.
daviplataSame as Nequi; documentType CC, CE or TI onlyOTP 123456, 000000 or 111111 completes it; any other code answers 400 invalid_otp.

Document numbers and phones are only format-checked in the sandbox: phone is 10 digits starting with 3 (+57 optional), documentNumber 3–20 letters, digits or dashes.