Conceptos
Errores problem+json
El formato RFC 9457 de los errores del gateway, la tabla de códigos y tipos, la excepción equivalente en cada SDK y cómo leer los errores de validación y los rechazos del motor.
En esta página
Toda respuesta que no es 2xx llega como application/problem+json
(RFC 9457), con la misma forma en los 13 módulos:
{
"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" }
]
}| Campo | Qué es |
|---|---|
type | URI del tipo de error (o about:blank). Sirve para distinguir casos que comparten código. |
title | Resumen corto en inglés. |
status | El código HTTP. |
detail | Explicació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_id | Igual a la cabecera X-Request-ID. Cítalo siempre a soporte. |
errors | Solo 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 #
| HTTP | type | Cuándo | Python / JS | PHP / .NET | Go (errors.Is) |
|---|---|---|---|---|---|
400 | about:blank | Petición malformada; falta Idempotency-Key donde es obligatoria. | CompenseAPIError | CompenseApiException | *APIError |
401 | about:blank | Falta X-API-Key o la firma de alto valor. | AuthError | AuthException | ErrAuth |
403 | about:blank | Llave inválida/revocada/vencida, sin scope, módulo no contratado o IP no permitida. | AuthError | AuthException | ErrAuth |
404 | about:blank | El recurso no existe o es de otro tenant. | NotFoundError | NotFoundException | ErrNotFound |
409 | …/idempotency-conflict | La petición original con esa llave sigue en proceso. | IdempotencyConflictError | IdempotencyConflictException | ErrIdempotencyConflict |
409 | otro | Rechazo de negocio (BILL_ALREADY_PAID, SLOT_NOT_AVAILABLE, REFERENCE_REUSED…). | IdempotencyConflictError | IdempotencyConflictException | ErrIdempotencyConflict |
413 | about:blank | Cuerpo mayor a 1 MB (25 MB en la carga de archivos). | CompenseAPIError | CompenseApiException | *APIError |
422 | …/validation | Cuerpo inválido; ver errors. También rechazos de negocio del motor. | ValidationError | ValidationException | ErrValidation |
429 | …/rate-limited | Límite por minuto o cuota mensual; ver Retry-After. | RateLimitError | RateLimitException | ErrRateLimited |
500 | about:blank | Error inesperado del gateway. | ServerError | ServerException | ErrServer |
502 | …/upstream-error | El motor devolvió un error. | UpstreamError | UpstreamException | ErrUpstream |
504 | …/upstream-error | El motor no respondió a tiempo. | UpstreamError | UpstreamException | ErrUpstream |
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)
raiseimport {
AuthError, CompenseAPIError, CompenseConnectionError, IdempotencyConflictError,
RateLimitError, ValidationError,
} from "@compense/sdk";
try {
await client.wallet.p2p.transfer({ document: "1032456789", resolutionToken: token,
amount: "30000", idempotencyKey: "nova-p2p-2026-11-02-000123" });
} catch (err) {
if (err instanceof ValidationError) {
for (const e of err.errors) console.log(e.field, e.message);
} else if (err instanceof IdempotencyConflictError) {
if (err.problem.type.endsWith("/idempotency-conflict")) { /* repetir con la misma llave */ }
else console.log("Rechazo del motor:", err.detail);
} else if (err instanceof RateLimitError) {
console.log("Reintenta en", err.retryAfter, "s");
} else if (err instanceof AuthError) {
throw err;
} else if (err instanceof CompenseAPIError) {
console.error(`Compense ${err.status} ${err.title} trace=${err.traceId}`);
throw err;
} else if (err instanceof CompenseConnectionError) {
// sin respuesta: reenviar con la MISMA idempotencyKey
} else throw err;
}use Compense\Sdk\Exception\AuthException;
use Compense\Sdk\Exception\CompenseApiException;
use Compense\Sdk\Exception\IdempotencyConflictException;
use Compense\Sdk\Exception\RateLimitException;
use Compense\Sdk\Exception\TransportException;
use Compense\Sdk\Exception\ValidationException;
try {
$client->wallet->p2p->transfer(document: '1032456789', resolutionToken: $token,
amount: '30000', idempotencyKey: 'nova-p2p-2026-11-02-000123');
} catch (ValidationException $e) {
foreach ($e->errors() as $err) {
echo $err['field'], ': ', $err['message'], PHP_EOL;
}
} catch (IdempotencyConflictException $e) {
if (str_ends_with($e->problem->type, '/idempotency-conflict')) {
// en curso: repetir con la misma llave
} else {
echo 'Rechazo del motor: ', $e->detail, PHP_EOL;
}
} catch (RateLimitException $e) {
echo 'Reintenta en ', $e->retryAfter, ' s', PHP_EOL;
} catch (AuthException $e) {
throw $e;
} catch (CompenseApiException $e) {
error_log("Compense {$e->status} {$e->title} trace={$e->traceId}");
throw $e;
} catch (TransportException $e) {
// sin respuesta: reenviar con la MISMA idempotencyKey
}try
{
await client.Wallet.P2P.TransferAsync("1032456789", token, "30000",
idempotencyKey: "nova-p2p-2026-11-02-000123");
}
catch (ValidationException e)
{
foreach (var err in e.Errors) Console.WriteLine($"{err["field"]}: {err["message"]}");
}
catch (IdempotencyConflictException e) when (e.Problem.Type.EndsWith("/idempotency-conflict"))
{
// en curso: repetir con la misma llave
}
catch (IdempotencyConflictException e)
{
Console.WriteLine($"Rechazo del motor: {e.Detail}");
}
catch (RateLimitException e)
{
Console.WriteLine($"Reintenta en {e.RetryAfter}");
}
catch (CompenseApiException e) when (e is not AuthException)
{
logger.LogError("Compense {Status} {Title} trace={TraceId}", e.Status, e.Title, e.TraceId);
throw;
}_, err := client.Wallet.P2P.Transfer(ctx, &compense.WalletTransferParams{
Document: "1032456789", ResolutionToken: token, Amount: "30000",
}, compense.WithIdempotencyKey("nova-p2p-2026-11-02-000123"))
var apiErr *compense.APIError
switch {
case err == nil:
case compense.IsValidationError(err):
errors.As(err, &apiErr)
for _, fe := range apiErr.Errors() {
fmt.Println(fe["field"], fe["message"])
}
case compense.IsIdempotencyConflict(err):
errors.As(err, &apiErr)
if strings.HasSuffix(apiErr.Problem.Type, "/idempotency-conflict") {
// en curso: repetir con la misma llave
} else {
fmt.Println("rechazo del motor:", apiErr.Detail)
}
case compense.IsRateLimited(err):
errors.As(err, &apiErr)
fmt.Println("reintenta en", *apiErr.RetryAfter)
case errors.As(err, &apiErr):
log.Printf("compense %d %s trace=%s", apiErr.StatusCode, apiErr.Title, apiErr.TraceID)
default:
// transporte o contexto: reenviar con la MISMA llave
}curl -s -X POST https://sandbox.dev.compense.co/gw/v1/gateway/wallet/p2p/transfers/ \
-H "X-API-Key: $COMPENSE_API_KEY" -H "Idempotency-Key: nova-p2p-2026-11-02-000123" \
-H "Content-Type: application/json" \
-d '{"document":"1032456789","resolution_token":"vencido","amount":"30000"}' \
-w '\nHTTP %{http_code}\n'
# {"type":"…","title":"…","status":409,"detail":"…","trace_id":"…"}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.