Cada petición al gateway lleva una cabecera de autenticación:

HTTP
X-API-Key: cmp_...

No hay OAuth, ni tokens de sesión, ni id de tenant en la URL: la llave es autoidentificable. El gateway la busca, comprueba que esté activa y vigente, y obtiene de ella el tenant (tu entidad), los scopes, el entorno (sandbox/prod), el límite por minuto y la cuota mensual. Los SDK ponen la cabecera por ti en cada llamada.

Qué se valida, en orden #

  1. Presencia. Sin X-API-Key → 401.
  2. Llave válida. Inexistente, revocada, suspendida o vencida → 403.
  3. IP permitida. Si la llave tiene lista de IP permitidas, la IP de origen debe estar en sus rangos (detrás de proxies solo se confía en X-Forwarded-For de proxies conocidos) → si no, 403.
  4. Scope. La operación exige un scope (por ejemplo wallet:transfer); si la llave no lo tiene → 403.
  5. Módulo contratado. Aunque la llave tenga el scope, tu entidad debe tener activo el módulo → 403.
  6. Firma de alto valor (opcional, por llave): ver abajo → 401 si falta o no es válida.
  7. Límites. Límite por minuto y cuota mensual → 429 (Límites de tasa).

El scope de cada operación aparece en la referencia del API y la tabla por módulo está en Obtén tu llave.

Comprobar la llave #

GET /v1/gateway/health/ acepta cualquier llave válida y devuelve el tenant y los scopes: úsalo al arrancar tu servicio para fallar rápido si la llave no sirve.

from compense_gateway import AuthError

try:
    health = client.system.health()
    assert "credits:submit" in health.scopes, "La llave no puede radicar créditos"
except AuthError as e:
    raise SystemExit(f"Llave de Compense inválida: {e.status} {e.detail} (trace {e.trace_id})")

401 frente a 403 #

CódigoCausaQué hacer
401Falta la cabecera X-API-Key, o falta/no cuadra la firma de alto valor.Revisa que tu cliente HTTP (o un proxy) no esté quitando la cabecera.
403Llave inválida, revocada, suspendida o vencida; sin el scope; módulo no contratado; IP fuera de la lista.Mira detail; compara los scopes con system.health(); cita el trace_id a soporte.

En los SDK ambos son la misma excepción: AuthError (Python, JS), AuthException (PHP, .NET) y compense.ErrAuth / compense.IsAuthError (Go). No reintentes un 401/403: no se arregla solo.

Firma de peticiones de alto valor #

Una llave puede tener activada la firma de peticiones para los scopes de alto valor (credits:decide, credits:disburse, subsidies:decide, subsidies:disburse). Entonces esas peticiones deben llevar, además de la llave:

HTTP
X-Compense-Signature: t=<unix>,v1=<hex>

donde <hex> es HMAC-SHA256(signing_secret, "<t>.<cuerpo crudo>") en hexadecimal; el gateway acepta hasta 5 minutos de desfase y compara en tiempo constante.

Los SDK 1.1.0 no firman peticiones

Ninguno de los cinco SDK genera X-Compense-Signature. Si tu llave tiene la firma activada, añade la cabecera con un cliente HTTP propio (WithHTTPClient en Go, un HttpMessageHandler en .NET, un fetch propio en JS, un cliente PSR-18 decorado en PHP) o usa HTTP directo. La firma es opcional y viene desactivada por defecto: confirma con Compense si tu llave la requiere.

Shell
BODY='{"decision":"APPROVE","approved_amount":"3500000"}'
T=$(date +%s)
SIG=$(printf '%s.%s' "$T" "$BODY" | openssl dgst -sha256 -hmac "$COMPENSE_SIGNING_SECRET" -hex | sed 's/^.* //')
curl -X POST "https://sandbox.dev.compense.co/gw/v1/gateway/credits/applications/$APP_ID/decision/" \
  -H "X-API-Key: $COMPENSE_API_KEY" \
  -H "Idempotency-Key: horizonte-decision-$APP_ID" \
  -H "X-Compense-Signature: t=$T,v1=$SIG" \
  -H "Content-Type: application/json" \
  -d "$BODY"

Buenas prácticas #

  • Una llave por sistema y por entorno, con el mínimo de scopes.
  • Guarda las llaves en un gestor de secretos y nunca en el código, el repositorio, una app móvil o un navegador.
  • Si tu infraestructura sale a Internet por IP fijas, pide la lista de IP permitidas para tu llave.
  • Registra el trace_id de cada error: es lo primero que te pedirá soporte.