Ir al contenido

Dispersar bolívares a un beneficiario

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. Entrega bolívares al beneficiario en su banco venezolano, por pago móvil o por transferencia.

El importe no se manda en esta llamada: lo fija la cotización (quoteId), que también fija la comisión.

El beneficiario va por beneficiaryId (dado de alta antes) o en línea en beneficiary.

Manda siempre Idempotency-Key. Un reintento de red sin ella es exactamente cómo se paga dos veces.

Importe mínimo: el riel tiene un piso por operación de 1 token, que a la tasa medida (924,5231 Bs/USDT) son ~925,00 Bs. El piso está en TOKENS y se comprueba con la tasa vigente en cada envío: su equivalente en Bs se mueve todos los días, así que no dejes tus importes al borde. Por debajo, la dispersión responde 201 con status: failed y reason: amount_below_rail_minimum, sin salir al riel y sin cobrarte.

Tope por operación: en pago_movil, 100.000,00 Bs (10000000); en transferencia, 500.000,00 Bs (50000000). Los fija el ecosistema bancario venezolano, no nosotros. Superarlo devuelve 422 limit_exceeded con el detalle en error.meta.

settled NO es la respuesta habitual: el riel de bolívares es asíncrono y acepta el envío antes de liquidarlo, así que lo normal es recibir submitted. Si tu código exige settled aquí, se rompe en producción.

Idempotency-Key
string

Único por operación. Reintentar con la misma key y payload devuelve la respuesta original; con otro payload da 409 idempotency_key_reused.

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. Necesario para los DOS instrumentos, no sólo para pago móvil.

string
nullable
asset
string
Allowed values: BS USD USDC VESC
country
string
customerId

Opcional: a qué usuario TUYO corresponde la dispersión. Para tu trazabilidad y para los límites por usuario.

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 al riel 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

Móvil venezolano (0412/0414/0416/0424/0426). Se aceptan las tres formas —04141234567, +584141234567 y 4141234567— y se normalizan a 04XXXXXXXXX. Un fijo se rechaza.

string
email
required
string format: email
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 — el riel la aceptó y está liquidando. Es la respuesta HABITUAL con el riel 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 — el riel no confirmó y no sabemos si pagó. NUNCA reintentes: lo resolvemos consultando al riel 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 del riel 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 = el riel la aceptó y está liquidando. Es el estado HABITUAL justo después de dispersar, porque el riel de bolívares es asíncrono. No reintentar: se cierra por webhook.

unknown = el riel no confirmó y no sabemos si pagó. NO reintentar: se resuelve consultando al proveedor y notificamos por webhook.

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

El comprobante que verá el beneficiario en su banco. null mientras el riel no haya liquidado.

string
nullable
asset
string
Allowed values: BS USD 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

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 el riel 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 — ocurre también en transferencia), beneficiary_missing_account, beneficiary_mismatch (el nombre o la cédula no coinciden con el titular), invalid_destination_account, sender_required, amount_below_rail_minimum, amount_above_rail_limit, amount_not_positive, amount_precision_loss, bank_unreachable, instrument_unsupported, rail_insufficient_balance, rail_rejected, no_route.

string
nullable
beneficiaryId
string format: uuid
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": "El riel 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",
"beneficiary": {
"fullName": "María Gómez",
"nationalId": "V12345678",
"bankCode": "0138",
"instrument": "pago_movil"
}
}

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