Ir al contenido

API

En esta página

Manejo de errores

Maneja los errores de la Merchant API de forma uniforme: formato de respuesta, grupos de estados HTTP, códigos legibles por máquina, límites y reintentos.

La API usa los códigos de estado HTTP estándar. Las respuestas correctas devuelven el cuerpo del recurso directamente. Los errores siguen RFC 9457 Problem Details for HTTP APIs (application/problem+json) con dos campos de extensión: un code de nivel superior sobre el que ramificas en tu código y un field de nivel superior (snake_case) en los errores de validación de un solo campo. Cuando fallan varios campos a la vez, field se sustituye por un array errors[] (un campo de extensión de Paymos, §3.2) con el desglose campo a campo.

Ramifica por code para el tratamiento programático y la localización, nunca por el texto de title o detail. Los campos de texto pueden reformularse entre versiones; code es el contrato estable.

{
  "type": "https://paymos.io/docs/errors/codes#insufficient_balance",
  "title": "Conflict",
  "status": 409,
  "detail": "Merchant balance is insufficient.",
  "code": "insufficient_balance"
}

Toda respuesta de error sigue RFC 9457 Problem Details for HTTP APIs y se entrega como Content-Type: application/problem+json.

Error único (la mayoría de las respuestas)

{
  "type": "https://paymos.io/docs/errors/codes#insufficient_balance",
  "title": "Conflict",
  "status": 409,
  "detail": "Merchant balance is insufficient.",
  "code": "insufficient_balance"
}

Validación de varios campos

Solo se emite cuando varios campos fallan la validación a la vez. El code de primer nivel siempre es validation_failed; el detalle por campo vive en errors[]:

{
  "type": "https://paymos.io/docs/errors/codes#validation_failed",
  "title": "Bad Request",
  "status": 400,
  "detail": "Validation failed.",
  "code": "validation_failed",
  "errors": [
    { "code": "field_required", "field": "currency", "message": "Currency is required." },
    { "code": "field_must_be_positive", "field": "amount", "message": "Amount must be > 0." }
  ]
}

Referencia de campos

Campo Tipo RFC Descripción
type string (URI) RFC 9457 §3.1.1 Enlace directo a la fila de documentación del code concreto. Estable por código: no cambia una vez publicado.
title string RFC 9457 §3.1.3 Título breve del estado HTTP ("Bad Request", "Conflict", …).
status integer RFC 9457 §3.1.2 Código de estado HTTP (coincide con el estado de la respuesta).
detail string RFC 9457 §3.1.4 Explicación en inglés, legible por humanos, de este caso concreto.
code string extensión (§3.2) Identificador estable legible por máquina; consulta Códigos de error. Ramifica tu integración por este valor.
field string extensión (§3.2) Nombre del campo tal y como viaja por la red (snake_case), para errores de validación de un solo campo. En los errores sin campo asociado la clave desaparece por completo: no está en el JSON, nunca llega como null. Solo aparece cuando no hay errors[].
errors array extensión (§3.2) Solo aparece cuando fallan varios campos a la vez. Cada entrada: { code, field, message }.

Por qué códigos estructurados y no solo message

El texto de message y detail puede evolucionar entre versiones, localizarse o ampliarse con contexto. El contrato estable es code: una vez publicado, su semántica no cambia nunca. Usa code para la lógica programática y para la localización.

import { RateLimitError, ValidationError } from '@paymos/sdk';

try {
  await paymos.withdrawals.create(request);
} catch (error) {
  if (error instanceof ValidationError) {
    for (const item of error.errors) highlight(item.field, item.message);
  } else if (error instanceof RateLimitError) {
    scheduleRetry(error.retryAfterSeconds);
  }
  throw error;
}
use paymos::{ApiErrorKind, Error};

match paymos.withdrawals().create(&request).await {
    Ok(withdrawal) => process(withdrawal),
    Err(Error::Api(error)) if error.kind == ApiErrorKind::Validation => {
        for item in &error.errors {
            highlight(item.field.as_deref(), &item.message);
        }
    }
    Err(Error::Api(error)) if error.kind == ApiErrorKind::RateLimit => {
        schedule_retry(error.retry_after);
    }
    Err(error) => return Err(error),
}

Tipos de error

Cada estado llega dentro del formato de error con un title y un code legible por máquina. Un mismo estado puede cubrir varios escenarios: ramifica por el code, no por el estado.

Código HTTP title Descripción
400 Bad Request Falló la validación de entrada o el cuerpo de la petición no es válido. Los fallos de un solo campo llevan code y field en el primer nivel; los de varios campos rellenan errors[].
401 Unauthorized Faltan las credenciales HMAC o están mal formadas, vencidas o no son válidas. El motivo exacto está en code (invalid_credentials, timestamp_expired, authorization_malformed, …).
403 Forbidden La autenticación es correcta, pero el tipo de clave, el entorno o el alcance no permiten la operación. El motivo exacto está en code (merchant_suspended, whitelist_required, etc.).
404 Not Found El recurso no existe, no es visible para quien llama (un recurso de otro comercio devuelve not_found, nunca forbidden) o ningún endpoint corresponde a la ruta de la petición (route_not_found).
405 Method Not Allowed La ruta existe, pero no para este método HTTP. Lleva el código method_not_allowed.
409 Conflict La petición choca con el estado actual: una transición no válida, un recurso que ya existe o una comprobación que bloquea la acción. El motivo exacto está en code (insufficient_balance, withdrawal_quota_exceeded, invoice_cannot_be_cancelled, …).
410 Gone El recurso ya no puede usarse (por ejemplo, una factura vencida o cancelada).
413 Payload Too Large El cuerpo de la petición supera el límite de tamaño (1 MiB). Lleva el código payload_too_large.
415 Unsupported Media Type El Content-Type no es application/json. Lleva el código unsupported_media_type.
429 Too Many Requests Se ha superado el límite de peticiones del comercio. Lee la cabecera Retry-After (en segundos) antes de reintentar.
500 Internal Server Error Algo ha fallado de nuestro lado. Reintenta con backoff exponencial.
503 Service Unavailable Problema temporal de una dependencia o del procesamiento (exchange_rate_unavailable, acceptance_disabled, outbound_frozen, …). Reintenta con backoff.

Guía siempre la lógica de tu cliente por code y no por el estado HTTP: varios escenarios de negocio distintos comparten un mismo estado, y es el código que viaja en la respuesta el que los distingue.

Límites de peticiones

La API permite por defecto 30 peticiones por segundo por comercio, con un límite más estricto en POST /v1/invoices (5 peticiones/segundo). Se cuentan por comercio y por ventana de un segundo —todas tus claves de API consumen el mismo presupuesto— y los dos límites se pueden configurar por comercio. Al superarlo recibirás una respuesta 429 Too Many Requests con la cabecera Retry-After: 1.

{
  "type": "https://paymos.io/docs/errors/codes#rate_limited",
  "title": "Too Many Requests",
  "status": 429,
  "detail": "Rate limit exceeded (30 req/sec). Retry after 1 second.",
  "code": "rate_limited"
}