Guía para desarrolladores
Sitio de VEXPayReferencia de API
Explorar documentación

Cobra en pesos colombianos

Cobra a compradores en Colombia con tarjeta, PSE, Bancolombia, Bre-B, Nequi o Daviplata. Los pagos se liquidan en pesos colombianos en un saldo COP propio — nunca se convierten a bolívares ni a USD.

Cómo funciona COP

  • Crea un pago con POST /v1/cop/payments eligiendo un channel: pse, bancolombia_transfer, bancolombia_qr, breb, nequi o daviplata. Las tarjetas (card) se cobran solo en el checkout alojado. Los montos son pesos enteros (amountCop), mínimo COP 20.000 por pago; un monto menor responde 400 amount_below_minimum con minimumCop.
  • GET /v1/cop/channels lista los canales que tu cuenta puede usar ahora, los bancos PSE y los enlaces de términos que piden algunos canales. Arma tu selector de canales con esa respuesta.
  • La respuesta indica qué hace el comprador (next.type). El pago se completa de forma asíncrona: escucha payment.completed / payment.failed o consúltalo con GET /v1/cop/payments/:id.
  • Los pagos completados se acreditan a tu saldo COP (GET /v1/cop/balance) descontando la comisión COP. COP se mantiene separado de tus saldos en VES y stablecoins.
  • Los pesos nuevos quedan retenidos 24 horas: GET /v1/cop/balance reporta availableCop, heldCop y nextReleaseAt. Ver Saldos, retenciones y payouts.
  • Los retiros en COP se envían por transferencia bancaria en Colombia; solicítalos a VEX Pay. Recibes payout.completed con currency: "COP" cuando se paga.

Los SDKs de Node (vexpay.cop), Python (vexpay.cop), PHP ($vexpay->cop) y Laravel (VexPay::cop()) cubren todos los endpoints de COP: payments.create, retrieve, submitOtp, cancel, refund y balance.retrieve.

Canales

channelEl compradorDatos del compradorReembolso
cardPaga con Visa o Mastercard en el checkout alojado (1 cuota)Los pide el checkoutTotal, dentro de 24 horas
pseVa a su banco y autoriza el débitobuyer + firstName, lastName, personType; pse.bankCodeNo por la API
bancolombia_transferVa a Bancolombia y autoriza la transferenciabuyerNo por la API
bancolombia_qrEscanea un QR con la app BancolombiabuyerNo por la API
brebEscanea el QR o ingresa la llave en cualquier app bancaria colombiana (~15 min)OpcionalesTotal, dentro de 96 horas
nequiAprueba la notificación push en la app Nequi (~5 min)buyerDepende de tu cuenta
daviplataEscribe un código SMS, o autoriza en una página de Daviplata (sigue next.type)buyer; documentType CC, CE o TIDepende de tu cuenta

buyer significa email, phone, documentType y documentNumber. buyer.phone es un celular colombiano (10 dígitos que empiezan por 3; +57 opcional). documentType es uno de CC, CE, TI, NIT, PP. personType (PSE) es natural o juridica.

Los canales cuya entrada del catálogo tiene needsTermsAcceptance: true piden que el comprador acepte los términos de la red de pagos: muestra los dos enlaces de terms junto a una casilla y envía buyer.termsAccepted: true. Sin eso el pago responde 400 invalid_data indicando buyer.termsAccepted.

curlbash
curl "${API_URL}/v1/cop/channels" \
  --header "x-api-key: ${VEXPAY_API_KEY}"
Respuestajson
{
  "channels": [
    { "channel": "pse", "label": "PSE", "needsBuyer": true, "needsTermsAcceptance": true, "checkoutOnly": false, "minAmountCop": 20000 },
    { "channel": "card", "label": "Tarjeta", "needsBuyer": true, "needsTermsAcceptance": true, "checkoutOnly": true, "minAmountCop": 20000 },
    { "channel": "breb", "label": "Bre-B", "needsBuyer": false, "needsTermsAcceptance": false, "checkoutOnly": false, "minAmountCop": 20000 }
  ],
  "pseBanks": [{ "code": "1007", "name": "Bancolombia" }],
  "terms": { "termsUrl": "https://…", "personalDataUrl": "https://…" }
}

Precios

MovimientoComisión
Pago COP completado4,5 % del monto + COP 1.500, en pesos (nunca más que el monto)
Reembolso totalLa comisión se devuelve con el reembolso

