Authentication
Cada request a Quetzal Invoicing lleva cuatro piezas de identidad. Tres son headers, una es el body. Si cualquiera está mal, el request falla cerrado.
Headers requeridos
| Header | Requerido en | Qué hace |
|---|---|---|
Authorization: Bearer <token> | Todos los endpoints | Access token OAuth2 / JWT. Lleva la identidad del caller, el tenant para el que puede actuar, y los scopes que tiene. |
X-Tenant-Id: <slug> | Todos los endpoints | Contexto explícito del tenant del request. Tiene que matchear el claim tenant de tu token. Se manda por separado para que los tokens multi-tenant (raros) sigan siendo inequívocos por request. |
Idempotency-Key: <uuid> | POST, PATCH | Un UUID que vos generás. Quetzal guarda la huella del request bajo este key por 24h. Reintentar el mismo (método, path, key) devuelve la respuesta original. |
X-Flow-Id: <id> (opcional) | Todos | Un correlation ID. Si mandás uno, Quetzal lo registra en cada evento del request — útil para tracing entre sistemas. |
Scopes del token
El bearer token lleva scopes. Cada endpoint requiere uno o más de:
| Scope | Habilita |
|---|---|
invoicing.document.read | GET /v1/documents, GET /v1/documents/{id}, GET /v1/documents/{id}/events, GET /v1/documents/{id}/evidence |
invoicing.document.write | POST /v1/documents, PATCH /v1/documents/{id}, POST /v1/documents/{id}/cancel, POST /v1/documents/{id}/void |
invoicing.party.read / .write | Las operaciones correspondientes bajo /v1/parties |
invoicing.issuer_profile.read / .write | Las operaciones correspondientes bajo /v1/issuer_profiles |
invoicing.webhook.read / .write | Las operaciones correspondientes bajo /v1/webhook_subscriptions |
Un token puede llevar múltiples scopes. Pedí el set más estrecho que necesites.
Cómo obtener un token
Quetzal no emite tokens por sí mismo — depende del identity provider de tu organización. Hablá con tu admin de Quetzal para:
- Registrar un cliente OAuth2 (o service account) para tu integración.
- Obtener un
client_idyclient_secret. - Pedir los scopes apropiados para tu workload.
El endpoint exacto del provider y el formato del token dependen del deployment. El contrato runtime es fijo: cualquier JWT compliant con RFC 8725, con los scopes correctos y un claim tenant, va a validar.
Por qué Idempotency-Key es obligatorio en writes
Reintentos de red, redelivery de queues, restarts de workers — todo sistema de producción reintenta. Si cada retry creara un documento nuevo, terminarías con emisiones fiscales duplicadas, lo cual significa declaraciones tributarias duplicadas, lo cual significa una conversación con auditoría que no querés.
Idempotency-Key resuelve esto. Generá un UUID por intención (un documento que querés emitir), incluilo en el request, y:
- El primer request que llega gana. Quetzal crea el documento y se acuerda del key.
- Cualquier request posterior con el mismo key — incluso con un body distinto — devuelve el documento original. El body no se re-procesa.
- El key tiene scope a tu tenant + endpoint. Mandar el mismo key en
POST /v1/partiesno choca con uno enPOST /v1/documents. - Los keys se recuerdan por 24 horas. Después se libera el slot.
Siempre generá un UUID fresco por documento nuevo. Reutilizar un key entre documentos distintos te va a devolver el equivocado en silencio.
Errores comunes de autenticación
| Status | Cuándo | Qué revisar |
|---|---|---|
401 Unauthorized | Falta el header Authorization, o el token está malformado / vencido / firmado por el issuer equivocado. | El claim exp del token. La signing key del lado de Quetzal. |
403 Forbidden | El token es válido pero no lleva el scope que necesita el endpoint. | Comparar el claim scope de tu token con la tabla de scopes arriba. |
403 Forbidden (tenant mismatch) | X-Tenant-Id no matchea el claim tenant del token. | El header y el claim tienen que ser el mismo slug. |
409 Conflict | El Idempotency-Key se reutiliza con un body distinto. | O reutilizás el key con el mismo body exacto (replay intencional), o generás un UUID nuevo. |
422 Unprocessable Entity | Idempotency-Key no es un UUID válido. | Generá uno con uuidgen, crypto.randomUUID(), etc. |
Todas las respuestas de error son Problem JSON (RFC 7807) — application/problem+json con un type machine-readable, un title humano, un detail y un instance.
Armándolo todo
Un POST completo se ve así:
POST /v1/documents HTTP/1.1
Host: quetzal-api.mattilda.io
Authorization: Bearer eyJhbGciOiJIUzI1NiI...
X-Tenant-Id: mattilda
Idempotency-Key: 9f8a1c4e-2e6a-4f9c-b3a5-1d4e5f6a7b8c
X-Flow-Id: req-0193a8c2-f4d1
Content-Type: application/json
{ ... }
Configurálo bien una vez en tu cliente HTTP; de ahí en adelante es invisible.
Siguiente
- Quickstart — probá un request completo de punta a punta.
- API Reference — cada endpoint con los scopes que requiere.
- Principios — por qué el contrato se ve así.