Every non-2xx response arrives as application/problem+json (RFC 9457), with the same shape across all 13 modules:

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" }
  ]
}
FieldWhat it is
typeURI of the error type (or about:blank). Use it to tell apart cases that share a code.
titleShort summary in English.
statusThe HTTP code.
detailExplanation. For engine rejections it carries the engine’s message in Spanish (for example «Saldo insuficiente»); when the engine sends no message, the gateway quotes its code ((code: INSUFFICIENT_FUNDS)).
trace_idSame as the X-Request-ID header. Always give it to support.
errorsOnly on 422: a list of {field, message}.

The gateway never forwards the engine’s raw body or stack trace: on 502/504 the detail is generic and the real error is logged under the trace_id.

Codes, types and exceptions by SDK #

HTTPtypeWhenPython / JSPHP / .NETGo (errors.Is)
400about:blankMalformed request; Idempotency-Key missing where it’s required.CompenseAPIErrorCompenseApiException*APIError
401about:blankX-API-Key or the high-value signature is missing.AuthErrorAuthExceptionErrAuth
403about:blankInvalid/revoked/expired key, missing scope, module not contracted or IP not allowed.AuthErrorAuthExceptionErrAuth
404about:blankThe resource doesn’t exist or belongs to another tenant.NotFoundErrorNotFoundExceptionErrNotFound
409…/idempotency-conflictThe original request with that key is still being processed.IdempotencyConflictErrorIdempotencyConflictExceptionErrIdempotencyConflict
409otherBusiness rejection (BILL_ALREADY_PAID, SLOT_NOT_AVAILABLE, REFERENCE_REUSED…).IdempotencyConflictErrorIdempotencyConflictExceptionErrIdempotencyConflict
413about:blankBody larger than 1 MB (25 MB for file uploads).CompenseAPIErrorCompenseApiException*APIError
422…/validationInvalid body; see errors. Also engine business rejections.ValidationErrorValidationExceptionErrValidation
429…/rate-limitedPer-minute limit or monthly quota; see Retry-After.RateLimitErrorRateLimitExceptionErrRateLimited
500about:blankUnexpected gateway error.ServerErrorServerExceptionErrServer
502…/upstream-errorThe engine returned an error.UpstreamErrorUpstreamExceptionErrUpstream
504…/upstream-errorThe engine didn’t respond in time.UpstreamErrorUpstreamExceptionErrUpstream

All HTTP exceptions inherit from CompenseAPIError (Python, JS) or CompenseApiException (PHP, .NET), which in turn inherit from CompenseError / CompenseException; in Go they’re all *compense.APIError and there are helpers (compense.IsNotFound, IsValidationError, IsRateLimited, IsAuthError, IsIdempotencyConflict, IsUpstreamError, IsServerError). Configuration errors (empty key, unknown environment) are thrown before anything is sent: CompenseConfigError / CompenseConfigException / *compense.ConfigError.

Errors with no HTTP response aren’t CompenseAPIError: Python lets httpx errors through, JS throws CompenseConnectionError (and CompenseTimeoutError), PHP TransportException, .NET HttpRequestException / TaskCanceledException, and Go returns the transport or context error.

Full handling #

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: per-field errors
        print(err["field"], err["message"])
    print(e.detail)                            # or the engine's message
except IdempotencyConflictError as e:
    if e.problem.type.endswith("/idempotency-conflict"):
        ...                                    # in progress: retry with the same key
    else:
        print("Engine rejection:", e.detail)   # e.g. expired token or insufficient funds
except RateLimitError as e:
    print("Retry in", 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

Validation errors (422) #

errors contains one item per field. Use them to show the problem in your form or to log which value from the ERP was wrong. The message text is in English and may change: don’t compare it literally; use field.

Engine rejections #

When the request is valid but the business rejects it (insufficient funds, bill already paid, slot taken, duplicate reference, closed period), the gateway returns the appropriate code (usually 409 or 422) with the engine’s message in detail. Each module guide lists its typical rejections: wallet, loans, health, PQRS…

Always log the trace_id

Every exception exposes the trace_id (e.trace_id, err.traceId, $e->traceId, e.TraceId, apiErr.TraceID). Store it in your logs alongside your own operation id: with it, support finds the exact request in seconds.