Ejemplo: un comprador paga 100.000 COP → comisión 6.000 (4.500 + 1.500) → se acreditan 94.000 COP. El pago mínimo es de 20.000 COP.

Checkout alojado

Los links de pago y las sesiones de checkout pueden cobrar en COP sin que construyas ninguna pantalla. La página alojada deja al comprador elegir la moneda (bolívares, cripto o pesos colombianos), muestra el total en cada una y ejecuta el paso de tarjeta, PSE, Bancolombia, Bre-B, Nequi o Daviplata. Agrega cop a methods al crear la sesión, o deja methods vacío: COP se ofrece siempre que esté habilitado en tu cuenta.

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" }'
  • La sesión tiene precio en USD. El monto en pesos es el monto en USD × la tasa USDT/COP de VEX Pay más un margen cambiario de 2,5 %, redondeado hacia arriba a pesos enteros, y queda fijo cuando el comprador inicia el pago.
  • Si no hay una tasa vigente, la página oculta la opción en pesos; bolívares y cripto siguen funcionando.
  • Las tarjetas solo se cobran aquí: el número de la tarjeta va del navegador del comprador directo a la red de pagos, nunca a ti ni a VEX Pay. Si la tarjeta es rechazada, el comprador puede probar otra tarjeta u otro canal.
  • PSE y Bancolombia llevan al comprador a su banco y lo regresan al mismo checkout, que retoma el pago y muestra el resultado.
  • Los links de vendedores de marketplace (merchants) también ofrecen COP: el pago se acredita al saldo COP de ese vendedor, descontando la comisión COP, cuando termina la más tardía entre tu retención de vendedores y la retención de 24 h del saldo.
  • COP se oculta cuando el total en pesos es menor al mínimo de COP 20.000 (en un link donde el comprador elige la cantidad, se muestra deshabilitado hasta que el total lo alcance).
  • Aplica la comisión COP como en cualquier pago COP. Los webhooks payment.* de pagos de checkout incluyen además amountUsd y copRate, y el bloque habitual checkoutSession (id, reference, metadata).

Paso 01

Bre-B (QR o llave)

Muestra la imagen QR (next.qrPngBase64, PNG en base64) y la llave (next.transferKey) para copiar. Solo se devuelven en esta respuesta.

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'],
);
Respuestajson
{
  "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-…" }
}

Muestra el QR con <img src="data:image/png;base64,{qrPngBase64}">.

Paso 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", "termsAccepted": true }
  }'
Node SDKjavascript
await vexpay.cop.payments.create({
  amountCop: 50000,
  channel: 'nequi',
  buyer: { email: 'juan@example.com', phone: '3001234567', documentType: 'CC', documentNumber: '1234567890', termsAccepted: true },
});
Python SDKpython
vexpay.cop.payments.create({
    "amountCop": 50000,
    "channel": "nequi",
    "buyer": {"email": "juan@example.com", "phone": "3001234567", "documentType": "CC", "documentNumber": "1234567890", "termsAccepted": True},
})
PHP SDKphp
$vexpay->cop->payments->create([
    'amountCop' => 50000,
    'channel' => 'nequi',
    'buyer' => ['email' => 'juan@example.com', 'phone' => '3001234567', 'documentType' => 'CC', 'documentNumber' => '1234567890', 'termsAccepted' => true],
]);

La respuesta trae next: { "type": "approve_in_app", "phoneMasked": "****567" }. Pídele al comprador que abra Nequi y apruebe. Envía termsAccepted: true cuando el catálogo de canales diga needsTermsAcceptance (si no, se ignora).

Paso 03

Daviplata (código SMS)

Crea el pago con channel: "daviplata" y un buyer. Con next.type: "submit_otp", el comprador recibe un código por SMS; envíalo para completar el pago. Un código incorrecto responde 400 invalid_otp y el pago sigue pendiente hasta vencer. Algunas cuentas responden next.type: "redirect": envía al comprador a next.url como en PSE (el endpoint del OTP responde entonces 409).

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']);

Paso 04

PSE y Bancolombia (redirección al banco)

