Errores

Esta página se abre cuando algo ya falló. Las dieciséis operaciones de la API comparten una forma de rechazar: un código estable, un texto que no lo es, y dos envolturas distintas que el código de estado no distingue. Lo que sigue es cómo leer cualquier fallo de esta API y qué hacer con él; los fallos de la credencial en concreto —qué significa cada 401, qué abre cada Permiso— están en Autenticación y Claves API.

Hay dos envolturas y el estado no dice cuál llegó

Esta API construye sus respuestas de error en dos sitios, y el reparto es por capa, no por código de estado.

Todo lo que responde un controlador —y todo lo que pasa por el manejador de excepciones— llega con success, code y message, más un errors o un meta cuando hay algo que poner en ellos:

{ "success": false, "code": "PAYMENT_LINK_NOT_FOUND", "message": "Payment link not found" }

Las capas que corren antes de los controladores —autenticación, origen y el límite por clave— responden con error y code. Ni success ni message:

{ "error": "API key required", "code": "missing_api_key" }

Un cliente que lea message a ciegas recibe undefined en cualquiera de estas últimas. Y no sirve mirar el estado para saber cuál llegó: el 401, el 403 y el 429 llegan en las dos envolturas, según qué capa haya rechazado la petición.

La comprobación fiable es qué clave trae el cuerpo: la envoltura ordinaria trae message, la de capa trae error. Mirar success no basta, porque hay un rechazo que trae success: false y pone el texto bajo error de todos modos —el de origen bloqueado, ORIGIN_NOT_ALLOWED—.

Ocho rechazos vienen de esas capas: los cinco 401 de autenticación, insufficient_scope, ORIGIN_NOT_ALLOWED —el que además trae success— y el 429 del límite de la clave. Todo lo demás de esta página usa la envoltura ordinaria.

Lo que no cubre ninguna de las dos es una respuesta que no llegó a construirse como respuesta de la API: un proxy, un balanceador o un tiempo de espera agotado devuelven lo que devuelvan, y puede no ser JSON. Por eso un cliente necesita un camino para un cuerpo que no se puede interpretar o que no trae code: ahí lo único que queda es el estado.

El code es lo que se compara

Sobre el code, y no sobre el message. Los mensajes son prosa: se reescriben, se afinan y en algunos casos interpolan valores de la petición. Un cliente que compare textos se rompe el día que alguien mejora una frase, y se rompe en silencio, porque la comparación simplemente deja de coincidir.

Tampoco sobre el estado a secas. El mismo estado lleva formas distintas: el 401 tiene dos envolturas, el 403 viene de tres sitios distintos, el 429 tiene dos cuerpos y el 500 tiene tres códigos de contrato, más uno que no lo es.

Y la comparación distingue mayúsculas. Hay dos pares de códigos que se diferencian solo en eso, y cada par son dos cosas distintas:

  • invalid_api_key es la clave que no verificó, en la envoltura de capa. INVALID_API_KEY es un registro de clave incompleto, en la ordinaria, y solo en seis de las dieciséis operaciones. Los dos son 401.
  • rate_limit_exceeded es el límite de la clave. RATE_LIMIT_EXCEEDED es el límite por IP. Los dos son 429, y las mayúsculas son la manera más rápida de saber cuál de los dos límites se agotó: los cuerpos también son distintos, y solo uno de los dos trae la cabecera Retry-After.

Un switch que normalice a minúsculas colapsa los dos pares y confunde dos fallos que piden cosas distintas.

Si llega un code que el cliente no conoce, la regla es no adivinar: se trata por estado y se registra el cuerpo entero.

Los códigos y qué hacer con cada uno

Qué hacer es una de tres cosas. Corregir: la petición está mal y hay que cambiarla antes de repetirla. Reintentar: el fallo puede ser temporal; se reintenta una vez, con espera, y si vuelve se escala. Parar: nada de lo que se cambie en la petición lo arregla —o el recurso ya está en su estado final, o hace falta una persona—.

La tabla reúne los códigos que declaran las dieciséis operaciones. Cada operación documenta cuáles de estos puede devolver de verdad, y ninguna los devuelve todos.

