Skip to content
LIQFYdocs
PTENENEN
Go to dashboard

Payouts — qué devuelve Liqfy cuando uno no se concreta

Este documento cierra el contrato de fallos de payout: qué recibes, cuándo y qué hacer en cada caso. Impleméntalo una vez; no deberías tener que volver a revisarlo.

Hay dos momentos en los que un payout puede fallar, y se comportan de forma muy distinta. No los manejes en la misma ruta de código. Un tercer caso es menos frecuente: un payout que fue pagado y luego devuelto por la institución receptora (ver sección 4).


#1. Rechazo inmediato — el saldo de tu cuenta no alcanza

Ocurre de forma síncrona, en la respuesta de POST /v1/withdrawals. No se crea ningún payout, no sale dinero de tu billetera y tu idempotencyKey NO se consume.

http
POST /v1/withdrawals
HTTP/1.1 400 Bad Request

{
  "error": {
    "type": "invalid_request_error",
    "code": "INSUFFICIENT_BALANCE",
    "message": "INSUFFICIENT_BALANCE",
    "details": { "available": 1000, "requested": 100005, "currency": "BRL" }
  },
  "request_id": "req_..."
}

Ramifica según error.code. El message es para tus logs; el código es el contrato.

CampoQué es
error.codeSiempre INSUFFICIENT_BALANCE aquí. Constante estable.
details.availableSaldo disponible para gastar, en la unidad menor de la moneda (centavos para BRL).
details.requestedLo que saldría de la billetera: amount + comisión. No el amount que enviaste.
details.currencyISO 4217 de la billetera que se evaluó.

#Tres cosas que te ahorran un ticket de soporte

La comisión se cobra ADEMÁS del monto. Para retirar amount, la billetera necesita amount + fee (amountType tiene como valor predeterminado "gross"). Por eso requested viene ya sumado — sin eso verías "pedí 1.000, tengo 1.000" y el rechazo parecería un error. (El modo heredado amountType: "net" todavía se acepta en POST /v1/withdrawals — ver Crear el payout: el destinatario recibe amount − fee, y ahí requested es solo el amount. No se acepta para retiros CRYPTO.)

Solo cuenta available. Es el saldo disponible para gastar. pending (liquidado pero aún retenido) no cuenta para un payout y nunca lo cubre. Si la cuenta tiene más de una billetera en la misma moneda, available es el mayor saldo individual, no la suma — la reserva sale de una sola billetera. Puedes fijar la billetera con el campo opcional walletId; si lo omites, se usa la billetera predeterminada de la moneda cuando tiene saldo; de lo contrario, cualquier billetera disponible con saldo suficiente.

Un rechazo no quema el idempotencyKey. Puedes recargar y reenviar con la MISMA clave; el payout se procesa normalmente. (Esto ya se cumplía para el rechazo habitual y, desde el 2026-08-19, también se cumple para la rara condición de carrera en la que el saldo desaparece entre la verificación y la reserva.)

#Cómo evitar llegar a esto

GET /v1/withdrawals/payout-info?currency= — sin efectos secundarios, hecho para esto. Devuelve { currency, available, pendingAdjustment, fee: { fixed, variablePct, minimum }, maxWithdrawable }: available es el saldo disponible para gastar, fee es el conjunto de parámetros aplicado hoy, y maxWithdrawable es el mayor monto bruto que puedes solicitar con la comisión ya considerada — el número correcto para un botón de "retirar todo". Contrato completo y ejemplo: Cotización, comisión y saldo retirable.

#Otros rechazos inmediatos

La misma regla se aplica a todo 4xx síncrono: no se crea nada y nada sale de la billetera.

HTTPCódigo / mensajeCuándo
400errores de validaciónEl cuerpo no cumple el esquema (p. ej., type fuera de PIX, TED, CRYPTO; amount no es un entero positivo).
400Idempotency-Key must be at most 255 charactersEl encabezado Idempotency-Key tiene más de 255 caracteres.
4xxTENANT_KYC_REQUIREDEl registro (KYC) de la cuenta no está aprobado.
4xxrechazo por regla de retiroUna regla de retiro configurada para la cuenta rechazó la solicitud. El rechazo puede incluir ruleId y reasonKey (withdrawal.rule.pixKeyTypeNotAllowed o withdrawal.rule.notAllowed).
403CRYPTO_PANEL_ONLYUn retiro CRYPTO enviado con una API key — cripto es solo desde el panel.
400INVALID_CRYPTO_WITHDRAWALUn retiro CRYPTO fuera de sus reglas (solo BRL, mínimo R$ 5.000,00, sin net, se requieren dirección de billetera y estimación).

Todo rechazo 4xx deja rastro. Se registra con monto, motivo y origen (API o panel) y aparece en la pantalla de retiros y en el registro de auditoría (WITHDRAWAL_REJECTED). La misma solicitud repetida con la misma clave de idempotencia y el mismo código se registra como máximo una vez por hora. Un 5xx es un fallo nuestro, no un rechazo, y no se registra de esta forma.

#2. Rechazo posterior — el payout fue aceptado y falló en el procesamiento

