Conceptos
Autenticación
La cabecera X-API-Key, cómo la llave resuelve el tenant y los scopes, qué significan 401 y 403, la lista de IP permitidas y la firma de peticiones de alto valor.
En esta página
Cada petición al gateway lleva una cabecera de autenticación:
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 #
- Presencia. Sin
X-API-Key→401. - Llave válida. Inexistente, revocada, suspendida o vencida →
403. - 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-Forde proxies conocidos) → si no,403. - Scope. La operación exige un scope (por ejemplo
wallet:transfer); si la llave no lo tiene →403. - Módulo contratado. Aunque la llave tenga el scope, tu entidad debe tener activo el módulo →
403. - Firma de alto valor (opcional, por llave): ver abajo →
401si falta o no es válida. - 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})")import { AuthError } from "@compense/sdk";
try {
const health = await client.system.health();
if (!health.scopes.includes("credits:submit")) throw new Error("La llave no puede radicar créditos");
} catch (err) {
if (err instanceof AuthError) {
console.error(`Llave inválida: ${err.status} ${err.detail} (trace ${err.traceId})`);
process.exit(1);
}
throw err;
}use Compense\Sdk\Exception\AuthException;
try {
$health = $client->system->health();
if (!in_array('credits:submit', $health->scopes, true)) {
throw new RuntimeException('La llave no puede radicar créditos');
}
} catch (AuthException $e) {
exit("Llave inválida: {$e->status} {$e->detail} (trace {$e->traceId})\n");
}try
{
GatewayHealth health = await client.System.HealthAsync();
if (!health.Scopes.Contains("credits:submit"))
throw new InvalidOperationException("La llave no puede radicar créditos");
}
catch (AuthException e)
{
Console.Error.WriteLine($"Llave inválida: {e.Status} {e.Detail} (trace {e.TraceId})");
throw;
}health, err := client.System.Health(ctx)
if errors.Is(err, compense.ErrAuth) {
var apiErr *compense.APIError
errors.As(err, &apiErr)
log.Fatalf("llave inválida: %d %s (trace %s)", apiErr.StatusCode, apiErr.Detail, apiErr.TraceID)
} else if err != nil {
log.Fatal(err)
}
if !slices.Contains(health.Scopes, "credits:submit") {
log.Fatal("la llave no puede radicar créditos")
}curl -i https://sandbox.dev.compense.co/gw/v1/gateway/health/ -H "X-API-Key: $COMPENSE_API_KEY"
# HTTP/2 403
# content-type: application/problem+json
# x-request-id: 9f1c2e7a4b8d4e6fa1c0d2e3f4a5b6c7
# {"type":"about:blank","title":"Forbidden","status":403,"detail":"…","trace_id":"9f1c2e7a…"}401 frente a 403 #
| Código | Causa | Qué hacer |
|---|---|---|
401 | Falta 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. |
403 | Llave 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:
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.
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_idde cada error: es lo primero que te pedirá soporte.