Saltar al contenido
ApiPay Hub · Docs

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"
}
MiembroQué es¿Estable?
typeURI del problema, https://api.apipay.io/problems/{code-en-kebab-case}
titleTítulo corto del tipo de problema
statusEl estado HTTP, repetido en el cuerpo
detailExplicación para humanos de esta ocurrencia concretaNo
instancePath de la petición que falló
codeIdentificador estable y legible por máquina del catálogo

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#

codeHTTPCuándo pasaQué hacer
validation_error400La petición no cumple el contrato: falta un campo, currency no es ISO 4217, o se pidió gateway_id: "sandbox" con clave liveArreglar la petición. No reintentar: va a fallar igual
invalid_api_key401X-API-Key ausente, malformada, revocada o de la partición contrariaRevisar la configuración del entorno. No reintentar
invalid_client_secret401El 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_signature401Una pasarela llamó a POST /v1/gateways/{gatewayId}/webhooks con una firma que no valida. Nunca lo verás como respuesta a una petición tuyaNada del lado del comercio
card_declined402El emisor rechazó el cobro: fondos, bloqueo, tarjeta vencidaMensaje claro al pagador e intent nuevo. El intent quedó FAILED, que es terminal
gateway_rejected402La pasarela rechazó por sus propias reglas, antes o después del emisorIgual que el anterior
insufficient_permissions403Credencial válida sin permiso: una pk_ intentando cancelar o reembolsarHacer la operación desde el servidor con la sk_. No reintentar
resource_not_found404El recurso no existe para tu tenant. Incluye el caso de un id de la otra partición livemodeRevisar el id y la clave. No reintentar
idempotency_key_reuse409La misma Idempotency-Key con un cuerpo distinto dentro de las 24 hNo cambiar la clave: revisar por qué cambió el cuerpo. Suele ser un monto recalculado
idempotency_key_in_flight409Una petición idéntica con esa clave se está procesando ahora mismoEsperar y reintentar con la misma clave. Nunca generar una nueva
invalid_state_transition409La matriz de estados no permite la operación: cancel sobre SUCCEEDED, confirm sobre EXPIRED, refund sobre un intent no SUCCEEDEDLeer el recurso y conciliar con su estado real. No reintentar la misma operación
currency_not_supported422La pasarela no opera esa divisa para tu tenantRevisar la configuración del tenant o elegir otra pasarela
amount_below_minimum422El monto está por debajo del mínimo de la pasarelaAjustar el monto
amount_exceeds_refundable422El reembolso supera el saldo reembolsable del intentRecalcular el saldo. No reintentar con el mismo monto
rate_limited429Se superó el límite de tasa de la API keyRespetar Retry-After. El SDK ya lo hace en los GET

5xx — el problema está de nuestro lado#

codeHTTPCuándo pasaQué hacer
internal_error500Fallo no controlado de la plataforma. detail nunca expone stack traces ni datos internosGuardar el X-Request-Id y contactar a soporte con él. Reintentable si la operación es idempotente
gateway_unavailable503Circuito abierto o bulkhead lleno hacia la pasarela. Trae gateway_id y retry_after_secondsRespetar Retry-After. Reintentar el POST , 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:

codeNode.js / PythonJavaPHP
validation_errorValidationErrorValidationExceptionValidationException
invalid_api_key, invalid_client_secret, invalid_webhook_signatureAuthenticationErrorAuthenticationExceptionAuthenticationException
card_declinedCardDeclinedErrorCardDeclinedExceptionGatewayDeclinedException
gateway_rejectedGatewayRejectedErrorGatewayRejectedExceptionGatewayDeclinedException
insufficient_permissionsInsufficientPermissionsErrorInsufficientPermissionsExceptionPermissionException
resource_not_foundResourceNotFoundErrorResourceNotFoundExceptionNotFoundException
idempotency_key_reuse, idempotency_key_in_flightIdempotencyErrorIdempotencyExceptionIdempotencyException
invalid_state_transitionInvalidStateTransitionErrorInvalidStateTransitionExceptionInvalidStateTransitionException
currency_not_supportedCurrencyNotSupportedErrorCurrencyNotSupportedExceptionValidationException
amount_below_minimumAmountBelowMinimumErrorAmountBelowMinimumExceptionValidationException
amount_exceeds_refundableAmountExceedsRefundableErrorAmountExceedsRefundableExceptionValidationException
rate_limitedRateLimitErrorRateLimitExceptionRateLimitException
internal_errorInternalServerErrorInternalServerExceptionServerException
gateway_unavailableGatewayUnavailableErrorGatewayUnavailableExceptionServiceUnavailableException

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ámetroValor
EstrategiaBackoff exponencial con jitter completo
Base500 ms
Factor2
Tope por espera8 s
maxRetries2, es decir 3 intentos en total
Retry-AfterSe respeta en 429 rate_limited y 503 gateway_unavailable, con tope de 60 s
Métodos reintentadosGET ú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:

CabeceraQué indica
Retry-AfterSegundos a esperar antes de reintentar
RateLimit-LimitLímite sostenido de la ventana actual
RateLimit-RemainingPeticiones que te quedan en la ventana
RateLimit-ResetSegundos 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:

FamiliaClaseCuándoQué hacer
Configuración localConfigurationError / ConfigurationExceptionUna pk_ pasada al SDK server-side, baseUrl inválida, timeout no positivo, id vacíoEs un bug de despliegue. Falla rápido; no lo captures para ignorarlo
ConexiónApiConnectionError / APIConnectionError / ApiConnectionExceptionNo hubo respuesta tras agotar los reintentos: DNS, red, timeoutNo asumas que la operación no ocurrió. Si era un POST, concilia leyendo el recurso o reintenta con la misma Idempotency-Key
Firma de webhookSignatureVerificationError / SignatureVerificationExceptionLa cabecera ApiPay-Signature no verifica, o su t está fuera de toleranciaResponder 400 y no procesar. Ver la guía de webhooks
PayloadInvalidPayloadError / InvalidPayloadException; en PHP, ApiPayExceptionLa respuesta o el envelope no tienen la forma que declara el contratoRegistrar 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 Problem y las respuestas de cada endpoint.