Aquí el POST devolvió 201 con status: "PENDING", y el dinero ya salió de tu billetera (está reservado). El resultado llega después, de dos formas:

  • mediante el webhook payout.failed;
  • o consultando periódicamente GET /v1/withdrawals/{id} (po_... o el id sin prefijo). La lista GET /v1/withdrawals?status=failed acepta las mismas palabras en minúsculas que los webhooks payout.*: pending, processing, paid, failed (failed = FAILED, REJECTED y CANCELLED). Un valor desconocido devuelve 400.

Los campos que importan:

CampoQué es
statusFAILED, REJECTED o CANCELLED (failed en el payout canónico)
failure_codeCódigo estable, en minúsculas y legible por máquina. Ramifica tu flujo según este. Presente cuando el payout está FAILED.
failure_messageFrase neutra en portugués para ese código. Segura para mostrar a tu usuario final.
rejectionReasonTexto libre para logs/soporte (reason en el webhook heredado). En FAILED el valor es el failure_message público (desde el 2026-10-21); en REJECTED/CANCELLED todavía puede traer el texto técnico de la institución receptora. No escribas ifs basados en él.
legs[].failureCodeSolo payouts divididos: el motivo de cada parte. Vocabulario cerrado: INVALID_PIX_KEY, ACCOUNT_CLOSED, ACCOUNT_BLOCKED, KYC_REJECTED, LIMIT_REJECTED, DESTINATION_UNAVAILABLE, DESTINATION_NOT_ALLOWED, INSUFFICIENT_BALANCE, TIMEOUT, PROVIDER_CANCELLED, PROVIDER_REJECTED, UNKNOWN.

Dónde aparece cada uno:

Superficiefailure_code / failure_messageTexto libre
GET /v1/withdrawals/{id} (y la lista)failure_code, failure_message en el nivel superiorrejectionReason
payout.faileddata.object.failure_code, data.object.failure_messagenunca
json
{
  "id": "evt_...",
  "object": "event",
  "type": "payout.failed",
  "data": {
    "object": {
      "id": "po_...",
      "object": "payout",
      "status": "failed",
      "failure_code": "destination_unavailable",
      "failure_message": "O banco de destino não confirmou o PIX a tempo. Tente novamente mais tarde."
    }
  }
}

#Valores de failure_code

failure_codefailure_message¿Vale la pena reintentar?
invalid_pix_keyA chave PIX de destino é inválida ou não foi encontrada.Solo con otra clave
account_blockedA conta de destino está bloqueada para receber PIX.Solo con otra cuenta
account_closedA conta de destino está encerrada ou inativa.Solo con otra cuenta
limit_rejectedO valor excede um limite permitido para esta transferência.Sí, con un monto menor
kyc_rejectedA transferência foi recusada na verificação cadastral do destinatário.No
destination_unavailableO banco de destino não confirmou o PIX a tempo. Tente novamente mais tarde.Sí, más tarde: el banco receptor no liquidó a tiempo
destination_not_allowedO provedor não aceita esse tipo de destino; use outra chave Pix.Solo con otro tipo de clave o destino
payout_cancelledA transferência foi cancelada antes de ser concluída.Sí
provider_rejectedA transferência PIX foi recusada e não foi concluída.Sí, pero investiga primero

provider_rejected es el código genérico: todo lo que no podemos atribuir con más precisión cae ahí. Trata un failure_code desconocido como provider_rejected — pueden añadirse códigos nuevos, pero nunca se renombran ni se eliminan.

Regla general: un payout FAILED es definitivo y el dinero se reembolsa automáticamente a tu billetera; no necesitas solicitar nada. Cuándo llega payout.failed depende de una configuración de la cuenta:

  • Solo fallos definitivos (recomendado si reembolsas a tus usuarios al recibir payout.failed): todo payout.failed llega con el reembolso ya registrado, así que puedes reembolsar a tu usuario en cuanto lo recibes. Si el reembolso necesita confirmación con el banco, payout.failed espera hasta que se registre; si el PIX salió, llega payout.paid en su lugar. Pide al soporte que la active.
  • Predeterminado: payout.failed se envía de inmediato. Casi siempre el reembolso ya está registrado. Cuando no se puede comprobar que el PIX nunca salió, el reembolso queda retenido hasta confirmarlo con el banco: failure_message es "Estamos confirmando com o banco; você será avisado." y legs[].refunded es false. No reembolses a tu usuario todavía: consulta GET /v1/withdrawals/{id} o el extracto de la billetera hasta que se registre. Liberarlo no envía otro webhook.

(failure_message no promete un reembolso — el movimiento de la billetera sí.)

#Payouts divididos en tramos

Cada PIX tiene un tope (R$ 15.000,00 por defecto). Un payout que lo supera se divide en varias transferencias PIX — los tramos —, cada uno completado hasta el tope, con el resto en el último (R$ 110.000,00 → 7 × R$ 15.000,00 + 1 × R$ 5.000,00). Cada tramo tiene su propio E2E y puede fallar por sí solo.

Cuando algunos tramos se pagan y otros fallan, el payout termina PARTIALLY_COMPLETED — paid en el vocabulario público — y los montos de los tramos fallidos vuelven a tu billetera cuando se registra el reembolso. Revisa legs[] para saber exactamente qué pasó:

