Pay out bolívares to a beneficiary
POST/payoutsProbar
La llamada la hace tu navegador, contra el servidor que elijas arriba. Si la documentación no está servida desde el mismo origen que la API, el navegador puede bloquear la respuesta por CORS: en ese caso abre estas páginas desde la propia API (/docs) o usa el curl de arriba.
const url = 'http://localhost:3000/v1/payouts';const options = { method: 'POST', headers: {'x-api-key': '<x-api-key>', 'Content-Type': 'application/json'}, body: '{"quoteId":"70ec44b5-c1df-45b1-86fe-8f92c3959689","method":"pago_movil","beneficiaryId":"8ade3e33-6f1c-4d0a-9d33-2c6b0a41f7e2"}'};
try { const response = await fetch(url, options); const data = await response.json(); console.log(data);} catch (error) { console.error(error);}curl --request POST \ --url http://localhost:3000/v1/payouts \ --header 'Content-Type: application/json' \ --header 'x-api-key: <x-api-key>' \ --data '{ "quoteId": "70ec44b5-c1df-45b1-86fe-8f92c3959689", "method": "pago_movil", "beneficiaryId": "8ade3e33-6f1c-4d0a-9d33-2c6b0a41f7e2" }'Scope payments:write. Delivers bolívares to the beneficiary at their Venezuelan bank, by pago móvil (pago_movil) or bank transfer (transferencia).
The amount is not sent in this call: the quote (quoteId) sets it, and it also sets the fee.
The beneficiary goes by beneficiaryId (registered beforehand) or inline in beneficiary.
Always send Idempotency-Key. A network retry without it is exactly how you pay twice.
Minimum amount: there is a per-operation floor of ~Bs 925.00 at the measured rate (924.5231 Bs/USDT). The floor is set in USDT and checked against the current rate on every send: its Bs equivalent moves every day, so don’t keep your amounts at the edge. Below it, the payout returns 201 with status: failed and reason: amount_below_minimum, without reaching the payment network and without charging you.
Per-operation cap: Bs 100,000.00 (10000000) for pago_movil; Bs 500,000.00 (50000000) for transferencia. The Venezuelan banking ecosystem sets them, not us. Going over returns 422 limit_exceeded with the detail in error.meta.
settled is NOT the usual response: the bolívar payment network is asynchronous and accepts the payout before settling it, so you normally get submitted. If your code requires settled here, it breaks in production.
Authorizations
Sección titulada «Authorizations»Parameters
Sección titulada «Parameters»Header Parameters
Sección titulada «Header Parameters»Único por operación. El alcance es tu cuenta y el modo de la key: una sk_test_ y una sk_live_ no comparten keys. Dura 24 h. Reintentar con la misma key y payload devuelve la respuesta original, con el mismo código HTTP y la cabecera Idempotent-Replayed: true; con otro payload da 409 idempotency_key_reused. Los campos secretos (el url de una gift card, una apiKey nueva, un webhookSecret) sólo llegan en la primera respuesta: al repetir llegan en null.
Request Bodyrequired
Sección titulada «Request Bodyrequired»object
Fija el importe en Bs, la tasa y nuestro margen
preferred permite entregar por el otro instrumento si el pedido no es posible (y te lo dice en methodChanged); strict sólo acepta el instrumento pedido.
Beneficiario ya dado de alta. Alternativa a beneficiary.
object
Nombre del titular TAL COMO lo tiene el banco destino
Cédula o RIF: V/E/J/G/P + 6 a 9 dígitos
Banco destino, 4 dígitos
Sólo dígitos, de 16 a 20 (en la práctica venezolana, 20), y debe empezar por el bankCode. Necesario para transferencia.
Móvil del beneficiario, formato 04XXXXXXXXX. Obligatorio para pago móvil; opcional en transferencia.
Decimales: BS y USD 2; USDT y USDC 6 (1000000 = 1 USDT).
Opcional, y no hace falta para dispersar: omítelo y manda el remitente en sender. Sólo sirve si ya registraste a ese usuario con nosotros (producto de banca, fuera de esta API pública). Si no existe en tu cuenta, 404 customer_not_found antes de tocar nada.
Datos del REMITENTE de la dispersión: quién origina el envío, no quién lo recibe. Es opcional en la API porque hay tres formas de obtenerlo, y se aplican en este orden:
- lo que mandes aquí (manda el cliente: es su usuario final quien origina el envío y sólo él lo conoce);
- el usuario referenciado por
customerId, si lo hay; - el remitente por defecto configurado para tu programa — el caso del cliente que dispersa nóminas por cuenta propia, donde el ordenante es siempre la misma empresa.
Si no hay ninguno de los tres, la dispersión falla antes de la primera llamada a la red de pago en vez de salir con un remitente inventado: un envío atribuido a una persona que no existe no se deshace después.
Si mandas el bloque, todos sus campos son obligatorios salvo nationality.
object
Documento del remitente. Si es venezolano (nationality ausente o VEN): V, E o J seguido de hasta 9 dígitos. Si es extranjero: de 5 a 20 alfanuméricos, tal como lo emite su país.
País de nacionalidad en ISO 3166-1 alfa-3. Ausente equivale a VEN, y cambia el formato que se exige en nationalId.
Teléfono del remitente, sea cual sea su nacionalidad (un venezolano que vive fuera manda el suyo). Dos formas:
- Móvil venezolano (0412/0414/0416/0422/0424/0426): se aceptan
04141234567,+584141234567y4141234567, y se normalizan a04XXXXXXXXX. Un fijo venezolano se rechaza, también con+58. - Número internacional E.164:
+, código de país y número, de 8 a 15 dígitos en total y distinto de+58(p. ej.+34612345678). Espacios, guiones, puntos y paréntesis se limpian; se guarda como+34612345678.
Opcional. Concepto del pago, escrito por ti o por tu usuario: llega al banco del beneficiario como concepto cuando la red de pago lo admite (si no lo admite, el pago sale igual, sin concepto). De 3 a 30 caracteres; los espacios de más y los caracteres de control se limpian antes de contar. Se guarda en el pago y sale en la respuesta, en GET /payments/{id}, en el extracto y en los webhooks payment.*. No es bankReference (el comprobante del banco) ni la Idempotency-Key.
Cuenta de pruebas (local y QA): elige el desenlace de ESTE pago empezando el concepto por TEST:SUBMITTED, TEST:UNKNOWN o TEST:TIMEOUT_AFTER_SUBMIT (TEST:SETTLED o sin escenario, liquida). Con cuenta propia (funding.model: own_account), sin escenario o con TEST:SETTLED el pago responde submitted y se liquida solo en unos 20 s (payment.settled), como en producción; TEST:SUBMITTED lo deja en curso hasta que lo resuelvas. Ver POST /test-helpers/payments/{id}/resolve. Fuera de la cuenta de pruebas es un concepto más.
Examples
Pago móvil a un beneficiario ya registrado
{ "quoteId": "70ec44b5-c1df-45b1-86fe-8f92c3959689", "method": "pago_movil", "beneficiaryId": "8ade3e33-6f1c-4d0a-9d33-2c6b0a41f7e2"}Transferencia con el beneficiario en la misma llamada
{ "quoteId": "70ec44b5-c1df-45b1-86fe-8f92c3959689", "method": "transferencia", "methodPolicy": "strict", "beneficiary": { "fullName": "María Gómez", "nationalId": "V12345678", "bankCode": "0134", "accountNumber": "01340000123456789012", "phone": "04141234567" }}Con datos del remitente (bloque `sender`)
{ "quoteId": "70ec44b5-c1df-45b1-86fe-8f92c3959689", "method": "pago_movil", "beneficiary": { "fullName": "José Marcano", "nationalId": "V18234112", "bankCode": "0138", "phone": "04141234567" }, "sender": { "nationalId": "V12345678", "firstName": "Ana", "lastName": "Pérez", "phone": "04241234567", "email": "ana@example.com", "nationality": "VEN" }}Remitente que vive fuera (teléfono E.164) y con concepto
{ "quoteId": "70ec44b5-c1df-45b1-86fe-8f92c3959689", "method": "pago_movil", "reference": "Ayuda familiar octubre", "beneficiary": { "fullName": "José Marcano", "nationalId": "V18234112", "bankCode": "0138", "phone": "04141234567" }, "sender": { "nationalId": "V12345678", "firstName": "Ana", "lastName": "Pérez", "phone": "+34612345678", "email": "ana@example.com", "nationality": "VEN" }}Responses
Sección titulada «Responses»Resultado de la dispersión. status puede ser:
submitted— la red de pago la aceptó y está liquidando. Es la respuesta HABITUAL con la red de pago de bolívares. No reintentes: espera el webhookpayment.settled/payment.failed, o consultaGET /payments/{id}.settled— liquidada. No hay nada que hacer.failed— rechazada, y el asiento ya está revertido:feeAmountviene en"0"y no se te cobra nada. El motivo está enreason(ver la lista en el esquemaPaymentResult). Corrige la causa y reintenta con otra cotización.unknown— la red de pago no confirmó y no sabemos si pagó. NUNCA reintentes: lo resolvemos consultando a la red de pago con nuestra referencia y te avisamos por webhook. En este caso la respuesta no traetotalDebited.
Ojo con la taxonomía: reason NO es error.code. Un rechazo de la red de pago llega como 201 con status: failed, no como un error HTTP.
El webhook de desenlace sale una sola vez, tanto si la dispersión se resolvió en el acto como si tardó.
object
submitted = la red de pago la aceptó y está liquidando. Es el estado HABITUAL justo después de dispersar, porque la red de pago de bolívares es asíncrona. No reintentar: se cierra por webhook.
unknown = la red de pago no confirmó y no sabemos si pagó. NO reintentar: se resuelve automáticamente y te notificamos por webhook.
Sólo con prefondeo por ticket: de qué tickets salió el pago, un tramo por ticket. La red de pago recibe UN solo pago.
De qué ticket salió una parte de un pago
object
Id del ticket
El pago falló o se devolvió y el tramo volvió a su ticket
El comprobante que verá el beneficiario en su banco. null mientras la red de pago no haya liquidado. En el entorno de pruebas es simulado y tiene la forma SIM- + 12 caracteres hexadecimales.
Decimales: BS y USD 2; USDT y USDC 6 (1000000 = 1 USDT).
Monto enviado al destino, en unidades mínimas
Comisión cobrada, en unidades mínimas. En un pago failed es 0 porque el asiento se revirtió completo.
Monto + comisión; es lo que bajó del saldo de origen. En un pago failed es 0 (no bajó nada). No viene en unknown.
True si se usó un canal distinto al pedido
Detalle legible del estado. En submitted explica que la red de pago aceptó y está liquidando; en unknown, que se resolverá solo.
Sólo en status: failed. Motivo del rechazo, con código estable. Es una taxonomía DISTINTA de error.code: llega dentro de una respuesta 201, no de un error HTTP.
Valores: beneficiary_missing_phone (falta el móvil en pago_movil), beneficiary_missing_account, beneficiary_mismatch (el nombre o la cédula no coinciden con el titular), invalid_destination_account, sender_required, amount_below_minimum, amount_above_limit, amount_not_positive, amount_precision_loss, bank_unreachable, instrument_unsupported, sender_phone_unsupported (ninguna red de pago disponible admitía el teléfono internacional del remitente), payout_insufficient_balance (sin saldo para el pago y la comisión de la red de pago en la cuenta que dispersa), payout_balance_unavailable (no se pudo leer ese saldo: no se envió nada; reintentable), payout_rejected, no_route.
Si dispersas desde tu propia cuenta en la red de pago, la falta de saldo llega aquí (201 failed); con prefunding en Bolívar Connect llega antes, como 422 insufficient_tenant_funds.
El concepto que mandaste, ya limpio. null si no mandaste ninguno.
object
Examples
submitted — el desenlace HABITUAL
{ "id": "89920401-26da-4f69-b57d-bc20e0366ce2", "status": "submitted", "message": "La red de pago aceptó el envío y todavía no lo ha liquidado. Se confirmará por webhook (payment.settled); no lo reintentes con otra Idempotency-Key.", "bankReference": null, "asset": "BS", "amount": "5000000", "feeAmount": "50000", "totalDebited": "5050000", "method": "pago_movil", "methodRequested": "pago_movil", "methodChanged": false, "beneficiaryId": "8ade3e33-6f1c-4d0a-9d33-2c6b0a41f7e2", "reference": "Nómina octubre 0007", "beneficiary": { "fullName": "María Gómez", "nationalId": "V12345678", "bankCode": "0138", "instrument": "pago_movil" }}failed — rechazo de la red de pago, asiento ya revertido
{ "id": "89920401-26da-4f69-b57d-bc20e0366ce2", "status": "failed", "reason": "beneficiary_missing_phone", "asset": "BS", "amount": "5000000", "feeAmount": "0", "totalDebited": "0", "bankReference": null, "method": "pago_movil", "methodRequested": "pago_movil", "methodChanged": false, "beneficiaryId": "8ade3e33-6f1c-4d0a-9d33-2c6b0a41f7e2", "beneficiary": { "fullName": "María Gómez", "nationalId": "V12345678", "bankCode": "0134", "instrument": "pago_movil" }}unknown — la red de pago no confirmó. NO reintentar
{ "id": "89920401-26da-4f69-b57d-bc20e0366ce2", "status": "unknown", "message": "La red de pago no confirmó el resultado. El pago se resolverá automáticamente; no lo reintentes con otra Idempotency-Key.", "asset": "BS", "amount": "5000000", "feeAmount": "50000", "beneficiaryId": "8ade3e33-6f1c-4d0a-9d33-2c6b0a41f7e2", "beneficiary": { "fullName": "María Gómez", "nationalId": "V12345678", "bankCode": "0138", "instrument": "pago_movil" }}Headers
Sección titulada «Headers»true cuando la respuesta es la repetición guardada de una petición anterior con la misma Idempotency-Key. No viene en la primera.
Error con código estable
object
object
Contexto estructurado del error cuando lo hay: qué límite se superó y de cuánto, qué campo es inválido, cuánto saldo había. No aparece en errores internos.
object
Código ESTABLE. Programa contra esto.
Example
{ "error": { "type": "invalid_request_error", "code": "insufficient_balance", "request_id": "req_8a0312ba896772bc441134cd" }}Error con código estable
object
object
Contexto estructurado del error cuando lo hay: qué límite se superó y de cuánto, qué campo es inválido, cuánto saldo había. No aparece en errores internos.
object
Código ESTABLE. Programa contra esto.
Example
{ "error": { "type": "invalid_request_error", "code": "insufficient_balance", "request_id": "req_8a0312ba896772bc441134cd" }}customer_not_found (el customerId no existe en tu cuenta; error.meta.field = customerId), o la cotización o el beneficiario no existen.
object
object
Contexto estructurado del error cuando lo hay: qué límite se superó y de cuánto, qué campo es inválido, cuánto saldo había. No aparece en errores internos.
object
Código ESTABLE. Programa contra esto.
Example
{ "error": { "type": "invalid_request_error", "code": "insufficient_balance", "request_id": "req_8a0312ba896772bc441134cd" }}Fondos, límites o cotización (code dice cuál). Con prefondeo con tasa por ticket, tenant_prefondeo_usa_payout_bs si la cotización convierte, e insufficient_tenant_funds si tus tickets cargados no alcanzan.
object
object
Contexto estructurado del error cuando lo hay: qué límite se superó y de cuánto, qué campo es inválido, cuánto saldo había. No aparece en errores internos.
object
Código ESTABLE. Programa contra esto.
Example
{ "error": { "type": "invalid_request_error", "code": "insufficient_balance", "request_id": "req_8a0312ba896772bc441134cd" }}Error con código estable
object
object
Contexto estructurado del error cuando lo hay: qué límite se superó y de cuánto, qué campo es inválido, cuánto saldo había. No aparece en errores internos.
object
Código ESTABLE. Programa contra esto.
Example
{ "error": { "type": "invalid_request_error", "code": "insufficient_balance", "request_id": "req_8a0312ba896772bc441134cd" }}