codeEstadoEnvolturaQué pasóQué hacer
KYC_VERIFICATION_REQUIRED400ordinaria, con metaLa cuenta no tiene completo el paso de Verificar IdentidadParar: se completa en el panel
KYB_VERIFICATION_REQUIRED400ordinaria, con metaLa cuenta no tiene completa la Verificación ComercialParar: se completa en el panel
BUSINESS_PAYMENT_LINK_MINIMUM_AMOUNT400ordinaria, con metaEl Monto no llega al mínimo de 2,00 USD equivalentes. Esta comprobación corre antes de la validación del cuerpoCorregir: subir el Monto. Si el Monto ya está muy por encima del mínimo, revisar que el cuerpo lleve currency
missing_api_key401de capaNo llegó credencial en ninguna de las cabeceras aceptadasCorregir
invalid_key_format401de capaEl prefijo de la clave no se pudo interpretarCorregir
invalid_tenant401de capaEl prefijo se interpreta y no corresponde a ninguna cuentaCorregir
tenant_init_failed401de capaLa cuenta se encontró y no se pudo cargarReintentar
invalid_api_key401de capaLa clave no verificó: mal copiada, desactivada o caducadaParar: revisar la clave en el panel
INVALID_API_KEY401ordinariaLa clave verificó y su registro está incompleto. Solo en las seis operaciones de Solicitud de Pago y de cobro con tarjetaParar: sustituir la clave
insufficient_scope403de capaLa clave no lleva el Permiso que la operación exige. Solo en las doce operaciones que exigen unoParar: ajustar los Permisos en el panel
ORIGIN_NOT_ALLOWED403success y errorEl Origin de la petición no está en los Orígenes Permitidos de la clave, o la petición no llevó esa cabeceraParar: corregir la lista en el panel
BUSINESS_TENANT_PAUSED403ordinariaLa cuenta está en pausa. Leer y cancelar siguen funcionando; crear noParar: hablar con soporte
CARD_PAYMENTS_NOT_ENABLED403ordinariaLa cuenta no tiene habilitado el cobro con tarjetaParar: hablar con soporte
USER_NOT_FOUND404ordinariaLa clave verificó y el usuario que la creó ya no existe. Nada de la petición lo provoca. Solo en las mismas seis operaciones que devuelven INVALID_API_KEYParar: sustituir la clave
PAYMENT_LINK_NOT_FOUND404ordinariaNo hay Solicitud de Pago con ese id al alcance de la claveCorregir el id
CARD_PAYMENT_LINK_NOT_FOUND404ordinariaLo mismo, en el carril de tarjetaCorregir el id
WEBHOOK_NOT_FOUND404ordinariaNo hay endpoint de webhook con ese id en la cuentaCorregir el id
RESOURCE_NOT_FOUND404ordinariaLa petición no corresponde a ninguna ruta. En la práctica, un {id} que no es un UUID bien formado. Llega antes de la autenticación, así que una petición sin clave y con un id mal formado vuelve como 404 y no como 401Corregir el id
VALIDATION_FAILED422ordinaria, errors opcionalEl cuerpo se entendió y se rechazó. Dos cuerpos distintos comparten este código y solo uno trae errorsCorregir
USD_ACCOUNT_NOT_ENABLED422ordinaria, sin errorsLa cuenta no tiene habilitado el cobro en USDParar: hablar con soporte
WALLET_NOT_FOUND422ordinaria, sin errorsLa cuenta tiene Billeteras, pero ninguna en el carril localParar: hablar con soporte
PAYMENT_LINK_NOT_ACTIVE422ordinaria, errors.status es un textoLa Solicitud de Pago ya no está activaParar: el Estado actual viene en errors.status
PAYMENT_LINK_CANNOT_CANCEL422ordinaria, errors.current_uses es un enteroLa Solicitud de Pago ya se usóParar: no se puede cancelar
CARD_PAYMENT_LINK_NOT_ACTIVE422ordinaria, errors.status es un textoEl cobro con tarjeta ya no está activoParar: el Estado actual viene en errors.status
EXCHANGE_RATE_UNAVAILABLE422ordinaria, sin errorsNo hay Tasa de Cambio utilizable para ese parDepende de la operación: ver bajo la tabla
WEBHOOK_ALREADY_EXISTS422ordinaria, sin errorsEsa URL ya está registrada en la cuenta, por cualquier Clave API de la cuenta y no solo por la que llamaParar
INVALID_CURRENCY422ordinaria, errors.error es un textoLa Moneda de origen pasó la validación y aun así no se puede cotizarCorregir la Moneda de origen
RATE_LIMIT_EXCEEDED429ordinaria, con retry_afterSe agotó el límite por IPReintentar tras retry_after
rate_limit_exceeded429de capa, con retry_afterSe agotó el límite de la claveReintentar tras retry_after
SYSTEM_DATABASE_ERROR500ordinariaFallo interno. Lo pueden devolver catorce de las dieciséis operaciones; las dos de conversión, no. Una cuenta sin ninguna Billetera aprovisionada lo recibe además en cada intento de crear una Solicitud de PagoReintentar. Si llega en todos los intentos de crear, es aprovisionamiento: parar y hablar con soporte
CARD_PAYMENT_ERROR500ordinariaFallo al hablar con la plataforma de tarjetas, al crear o al cancelar un cobro con tarjetaReintentar
CONVERSION_QUOTE_ERROR500ordinariaLa cotización no se pudo calcular. Un destination que la operación no puede cotizar llega también con este códigoRevisar primero destination; si es el correcto, reintentar una vez. Si el problema es el destination, reintentar no lo arregla nunca

