Ir al contenido

Pay out bolívares to a beneficiary

POST/payoutsProbar
Cabeceras

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.

POST
/payouts
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.

Idempotency-Key
string

Ú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.

Media typeapplication/json
object
quoteId
required

Fija el importe en Bs, la tasa y nuestro margen

string format: uuid
method
required
string
Allowed values: pago_movil transferencia
methodPolicy

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.

string
default: preferred
Allowed values: preferred strict
beneficiaryId

Beneficiario ya dado de alta. Alternativa a beneficiary.

string format: uuid
beneficiary
object
fullName
required

Nombre del titular TAL COMO lo tiene el banco destino

string
nationalId
required

Cédula o RIF: V/E/J/G/P + 6 a 9 dígitos

string
bankCode
required

Banco destino, 4 dígitos

string
accountNumber

Sólo dígitos, de 16 a 20 (en la práctica venezolana, 20), y debe empezar por el bankCode. Necesario para transferencia.

string
nullable
phone

Móvil del beneficiario, formato 04XXXXXXXXX. Obligatorio para pago móvil; opcional en transferencia.

string
nullable
asset

Decimales: BS y USD 2; USDT y USDC 6 (1000000 = 1 USDT).

string
Allowed values: BS USD USDT USDC VESC
country
string
customerId

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.

string format: uuid
sender

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:

  1. lo que mandes aquí (manda el cliente: es su usuario final quien origina el envío y sólo él lo conoce);
  2. el usuario referenciado por customerId, si lo hay;
  3. 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
nationalId
required

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.

string
>= 5 characters <= 20 characters
nationality

País de nacionalidad en ISO 3166-1 alfa-3. Ausente equivale a VEN, y cambia el formato que se exige en nationalId.

string
firstName
required
string
>= 2 characters
lastName
required
string
>= 2 characters
phone
required

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, +584141234567 y 4141234567, y se normalizan a 04XXXXXXXXX. 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.
string
email
required
string format: email
reference

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.

string
>= 3 characters <= 30 characters
Examples

Pago móvil a un beneficiario ya registrado

{
"quoteId": "70ec44b5-c1df-45b1-86fe-8f92c3959689",
"method": "pago_movil",
"beneficiaryId": "8ade3e33-6f1c-4d0a-9d33-2c6b0a41f7e2"
}

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 webhook payment.settled / payment.failed, o consulta GET /payments/{id}.
  • settled — liquidada. No hay nada que hacer.
  • failed — rechazada, y el asiento ya está revertido: feeAmount viene en "0" y no se te cobra nada. El motivo está en reason (ver la lista en el esquema PaymentResult). 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 trae totalDebited.

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ó.

Media typeapplication/json
object
id
string format: uuid
status

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.

string
Allowed values: created pending submitted unknown settled failed returned reversed
prefondeo

Sólo con prefondeo por ticket: de qué tickets salió el pago, un tramo por ticket. La red de pago recibe UN solo pago.

Array<object>

De qué ticket salió una parte de un pago

object
id

Id del ticket

string format: uuid
montoBs
string
tasa
string
costoUsdt
string
revertido

El pago falló o se devolvió y el tramo volvió a su ticket

boolean
bankReference

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.

string
nullable
asset

Decimales: BS y USD 2; USDT y USDC 6 (1000000 = 1 USDT).

string
Allowed values: BS USD USDT USDC VESC
amount

Monto enviado al destino, en unidades mínimas

string
feeAmount

Comisión cobrada, en unidades mínimas. En un pago failed es 0 porque el asiento se revirtió completo.

string
totalDebited

Monto + comisión; es lo que bajó del saldo de origen. En un pago failed es 0 (no bajó nada). No viene en unknown.

string
method
string
Allowed values: pago_movil transferencia
methodRequested
string
Allowed values: pago_movil transferencia
methodChanged

True si se usó un canal distinto al pedido

boolean
message

Detalle legible del estado. En submitted explica que la red de pago aceptó y está liquidando; en unknown, que se resolverá solo.

string
nullable
reason

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.

string
nullable
beneficiaryId
string format: uuid
reference

El concepto que mandaste, ya limpio. null si no mandaste ninguno.

string
nullable
beneficiary
object
fullName
string
nationalId
string
bankCode
string
instrument
string
Allowed values: pago_movil transferencia
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"
}
}
Idempotent-Replayed
string
Allowed values: true

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

Media typeapplication/json
object
error
object
meta

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
key
additional properties
any
type
string
Allowed values: invalid_request_error authentication_error rate_limit_error upstream_error api_error
code

Código ESTABLE. Programa contra esto.

string
message
string
request_id
string
Example
{
"error": {
"type": "invalid_request_error",
"code": "insufficient_balance",
"request_id": "req_8a0312ba896772bc441134cd"
}
}

Error con código estable

Media typeapplication/json
object
error
object
meta

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
key
additional properties
any
type
string
Allowed values: invalid_request_error authentication_error rate_limit_error upstream_error api_error
code

Código ESTABLE. Programa contra esto.

string
message
string
request_id
string
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.

Media typeapplication/json
object
error
object
meta

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
key
additional properties
any
type
string
Allowed values: invalid_request_error authentication_error rate_limit_error upstream_error api_error
code

Código ESTABLE. Programa contra esto.

string
message
string
request_id
string
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.

Media typeapplication/json
object
error
object
meta

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
key
additional properties
any
type
string
Allowed values: invalid_request_error authentication_error rate_limit_error upstream_error api_error
code

Código ESTABLE. Programa contra esto.

string
message
string
request_id
string
Example
{
"error": {
"type": "invalid_request_error",
"code": "insufficient_balance",
"request_id": "req_8a0312ba896772bc441134cd"
}
}

Error con código estable

Media typeapplication/json
object
error
object
meta

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
key
additional properties
any
type
string
Allowed values: invalid_request_error authentication_error rate_limit_error upstream_error api_error
code

Código ESTABLE. Programa contra esto.

string
message
string
request_id
string
Example
{
"error": {
"type": "invalid_request_error",
"code": "insufficient_balance",
"request_id": "req_8a0312ba896772bc441134cd"
}
}