Crea el pago y envía al comprador a next.url (next.type: "redirect"). La URL del banco puede tardar unos segundos en existir: si next.type es redirect_pending, vuelve a consultar el pago (GET /v1/cop/payments/:id) hasta que traiga la redirección. Tras autorizar, el comprador llega a una página de VEX Pay que le dice que puede cerrar la pestaña; tú recibes el resultado por webhook.

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-1043" \
  --data '{
    "amountCop": 80000,
    "channel": "pse",
    "pse": { "bankCode": "1007" },
    "buyer": {
      "firstName": "Juan", "lastName": "Pérez", "personType": "natural",
      "email": "juan@example.com", "phone": "3001234567",
      "documentType": "CC", "documentNumber": "1234567890",
      "termsAccepted": true
    }
  }'
Respuestajson
{
  "id": "9a2e…",
  "status": "pending",
  "method": "COP",
  "channel": "pse",
  "amountCop": 80000,
  "next": { "type": "redirect", "url": "https://…" }
}

bancolombia_transfer funciona igual, solo con buyer. bancolombia_qr responde next: { "type": "show_qr", "qrSvgBase64": "…" }: muéstralo con <img src="data:image/svg+xml;base64,{qrSvgBase64}"> y espera el webhook.

Estados, cancelación y reembolsos

statusSignificado
pendingEsperando al comprador.
completedPagado y acreditado a tu saldo COP (feeCop presente).
failedfailureCode: "expired" (no pagó a tiempo) o "rejected" (lo rechazó el comprador, el banco o el emisor de la tarjeta).
canceledLo cancelaste con POST /v1/cop/payments/:id/cancel antes de que se pagara.
refundedReembolsado por completo con POST /v1/cop/payments/:id/refund, o un contracargo de tarjeta (failureCode: "chargeback").

La cancelación funciona mientras el pago está pendiente. En los canales con redirección al banco (PSE, Bancolombia) la cancelación queda registrada en VEX Pay; si el banco lo aprueba igual, VEX Pay lo revisa y le devuelve el dinero al comprador o te lo acredita.

Los reembolsos son solo totales y dependen del canal (ver la tabla de Canales): tarjetas dentro de 24 horas, Bre-B dentro de 96 horas (después: 409 refund_window_expired). Un canal que no se puede reembolsar por la API responde 409 refund_not_supported; contacta a VEX Pay para devolverle el dinero al comprador. Lo acreditado se descuenta de tu saldo COP (una venta que sigue en la retención de 24 h se toma de lo retenido) y la comisión se devuelve; si los pesos disponibles más los retenidos no alcanzan, la respuesta es 409 insufficient_balance. Poco después del pago un reembolso también puede responder 503 processor_unavailable (no disponible temporalmente): no se descuenta nada; reintenta más tarde.

Webhooks

Los pagos COP envían payment.completed, payment.failed, payment.canceled y payment.reversed (reembolso, o reason: "chargeback" cuando el comprador disputa un cobro con tarjeta ante su banco — el monto se descuenta de tu saldo COP). El payload incluye method: "COP", channel, amountCop, feeCop, reference y metadata. Los pagos con tarjeta (en webhooks y en GET /v1/cop/payments/:id) traen además card: { brand, last4 }.

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
  }
}

Modo de prueba

Las llaves de prueba (y los tenants en modo de prueba) envían los pagos COP al sandbox del procesador. Usa estos valores:

CanalDatos del compradorQué pasa
brebNingunoEl sandbox devuelve un QR y una llave. Confirma consultando GET /v1/cop/payments/:id o por webhook.
nequiTeléfono 3991111111, más cualquier documento y correo válidosSe completa solo en alrededor de un minuto. 3992222222 se rechaza.
daviplataIgual que Nequi; documentType solo CC, CE o TICon next.type: "submit_otp", el OTP 123456, 000000 o 111111 lo completa; cualquier otro código responde 400 invalid_otp. Con redirect, abre next.url y sigue la página de prueba.
card (checkout)Tarjeta 4242 4242 4242 4242, cualquier vencimiento futuro y CVCAprobada. 4111 1111 1111 1111 se rechaza.
pseCualquier dato válido del compradorEl banco con código 1 aprueba, 2 rechaza.
bancolombia_transferCualquier dato válido del compradorAbre next.url y elige el resultado en la página de prueba.

En el sandbox los documentos y teléfonos solo se validan por formato: phone son 10 dígitos que empiezan por 3 (+57 opcional), documentNumber de 3 a 20 letras, dígitos o guiones.