Toda respuesta que no es 2xx llega como application/problem+json (RFC 9457), con la misma forma en los 13 módulos:

JSON
{
  "type": "https://errors.compense.co/gateway/validation",
  "title": "Validation error",
  "status": 422,
  "detail": "One or more fields are invalid.",
  "trace_id": "9f1c2e7a4b8d4e6fa1c0d2e3f4a5b6c7",
  "errors": [
    { "field": "term_months", "message": "Input should be greater than or equal to 1" }
  ]
}
CampoQué es
typeURI del tipo de error (o about:blank). Sirve para distinguir casos que comparten código.
titleResumen corto en inglés.
statusEl código HTTP.
detailExplicación. En los rechazos del motor trae el mensaje del motor en español (por ejemplo «Saldo insuficiente»); cuando el motor no envía mensaje, el gateway cita su código ((code: INSUFFICIENT_FUNDS)).
trace_idIgual a la cabecera X-Request-ID. Cítalo siempre a soporte.
errorsSolo en 422: lista de {field, message}.

El gateway nunca reenvía el cuerpo crudo ni la traza del motor: en 502/504 el detalle es genérico y el error real queda registrado bajo el trace_id.

Códigos, tipos y excepciones por SDK #

HTTPtypeCuándoPython / JSPHP / .NETGo (errors.Is)
400about:blankPetición malformada; falta Idempotency-Key donde es obligatoria.CompenseAPIErrorCompenseApiException*APIError
401about:blankFalta X-API-Key o la firma de alto valor.AuthErrorAuthExceptionErrAuth
403about:blankLlave inválida/revocada/vencida, sin scope, módulo no contratado o IP no permitida.AuthErrorAuthExceptionErrAuth
404about:blankEl recurso no existe o es de otro tenant.NotFoundErrorNotFoundExceptionErrNotFound
409…/idempotency-conflictLa petición original con esa llave sigue en proceso.IdempotencyConflictErrorIdempotencyConflictExceptionErrIdempotencyConflict
409otroRechazo de negocio (BILL_ALREADY_PAID, SLOT_NOT_AVAILABLE, REFERENCE_REUSED…).IdempotencyConflictErrorIdempotencyConflictExceptionErrIdempotencyConflict
413about:blankCuerpo mayor a 1 MB (25 MB en la carga de archivos).CompenseAPIErrorCompenseApiException*APIError
422…/validationCuerpo inválido; ver errors. También rechazos de negocio del motor.ValidationErrorValidationExceptionErrValidation
429…/rate-limitedLímite por minuto o cuota mensual; ver Retry-After.RateLimitErrorRateLimitExceptionErrRateLimited
500about:blankError inesperado del gateway.ServerErrorServerExceptionErrServer
502…/upstream-errorEl motor devolvió un error.UpstreamErrorUpstreamExceptionErrUpstream
504…/upstream-errorEl motor no respondió a tiempo.UpstreamErrorUpstreamExceptionErrUpstream

Todas las excepciones HTTP heredan de CompenseAPIError (Python, JS) o CompenseApiException (PHP, .NET), que a su vez heredan de CompenseError / CompenseException; en Go todas son *compense.APIError y hay helpers (compense.IsNotFound, IsValidationError, IsRateLimited, IsAuthError, IsIdempotencyConflict, IsUpstreamError, IsServerError). Los errores de configuración (llave vacía, entorno desconocido) se lanzan antes de enviar nada: CompenseConfigError / CompenseConfigException / *compense.ConfigError.

Los errores sin respuesta HTTP no son CompenseAPIError: Python deja pasar los de httpx, JS lanza CompenseConnectionError (y CompenseTimeoutError), PHP TransportException, .NET HttpRequestException / TaskCanceledException y Go el error de transporte o del contexto.

Manejo completo #

from compense_gateway import (AuthError, CompenseAPIError, IdempotencyConflictError,
                              NotFoundError, RateLimitError, ValidationError)

try:
    client.wallet.p2p.transfer(document="1032456789", resolution_token=token,
                               amount="30000", idempotency_key="nova-p2p-2026-11-02-000123")
except ValidationError as e:
    for err in e.errors:                       # 422: errores por campo
        print(err["field"], err["message"])
    print(e.detail)                            # o el mensaje del motor
except IdempotencyConflictError as e:
    if e.problem.type.endswith("/idempotency-conflict"):
        ...                                    # en curso: repetir con la misma llave
    else:
        print("Rechazo del motor:", e.detail)  # p. ej. token vencido o fondos insuficientes
except RateLimitError as e:
    print("Reintenta en", e.retry_after, "s")
except (AuthError, NotFoundError):
    raise
except CompenseAPIError as e:
    log.error("Compense %s %s trace=%s", e.status, e.title, e.trace_id)
    raise

Errores de validación (422) #

errors trae un elemento por campo. Úsalos para mostrar el problema en tu formulario o para registrar qué dato del ERP vino mal. El texto de message está en inglés y puede cambiar: no lo compares literalmente; usa field.

Rechazos del motor #

Cuando la petición es válida pero el negocio la rechaza (fondos insuficientes, factura ya pagada, cupo ocupado, referencia repetida, período cerrado), el gateway devuelve el código que corresponda (409 o 422 normalmente) con el mensaje del motor en detail. Cada guía de módulo lista sus rechazos típicos: billetera, cartera, salud, PQRS…

Registra siempre el trace_id

Todas las excepciones exponen el trace_id (e.trace_id, err.traceId, $e->traceId, e.TraceId, apiErr.TraceID). Guárdalo en tus logs junto con tu id de operación: con él soporte encuentra la petición exacta en segundos.