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 achannel:breb,nequiordaviplata. Amounts are whole pesos (amountCop), at least COP 20,000 per payment; less answers 400amount_below_minimumwithminimumCop. - The response says what the buyer does next (
next.type). The payment completes asynchronously — listen forpayment.completed/payment.failed, or read it withGET /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/balancereportsavailableCop,heldCopandnextReleaseAt. See Balances, holds and payouts. - COP payouts are sent by bank transfer in Colombia; contact VEX Pay to request one. You receive
payout.completedwithcurrency: "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
| channel | Buyer does | Buyer data | Expires |
|---|---|---|---|
breb | Scans the QR or enters the transfer key in any Colombian bank app | Optional | ~15 min |
nequi | Approves the push notification in the Nequi app | buyer required (email, phone, documentType, documentNumber) | ~5 min |
daviplata | Reads you the code received by SMS; you submit it | buyer 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
| Movement | Fee |
|---|---|
| COP payment completed | 4.5% of the amount + COP 1,500, in pesos (never more than the amount) |
| Full refund | The 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.
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 carryamountUsdandcopRate, plus the usualcheckoutSessionblock (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.
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" }'const payment = await vexpay.cop.payments.create(
{ amountCop: 100000, channel: 'breb', reference: 'order-1042' },
{ idempotencyKey: 'order-1042' },
);
// payment.next.qrPngBase64, payment.next.transferKeypayment = vexpay.cop.payments.create(
{"amountCop": 100000, "channel": "breb", "reference": "order-1042"},
idempotency_key="order-1042",
)$payment = $vexpay->cop->payments->create(
['amountCop' => 100000, 'channel' => 'breb', 'reference' => 'order-1042'],
['idempotency_key' => 'order-1042'],
);{
"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)
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" }
}'await vexpay.cop.payments.create({
amountCop: 50000,
channel: 'nequi',
buyer: { email: 'juan@example.com', phone: '3001234567', documentType: 'CC', documentNumber: '1234567890' },
});vexpay.cop.payments.create({
"amountCop": 50000,
"channel": "nequi",
"buyer": {"email": "juan@example.com", "phone": "3001234567", "documentType": "CC", "documentNumber": "1234567890"},
})$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.
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" }'await vexpay.cop.payments.submitOtp(payment.id, { otp: '123456' });vexpay.cop.payments.submit_otp(payment.id, {"otp": "123456"})$vexpay->cop->payments->submitOtp($payment->id, ['otp' => '123456']);Statuses, cancel and refunds
| status | Meaning |
|---|---|
pending | Waiting for the buyer. |
completed | Paid and credited to your COP balance (feeCop is set). |
failed | failureCode: "expired" (not paid in time) or "rejected" (buyer declined). |
canceled | You canceled it with POST /v1/cop/payments/:id/cancel before it was paid. |
refunded | Fully 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.
{
"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:
| Channel | Buyer data | What happens |
|---|---|---|
breb | None | The sandbox returns a QR and a transfer key. Confirm by polling GET /v1/cop/payments/:id or by webhook. |
nequi | Any valid data, e.g. phone 3001234567, CC 1234567890, email juan@example.com | Completes on its own about a minute after you create it. |
daviplata | Same as Nequi; documentType CC, CE or TI only | OTP 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.