Autenticación
InfraIO Pay tiene dos superficies de API con modelos de auth distintos. Elige la que coincida con quién llama:
| Superficie | Prefijo de ruta | Audiencia | Auth |
|---|---|---|---|
| B2B del comerciante | /b2b/v1/* | Tu servidor | Firma de petición HMAC-SHA256 |
| Dashboard | Por servicio: /auth/*, /payment/*, /merchant/*, /event/*, /user/*, … | Sesiones de navegador para el dashboard del comerciante | Bearer JWT |
Esta página cubre la superficie B2B: la que llamas desde tu servidor con un par de claves de API. Si estás incrustando el dashboard de InfraIO o construyendo tooling interno, usa la superficie del dashboard (docs separadas, aún no públicas).
El gateway enruta cada superficie por un prefijo inicial que
elimina antes de reenviar: /b2b/v1/checkout-sessions/quick
llega a payment-service como /v1/checkout-sessions/quick, y el
/payment/v1/orders del dashboard le llega como /v1/orders. Así
que si ves rutas /v1/* desnudas en otro sitio, esa es la ruta
interna del backend después de que se haya eliminado el prefijo
público: tu cliente siempre envía la forma con prefijo. (Una
consecuencia para la firma: la cadena canónica B2B firma la ruta
con el prefijo /b2b aún adjunto: ver abajo.)
Endpoints
| Entorno | Base URL |
|---|---|
| Test | https://api-dev.infraio.xyz |
| Live | https://api.infraio.xyz |
Mismo patrón de URL: el entorno se controla mediante el prefijo de
la clave (pk_test_… vs pk_live_…), no la URL.
Par de claves
Obtienes dos valores del dashboard del comerciante (Developers → API keys → + Add key):
- Publishable key (
pk_test_…opk_live_…): identifica tu cuenta. Se envía comoX-Client-ID. Seguro de incrustar en tu bundle de navegador (el SDK ya lo hace). - Secret key (
sk_test_…osk_live_…): la clave de firma HMAC. Solo servidor. Trátala como una contraseña de base de datos.
Si una clave secreta alguna vez aterriza en un bundle de navegador, un repo de git, una línea de log o un chat compartido, revócala inmediatamente desde el dashboard. No hay ventana de solapamiento; la revocación es instantánea. Emite una nueva clave y redespliega.
Firmar una petición
Cada llamada a /b2b/v1/* lleva tres cabeceras:
X-Client-ID: pk_live_70d0becbd7a53047b0dfe88a960d10ad
X-Timestamp: 1715990400
X-Signature: 9a8b7c6d… (hex HMAC-SHA256)La firma se calcula sobre una cadena canónica:
METHOD + "\n" + PATH + "\n" + TIMESTAMP + "\n" + BODYMETHOD: verbo HTTP en mayúsculas (POST,GET, …).PATH: ruta de la petición incluyendo el prefijo/b2b, sin el host y sin la query string (p. ej./b2b/v1/checkout-sessions/quick). El gateway verifica la firma sobre la ruta entrante cruda antes de eliminar/b2b, así que el prefijo debe estar presente. Los parámetros de query no se firman: para unGET …?cursor=…&limit=20, firma solo la ruta, no la parte?….TIMESTAMP: unix-segundos, como cadena decimal (p. ej."1715990400"), coincidiendo exactamente conX-Timestamp.BODY: bytes crudos del request body. Cadena vacía paraGET/DELETE.
Firma con HMAC-SHA256 con la clave secreta como llave, salida hex:
Node / TS
import { createHmac } from "node:crypto";
function sign({ method, path, body, secret }: {
method: string; path: string; body: string; secret: string;
}) {
const timestamp = Math.floor(Date.now() / 1000).toString();
const input = [method.toUpperCase(), path, timestamp, body].join("\n");
const signature = createHmac("sha256", secret).update(input).digest("hex");
return { timestamp, signature };
}¿Por qué HMAC y no Bearer?
Una API Bearer-token simple envía tu único secreto por el cable en cada petición. Cualquiera que capture un log de proxy con TLS terminado obtiene las llaves de tu cuenta. La firma HMAC significa que el secreto nunca viaja, solo su firma derivada, que es de un solo uso (atada a esa petición exacta + ese minuto exacto).
El compromiso: tienes que calcular la firma para cada llamada. Un SDK del servidor ocultaría esto; hasta que publiquemos uno, el helper de arriba son ~15 líneas por lenguaje.
Tolerancia de timestamp
La tolerancia vinculante es ±5 minutos (300 segundos), aplicada
por merchant-service cuando verifica la firma. El propio gateway es
ligeramente más laxo (310s) como defensa en profundidad, pero una
petición que pasa el gateway y falla la comprobación interna aún
termina en 401 invalid_signature: asume 300s como el contrato. Dos
implicaciones:
- Sincroniza el reloj de tu servidor con NTP. Un cron de larga duración con un reloj desviado fallará intermitentemente.
- No precalcules y encoles firmas. Si una petición se queda en una cola de reintento durante más de 5 min, su firma expira.
Scopes de clave
Las claves secretas llevan uno o más de estos paquetes de scope:
| Scope | Uso previsto |
|---|---|
read | Listar/leer órdenes, sesiones, reembolsos |
write_order | Crear sesiones de checkout, órdenes |
write_refund | Emitir reembolsos, emitir tokens de solicitud de reembolso |
webhook_manage | Crear/actualizar/eliminar endpoints de webhook |
El dashboard emite una clave de “acceso total” por defecto (los cuatro scopes). Puedes emitir una clave con scope restringido desde Developers → API keys → + Add key y marcar solo los scopes que necesite la integración.
La aplicación de scopes es actualmente consultiva, no obligatoria.
Los scopes se registran en la clave y se te muestran de vuelta en
el dashboard, pero el middleware del gateway aún no rechaza llamadas
fuera de scope: cualquier clave sk_… válida se comporta como
acceso total hoy. El gating de scope por endpoint llega en el
siguiente release. No te apoyes aún en los scopes como límite de
seguridad; trátalos como etiquetas y rota / revoca claves para
restringir el acceso mientras tanto.
Dónde se verifica la firma
La validación HMAC ocurre una vez, en el gateway. El gateway:
- Lee
X-Client-ID,X-Timestamp,X-Signature. - Busca el comerciante + secreto por
pk_…, ejecuta la comprobación de ventana de timestamp, recalcula la firma, compara en tiempo constante. - En éxito, elimina las cabeceras de auth, sella la petición con
cabeceras internas (
X-B2B-Auth: 1,X-Merchant-ID,X-Merchant-Domain) y reenvía al servicio aguas abajo (payment-service, merchant-service, etc.). El entorno + scopes resueltos NO se inyectan hoy: el código aguas abajo que necesita el entorno lo deriva del request body / config por comerciante, no de las cabeceras. - En fallo, devuelve 401
INVALID_SIGNATUREsin tocar el backend.
Los servicios aguas abajo no re-ejecutan HMAC: confían en las
cabeceras inyectadas por el gateway y actúan sobre el comerciante
que el gateway resolvió. Tampoco hacen scope-gating por endpoint:
como se ha mencionado, el scope de la clave no se inyecta, así que
cualquier sk_… autenticada alcanza cualquier endpoint para su
comerciante (la aplicación de scopes es consultiva hoy: ver el
callout bajo Scopes de clave). Esto importa en dos
sentidos:
- Si operas tu propio reverse proxy delante de InfraIO Pay, no
elimines
X-B2B-Auth/X-Merchant-ID(y no las falsifiques tampoco: el gateway rechaza peticiones entrantes que las lleven en el borde público). - Las rutas de red pública (
/b2b/v1/*) son la única superficie que ejecuta el paso HMAC. El gRPC interno entre nuestros servicios usa mTLS: un modelo de confianza distinto que no aceptaX-Client-ID.
Qué sigue
- Errores: forma de la respuesta en 4xx/5xx.
- Seguridad → Claves de API: rotación, revocación, qué hacer si una clave secreta se filtra.
- Webhooks → Verificación de firma:
usa un esquema HMAC diferente (cabecera
X-Signature: sha256=…, firmaX-Timestamp + "." + raw_body, más unX-Signature-Prevopcional durante la ventana de gracia de rotación de 24 horas). No mezcles los esquemas: comparten el algoritmo de hash pero los bytes firmados y la familia de secretos (whsec_…vssk_…) son diferentes.