EXCHANGE_RATE_UNAVAILABLE no significa lo mismo en las dos operaciones que lo devuelven. En POST /v1/conversions/rate puede ser temporal: esa operación no declara ningún 500 propio y convierte a propósito los fallos que ocurren dentro de ella en este mismo 422, así que conviene reintentar una vez antes de concluir que el par no se puede convertir. Al crear un cobro con tarjeta significa otra cosa —hay una Tasa de Cambio guardada y no es utilizable—, y ahí no hay reintento que valga.

Un VALIDATION_FAILED no siempre nombra el campo

Este es el rechazo que más clientes rompe, porque parece el más predecible.

El que produce un validador trae errors, un mapa de nombre de campo a lista de textos:

{
  "success": false,
  "code": "VALIDATION_FAILED",
  "message": "Validation failed",
  "errors": { "amount": ["The amount field is required."] }
}

Hay otro rechazo con el mismo code y el mismo estado que trae un message en prosa y ningún errors. Un cliente que ramifique sobre el código y acto seguido entre en errors se rompe con ese, no con este. Al crear una Solicitud de Pago, una currency fuera de las admitidas puede llegar en esa segunda forma, y es justo el error más fácil de cometer. Y si ese mensaje enumera monedas, esa lista no es el contrato: la que manda es la que declara cada operación en su referencia.

La regla es leer message siempre y tratar errors como detalle adicional cuando esté.

Cuando errors está, sus valores no son siempre listas de textos. Esta API pone tres tipos distintos ahí dentro:

  • Una lista de textos por campo, que es lo que produce un validador.
  • Un texto suelto: PAYMENT_LINK_NOT_ACTIVE y CARD_PAYMENT_LINK_NOT_ACTIVE ponen el Estado actual bajo errors.status, e INVALID_CURRENCY repite su propio mensaje bajo errors.error.
  • Un entero:
{
  "success": false,
  "code": "PAYMENT_LINK_CANNOT_CANCEL",
  "message": "Payment link cannot be cancelled because it has already been used",
  "errors": { "current_uses": 1 }
}

Y varios 422 no traen errors en absoluto —WALLET_NOT_FOUND, USD_ACCOUNT_NOT_ENABLED, EXCHANGE_RATE_UNAVAILABLE y WEBHOOK_ALREADY_EXISTS entre ellos—:

{ "success": false, "code": "WEBHOOK_ALREADY_EXISTS", "message": "A webhook with this URL already exists" }

En un lenguaje con tipos, errors se declara como un objeto de valores desconocidos y se comprueba antes de usarlo. En cualquier otro, se comprueba igual.

Los dos 429

Hay dos límites y corren los dos en las dieciséis operaciones: uno por IP y otro por Clave API. Se agotan por separado y contestan distinto.

{ "success": false, "message": "Too many requests. Please try again later.", "code": "RATE_LIMIT_EXCEEDED", "retry_after": 37 }
{ "error": "Too many requests", "code": "rate_limit_exceeded", "retry_after": 37 }