CampoQué es
legIndexNúmero de tramo, empezando en 1.
amountMonto del tramo, en centavos.
statusPENDING, PROCESSING, COMPLETED, FAILED o REVERSED.
endToEndIdEl E2E del tramo, para conciliar con el extracto bancario.
failureCodeVocabulario cerrado de arriba; null cuando el tramo no falló.
refundedSolo tramo FAILED: true cuando su reembolso ya se registró en la billetera.
reversalCreditedSolo tramo REVERSED: true cuando el monto devuelto se te acreditó (sección 4).
reversalEndToEndId, reversedAmount, reversedAtDatos de la devolución, en un tramo REVERSED.

Un tramo reemplazado por un reenvío nunca aparece en legs[]; su monto ya está en el tramo que lo reemplazó.

#Cancelar un payout que todavía está PENDING

Mientras el payout está PENDING (todavía no se envió nada — por ejemplo, esperando aprobación), puedes cancelarlo tú mismo:

http
POST /v1/withdrawals/{id}/cancel
{ "reason": "optional, up to 500 characters" }

Devuelve 200 con el payout como CANCELLED, y el monto vuelve al saldo. Fuera de PENDING devuelve 409 WITHDRAWAL_NOT_CANCELLABLE. Desde el panel, solo el titular de la cuenta o un administrador puede cancelar.


#3. Liquidez del procesador

Cuando el procesador que enviaría el PIX no tiene saldo para pagarlo, no es culpa tuya. Hoy el payout te llega como FAILED con failure_code: "provider_rejected", y el dinero se reembolsa automáticamente a tu billetera cuando se registra el movimiento de la billetera (aquí también aplica el reembolso retenido — ver la regla general de arriba). No hay reintento automático ni ventana de espera: si quieres, vuelve a solicitar el payout — preferiblemente un poco más tarde.

Qué hacer: mientras un payout está PROCESSING, no repitas la solicitud. Un nuevo POST crearía un segundo payout y volvería a debitar la billetera. Muestra a tu usuario final algo como "pago en proceso" y espera el webhook final.

PROCESSING puede durar más de lo habitual cuando todavía no se conoce el resultado del envío del PIX. En ese caso el payout permanece PROCESSING mientras Liqfy confirma el resultado con el proveedor: nunca se marca como fallido ni se reembolsa solo porque pasó el tiempo — únicamente con evidencia positiva de que el PIX no salió.


#4. Pagado y luego devuelto (REVERSED)

Un PIX que ya fue pagado puede ser devuelto por la institución receptora. El payout (o el tramo) pasa entonces a REVERSED. Este estado no tiene palabra en el vocabulario público de payout.*: para listarlo, filtra con ?status=REVERSED.

El dinero devuelto no va directamente a tu saldo. Una vez confirmada la devolución en el extracto del proveedor, entra en una bandeja de reversiones Liqfy y se te acredita automáticamente solo cuando la política de la marca lo permite y la devolución no está marcada como fraude o sospecha; de lo contrario, una persona decide — acreditártelo o retenerlo. Hasta entonces, legs[].reversalCredited es false: trátalo como "todavía no volvió a tu saldo".


#Resumen de implementación

text
POST /v1/withdrawals
├── 400 INSUFFICIENT_BALANCE ....... your wallet doesn't cover it. Nothing created.
│                                    → check /v1/withdrawals/payout-info first
├── other 4xx ...................... refused at entry. Nothing created.
└── 201 PENDING .................... accepted, funds reserved
    ├── still PENDING .............. you can cancel: POST /v1/withdrawals/{id}/cancel
    ├── stays PROCESSING ........... do NOT retry. Wait for the webhook.
    ├── PARTIALLY_COMPLETED (paid) . some legs paid, failed legs refunded
    │                                (legs[].refunded tells which already landed)
    ├── REVERSED ................... paid and returned; credit only when
    │                                legs[].reversalCredited is true
    └── webhook payout.failed
        └── failure_code = invalid_pix_key | account_* | limit_rejected |
            kyc_rejected | destination_unavailable | destination_not_allowed |
            payout_cancelled |
            provider_rejected (generic / unknown)
            → definitive; the refund is automatic once the wallet
              movement lands. If failure_message is "Estamos confirmando
              com o banco; você será avisado." the refund is held
              (legs[].refunded is false): check the payout or the
              statement until it lands. Does not happen with
              "final failures only".
              Retry only after fixing the cause (e.g. the PIX key), or later
              for destination_unavailable.

Idempotencia: envía siempre el encabezado Idempotency-Key en POST /v1/withdrawals (el campo del cuerpo idempotencyKey es una alternativa; el encabezado prevalece; como máximo 255 caracteres). Es tu protección contra un payout duplicado en cualquier reintento de red — reenviar la misma clave devuelve el payout original en lugar de crear otro. Si una solicitud agotó el tiempo de espera y no sabes si el payout se creó, búscalo por clave: GET /v1/withdrawals/by-idempotency-key/{key} or GET /v1/withdrawals?idempotency_key={key}.