Manejo de errores y catálogo de códigos#
Todos los errores de la API son documentos application/problem+json según la
RFC 9457 (se abre en una pestaña nueva), con un miembro de extensión propio: code.
{
"type": "https://api.apipay.io/problems/amount-exceeds-refundable",
"title": "Unprocessable Entity",
"status": 422,
"detail": "El monto solicitado supera el saldo reembolsable del payment intent.",
"instance": "/v1/refunds",
"code": "amount_exceeds_refundable"
}
| Miembro | Qué es | ¿Estable? |
|---|---|---|
type | URI del problema, https://api.apipay.io/problems/{code-en-kebab-case} | Sí |
title | Título corto del tipo de problema | Sí |
status | El estado HTTP, repetido en el cuerpo | Sí |
detail | Explicación para humanos de esta ocurrencia concreta | No |
instance | Path de la petición que falló | Sí |
code | Identificador estable y legible por máquina del catálogo | Sí |
Además puede traer miembros de extensión específicos del problema. Por ejemplo, un 503:
{
"type": "https://api.apipay.io/problems/gateway-unavailable",
"title": "Service Unavailable",
"status": 503,
"detail": "La pasarela webpay no esta disponible en este momento.",
"instance": "/v1/payment-intents",
"code": "gateway_unavailable",
"gateway_id": "webpay",
"retry_after_seconds": 30
}
La regla de oro#
También sirven status y type —los tres son estables—, pero code es el más preciso: distingue
card_declined de gateway_rejected con el mismo 402, y idempotency_key_reuse de
invalid_state_transition con el mismo 409.
El catálogo: 17 códigos#
4xx — el problema está en la petición#
code | HTTP | Cuándo pasa | Qué hacer |
|---|---|---|---|
validation_error | 400 | La petición no cumple el contrato: falta un campo, currency no es ISO 4217, o se pidió gateway_id: "sandbox" con clave live | Arreglar la petición. No reintentar: va a fallar igual |
invalid_api_key | 401 | X-API-Key ausente, malformada, revocada o de la partición contraria | Revisar la configuración del entorno. No reintentar |
invalid_client_secret | 401 | El X-Client-Secret no corresponde al intent, o falta con una clave pk_ | Crear una sesión nueva desde el servidor. No reintentar |
invalid_webhook_signature | 401 | Una pasarela llamó a POST /v1/gateways/{gatewayId}/webhooks con una firma que no valida. Nunca lo verás como respuesta a una petición tuya | Nada del lado del comercio |
card_declined | 402 | El emisor rechazó el cobro: fondos, bloqueo, tarjeta vencida | Mensaje claro al pagador e intent nuevo. El intent quedó FAILED, que es terminal |
gateway_rejected | 402 | La pasarela rechazó por sus propias reglas, antes o después del emisor | Igual que el anterior |
insufficient_permissions | 403 | Credencial válida sin permiso: una pk_ intentando cancelar o reembolsar | Hacer la operación desde el servidor con la sk_. No reintentar |
resource_not_found | 404 | El recurso no existe para tu tenant. Incluye el caso de un id de la otra partición livemode | Revisar el id y la clave. No reintentar |
idempotency_key_reuse | 409 | La misma Idempotency-Key con un cuerpo distinto dentro de las 24 h | No cambiar la clave: revisar por qué cambió el cuerpo. Suele ser un monto recalculado |
idempotency_key_in_flight | 409 | Una petición idéntica con esa clave se está procesando ahora mismo | Esperar y reintentar con la misma clave. Nunca generar una nueva |
invalid_state_transition | 409 | La matriz de estados no permite la operación: cancel sobre SUCCEEDED, confirm sobre EXPIRED, refund sobre un intent no SUCCEEDED | Leer el recurso y conciliar con su estado real. No reintentar la misma operación |
currency_not_supported | 422 | La pasarela no opera esa divisa para tu tenant | Revisar la configuración del tenant o elegir otra pasarela |
amount_below_minimum | 422 | El monto está por debajo del mínimo de la pasarela | Ajustar el monto |
amount_exceeds_refundable | 422 | El reembolso supera el saldo reembolsable del intent | Recalcular el saldo. No reintentar con el mismo monto |
rate_limited | 429 | Se superó el límite de tasa de la API key | Respetar Retry-After. El SDK ya lo hace en los GET |
5xx — el problema está de nuestro lado#
code | HTTP | Cuándo pasa | Qué hacer |
|---|---|---|---|
internal_error | 500 | Fallo no controlado de la plataforma. detail nunca expone stack traces ni datos internos | Guardar el X-Request-Id y contactar a soporte con él. Reintentable si la operación es idempotente |
gateway_unavailable | 503 | Circuito abierto o bulkhead lleno hacia la pasarela. Trae gateway_id y retry_after_seconds | Respetar Retry-After. Reintentar el POST tú, con la misma Idempotency-Key |
X-Request-Id: el dato que pide soporte#
Toda respuesta de la API, exitosa o no, incluye la cabecera X-Request-Id. Es el identificador con
el que se correlaciona una petición concreta en las trazas de la plataforma.
Los cuatro SDKs lo exponen en cada error de la API. Regístralo siempre que loguees un fallo: sin él, un ticket de soporte que dice «un pago falló ayer por la tarde» no es investigable.
import { ApiError } from '@apipay/node';
try {
await apipay.paymentIntents.confirm(intentId, { payment_method_token: token });
} catch (error) {
if (error instanceof ApiError) {
logger.error('apipay_error', {
code: error.code, // estable: sobre esto se ramifica
status: error.status,
requestId: error.requestId, // X-Request-Id, para soporte
});
// Cuidado con volcar error.problem entero a un logger de terceros:
// detail puede contener texto libre de la pasarela.
}
throw error;
}Errores tipados en los SDKs#
Cada code se materializa como una excepción de una clase concreta, así que puedes ramificar con el
sistema de tipos en vez de comparar cadenas:
code | Node.js / Python | Java | PHP |
|---|---|---|---|
validation_error | ValidationError | ValidationException | ValidationException |
invalid_api_key, invalid_client_secret, invalid_webhook_signature | AuthenticationError | AuthenticationException | AuthenticationException |
card_declined | CardDeclinedError | CardDeclinedException | GatewayDeclinedException |
gateway_rejected | GatewayRejectedError | GatewayRejectedException | GatewayDeclinedException |
insufficient_permissions | InsufficientPermissionsError | InsufficientPermissionsException | PermissionException |
resource_not_found | ResourceNotFoundError | ResourceNotFoundException | NotFoundException |
idempotency_key_reuse, idempotency_key_in_flight | IdempotencyError | IdempotencyException | IdempotencyException |
invalid_state_transition | InvalidStateTransitionError | InvalidStateTransitionException | InvalidStateTransitionException |
currency_not_supported | CurrencyNotSupportedError | CurrencyNotSupportedException | ValidationException |
amount_below_minimum | AmountBelowMinimumError | AmountBelowMinimumException | ValidationException |
amount_exceeds_refundable | AmountExceedsRefundableError | AmountExceedsRefundableException | ValidationException |
rate_limited | RateLimitError | RateLimitException | RateLimitException |
internal_error | InternalServerError | InternalServerException | ServerException |
gateway_unavailable | GatewayUnavailableError | GatewayUnavailableException | ServiceUnavailableException |
Un code que tu versión del SDK no conozca —uno añadido después de publicarla— aterriza en la clase base
(ApiError en Node.js, APIError en Python, ApiException en Java y PHP) o en la que corresponda a su
estado HTTP, sin romper la integración.
Capturar por familias#
import {
ApiPayError,
ConflictError,
PaymentError,
RateLimitError,
UnprocessableError,
} from '@apipay/node';
try {
await cobrar();
} catch (error) {
if (error instanceof PaymentError) {
// 402: card_declined o gateway_rejected. Rechazo, no bug.
return mostrarRechazoAlPagador(error.code);
}
if (error instanceof RateLimitError) {
// El SDK ya respeto Retry-After en los GET; aqui llega tras agotar reintentos.
return reencolarConEspera(error.retryAfter);
}
if (error instanceof ConflictError) {
// 409: idempotencia o transicion invalida. Conciliar, no reintentar.
return conciliar();
}
if (error instanceof UnprocessableError) {
// 422: regla de negocio. La peticion hay que cambiarla.
return corregirSolicitud(error.code);
}
if (error instanceof ApiPayError) {
// Raiz de todo el SDK: incluye errores locales de configuracion y de red.
return registrarYFallar(error);
}
throw error;
}Reintentos: qué hace el SDK y qué te toca a ti#
Parámetros del backoff, idénticos en los cuatro SDKs:
| Parámetro | Valor |
|---|---|
| Estrategia | Backoff exponencial con jitter completo |
| Base | 500 ms |
| Factor | 2 |
| Tope por espera | 8 s |
maxRetries | 2, es decir 3 intentos en total |
Retry-After | Se respeta en 429 rate_limited y 503 gateway_unavailable, con tope de 60 s |
| Métodos reintentados | GET únicamente |
El tope de 60 s sobre Retry-After es una salvaguarda: un proxy o un WAF que devuelva
Retry-After: 3600 dormiría tu proceso una hora.
Cabeceras de límite de tasa#
Un 429 viene acompañado de:
| Cabecera | Qué indica |
|---|---|
Retry-After | Segundos a esperar antes de reintentar |
RateLimit-Limit | Límite sostenido de la ventana actual |
RateLimit-Remaining | Peticiones que te quedan en la ventana |
RateLimit-Reset | Segundos hasta que la ventana se reinicia |
Errores que no vienen de la API#
No todo error del SDK es una respuesta HTTP. Hay tres familias más, y conviene distinguirlas porque la reacción correcta es distinta:
| Familia | Clase | Cuándo | Qué hacer |
|---|---|---|---|
| Configuración local | ConfigurationError / ConfigurationException | Una pk_ pasada al SDK server-side, baseUrl inválida, timeout no positivo, id vacío | Es un bug de despliegue. Falla rápido; no lo captures para ignorarlo |
| Conexión | ApiConnectionError / APIConnectionError / ApiConnectionException | No hubo respuesta tras agotar los reintentos: DNS, red, timeout | No asumas que la operación no ocurrió. Si era un POST, concilia leyendo el recurso o reintenta con la misma Idempotency-Key |
| Firma de webhook | SignatureVerificationError / SignatureVerificationException | La cabecera ApiPay-Signature no verifica, o su t está fuera de tolerancia | Responder 400 y no procesar. Ver la guía de webhooks |
| Payload | InvalidPayloadError / InvalidPayloadException; en PHP, ApiPayException | La respuesta o el envelope no tienen la forma que declara el contrato | Registrar con el X-Request-Id y avisar a soporte |
Siguientes pasos#
- Webhooks: cómo llegan los desenlaces que no ves en la respuesta HTTP.
- Modo test: cómo provocar cada código a voluntad para probar tu manejo.
- Payment intents: la matriz que genera los
invalid_state_transition. - Referencia de API: el esquema
Problemy las respuestas de cada endpoint.