El primero es el de IP y el segundo el de la clave. Aparte de las mayúsculas del code, la diferencia que importa para reintentar es esta: solo el límite por IP pone la cabecera Retry-After. Un 429 del límite de la clave no la lleva, así que un cliente que espere leyendo cabeceras se queda sin saber cuánto esperar.

retry_after está en los dos cuerpos, en segundos enteros. Es el valor sobre el que se espera.

Las dos ventanas son fijas, no deslizantes: se abren con la primera petición y el contador se reinicia entero al expirar. Eso significa que esperar retry_after es suficiente, y que repartir las peticiones no adelanta la reapertura.

Una respuesta correcta trae cuatro contadores —X-RateLimit-Limit-IP y X-RateLimit-Remaining-IP para el límite por IP, X-RateLimit-Limit y X-RateLimit-Remaining para el de la clave— y ninguna cabecera de reinicio. El presupuesto de cada uno y por qué el de IP se comparte entre cuentas están en Autenticación y Claves API.

Un 500 dice qué falló, no por qué

En un 500, solo el code es contractual. El message no lo es. El texto de esa respuesta describe un fallo interno, no sale de ningún conjunto de cadenas documentado y puede cambiar sin aviso.

Un 500 puede llegar además con un code que la tabla de arriba no recoge —hoy, SYSTEM_UNEXPECTED_ERROR—. No es contrato, no se documenta su causa y se trata como cualquier otro 500.

De ahí tres cosas: no se interpreta, no se compara, y no se le enseña al pagador. Guardarlo entero en los registros propios sí ayuda, y es lo que conviene pegar en un ticket de soporte.

Un 500 tampoco suele ser una señal para cambiar la petición. Se reintenta una vez y, si vuelve, se escala: un 500 que se repite casi siempre es nuestro y no de quien integra, y un cliente que reintente en bucle solo añade carga a algo que ya está roto.

Ese «casi» es deliberado: hay entradas conocidas que sí provocan un 500 y que sí se arreglan cambiando la petición. Están en la sección siguiente, y son pocas y concretas.

Entradas que la validación acepta y la operación no puede servir

Hay entradas que pasan la validación y aun así no se pueden atender. Evitarlas es más barato que interpretar lo que devuelven.

  • Un campo de moneda que no sea un texto. Algunos clientes serializan un campo de formulario repetido o vacío como una lista JSON. Al crear un cobro con tarjeta y en las dos operaciones de conversión, eso no vuelve como un 422 que nombre el campo. Se manda siempre un texto de tres letras.
  • amount_type vacío o nulo en POST /v1/conversions/quote. Para tomar el valor por defecto se omite el campo; mandarlo como "" o como null no es lo mismo que omitirlo. Es la forma que toma un desplegable sin seleccionar o un campo opcional que el cliente rellena siempre.

Ninguna de esas dos respuestas está documentada y no se promete ningún estado para ellas. No conviene construir sobre lo que se observe hoy.

Dos más, estas sí con respuesta conocida:

  • Una Moneda de origen fuera de las cinco verificadas en POST /v1/conversions/quote —ARS, BRL, CLP, COP y MXN—. PEN es el caso conocido: pasa la validación y vuelve como 422 INVALID_CURRENCY nombrando la Moneda. Para convertir a PEN o desde PEN sin cotizar una liquidación sirve POST /v1/conversions/rate.
  • Un destination que la operación no pueda cotizar. La validación de ese campo solo comprueba que sean tres caracteres, sin lista de Monedas admitidas, así que un código desconocido no se rechaza ahí: llega hasta el cálculo y vuelve como un 500 CONVERSION_QUOTE_ERROR, no como un 422. Reintentarlo no lo arregla nunca; hay que cambiar el destination. Conviene mandar la Moneda de la Billetera en la que se quiere cobrar.

Seguir por aquí

  • Autenticación y Claves API — qué significa cada 401, qué abre cada Permiso y qué presupuesto tiene cada límite.
  • Inicio rápido — las cuatro llamadas que van de la clave al cobro, con las respuestas que llegan de verdad.
  • Ambientes y Sandbox — qué se puede reproducir en Sandbox y qué no.
  • Glosario — qué significa cada término que aparece en esta página.

Did this page help you?