# Entornos (/docs/entornos)
Un certificado y una configuración SUNAT declaran su entorno, y ese entorno es inmutable en la
operación: se fija al crearla y se copia a cada intento de envío. No existe un interruptor global.
## BETA [#beta]
Homologación de SUNAT. El comprobante se firma, se comprime, se envía por SOAP y vuelve con un CDR
igual que en producción.
Con una diferencia que conviene conocer antes de perder una madrugada: **el CDR de beta no viene
firmado de verdad**. SUNAT devuelve marcadores literales en lugar de una firma
(`*Private key 'BetaPublicCert' not up*` y equivalentes). La plataforma reconoce ese caso como
`BETA_PLACEHOLDER` y lo acepta **solo** en beta.
Ese mismo CDR con firma de marcador es **rechazado en producción**. No es una comprobación que se
pueda relajar: es lo que distingue un CDR emitido por SUNAT de uno fabricado.
## PRODUCTION [#production]
Requiere un certificado digital real y credenciales SOL del contribuyente.
La activación de un certificado de producción **no se puede pedir**. Un certificado alcanza el
estado `ACTIVE` únicamente después de que la plataforma haya firmado una factura con él en la serie
reservada `F000` y SUNAT beta la haya aceptado. Un certificado de producción no se puede probar
contra beta, y emitir un comprobante real solo para demostrarlo no es una decisión que le
corresponda tomar a la plataforma.
## Antes de cualquiera de los dos [#antes-de-cualquiera-de-los-dos]
`POST /api/v2/documents/validations` no toca ninguno de los dos entornos. Corre en una transacción
`READ ONLY`, no consume cuota, no toma `Idempotency-Key` y está limitada a 60 peticiones por minuto
por credencial. Úsala hasta que el mapeo esté limpio; recién entonces emite en beta.
# Introducción (/docs)
Fac-360 emite comprobantes de pago electrónicos ante SUNAT: factura, boleta, notas de crédito y
débito, retención, percepción, resumen diario y comunicación de baja.
## Las dos superficies [#las-dos-superficies]
La plataforma publica dos APIs sobre el mismo host, cada una con su propio documento OpenAPI y su
propio mecanismo de autenticación.
| Superficie | Para | Autenticación |
| --------------- | -------- | -------------------------------------- |
| `/api/v2/*` | Máquinas | Credencial de API opaca, con scopes |
| `/console/v1/*` | Personas | Sesión de servidor en cookie `__Host-` |
No son la misma API con dos puertas: tienen contratos distintos y privilegios distintos en la base
de datos. Si estás integrando un ERP, la tuya es `/api/v2`.
## Empieza por BETA [#empieza-por-beta]
SUNAT expone un entorno de homologación (`e-beta.sunat.gob.pe`) donde un comprobante recorre el
mismo camino que en producción y devuelve un CDR real. Todo el desarrollo se hace ahí.
Antes incluso de eso existe un paso más barato: `POST /api/v2/documents/validations` corre
exactamente las mismas validaciones que la creación de un comprobante, responde con `valid`,
`findings` y los `totals` calculados, y **no crea nada** — ni documento, ni correlativo, ni
operación, ni consumo de cuota. Es la herramienta para construir un mapeo sin emitir.
El scope `documents:validate` no está implicado por `documents:create`. Es deliberado: un entorno
de desarrollo puede tener una credencial que ensaya y no puede emitir.
## Estado de esta documentación [#estado-de-esta-documentación]
La referencia **se genera desde los contratos publicados** y no se escribe a mano: cada página de
`/docs/api` y `/docs/console` sale de `APIFACT_OPENAPI_V2` y `CONSOLE_OPENAPI_V1` en cada build, con
sus ejemplos de request y sus snippets por lenguaje. Una referencia que se pudiera editar acabaría
contradiciendo a la API, y el lector no tendría forma de saber cuál de las dos miente.
Los mismos documentos se publican tal cual, para un generador de clientes o para un agente:
| Documento | URL |
| ----------------- | ------------------------------------------------------ |
| API para máquinas | [`/openapi/apifact-v2.json`](/openapi/apifact-v2.json) |
| API de la consola | [`/openapi/console-v1.json`](/openapi/console-v1.json) |
Y en `/llms.txt` y `/llms-full.txt` está el sitio entero en texto plano, con la operación de cada
página, si prefieres dárselo a un modelo.
Lo que todavía no está: la guía end-to-end de BETA hasta el CDR, y snippets propios con nuestro SDK.
El playground está **desactivado a propósito** — cuando se encienda hablará solo con BETA, porque un
formulario público capaz de disparar contra la API real emite comprobantes que solo una comunicación
de baja aceptada retira.
# Quickstart (/docs/quickstart)
Esta guía recorre el camino completo de una integración: conseguir una credencial, ensayar el
cuerpo, crear el comprobante, enviarlo a SUNAT y recoger el CDR. Todo contra el edge de staging, que
emite contra SUNAT beta:
```
https://apifact-staging.fac-360.com
```
Nada de lo que se envíe por ahí es fiscalmente válido y nada cuesta. Es donde se construye una
integración, y también donde la prueba de activación del certificado es gratis.
## 1. La credencial y sus scopes [#1-la-credencial-y-sus-scopes]
La API para máquinas autentica con una credencial opaca en `Authorization: Bearer`. Está ligada a un
tenant, expira y lleva scopes; cada operación declara en `x-required-scope` el único scope que
exige, y ningún scope implica otro.
Para el camino de esta guía hacen falta seis:
| Scope | Habilita |
| -------------------- | ---------------------------------------------------- |
| `documents:validate` | `POST /api/v2/documents/validations` |
| `documents:create` | `POST /api/v2/documents` |
| `documents:submit` | `POST /api/v2/documents/{id}/submissions` |
| `documents:read` | `GET /api/v2/documents/{id}` |
| `operations:read` | `GET /api/v2/operations/{id}` |
| `artifacts:read` | `GET /api/v2/documents/{id}/artifacts` y su descarga |
`documents:create` **no** implica `documents:validate`, igual que no implica `documents:submit`.
Es deliberado: separa "puede ensayar" de "puede emitir" y de "puede enviar a SUNAT", así que una
credencial de desarrollo puede tener la primera y ninguna de las otras dos. Si el scope falta, la
respuesta es `403`, no `401`.
Hay dos formas de acuñar una credencial de empresa, y son la misma función de base de datos con dos
puertas:
* Desde la consola,
[`POST /console/v1/companies/{companyId}/credentials`](/docs/console/companies/mintConsoleCredential),
con rol `DEVELOPER` o superior.
* Desde la API de organización,
[`POST /api/v2/organizations/companies/{id}/credentials`](/docs/api/organizations/mintCompanyCredential),
con el scope `companies:manage`.
El token va en el `201` y en ningún otro sitio: solo se guarda su SHA-256, no hay ruta que lo vuelva
a leer, y un token perdido se reemplaza acuñando otro y revocando este. `companies:manage` no está
en el vocabulario que una credencial de empresa puede llevar y no puede estarlo — una credencial que
emite comprobantes y además acuña credenciales es la escalada de privilegios que el modelo confina
al lado de la organización.
**Una credencial pertenece a un edge.** La de staging presentada a producción es desconocida y
responde `401`. El entorno no se puede equivocar en silencio.
## 2. Ensaya antes de emitir [#2-ensaya-antes-de-emitir]
`POST /api/v2/documents/validations` corre exactamente las mismas comprobaciones que la creación y
tira el resultado: no hay comprobante, ni correlativo, ni operación, ni evento de outbox, ni consumo
de cuota. No es un flag de dry-run sobre la ruta de creación — eso se rechazó por la razón obvia:
el día que alguien lo deje puesto, existe un comprobante real. Es una ruta cuyo repositorio no
tiene un solo método de escritura y que corre dentro de una transacción PostgreSQL `READ ONLY`.
No lleva `Idempotency-Key`, porque no crea nada:
```bash
curl -sS https://apifact-staging.fac-360.com/api/v2/documents/validations \
-H "Authorization: Bearer $APIFACT_TOKEN" \
-H "Content-Type: application/json" \
-d @factura.json
```
Con `factura.json`, que es el ejemplo publicado `facturaGravada` tal cual:
```json
{
"schemaVersion": "1.0",
"documentType": "01",
"series": "F001",
"number": "1024",
"issueDate": "2026-08-12",
"issueTime": "10:15:00",
"currency": "PEN",
"supplier": {
"documentType": "6",
"documentNumber": "20601030405",
"legalName": "MI EMPRESA EMISORA S.A.C.",
"address": "AV. JAVIER PRADO ESTE 1234",
"ubigeo": "150131",
"district": "SAN ISIDRO",
"province": "LIMA",
"department": "LIMA"
},
"customer": {
"documentType": "6",
"documentNumber": "20512345678",
"legalName": "COMERCIAL LOS ANDES S.A.C.",
"address": "AV. AREQUIPA 4321",
"ubigeo": "150122",
"district": "MIRAFLORES",
"province": "LIMA",
"department": "LIMA"
},
"lines": [
{
"productCode": "SERV-001",
"description": "Consultoria de implementacion - plan mensual",
"unitCode": "ZZ",
"quantity": "1",
"unitValue": "1000.00",
"affectationCode": "10"
}
]
}
```
Los once ejemplos restantes — boleta con DNI, boleta anónima, notas 07 y 08, detracción, venta al
crédito, exportación, ICBPER, transferencia gratuita — están publicados en la página de
[`createDocument`](/docs/api/documents/createDocument). Ninguno está escrito de memoria, así que
copiar uno y cambiarle los datos es más rápido que deducir la forma del `oneOf`.
**La ruta responde `200` aunque el documento sea inválido.** El veredicto está en el cuerpo, en
`valid`, y el status dice solo si el servicio pudo contestar. Es lo contrario del resto de la API y
es a propósito: esta es la ruta que se llama en bucle mientras se programa, donde "inválido" es el
resultado esperado, y la mayoría de clientes HTTP convierten un 4xx en una excepción que habría que
atrapar y desenvolver para leer la lista.
```json
{
"schemaVersion": "2.0",
"requestId": "9f2f2a4c-8e3a-4a1a-9d1e-0d1a2b3c4d5e",
"valid": true,
"findings": [],
"totals": {
"currency": "PEN",
"lineExtensionAmount": "1000.00",
"igvAmount": "180.00",
"taxAmount": "180.00",
"payableAmount": "1180.00"
}
}
```
`totals` va abreviado arriba: el objeto completo tiene quince campos obligatorios, más `detraction`
y `credit` cuando la operación los tiene, y está descrito entero en
[`validateDocument`](/docs/api/documents/validateDocument). Aparece siempre que la aritmética
cierra, incluso en un documento inválido por otra razón, porque ver el número es la mitad del
trabajo de armar un mapeo.
Cuando `valid` es `false`, `findings` trae los problemas en el orden en que la ruta de creación los
evalúa: `findings[0]` es aquel con el que `POST /api/v2/documents` contestaría, con su `code`
literal — `INVALID_DOCUMENT`, `DOCUMENT_ARITHMETIC_INVALID`, `SUPPLIER_RUC_MISMATCH`,
`AFFECTED_DOCUMENT_NOT_FOUND`, `AFFECTED_FACTURA_CREDIT_EXCEEDED`… — y el `status` HTTP que
devolvería.
Un límite que conviene conocer antes de montar el bucle: 60 peticiones por 60 segundos y por
credencial. Al pasarse, la respuesta es el sobre de error normal con `code` `VALIDATION_RATE_LIMITED`
y una cabecera `Retry-After`, **no** el cuerpo `rate_limit_exceeded` del edge que publican las rutas
de escritura. Así se distingue "estás preguntando demasiado seguido" de "tu comprobante es inválido"
sin parsear un mensaje.
## 3. Crea el comprobante [#3-crea-el-comprobante]
`POST /api/v2/documents` toma el mismo cuerpo y exige la cabecera `Idempotency-Key` (obligatoria, de
1 a 200 caracteres):
```bash
curl -sS https://apifact-staging.fac-360.com/api/v2/documents \
-H "Authorization: Bearer $APIFACT_TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: f001-1024-primera-emision" \
-d @factura.json
```
```json
{
"schemaVersion": "2.0",
"requestId": "1c0f4a9b-3d21-4f5e-8a77-2b6c9e0d1f34",
"operationId": "3a5b7c9d-1e2f-4a6b-8c0d-5e7f9a1b3c5d",
"resourceId": "7d9e1f3a-5b7c-4d9e-8f1a-3b5c7d9e1f3a",
"state": "VALIDATED",
"replay": false
}
```
El `201` crea el documento en estado `VALIDATED`; `resourceId` es el `documentId`. Repetir la misma
`Idempotency-Key` con el mismo cuerpo devuelve `200` con `replay: true` y el mismo `resourceId`;
repetirla con un cuerpo distinto es `409 IDEMPOTENCY_CONFLICT`.
**Crear no es enviar.** El comprobante existe, tiene número y está validado, pero SUNAT todavía no
sabe nada de él. Ese es el paso siguiente.
Dos rechazos que sorprenden aquí. Las series `F000` y `B000` están reservadas para la plataforma y
se rechazan con `422 INVALID_DOCUMENT` en todo tipo de documento, notas incluidas: `F000` es la
serie en la que emite la prueba de activación de certificado y SUNAT la registra bajo el RUC de este
contribuyente, así que un `F000` propio chocaría con un número que SUNAT ya tiene. Y `422
DOCUMENT_ARITHMETIC_INVALID` rechaza las cuatro condiciones aritméticas que ningún esquema puede
expresar — descuentos de línea que superan el valor de la línea, un descuento global que supera el
total, un ajuste sobre base cero, y un cronograma al crédito cuyas cuotas no liquidan lo pendiente.
## 4. Envía a SUNAT y sigue la operación [#4-envía-a-sunat-y-sigue-la-operación]
```bash
curl -sS -X POST \
https://apifact-staging.fac-360.com/api/v2/documents/$DOCUMENT_ID/submissions \
-H "Authorization: Bearer $APIFACT_TOKEN" \
-H "Idempotency-Key: f001-1024-primer-envio"
```
También exige `Idempotency-Key`. Responde `202` con el mismo cuerpo que la creación —
`operationId`, `resourceId`, `state`, `replay` — o `200` si es una repetición idéntica. El `202`
significa que el envío quedó encolado para el Workflow privado de SUNAT y llegará a un estado
terminal; **no** significa que SUNAT lo haya aceptado.
Un `409 TENANT_CAPABILITY_DISABLED` aquí no es un error del cuerpo: es una capacidad apagada en la
empresa, y `error.details` nombra cada requisito por su columna exacta de PostgreSQL —
`submission_publication_enabled` para cualquier tipo, más `boleta_submission_enabled`,
`credit_note_submission_enabled` o `debit_note_submission_enabled` para 03, 07 y 08. Un
administrador la enciende y la misma petición funciona; la petición no se queda esperando a que
alguien la encienda.
Detrás del `202`, el Workflow hace en orden: construye el XML UBL, lo firma, prepara el ZIP de
envío, ejecuta `sendBill` **una sola vez** y finaliza el CDR. Si acaba aceptado u observado, genera
además el QR y las representaciones A4 y ticket.
Para saber en qué va, `GET /api/v2/operations/{id}` con el `operationId` que devolvió el envío:
```bash
curl -sS https://apifact-staging.fac-360.com/api/v2/operations/$OPERATION_ID \
-H "Authorization: Bearer $APIFACT_TOKEN"
```
```json
{
"schemaVersion": "2.0",
"requestId": "5e7f9a1b-3c5d-4e7f-9a1b-3c5d7e9f1a3b",
"operationId": "3a5b7c9d-1e2f-4a6b-8c0d-5e7f9a1b3c5d",
"documentId": "7d9e1f3a-5b7c-4d9e-8f1a-3b5c7d9e1f3a",
"resourceKind": "document",
"resourceId": "7d9e1f3a-5b7c-4d9e-8f1a-3b5c7d9e1f3a",
"operationType": "SUBMIT_DOCUMENT_V2",
"status": "SUCCEEDED",
"startedAt": "2026-08-12T15:16:02.114Z",
"finishedAt": "2026-08-12T15:16:09.887Z"
}
```
`status` recorre `PENDING` y `RUNNING` y termina en `SUCCEEDED` o `FAILED`. Un `operationId` de otro
tenant es indistinguible de uno que nunca existió: ambos responden `404 OPERATION_NOT_FOUND`.
`SUCCEEDED` describe el despacho, no el veredicto fiscal. Ese está en el documento, con
`GET /api/v2/documents/{id}`:
```json
{
"schemaVersion": "2.0",
"requestId": "8c0d5e7f-9a1b-4c3d-8e5f-7a9b1c3d5e7f",
"documentId": "7d9e1f3a-5b7c-4d9e-8f1a-3b5c7d9e1f3a",
"environment": "BETA",
"documentType": "01",
"series": "F001",
"number": "1024",
"state": "ACCEPTED",
"fiscalState": "ISSUED",
"stateVersion": 6,
"createdAt": "2026-08-12T15:15:41.002Z",
"updatedAt": "2026-08-12T15:16:09.887Z",
"responseCode": "0",
"responseMessage": "La Factura numero F001-1024, ha sido aceptada",
"sunat": {
"severity": "ACCEPTED",
"standing": "REGISTERED",
"action": "NONE",
"guidance": "SUNAT accepted and registered the document. No action is required.",
"catalogVersion": "sunat-response-catalog-v2"
}
}
```
`responseCode` `0` es la aceptación; cualquier otro valor es el código de rechazo. El bloque `sunat`
clasifica ese código contra el catálogo versionado de respuestas: `standing` dice si el comprobante
existe fiscalmente en SUNAT y `action` qué hacer a continuación. Dos casos que importan más que el
resto: `standing` `MAY_BE_REGISTERED` con `action` `RECONCILE_DO_NOT_RESEND` significa que reenviar
duplicaría un documento fiscal, y hay que reconciliar antes de tocar nada. Y un comprobante
observado reporta `severity` `ACCEPTED` aquí, porque SUNAT contesta las observaciones con
`ResponseCode` `0` y las lleva como notas; quien dice que hubo observaciones es `state`, con el
valor `OBSERVED`.
`fiscalState` va en un eje aparte y responde otra pregunta: si el comprobante todavía existe.
`ISSUED` es lo normal; `VOIDED` es un comprobante que fue aceptado y después retirado por una
comunicación de baja.
## 5. Recoge el CDR [#5-recoge-el-cdr]
`GET /api/v2/documents/{id}/artifacts` lista los artefactos con metadatos seguros — `kind`,
`status`, `mediaType`, `byteSize`, `sha256`, `readyAt` — y nunca claves de objeto internas ni XML
sin firmar. Los `kind` posibles son `signed-xml`, `submission-zip`, `cdr-zip`, `cdr-xml`, `pdf-a4`,
`pdf-ticket` y `qr-png`.
La descarga es por `kind`, y pasa por un servicio privado que verifica la integridad de los
metadatos antes de transmitir los bytes:
```bash
curl -sS -o cdr-F001-1024.xml \
https://apifact-staging.fac-360.com/api/v2/documents/$DOCUMENT_ID/artifacts/cdr-xml \
-H "Authorization: Bearer $APIFACT_TOKEN"
```
No soporta peticiones `Range`. Un artefacto que todavía no está `READY` responde `409`, no un
fichero a medias.
## 6. La firma del CDR en beta [#6-la-firma-del-cdr-en-beta]
Este es el detalle que cuesta una madrugada si nadie lo escribió: **el CDR que devuelve SUNAT beta
no viene firmado de verdad**. Trae marcadores literales en lugar de la firma y del certificado
(`*Private key 'BetaPublicCert' not up*` y `*Named certificate 'BetaPrivateKey' not up*`). La
plataforma reconoce esa forma exacta como `BETA_PLACEHOLDER` y la acepta **solo** en beta.
Ese mismo CDR se rechaza en producción, y no es una comprobación que se pueda relajar. Si llega un
placeholder de beta mientras creemos estar en producción, el endpoint está mal apuntado y todo lo
"emitido" es ficción; aceptarlo por ser menos estricto convertiría una mala configuración ruidosa
en una silenciosa. Los detalles de cada entorno están en [Entornos](/docs/entornos).
## 7. El paso a producción [#7-el-paso-a-producción]
Lo que cambia de verdad, y poco más:
1. **Otra credencial.** Una credencial pertenece a un edge, así que hay que acuñar una en
`https://apifact.fac-360.com`. La de staging responde `401` allí.
2. **Un certificado digital real y credenciales SOL del contribuyente.** Su activación no se puede
pedir: un certificado llega a `ACTIVE` solo después de que la plataforma haya firmado con él en
la serie reservada `F000` y SUNAT beta lo haya aceptado. Está explicado en
[Entornos](/docs/entornos).
3. **La política de firma del CDR se endurece**, como acaba de decirse.
4. **Las capacidades de la empresa** siguen decidiendo qué tipos se pueden enviar, y son
independientes por tipo.
Y sobre todo, lo que ahí sí es distinto: cada documento aceptado es un comprobante real, entra en el
registro de ventas del contribuyente y solo se retira con una comunicación de baja aceptada.
Lo que **no** cambia: el cuerpo del comprobante, los scopes, la semántica de `Idempotency-Key` y el
significado de cada estado. Y `POST /api/v2/documents/validations` sigue siendo gratis y sin
escritura en los dos entornos — es la ruta con la que se verifica un mapeo nuevo antes de tocar
nada en producción.
## Referencia [#referencia]
* [`validateDocument`](/docs/api/documents/validateDocument) — ensayo sin efectos
* [`createDocument`](/docs/api/documents/createDocument) — creación, con los doce ejemplos publicados
* [`requestDocumentSubmission`](/docs/api/documents/requestDocumentSubmission) — envío a SUNAT
* [`getOperation`](/docs/api/operations/getOperation) — estado de la operación
* [`getDocument`](/docs/api/documents/getDocument) — estado canónico del comprobante
* [`listDocumentArtifacts`](/docs/api/artifacts/listDocumentArtifacts) y
[`downloadDocumentArtifact`](/docs/api/artifacts/downloadDocumentArtifact) — CDR y XML firmado
# Get Health (/docs/api/system/getHealth)
## GET /health
```json
{
"tags": [
"System"
],
"operationId": "getHealth",
"responses": {
"200": {
"description": "Service health",
"content": {
"application/json": {
"schema": {
"type": "object",
"additionalProperties": false,
"required": [
"status",
"phase",
"environment",
"legacyV1AuthConfigured"
],
"properties": {
"status": {
"type": "string",
"enum": [
"ok"
]
},
"phase": {
"type": "string",
"enum": [
"phase2"
]
},
"environment": {
"type": "string"
},
"legacyV1AuthConfigured": {
"type": "boolean"
}
}
}
}
}
}
}
}
```
Any `$ref` above resolves against `/openapi/apifact-v2.json`, the document this page was generated from.
# Get Open Api V2 (/docs/api/system/getOpenApiV2)
## GET /api/v2/openapi.json
```json
{
"tags": [
"System"
],
"operationId": "getOpenApiV2",
"responses": {
"200": {
"description": "Bundled OpenAPI 3.0.3 document"
}
}
}
```
Any `$ref` above resolves against `/openapi/apifact-v2.json`, the document this page was generated from.
# Validate and store a legacy factura or boleta payload (/docs/api/legacy-compatibility/createLegacyInvoice)
Safe asynchronous compatibility route. Requires documents:create; unsupported legacy fields are rejected.
## POST /api/v1/invoice/create
```json
{
"tags": [
"Legacy compatibility"
],
"operationId": "createLegacyInvoice",
"summary": "Validate and store a legacy factura or boleta payload",
"deprecated": true,
"description": "Safe asynchronous compatibility route. Requires documents:create; unsupported legacy fields are rejected.",
"security": [
{
"bearerAuth": []
}
],
"x-required-scope": "documents:create",
"parameters": [
{
"name": "Idempotency-Key",
"in": "header",
"required": false,
"description": "Optional. A deterministic document-identity key is derived when omitted.",
"schema": {
"type": "string",
"minLength": 1,
"maxLength": 200,
"pattern": "^[!-~]+$"
}
}
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/LegacyInvoiceCreateRequest"
},
"examples": {
"default": {
"value": {
"comprobante": {
"tipoOperacion": "0101",
"tipoDoc": "01",
"serie": "F001",
"correlativo": "1030",
"fechaEmision": "2026-08-12",
"horaEmision": "09:30:00",
"tipoMoneda": "PEN",
"tipoPago": "Contado"
},
"cliente": {
"tipoDoc": "6",
"numDoc": "20512345678",
"rznSocial": "COMERCIAL LOS ANDES S.A.C.",
"codigoPais": "PE",
"direccion": "AV. AREQUIPA 4321"
},
"items": [
{
"codProducto": "SERV-001",
"descripcion": "Consultoria de implementacion - plan mensual",
"unidad": "ZZ",
"cantidad": "1",
"mtoValorUnitario": "1000.00",
"codeAfect": "10",
"igvPorcent": "18"
}
]
}
}
}
}
}
},
"responses": {
"200": {
"description": "Idempotent replay or operation status poll",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/LegacyInvoiceResponse"
}
}
}
},
"201": {
"description": "Document created",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/LegacyInvoiceResponse"
}
}
}
},
"400": {
"description": "Legacy compatibility error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/LegacyErrorEnvelope"
}
}
}
},
"401": {
"description": "Legacy compatibility error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/LegacyErrorEnvelope"
}
}
}
},
"403": {
"description": "Legacy compatibility error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/LegacyErrorEnvelope"
}
}
}
},
"404": {
"description": "Legacy compatibility error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/LegacyErrorEnvelope"
}
}
}
},
"409": {
"description": "Legacy compatibility error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/LegacyErrorEnvelope"
}
}
}
},
"413": {
"description": "Legacy compatibility error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/LegacyErrorEnvelope"
}
}
}
},
"415": {
"description": "Legacy compatibility error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/LegacyErrorEnvelope"
}
}
}
},
"422": {
"description": "Legacy compatibility error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/LegacyErrorEnvelope"
}
}
}
},
"429": {
"description": "Request blocked by the edge rate limit",
"content": {
"application/json": {
"schema": {
"type": "object",
"additionalProperties": false,
"required": [
"error"
],
"properties": {
"error": {
"type": "string",
"enum": [
"rate_limit_exceeded"
]
}
}
}
}
}
},
"500": {
"description": "Legacy compatibility error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/LegacyErrorEnvelope"
}
}
}
}
}
}
```
Any `$ref` above resolves against `/openapi/apifact-v2.json`, the document this page was generated from.
# Durably request SUNAT submission for a legacy document identity (/docs/api/legacy-compatibility/submitLegacyInvoice)
Safe asynchronous compatibility route. Requires documents:submit; unsupported legacy fields are rejected.
## POST /api/v1/invoice/send
```json
{
"tags": [
"Legacy compatibility"
],
"operationId": "submitLegacyInvoice",
"summary": "Durably request SUNAT submission for a legacy document identity",
"deprecated": true,
"description": "Safe asynchronous compatibility route. Requires documents:submit; unsupported legacy fields are rejected.",
"security": [
{
"bearerAuth": []
}
],
"x-required-scope": "documents:submit",
"parameters": [
{
"name": "Idempotency-Key",
"in": "header",
"required": false,
"description": "Optional. A deterministic document-identity key is derived when omitted.",
"schema": {
"type": "string",
"minLength": 1,
"maxLength": 200,
"pattern": "^[!-~]+$"
}
}
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/LegacyInvoiceReferenceRequest"
},
"examples": {
"default": {
"value": {
"comprobante": {
"tipoDoc": "01",
"serie": "F001",
"correlativo": "1030"
}
}
}
}
}
}
},
"responses": {
"200": {
"description": "Idempotent replay or operation status poll",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/LegacyInvoiceResponse"
}
}
}
},
"202": {
"description": "Operation accepted",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/LegacyInvoiceResponse"
}
}
}
},
"400": {
"description": "Legacy compatibility error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/LegacyErrorEnvelope"
}
}
}
},
"401": {
"description": "Legacy compatibility error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/LegacyErrorEnvelope"
}
}
}
},
"403": {
"description": "Legacy compatibility error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/LegacyErrorEnvelope"
}
}
}
},
"404": {
"description": "Legacy compatibility error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/LegacyErrorEnvelope"
}
}
}
},
"409": {
"description": "Legacy compatibility error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/LegacyErrorEnvelope"
}
}
}
},
"413": {
"description": "Legacy compatibility error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/LegacyErrorEnvelope"
}
}
}
},
"415": {
"description": "Legacy compatibility error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/LegacyErrorEnvelope"
}
}
}
},
"422": {
"description": "Legacy compatibility error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/LegacyErrorEnvelope"
}
}
}
},
"429": {
"description": "Request blocked by the edge rate limit",
"content": {
"application/json": {
"schema": {
"type": "object",
"additionalProperties": false,
"required": [
"error"
],
"properties": {
"error": {
"type": "string",
"enum": [
"rate_limit_exceeded"
]
}
}
}
}
}
},
"500": {
"description": "Legacy compatibility error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/LegacyErrorEnvelope"
}
}
}
}
}
}
```
Any `$ref` above resolves against `/openapi/apifact-v2.json`, the document this page was generated from.
# Durably request an immutable A4 representation (/docs/api/legacy-compatibility/generateLegacyInvoiceA4)
Safe asynchronous compatibility route. Requires documents:pdf; unsupported legacy fields are rejected.
## POST /api/v1/invoice/a4
```json
{
"tags": [
"Legacy compatibility"
],
"operationId": "generateLegacyInvoiceA4",
"summary": "Durably request an immutable A4 representation",
"deprecated": true,
"description": "Safe asynchronous compatibility route. Requires documents:pdf; unsupported legacy fields are rejected.",
"security": [
{
"bearerAuth": []
}
],
"x-required-scope": "documents:pdf",
"parameters": [
{
"name": "Idempotency-Key",
"in": "header",
"required": false,
"description": "Optional. A deterministic document-identity key is derived when omitted.",
"schema": {
"type": "string",
"minLength": 1,
"maxLength": 200,
"pattern": "^[!-~]+$"
}
}
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/LegacyInvoiceReferenceRequest"
},
"examples": {
"default": {
"value": {
"comprobante": {
"tipoDoc": "01",
"serie": "F001",
"correlativo": "1030"
}
}
}
}
}
}
},
"responses": {
"200": {
"description": "Idempotent replay or operation status poll",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/LegacyInvoiceResponse"
}
}
}
},
"202": {
"description": "Operation accepted",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/LegacyInvoiceResponse"
}
}
}
},
"400": {
"description": "Legacy compatibility error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/LegacyErrorEnvelope"
}
}
}
},
"401": {
"description": "Legacy compatibility error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/LegacyErrorEnvelope"
}
}
}
},
"403": {
"description": "Legacy compatibility error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/LegacyErrorEnvelope"
}
}
}
},
"404": {
"description": "Legacy compatibility error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/LegacyErrorEnvelope"
}
}
}
},
"409": {
"description": "Legacy compatibility error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/LegacyErrorEnvelope"
}
}
}
},
"413": {
"description": "Legacy compatibility error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/LegacyErrorEnvelope"
}
}
}
},
"415": {
"description": "Legacy compatibility error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/LegacyErrorEnvelope"
}
}
}
},
"422": {
"description": "Legacy compatibility error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/LegacyErrorEnvelope"
}
}
}
},
"429": {
"description": "Request blocked by the edge rate limit",
"content": {
"application/json": {
"schema": {
"type": "object",
"additionalProperties": false,
"required": [
"error"
],
"properties": {
"error": {
"type": "string",
"enum": [
"rate_limit_exceeded"
]
}
}
}
}
}
},
"500": {
"description": "Legacy compatibility error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/LegacyErrorEnvelope"
}
}
}
}
}
}
```
Any `$ref` above resolves against `/openapi/apifact-v2.json`, the document this page was generated from.
# Durably request an immutable ticket representation (/docs/api/legacy-compatibility/generateLegacyInvoiceTicket)
Safe asynchronous compatibility route. Requires documents:pdf; unsupported legacy fields are rejected.
## POST /api/v1/invoice/ticket
```json
{
"tags": [
"Legacy compatibility"
],
"operationId": "generateLegacyInvoiceTicket",
"summary": "Durably request an immutable ticket representation",
"deprecated": true,
"description": "Safe asynchronous compatibility route. Requires documents:pdf; unsupported legacy fields are rejected.",
"security": [
{
"bearerAuth": []
}
],
"x-required-scope": "documents:pdf",
"parameters": [
{
"name": "Idempotency-Key",
"in": "header",
"required": false,
"description": "Optional. A deterministic document-identity key is derived when omitted.",
"schema": {
"type": "string",
"minLength": 1,
"maxLength": 200,
"pattern": "^[!-~]+$"
}
}
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/LegacyInvoiceReferenceRequest"
},
"examples": {
"default": {
"value": {
"comprobante": {
"tipoDoc": "01",
"serie": "F001",
"correlativo": "1030"
}
}
}
}
}
}
},
"responses": {
"200": {
"description": "Idempotent replay or operation status poll",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/LegacyInvoiceResponse"
}
}
}
},
"202": {
"description": "Operation accepted",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/LegacyInvoiceResponse"
}
}
}
},
"400": {
"description": "Legacy compatibility error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/LegacyErrorEnvelope"
}
}
}
},
"401": {
"description": "Legacy compatibility error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/LegacyErrorEnvelope"
}
}
}
},
"403": {
"description": "Legacy compatibility error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/LegacyErrorEnvelope"
}
}
}
},
"404": {
"description": "Legacy compatibility error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/LegacyErrorEnvelope"
}
}
}
},
"409": {
"description": "Legacy compatibility error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/LegacyErrorEnvelope"
}
}
}
},
"413": {
"description": "Legacy compatibility error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/LegacyErrorEnvelope"
}
}
}
},
"415": {
"description": "Legacy compatibility error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/LegacyErrorEnvelope"
}
}
}
},
"422": {
"description": "Legacy compatibility error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/LegacyErrorEnvelope"
}
}
}
},
"429": {
"description": "Request blocked by the edge rate limit",
"content": {
"application/json": {
"schema": {
"type": "object",
"additionalProperties": false,
"required": [
"error"
],
"properties": {
"error": {
"type": "string",
"enum": [
"rate_limit_exceeded"
]
}
}
}
}
}
},
"500": {
"description": "Legacy compatibility error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/LegacyErrorEnvelope"
}
}
}
}
}
}
```
Any `$ref` above resolves against `/openapi/apifact-v2.json`, the document this page was generated from.
# Validate and durably create a canonical invoice, receipt or fiscal note (/docs/api/documents/createDocument)
Rehearse a body against POST /api/v2/documents/validations first: it runs every check this route runs, answers 200 with the verdict and the computed totals, creates nothing and costs nothing. NEW: 422 DOCUMENT_ARITHMETIC_INVALID refuses the four arithmetic conditions no schema can express, because each needs the document totals — a line whose allowances exceed its own value, a global allowance exceeding the document total, an adjustment over a zero base, and a Credito schedule whose instalments do not settle what is pending (payableAmount, or the detraction's netPayableAmount under the SPOT). Those four were previously accepted here with 201 and failed for the first time during submission, on a comprobante that could no longer be withdrawn. Credit/debit note creation is capability-gated per tenant. Submission uses separate default-off capabilities and, once accepted or observed, produces the QR and the A4/ticket representations for every supported document type. On-demand versioned generation through POST /api/v2/documents/{id}/pdfs is still limited to 01 and 03. The series F000 and B000 are reserved for the platform and are refused with 422 INVALID_DOCUMENT on every document type, notes included: F000 is what the certificate activation probe issues on, and although that probe is not a document of this tenant's, SUNAT records it under this tenant's RUC — so a tenant-issued F000 would collide with a number SUNAT already holds. B000 is reserved beside it although nothing issues on it, because reserving the factura series and leaving its boleta twin free invites the next caller to reach for B000 as the obviously unused one.
## POST /api/v2/documents
```json
{
"tags": [
"Documents"
],
"operationId": "createDocument",
"summary": "Validate and durably create a canonical invoice, receipt or fiscal note",
"description": "Rehearse a body against POST /api/v2/documents/validations first: it runs every check this route runs, answers 200 with the verdict and the computed totals, creates nothing and costs nothing. NEW: 422 DOCUMENT_ARITHMETIC_INVALID refuses the four arithmetic conditions no schema can express, because each needs the document totals — a line whose allowances exceed its own value, a global allowance exceeding the document total, an adjustment over a zero base, and a Credito schedule whose instalments do not settle what is pending (payableAmount, or the detraction's netPayableAmount under the SPOT). Those four were previously accepted here with 201 and failed for the first time during submission, on a comprobante that could no longer be withdrawn. Credit/debit note creation is capability-gated per tenant. Submission uses separate default-off capabilities and, once accepted or observed, produces the QR and the A4/ticket representations for every supported document type. On-demand versioned generation through POST /api/v2/documents/{id}/pdfs is still limited to 01 and 03. The series F000 and B000 are reserved for the platform and are refused with 422 INVALID_DOCUMENT on every document type, notes included: F000 is what the certificate activation probe issues on, and although that probe is not a document of this tenant's, SUNAT records it under this tenant's RUC — so a tenant-issued F000 would collide with a number SUNAT already holds. B000 is reserved beside it although nothing issues on it, because reserving the factura series and leaving its boleta twin free invites the next caller to reach for B000 as the obviously unused one.",
"security": [
{
"bearerAuth": []
}
],
"x-required-scope": "documents:create",
"parameters": [
{
"name": "Idempotency-Key",
"in": "header",
"required": true,
"schema": {
"type": "string",
"minLength": 1,
"maxLength": 200
}
}
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/CreateDocumentRequest"
},
"examples": {
"facturaGravada": {
"summary": "Factura 01 gravada, la venta domestica ordinaria",
"value": {
"schemaVersion": "1.0",
"documentType": "01",
"series": "F001",
"number": "1024",
"issueDate": "2026-08-12",
"issueTime": "10:15:00",
"currency": "PEN",
"supplier": {
"documentType": "6",
"documentNumber": "20601030405",
"legalName": "MI EMPRESA EMISORA S.A.C.",
"address": "AV. JAVIER PRADO ESTE 1234",
"ubigeo": "150131",
"district": "SAN ISIDRO",
"province": "LIMA",
"department": "LIMA"
},
"customer": {
"documentType": "6",
"documentNumber": "20512345678",
"legalName": "COMERCIAL LOS ANDES S.A.C.",
"address": "AV. AREQUIPA 4321",
"ubigeo": "150122",
"district": "MIRAFLORES",
"province": "LIMA",
"department": "LIMA"
},
"lines": [
{
"productCode": "SERV-001",
"description": "Consultoria de implementacion - plan mensual",
"unitCode": "ZZ",
"quantity": "1",
"unitValue": "1000.00",
"affectationCode": "10"
}
]
}
},
"boletaConDni": {
"summary": "Boleta 03 con adquirente identificado por DNI (catalogo 06 1)",
"value": {
"schemaVersion": "1.0",
"documentType": "03",
"series": "B001",
"number": "3310",
"issueDate": "2026-08-12",
"issueTime": "18:40:00",
"currency": "PEN",
"supplier": {
"documentType": "6",
"documentNumber": "20601030405",
"legalName": "MI EMPRESA EMISORA S.A.C.",
"address": "AV. JAVIER PRADO ESTE 1234",
"ubigeo": "150131",
"district": "SAN ISIDRO",
"province": "LIMA",
"department": "LIMA"
},
"customer": {
"documentType": "1",
"documentNumber": "45678912",
"legalName": "MARIA FERNANDA QUISPE ROJAS"
},
"lines": [
{
"productCode": "PROD-001",
"description": "Polo de algodon talla M",
"unitCode": "NIU",
"quantity": "2",
"unitValue": "25.00",
"affectationCode": "10"
}
]
}
},
"boletaAnonima": {
"summary": "Boleta 03 anonima: catalogo 06 0 con documentNumber '-'",
"description": "La venta de mostrador que no identifica a nadie, legal hasta un total de PEN 700.00 (SUNAT 2514). Es el perfil que menos se adivina y el que el documento publicado llego a declarar imposible.",
"value": {
"schemaVersion": "1.0",
"documentType": "03",
"series": "B001",
"number": "3311",
"issueDate": "2026-08-12",
"issueTime": "18:40:00",
"currency": "PEN",
"supplier": {
"documentType": "6",
"documentNumber": "20601030405",
"legalName": "MI EMPRESA EMISORA S.A.C.",
"address": "AV. JAVIER PRADO ESTE 1234",
"ubigeo": "150131",
"district": "SAN ISIDRO",
"province": "LIMA",
"department": "LIMA"
},
"customer": {
"documentType": "0",
"documentNumber": "-",
"legalName": "CLIENTES VARIOS"
},
"lines": [
{
"productCode": "PROD-001",
"description": "Polo de algodon talla M",
"unitCode": "NIU",
"quantity": "2",
"unitValue": "25.00",
"affectationCode": "10"
}
]
}
},
"notaCreditoTotal": {
"summary": "Nota de credito 07 catalogo 09 01: anula la factura entera",
"description": "Los codigos 01, 02, 03 y 06 anulan: reproducen las lineas y los totales del comprobante afectado, y solo se admite una por factura.",
"value": {
"schemaVersion": "1.0",
"documentType": "07",
"series": "FC01",
"number": "18",
"issueDate": "2026-08-12",
"currency": "PEN",
"reasonCode": "01",
"affectedDocument": {
"documentType": "01",
"series": "F001",
"number": "1025"
},
"supplier": {
"documentType": "6",
"documentNumber": "20601030405",
"legalName": "MI EMPRESA EMISORA S.A.C.",
"address": "AV. JAVIER PRADO ESTE 1234",
"ubigeo": "150131",
"district": "SAN ISIDRO",
"province": "LIMA",
"department": "LIMA"
},
"customer": {
"documentType": "6",
"documentNumber": "20512345678",
"legalName": "COMERCIAL LOS ANDES S.A.C.",
"address": "AV. AREQUIPA 4321",
"ubigeo": "150122",
"district": "MIRAFLORES",
"province": "LIMA",
"department": "LIMA"
},
"lines": [
{
"productCode": "SERV-001",
"description": "Consultoria de implementacion - plan mensual",
"unitCode": "ZZ",
"quantity": "1",
"unitValue": "1000.00",
"affectationCode": "10"
}
]
}
},
"notaCreditoParcial": {
"summary": "Nota de credito 07 catalogo 09 09: acredita parte de la factura",
"description": "Los codigos 04, 05, 07, 08 y 09 llevan lineas propias. El credito acumulado contra una factura no puede superar su total: la nota que lo sobrepasa se rechaza con 409 AFFECTED_FACTURA_CREDIT_EXCEEDED.",
"value": {
"schemaVersion": "1.0",
"documentType": "07",
"series": "FC01",
"number": "19",
"issueDate": "2026-08-12",
"currency": "PEN",
"reasonCode": "09",
"affectedDocument": {
"documentType": "01",
"series": "F001",
"number": "1026"
},
"supplier": {
"documentType": "6",
"documentNumber": "20601030405",
"legalName": "MI EMPRESA EMISORA S.A.C.",
"address": "AV. JAVIER PRADO ESTE 1234",
"ubigeo": "150131",
"district": "SAN ISIDRO",
"province": "LIMA",
"department": "LIMA"
},
"customer": {
"documentType": "6",
"documentNumber": "20512345678",
"legalName": "COMERCIAL LOS ANDES S.A.C.",
"address": "AV. AREQUIPA 4321",
"ubigeo": "150122",
"district": "MIRAFLORES",
"province": "LIMA",
"department": "LIMA"
},
"lines": [
{
"productCode": "SERV-001",
"description": "Disminucion en el valor - descuento comercial acordado",
"unitCode": "ZZ",
"quantity": "1",
"unitValue": "200.00",
"affectationCode": "10"
}
]
}
},
"notaCreditoSobreBoleta": {
"summary": "Nota de credito 07 sobre una boleta 03: serie B, adquirente de la boleta",
"value": {
"schemaVersion": "1.0",
"documentType": "07",
"series": "BC01",
"number": "4",
"issueDate": "2026-08-12",
"currency": "PEN",
"reasonCode": "01",
"affectedDocument": {
"documentType": "03",
"series": "B001",
"number": "3310"
},
"supplier": {
"documentType": "6",
"documentNumber": "20601030405",
"legalName": "MI EMPRESA EMISORA S.A.C.",
"address": "AV. JAVIER PRADO ESTE 1234",
"ubigeo": "150131",
"district": "SAN ISIDRO",
"province": "LIMA",
"department": "LIMA"
},
"customer": {
"documentType": "1",
"documentNumber": "45678912",
"legalName": "MARIA FERNANDA QUISPE ROJAS"
},
"lines": [
{
"productCode": "PROD-001",
"description": "Polo de algodon talla M",
"unitCode": "NIU",
"quantity": "2",
"unitValue": "25.00",
"affectationCode": "10"
}
]
}
},
"notaDebito": {
"summary": "Nota de debito 08 catalogo 10 03",
"value": {
"schemaVersion": "1.0",
"documentType": "08",
"series": "FD01",
"number": "7",
"issueDate": "2026-08-12",
"currency": "PEN",
"reasonCode": "03",
"affectedDocument": {
"documentType": "01",
"series": "F001",
"number": "1026"
},
"supplier": {
"documentType": "6",
"documentNumber": "20601030405",
"legalName": "MI EMPRESA EMISORA S.A.C.",
"address": "AV. JAVIER PRADO ESTE 1234",
"ubigeo": "150131",
"district": "SAN ISIDRO",
"province": "LIMA",
"department": "LIMA"
},
"customer": {
"documentType": "6",
"documentNumber": "20512345678",
"legalName": "COMERCIAL LOS ANDES S.A.C.",
"address": "AV. AREQUIPA 4321",
"ubigeo": "150122",
"district": "MIRAFLORES",
"province": "LIMA",
"department": "LIMA"
},
"lines": [
{
"productCode": "PEN-001",
"description": "Penalidad por incumplimiento de plazo contractual",
"unitCode": "ZZ",
"quantity": "1",
"unitValue": "100.00",
"affectationCode": "10"
}
]
}
},
"facturaConDetraccion": {
"summary": "Factura con detraccion (SPOT), con su operationType 1001",
"value": {
"schemaVersion": "1.0",
"documentType": "01",
"series": "F001",
"number": "1027",
"issueDate": "2026-08-12",
"issueTime": "10:15:00",
"currency": "PEN",
"supplier": {
"documentType": "6",
"documentNumber": "20601030405",
"legalName": "MI EMPRESA EMISORA S.A.C.",
"address": "AV. JAVIER PRADO ESTE 1234",
"ubigeo": "150131",
"district": "SAN ISIDRO",
"province": "LIMA",
"department": "LIMA"
},
"customer": {
"documentType": "6",
"documentNumber": "20512345678",
"legalName": "COMERCIAL LOS ANDES S.A.C.",
"address": "AV. AREQUIPA 4321",
"ubigeo": "150122",
"district": "MIRAFLORES",
"province": "LIMA",
"department": "LIMA"
},
"lines": [
{
"productCode": "TRA-001",
"description": "Servicio de transporte de carga Lima - Arequipa",
"unitCode": "ZZ",
"quantity": "1",
"unitValue": "5000.00",
"affectationCode": "10"
}
],
"operationType": "1001",
"detraction": {
"code": "027",
"percent": "4",
"supplierAccount": "00065012345",
"paymentMeansCode": "001"
}
}
},
"facturaAlCredito": {
"summary": "Factura al credito: paymentMethod y el calendario de cuotas",
"value": {
"schemaVersion": "1.0",
"documentType": "01",
"series": "F001",
"number": "1028",
"issueDate": "2026-08-12",
"issueTime": "10:15:00",
"currency": "PEN",
"supplier": {
"documentType": "6",
"documentNumber": "20601030405",
"legalName": "MI EMPRESA EMISORA S.A.C.",
"address": "AV. JAVIER PRADO ESTE 1234",
"ubigeo": "150131",
"district": "SAN ISIDRO",
"province": "LIMA",
"department": "LIMA"
},
"customer": {
"documentType": "6",
"documentNumber": "20512345678",
"legalName": "COMERCIAL LOS ANDES S.A.C.",
"address": "AV. AREQUIPA 4321",
"ubigeo": "150122",
"district": "MIRAFLORES",
"province": "LIMA",
"department": "LIMA"
},
"lines": [
{
"productCode": "SERV-001",
"description": "Consultoria de implementacion - plan mensual",
"unitCode": "ZZ",
"quantity": "1",
"unitValue": "1000.00",
"affectationCode": "10"
}
],
"paymentMethod": "Credito",
"installments": [
{
"amount": "590.00",
"dueDate": "2026-09-11"
},
{
"amount": "590.00",
"dueDate": "2026-10-11"
}
]
}
},
"facturaExportacion": {
"summary": "Exportacion: afectacion 40, adquirente no domiciliado y USD",
"value": {
"schemaVersion": "1.0",
"documentType": "01",
"series": "F001",
"number": "1029",
"issueDate": "2026-08-12",
"currency": "USD",
"operationType": "0200",
"supplier": {
"documentType": "6",
"documentNumber": "20601030405",
"legalName": "MI EMPRESA EMISORA S.A.C.",
"address": "AV. JAVIER PRADO ESTE 1234",
"ubigeo": "150131",
"district": "SAN ISIDRO",
"province": "LIMA",
"department": "LIMA"
},
"customer": {
"documentType": "7",
"documentNumber": "AB1234567",
"legalName": "GLOBAL TRADING LLC",
"countryCode": "US",
"address": "1200 BRICKELL AVE, MIAMI FL"
},
"lines": [
{
"productCode": "EXP-001",
"description": "Cafe verde arabica en sacos de 60 kg",
"unitCode": "KGM",
"quantity": "600",
"unitValue": "4.50",
"affectationCode": "40"
}
]
}
},
"boletaConIcbper": {
"summary": "Boleta con ICBPER: el impuesto va en la linea que lo soporta",
"value": {
"schemaVersion": "1.0",
"documentType": "03",
"series": "B001",
"number": "3312",
"issueDate": "2026-08-12",
"issueTime": "18:40:00",
"currency": "PEN",
"supplier": {
"documentType": "6",
"documentNumber": "20601030405",
"legalName": "MI EMPRESA EMISORA S.A.C.",
"address": "AV. JAVIER PRADO ESTE 1234",
"ubigeo": "150131",
"district": "SAN ISIDRO",
"province": "LIMA",
"department": "LIMA"
},
"customer": {
"documentType": "1",
"documentNumber": "45678912",
"legalName": "MARIA FERNANDA QUISPE ROJAS"
},
"lines": [
{
"productCode": "PROD-001",
"description": "Polo de algodon talla M",
"unitCode": "NIU",
"quantity": "2",
"unitValue": "25.00",
"affectationCode": "10"
},
{
"productCode": "BOL-001",
"description": "Bolsa plastica",
"unitCode": "NIU",
"quantity": "3",
"unitValue": "0.10",
"affectationCode": "10",
"icbperUnitAmount": "0.50"
}
]
}
},
"facturaConTransferenciaGratuita": {
"summary": "Transferencia gratuita: afectacion 15 con igvRate explicito",
"value": {
"schemaVersion": "1.0",
"documentType": "01",
"series": "F001",
"number": "1031",
"issueDate": "2026-08-12",
"issueTime": "10:15:00",
"currency": "PEN",
"supplier": {
"documentType": "6",
"documentNumber": "20601030405",
"legalName": "MI EMPRESA EMISORA S.A.C.",
"address": "AV. JAVIER PRADO ESTE 1234",
"ubigeo": "150131",
"district": "SAN ISIDRO",
"province": "LIMA",
"department": "LIMA"
},
"customer": {
"documentType": "6",
"documentNumber": "20512345678",
"legalName": "COMERCIAL LOS ANDES S.A.C.",
"address": "AV. AREQUIPA 4321",
"ubigeo": "150122",
"district": "MIRAFLORES",
"province": "LIMA",
"department": "LIMA"
},
"lines": [
{
"productCode": "SERV-001",
"description": "Consultoria de implementacion - plan mensual",
"unitCode": "ZZ",
"quantity": "1",
"unitValue": "1000.00",
"affectationCode": "10"
},
{
"productCode": "BON-001",
"description": "Muestra comercial entregada sin cargo",
"unitCode": "NIU",
"quantity": "1",
"unitValue": "80.00",
"affectationCode": "15",
"igvRate": "18"
}
]
}
}
}
}
}
},
"responses": {
"200": {
"description": "Idempotent replay",
"headers": {
"X-Request-Id": {
"schema": {
"type": "string",
"format": "uuid"
}
}
},
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/DocumentOperationResponse"
}
}
}
},
"201": {
"description": "Document created in VALIDATED state",
"headers": {
"X-Request-Id": {
"schema": {
"type": "string",
"format": "uuid"
}
}
},
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/DocumentOperationResponse"
}
}
}
},
"400": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"401": {
"description": "Missing, malformed, expired, revoked or invalid credential",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
},
"headers": {
"WWW-Authenticate": {
"schema": {
"type": "string"
}
}
}
},
"403": {
"description": "Authenticated credential lacks documents:create",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"409": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"413": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"415": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"422": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"429": {
"description": "Request blocked by the edge rate limit",
"content": {
"application/json": {
"schema": {
"type": "object",
"additionalProperties": false,
"required": [
"error"
],
"properties": {
"error": {
"type": "string",
"enum": [
"rate_limit_exceeded"
]
}
}
}
}
}
},
"500": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
}
}
}
```
Any `$ref` above resolves against `/openapi/apifact-v2.json`, the document this page was generated from.
# Ask what POST /api/v2/documents would answer, without creating anything (/docs/api/documents/validateDocument)
The development tool. It runs every check the create route runs and then throws the result away: no comprobante, no correlativo, no operation, no outbox event, no quota. Free, and rate limited to 60 requests per 60 seconds per credential — a limit about how OFTEN you ask, never about what you sent, which is why it is a 429 with an error envelope and never a verdict.
IT CANNOT WRITE. Not 'it does not': the route holds a repository class with no writing method, and that method runs inside a PostgreSQL READ ONLY transaction, so every INSERT, UPDATE and DELETE inside it — including one a trigger would fire — is refused by the server. A dry-run flag on the create route was rejected for the obvious reason: the day somebody leaves it set, a real comprobante exists.
IT ANSWERS 200 EVEN WHEN THE DOCUMENT IS INVALID. The verdict is `valid`, in the body, and the status says only whether the service could answer. That is the opposite of the rest of this API and it is on purpose: this is the one route you call in a loop while programming, where 'invalid' is the expected result, and most HTTP clients turn a 4xx into a thrown exception you would then have to catch and unwrap to read the list you asked for.
findings[0] IS THE ERROR THE CREATE ROUTE WOULD ANSWER WITH — same code, same status, same details, because both routes read one implementation of the taxonomy. The remaining findings are the problems it would only have reported on your NEXT attempt; collecting them is what removes the round trips. `totals` is present whenever the arithmetic closes, so you can compare the IGV, the payable amount, the detraction split and the derived Cuota identifiers against your own before you issue anything.
NO Idempotency-Key. There is nothing to replay.
THE TWO THINGS IT DOES NOT ANSWER. It does not tell you whether YOUR CREDENTIAL may create the document: a credential holding only documents:validate is told valid: true here and refused with 403 there, which is the point of the separate scope. And two of the rules it reports are snapshots of shared state — the accumulated credit on the affected comprobante, and whether that comprobante already has a cancellation note — so a note created by another request between this answer and yours moves them. That is a race, not a disagreement.
## POST /api/v2/documents/validations
```json
{
"tags": [
"Documents"
],
"operationId": "validateDocument",
"summary": "Ask what POST /api/v2/documents would answer, without creating anything",
"description": "The development tool. It runs every check the create route runs and then throws the result away: no comprobante, no correlativo, no operation, no outbox event, no quota. Free, and rate limited to 60 requests per 60 seconds per credential — a limit about how OFTEN you ask, never about what you sent, which is why it is a 429 with an error envelope and never a verdict.\n\nIT CANNOT WRITE. Not 'it does not': the route holds a repository class with no writing method, and that method runs inside a PostgreSQL READ ONLY transaction, so every INSERT, UPDATE and DELETE inside it — including one a trigger would fire — is refused by the server. A dry-run flag on the create route was rejected for the obvious reason: the day somebody leaves it set, a real comprobante exists.\n\nIT ANSWERS 200 EVEN WHEN THE DOCUMENT IS INVALID. The verdict is `valid`, in the body, and the status says only whether the service could answer. That is the opposite of the rest of this API and it is on purpose: this is the one route you call in a loop while programming, where 'invalid' is the expected result, and most HTTP clients turn a 4xx into a thrown exception you would then have to catch and unwrap to read the list you asked for.\n\nfindings[0] IS THE ERROR THE CREATE ROUTE WOULD ANSWER WITH — same code, same status, same details, because both routes read one implementation of the taxonomy. The remaining findings are the problems it would only have reported on your NEXT attempt; collecting them is what removes the round trips. `totals` is present whenever the arithmetic closes, so you can compare the IGV, the payable amount, the detraction split and the derived Cuota identifiers against your own before you issue anything.\n\nNO Idempotency-Key. There is nothing to replay.\n\nTHE TWO THINGS IT DOES NOT ANSWER. It does not tell you whether YOUR CREDENTIAL may create the document: a credential holding only documents:validate is told valid: true here and refused with 403 there, which is the point of the separate scope. And two of the rules it reports are snapshots of shared state — the accumulated credit on the affected comprobante, and whether that comprobante already has a cancellation note — so a note created by another request between this answer and yours moves them. That is a race, not a disagreement.",
"security": [
{
"bearerAuth": []
}
],
"x-required-scope": "documents:validate",
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/CreateDocumentRequest"
},
"examples": {
"default": {
"value": {
"schemaVersion": "1.0",
"documentType": "01",
"series": "F001",
"number": "1024",
"issueDate": "2026-08-12",
"issueTime": "10:15:00",
"currency": "PEN",
"supplier": {
"documentType": "6",
"documentNumber": "20601030405",
"legalName": "MI EMPRESA EMISORA S.A.C.",
"address": "AV. JAVIER PRADO ESTE 1234",
"ubigeo": "150131",
"district": "SAN ISIDRO",
"province": "LIMA",
"department": "LIMA"
},
"customer": {
"documentType": "6",
"documentNumber": "20512345678",
"legalName": "COMERCIAL LOS ANDES S.A.C.",
"address": "AV. AREQUIPA 4321",
"ubigeo": "150122",
"district": "MIRAFLORES",
"province": "LIMA",
"department": "LIMA"
},
"lines": [
{
"productCode": "SERV-001",
"description": "Consultoria de implementacion - plan mensual",
"unitCode": "ZZ",
"quantity": "1",
"unitValue": "1000.00",
"affectationCode": "10"
}
]
}
}
}
}
}
},
"responses": {
"200": {
"description": "The verdict. `valid: true` with an empty `findings` array, or `valid: false` with the problems in the order the create route would hit them. `totals` is present whenever the arithmetic closes — including for a document that is invalid for some other reason, because seeing the number is half the work of building a mapping — and absent only when the schema refused the body outright or the totals do not close.",
"headers": {
"X-Request-Id": {
"schema": {
"type": "string",
"format": "uuid"
}
}
},
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/DocumentValidationResponse"
}
}
}
},
"400": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"401": {
"description": "Missing, malformed, expired, revoked or invalid credential",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
},
"headers": {
"WWW-Authenticate": {
"schema": {
"type": "string"
}
}
}
},
"403": {
"description": "Authenticated credential lacks documents:validate. documents:create does not imply it, exactly as it does not imply documents:submit.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"413": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"415": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"429": {
"description": "The per-credential validation rate limit. This route answers the ordinary error envelope with code VALIDATION_RATE_LIMITED and a Retry-After header, NOT the edge `rate_limit_exceeded` body the write routes publish, so a caller in a loop can tell 'you are asking too often' from 'your comprobante is invalid' without parsing a message.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
},
"headers": {
"Retry-After": {
"schema": {
"type": "integer"
}
}
}
},
"500": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"503": {
"description": "This deployment has no rate limiter bound (VALIDATION_RATE_LIMITER_UNCONFIGURED). The route fails closed rather than becoming an unmetered free endpoint; nothing else is affected.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
}
}
}
```
Any `$ref` above resolves against `/openapi/apifact-v2.json`, the document this page was generated from.
# Get a tenant-owned document and its canonical state (/docs/api/documents/getDocument)
## GET /api/v2/documents/{id}
```json
{
"tags": [
"Documents"
],
"operationId": "getDocument",
"summary": "Get a tenant-owned document and its canonical state",
"security": [
{
"bearerAuth": []
}
],
"x-required-scope": "documents:read",
"parameters": [
{
"name": "id",
"in": "path",
"required": true,
"schema": {
"type": "string",
"format": "uuid"
}
}
],
"responses": {
"200": {
"description": "Canonical document state",
"headers": {
"X-Request-Id": {
"schema": {
"type": "string",
"format": "uuid"
}
}
},
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/DocumentResponse"
}
}
}
},
"401": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"403": {
"description": "Authenticated credential lacks documents:read",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"404": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"429": {
"description": "Request blocked by the edge rate limit",
"content": {
"application/json": {
"schema": {
"type": "object",
"additionalProperties": false,
"required": [
"error"
],
"properties": {
"error": {
"type": "string",
"enum": [
"rate_limit_exceeded"
]
}
}
}
}
}
},
"500": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
}
}
}
```
Any `$ref` above resolves against `/openapi/apifact-v2.json`, the document this page was generated from.
# Durably request document submission (/docs/api/documents/requestDocumentSubmission)
Creates an idempotent operation and outbox event. Factura, boleta and the restricted 07 and 08 note profiles are admitted only when their independent tenant capabilities are enabled: the capabilities are evaluated when the request arrives, and a request that does not meet them is REFUSED with 409 TENANT_CAPABILITY_DISABLED naming the exact capability column, never accepted and held. A 202 therefore means the submission is queued for the private SUNAT Workflow and will reach a terminal state; it never means the request is waiting for a capability to be turned on. A note profile is pinned per type and not per reason, so a 07 that voids its factura and a 07 that credits part of it are dispatched on identical terms. Successful admission does not imply SUNAT acceptance. An accepted or observed submission of any of these document types produces signed XML, submission ZIP, CDR, QR PNG and the A4 and ticket representations; a note prints its own series-number and amounts and names the document it modifies with its catalog 09/10 reason.
## POST /api/v2/documents/{id}/submissions
```json
{
"tags": [
"Documents"
],
"operationId": "requestDocumentSubmission",
"summary": "Durably request document submission",
"description": "Creates an idempotent operation and outbox event. Factura, boleta and the restricted 07 and 08 note profiles are admitted only when their independent tenant capabilities are enabled: the capabilities are evaluated when the request arrives, and a request that does not meet them is REFUSED with 409 TENANT_CAPABILITY_DISABLED naming the exact capability column, never accepted and held. A 202 therefore means the submission is queued for the private SUNAT Workflow and will reach a terminal state; it never means the request is waiting for a capability to be turned on. A note profile is pinned per type and not per reason, so a 07 that voids its factura and a 07 that credits part of it are dispatched on identical terms. Successful admission does not imply SUNAT acceptance. An accepted or observed submission of any of these document types produces signed XML, submission ZIP, CDR, QR PNG and the A4 and ticket representations; a note prints its own series-number and amounts and names the document it modifies with its catalog 09/10 reason.",
"security": [
{
"bearerAuth": []
}
],
"x-required-scope": "documents:submit",
"parameters": [
{
"name": "id",
"in": "path",
"required": true,
"schema": {
"type": "string",
"format": "uuid"
}
},
{
"name": "Idempotency-Key",
"in": "header",
"required": true,
"schema": {
"type": "string",
"minLength": 1,
"maxLength": 200
}
}
],
"responses": {
"200": {
"description": "Idempotent replay",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/DocumentOperationResponse"
}
}
}
},
"202": {
"description": "Submission request durably accepted",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/DocumentOperationResponse"
}
}
}
},
"400": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"401": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"403": {
"description": "Authenticated credential lacks documents:submit",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"404": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"409": {
"description": "TENANT_CAPABILITY_DISABLED: a capability this company must have for this document type is off, and error.details lists every requirement of the act with its own verdict, each named by its exact PostgreSQL column — submission_publication_enabled for any type, plus boleta_submission_enabled, credit_note_submission_enabled or debit_note_submission_enabled for 03, 07 and 08. An administrator of the company enables it and the same request then succeeds; GET /api/v2/organizations/companies/{id} reports the current state of all nine. DOCUMENT_TYPE_NOT_SUBMITTABLE, DOCUMENT_NOT_SUBMITTABLE and SUBMISSION_ALREADY_PENDING are the other three: the type cannot be sent on this route, the document is not in a submittable state, or a submission of it is already in flight.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"429": {
"description": "Request blocked by the edge rate limit",
"content": {
"application/json": {
"schema": {
"type": "object",
"additionalProperties": false,
"required": [
"error"
],
"properties": {
"error": {
"type": "string",
"enum": [
"rate_limit_exceeded"
]
}
}
}
}
}
},
"500": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
}
}
}
```
Any `$ref` above resolves against `/openapi/apifact-v2.json`, the document this page was generated from.
# Durably request immutable PDF representations (/docs/api/artifacts/requestPdfGeneration)
Pins canonical and signed-XML sources plus explicit template/renderer versions. Generation is asynchronous and never changes fiscal document state. Admission is limited to factura 01 and boleta 03; a 07 or 08 note obtains its QR and representations from its own accepted or observed submission.
## POST /api/v2/documents/{id}/pdfs
```json
{
"tags": [
"Artifacts"
],
"operationId": "requestPdfGeneration",
"summary": "Durably request immutable PDF representations",
"description": "Pins canonical and signed-XML sources plus explicit template/renderer versions. Generation is asynchronous and never changes fiscal document state. Admission is limited to factura 01 and boleta 03; a 07 or 08 note obtains its QR and representations from its own accepted or observed submission.",
"security": [
{
"bearerAuth": []
}
],
"x-required-scope": "documents:pdf",
"parameters": [
{
"name": "id",
"in": "path",
"required": true,
"schema": {
"type": "string",
"format": "uuid"
}
},
{
"name": "Idempotency-Key",
"in": "header",
"required": true,
"schema": {
"type": "string",
"minLength": 1,
"maxLength": 200
}
}
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/GeneratePdfRequest"
},
"examples": {
"default": {
"value": {
"schemaVersion": "2.0",
"outputs": [
{
"kind": "pdf-a4",
"templateVersion": "invoice-a4-v2"
},
{
"kind": "pdf-ticket",
"templateVersion": "invoice-ticket-v1"
}
]
}
}
}
}
}
},
"responses": {
"200": {
"description": "Idempotent replay",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/PdfGenerationResponse"
}
}
}
},
"202": {
"description": "PDF generation durably accepted",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/PdfGenerationResponse"
}
}
}
},
"400": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"401": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"403": {
"description": "Authenticated credential lacks documents:pdf",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"404": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"409": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"413": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"415": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"422": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"429": {
"description": "Request blocked by the edge rate limit",
"content": {
"application/json": {
"schema": {
"type": "object",
"additionalProperties": false,
"required": [
"error"
],
"properties": {
"error": {
"type": "string",
"enum": [
"rate_limit_exceeded"
]
}
}
}
}
}
},
"500": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
}
}
}
```
Any `$ref` above resolves against `/openapi/apifact-v2.json`, the document this page was generated from.
# List public artifacts for a tenant-owned document (/docs/api/artifacts/listDocumentArtifacts)
Returns safe metadata only. Internal object keys, HTML render inputs and unsigned XML are never exposed.
## GET /api/v2/documents/{id}/artifacts
```json
{
"tags": [
"Artifacts"
],
"operationId": "listDocumentArtifacts",
"summary": "List public artifacts for a tenant-owned document",
"description": "Returns safe metadata only. Internal object keys, HTML render inputs and unsigned XML are never exposed.",
"security": [
{
"bearerAuth": []
}
],
"x-required-scope": "artifacts:read",
"parameters": [
{
"name": "id",
"in": "path",
"required": true,
"schema": {
"type": "string",
"format": "uuid"
}
},
{
"name": "representationLimit",
"in": "query",
"required": false,
"schema": {
"type": "integer",
"minimum": 1,
"maximum": 100
}
},
{
"name": "representationCursor",
"in": "query",
"required": false,
"schema": {
"type": "string",
"minLength": 1,
"maxLength": 256
}
}
],
"responses": {
"200": {
"description": "Public artifact metadata",
"headers": {
"X-Request-Id": {
"schema": {
"type": "string",
"format": "uuid"
}
}
},
"content": {
"application/json": {
"schema": {
"oneOf": [
{
"$ref": "#/components/schemas/ArtifactListResponse"
},
{
"$ref": "#/components/schemas/PaginatedArtifactListResponse"
}
]
}
}
}
},
"400": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"401": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"403": {
"description": "Authenticated credential lacks artifacts:read",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"404": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"429": {
"description": "Request blocked by the edge rate limit",
"content": {
"application/json": {
"schema": {
"type": "object",
"additionalProperties": false,
"required": [
"error"
],
"properties": {
"error": {
"type": "string",
"enum": [
"rate_limit_exceeded"
]
}
}
}
}
}
},
"500": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
}
}
}
```
Any `$ref` above resolves against `/openapi/apifact-v2.json`, the document this page was generated from.
# Stream and integrity-check a public document artifact (/docs/api/artifacts/downloadDocumentArtifact)
Streams immutable bytes through a private R2-only service after tenant authorization and metadata integrity checks. Range requests are not supported.
## GET /api/v2/documents/{id}/artifacts/{kind}
```json
{
"tags": [
"Artifacts"
],
"operationId": "downloadDocumentArtifact",
"summary": "Stream and integrity-check a public document artifact",
"description": "Streams immutable bytes through a private R2-only service after tenant authorization and metadata integrity checks. Range requests are not supported.",
"security": [
{
"bearerAuth": []
}
],
"x-required-scope": "artifacts:read",
"parameters": [
{
"name": "id",
"in": "path",
"required": true,
"schema": {
"type": "string",
"format": "uuid"
}
},
{
"name": "kind",
"in": "path",
"required": true,
"schema": {
"type": "string",
"enum": [
"signed-xml",
"submission-zip",
"cdr-zip",
"cdr-xml",
"pdf-a4",
"pdf-ticket",
"qr-png"
]
}
}
],
"responses": {
"200": {
"description": "Integrity-checked artifact stream",
"headers": {
"Cache-Control": {
"schema": {
"type": "string",
"enum": [
"private, no-store"
]
}
},
"Content-Digest": {
"schema": {
"type": "string"
}
},
"Content-Disposition": {
"schema": {
"type": "string"
}
},
"Content-Length": {
"schema": {
"type": "integer",
"minimum": 0
}
},
"X-Request-Id": {
"schema": {
"type": "string",
"format": "uuid"
}
}
},
"content": {
"application/xml": {
"schema": {
"type": "string",
"contentMediaType": "application/octet-stream"
}
},
"application/zip": {
"schema": {
"type": "string",
"contentMediaType": "application/octet-stream"
}
},
"application/pdf": {
"schema": {
"type": "string",
"contentMediaType": "application/octet-stream"
}
},
"image/png": {
"schema": {
"type": "string",
"contentMediaType": "application/octet-stream"
}
}
}
},
"400": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"401": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"403": {
"description": "Authenticated credential lacks artifacts:read",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"404": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"409": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"429": {
"description": "Request blocked by the edge rate limit",
"content": {
"application/json": {
"schema": {
"type": "object",
"additionalProperties": false,
"required": [
"error"
],
"properties": {
"error": {
"type": "string",
"enum": [
"rate_limit_exceeded"
]
}
}
}
}
}
},
"500": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"503": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
}
}
}
```
Any `$ref` above resolves against `/openapi/apifact-v2.json`, the document this page was generated from.
# Download an immutable versioned PDF representation (/docs/api/artifacts/downloadPdfRepresentation)
## GET /api/v2/documents/{id}/artifacts/versions/{representationId}/{kind}
```json
{
"tags": [
"Artifacts"
],
"operationId": "downloadPdfRepresentation",
"summary": "Download an immutable versioned PDF representation",
"security": [
{
"bearerAuth": []
}
],
"x-required-scope": "artifacts:read",
"parameters": [
{
"name": "id",
"in": "path",
"required": true,
"schema": {
"type": "string",
"format": "uuid"
}
},
{
"name": "representationId",
"in": "path",
"required": true,
"schema": {
"type": "string",
"format": "uuid"
}
},
{
"name": "kind",
"in": "path",
"required": true,
"schema": {
"type": "string",
"enum": [
"pdf-a4",
"pdf-ticket"
]
}
}
],
"responses": {
"200": {
"description": "Integrity-checked immutable PDF stream",
"content": {
"application/pdf": {
"schema": {
"type": "string",
"contentMediaType": "application/octet-stream"
}
}
}
},
"400": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"401": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"403": {
"description": "Authenticated credential lacks artifacts:read",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"404": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"409": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"429": {
"description": "Request blocked by the edge rate limit",
"content": {
"application/json": {
"schema": {
"type": "object",
"additionalProperties": false,
"required": [
"error"
],
"properties": {
"error": {
"type": "string",
"enum": [
"rate_limit_exceeded"
]
}
}
}
}
}
},
"500": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"503": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
}
}
}
```
Any `$ref` above resolves against `/openapi/apifact-v2.json`, the document this page was generated from.
# Validate and durably create a daily summary (RC) (/docs/api/summaries/createSummary)
A daily summary (RC) reports the boletas issued on one day and the notes that affect them, and it is the only way to annul a boleta: a line with statusCode 3, never a void communication. Creation is capability-gated per tenant and does not contact SUNAT. A line naming a comprobante this API holds is checked against it: the comprobante must have been accepted or observed by SUNAT, and must not already be withdrawn or carried by another live communication. A line naming a comprobante this API does not hold is accepted and recorded as unlinked — see `references` — because an emitter may have issued it through another provider, on paper, or before integrating; SUNAT's own ERROR 2105 and 2323 police that case.
## POST /api/v2/summaries
```json
{
"tags": [
"Summaries"
],
"operationId": "createSummary",
"summary": "Validate and durably create a daily summary (RC)",
"description": "A daily summary (RC) reports the boletas issued on one day and the notes that affect them, and it is the only way to annul a boleta: a line with statusCode 3, never a void communication. Creation is capability-gated per tenant and does not contact SUNAT. A line naming a comprobante this API holds is checked against it: the comprobante must have been accepted or observed by SUNAT, and must not already be withdrawn or carried by another live communication. A line naming a comprobante this API does not hold is accepted and recorded as unlinked — see `references` — because an emitter may have issued it through another provider, on paper, or before integrating; SUNAT's own ERROR 2105 and 2323 police that case.",
"security": [
{
"bearerAuth": []
}
],
"x-required-scope": "documents:create",
"parameters": [
{
"name": "Idempotency-Key",
"in": "header",
"required": true,
"schema": {
"type": "string",
"minLength": 1,
"maxLength": 200
}
}
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/CreateSummaryRequest"
},
"examples": {
"default": {
"value": {
"schemaVersion": "1.0",
"documentType": "RC",
"issueDate": "2026-08-12",
"referenceDate": "2026-08-12",
"correlative": "1",
"supplier": {
"documentType": "6",
"documentNumber": "20601030405",
"legalName": "MI EMPRESA EMISORA S.A.C."
},
"lines": [
{
"documentType": "03",
"series": "B002",
"number": "1",
"statusCode": "1",
"currency": "PEN",
"totalAmount": "59.00",
"payments": {
"taxable": "50.00"
},
"taxes": {
"igv": "9.00"
}
},
{
"documentType": "03",
"series": "B002",
"number": "2",
"statusCode": "1",
"currency": "PEN",
"customer": {
"documentType": "1",
"documentNumber": "45678912"
},
"totalAmount": "826.00",
"payments": {
"taxable": "700.00"
},
"taxes": {
"igv": "126.00"
}
}
]
}
}
}
}
}
},
"responses": {
"200": {
"description": "Idempotent replay",
"headers": {
"X-Request-Id": {
"schema": {
"type": "string",
"format": "uuid"
}
}
},
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/SummaryOperationResponse"
}
}
}
},
"201": {
"description": "daily summary (RC) created in VALIDATED state",
"headers": {
"X-Request-Id": {
"schema": {
"type": "string",
"format": "uuid"
}
}
},
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/SummaryOperationResponse"
}
}
}
},
"400": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"401": {
"description": "Missing, malformed, expired, revoked or invalid credential",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
},
"headers": {
"WWW-Authenticate": {
"schema": {
"type": "string"
}
}
}
},
"403": {
"description": "Authenticated credential lacks documents:create",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"409": {
"description": "A line names a comprobante this API holds that cannot be reported or withdrawn. AFFECTED_DOCUMENT_NOT_REGISTERED: its submission state is neither ACCEPTED nor OBSERVED, so send it to SUNAT first (SUNAT ERROR 2105 / 2398). AFFECTED_DOCUMENT_ALREADY_VOIDED: it was already withdrawn by a communication SUNAT accepted, which is permanent and has no remedy (SUNAT ERROR 2323 / 2987). AFFECTED_DOCUMENT_VOID_IN_FLIGHT: another baja holds it, or one ended AMBIGUOUS — wait for that communication to settle, and if it is rejected the comprobante is withdrawable again; never send a second baja. AFFECTED_DOCUMENT_ALREADY_REPORTED: another resumen diario already carries it, so look at that summary (SUNAT ERROR 2282). A line naming a comprobante this API does not hold is never refused here.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"413": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"415": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"422": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"429": {
"description": "Request blocked by the edge rate limit",
"content": {
"application/json": {
"schema": {
"type": "object",
"additionalProperties": false,
"required": [
"error"
],
"properties": {
"error": {
"type": "string",
"enum": [
"rate_limit_exceeded"
]
}
}
}
}
}
},
"500": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
}
}
}
```
Any `$ref` above resolves against `/openapi/apifact-v2.json`, the document this page was generated from.
# Get a tenant-owned daily summary (RC) and its ticket operation (/docs/api/summaries/getSummary)
Reports the durable operation behind the summary, including the SUNAT ticket once it exists. The ticket is the only handle that can resolve a dispatch out of band, so it is published rather than hidden.
## GET /api/v2/summaries/{id}
```json
{
"tags": [
"Summaries"
],
"operationId": "getSummary",
"summary": "Get a tenant-owned daily summary (RC) and its ticket operation",
"description": "Reports the durable operation behind the summary, including the SUNAT ticket once it exists. The ticket is the only handle that can resolve a dispatch out of band, so it is published rather than hidden.",
"security": [
{
"bearerAuth": []
}
],
"x-required-scope": "documents:read",
"parameters": [
{
"name": "id",
"in": "path",
"required": true,
"schema": {
"type": "string",
"format": "uuid"
}
}
],
"responses": {
"200": {
"description": "Summary state and ticket operation",
"headers": {
"X-Request-Id": {
"schema": {
"type": "string",
"format": "uuid"
}
}
},
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/SummaryResponse"
}
}
}
},
"401": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"403": {
"description": "Authenticated credential lacks documents:read",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"404": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"429": {
"description": "Request blocked by the edge rate limit",
"content": {
"application/json": {
"schema": {
"type": "object",
"additionalProperties": false,
"required": [
"error"
],
"properties": {
"error": {
"type": "string",
"enum": [
"rate_limit_exceeded"
]
}
}
}
}
}
},
"500": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
}
}
}
```
Any `$ref` above resolves against `/openapi/apifact-v2.json`, the document this page was generated from.
# Durably request daily summary (RC) dispatch (/docs/api/summaries/requestSummarySubmission)
Builds the SummaryDocuments UBL from the stored canonical payload, signs it, packages it and dispatches it with sendSummary, all inside a private durable Workflow. sendSummary answers with a ticket rather than a CDR, so the operation stays in TICKET_PENDING until getStatus resolves it. A dispatch whose ticket is lost is terminal and AMBIGUOUS: it is never resent automatically.
## POST /api/v2/summaries/{id}/submissions
```json
{
"tags": [
"Summaries"
],
"operationId": "requestSummarySubmission",
"summary": "Durably request daily summary (RC) dispatch",
"description": "Builds the SummaryDocuments UBL from the stored canonical payload, signs it, packages it and dispatches it with sendSummary, all inside a private durable Workflow. sendSummary answers with a ticket rather than a CDR, so the operation stays in TICKET_PENDING until getStatus resolves it. A dispatch whose ticket is lost is terminal and AMBIGUOUS: it is never resent automatically.",
"security": [
{
"bearerAuth": []
}
],
"x-required-scope": "documents:submit",
"parameters": [
{
"name": "id",
"in": "path",
"required": true,
"schema": {
"type": "string",
"format": "uuid"
}
},
{
"name": "Idempotency-Key",
"in": "header",
"required": true,
"schema": {
"type": "string",
"minLength": 1,
"maxLength": 200
}
}
],
"responses": {
"200": {
"description": "Idempotent replay",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/SummaryOperationResponse"
}
}
}
},
"202": {
"description": "Dispatch request durably accepted",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/SummaryOperationResponse"
}
}
}
},
"400": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"401": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"403": {
"description": "Authenticated credential lacks documents:submit",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"404": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"409": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"429": {
"description": "Request blocked by the edge rate limit",
"content": {
"application/json": {
"schema": {
"type": "object",
"additionalProperties": false,
"required": [
"error"
],
"properties": {
"error": {
"type": "string",
"enum": [
"rate_limit_exceeded"
]
}
}
}
}
}
},
"500": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"503": {
"description": "The durable dispatch handle could not be started. Nothing reached SUNAT; retry with the same Idempotency-Key.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
}
}
}
```
Any `$ref` above resolves against `/openapi/apifact-v2.json`, the document this page was generated from.
# Validate and durably create a void communication (RA) (/docs/api/summaries/createVoidedCommunication)
A void communication (RA) withdraws already-issued facturas and the notes over them. Boleta 03 is deliberately not accepted: a boleta is annulled through a daily summary line with statusCode 3. Creation is capability-gated per tenant and does not contact SUNAT. The response reports the communication deadline; it never enforces it. A line naming a comprobante this API holds is checked against it: the comprobante must have been accepted or observed by SUNAT, and must not already be withdrawn or carried by another live communication. A line naming a comprobante this API does not hold is accepted and recorded as unlinked — see `references` — because an emitter may have issued it through another provider, on paper, or before integrating; SUNAT's own ERROR 2105 and 2323 police that case.
## POST /api/v2/voided
```json
{
"tags": [
"Summaries"
],
"operationId": "createVoidedCommunication",
"summary": "Validate and durably create a void communication (RA)",
"description": "A void communication (RA) withdraws already-issued facturas and the notes over them. Boleta 03 is deliberately not accepted: a boleta is annulled through a daily summary line with statusCode 3. Creation is capability-gated per tenant and does not contact SUNAT. The response reports the communication deadline; it never enforces it. A line naming a comprobante this API holds is checked against it: the comprobante must have been accepted or observed by SUNAT, and must not already be withdrawn or carried by another live communication. A line naming a comprobante this API does not hold is accepted and recorded as unlinked — see `references` — because an emitter may have issued it through another provider, on paper, or before integrating; SUNAT's own ERROR 2105 and 2323 police that case.",
"security": [
{
"bearerAuth": []
}
],
"x-required-scope": "documents:create",
"parameters": [
{
"name": "Idempotency-Key",
"in": "header",
"required": true,
"schema": {
"type": "string",
"minLength": 1,
"maxLength": 200
}
}
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/CreateVoidedCommunicationRequest"
},
"examples": {
"default": {
"value": {
"schemaVersion": "1.0",
"documentType": "RA",
"issueDate": "2026-08-12",
"referenceDate": "2026-08-12",
"correlative": "1",
"supplier": {
"documentType": "6",
"documentNumber": "20601030405",
"legalName": "MI EMPRESA EMISORA S.A.C."
},
"lines": [
{
"documentType": "01",
"series": "F001",
"number": "1024",
"reason": "ERROR EN EL IMPORTE UNITARIO"
}
]
}
}
}
}
}
},
"responses": {
"200": {
"description": "Idempotent replay",
"headers": {
"X-Request-Id": {
"schema": {
"type": "string",
"format": "uuid"
}
}
},
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/SummaryOperationResponse"
}
}
}
},
"201": {
"description": "void communication (RA) created in VALIDATED state",
"headers": {
"X-Request-Id": {
"schema": {
"type": "string",
"format": "uuid"
}
}
},
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/SummaryOperationResponse"
}
}
}
},
"400": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"401": {
"description": "Missing, malformed, expired, revoked or invalid credential",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
},
"headers": {
"WWW-Authenticate": {
"schema": {
"type": "string"
}
}
}
},
"403": {
"description": "Authenticated credential lacks documents:create",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"409": {
"description": "A line names a comprobante this API holds that cannot be reported or withdrawn. AFFECTED_DOCUMENT_NOT_REGISTERED: its submission state is neither ACCEPTED nor OBSERVED, so send it to SUNAT first (SUNAT ERROR 2105 / 2398). AFFECTED_DOCUMENT_ALREADY_VOIDED: it was already withdrawn by a communication SUNAT accepted, which is permanent and has no remedy (SUNAT ERROR 2323 / 2987). AFFECTED_DOCUMENT_VOID_IN_FLIGHT: another baja holds it, or one ended AMBIGUOUS — wait for that communication to settle, and if it is rejected the comprobante is withdrawable again; never send a second baja. AFFECTED_DOCUMENT_ALREADY_REPORTED: another resumen diario already carries it, so look at that summary (SUNAT ERROR 2282). A line naming a comprobante this API does not hold is never refused here.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"413": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"415": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"422": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"429": {
"description": "Request blocked by the edge rate limit",
"content": {
"application/json": {
"schema": {
"type": "object",
"additionalProperties": false,
"required": [
"error"
],
"properties": {
"error": {
"type": "string",
"enum": [
"rate_limit_exceeded"
]
}
}
}
}
}
},
"500": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
}
}
}
```
Any `$ref` above resolves against `/openapi/apifact-v2.json`, the document this page was generated from.
# Get a tenant-owned void communication (RA) and its ticket operation (/docs/api/summaries/getVoidedCommunication)
Reports the durable operation behind the summary, including the SUNAT ticket once it exists. The ticket is the only handle that can resolve a dispatch out of band, so it is published rather than hidden.
## GET /api/v2/voided/{id}
```json
{
"tags": [
"Summaries"
],
"operationId": "getVoidedCommunication",
"summary": "Get a tenant-owned void communication (RA) and its ticket operation",
"description": "Reports the durable operation behind the summary, including the SUNAT ticket once it exists. The ticket is the only handle that can resolve a dispatch out of band, so it is published rather than hidden.",
"security": [
{
"bearerAuth": []
}
],
"x-required-scope": "documents:read",
"parameters": [
{
"name": "id",
"in": "path",
"required": true,
"schema": {
"type": "string",
"format": "uuid"
}
}
],
"responses": {
"200": {
"description": "Summary state and ticket operation",
"headers": {
"X-Request-Id": {
"schema": {
"type": "string",
"format": "uuid"
}
}
},
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/SummaryResponse"
}
}
}
},
"401": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"403": {
"description": "Authenticated credential lacks documents:read",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"404": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"429": {
"description": "Request blocked by the edge rate limit",
"content": {
"application/json": {
"schema": {
"type": "object",
"additionalProperties": false,
"required": [
"error"
],
"properties": {
"error": {
"type": "string",
"enum": [
"rate_limit_exceeded"
]
}
}
}
}
}
},
"500": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
}
}
}
```
Any `$ref` above resolves against `/openapi/apifact-v2.json`, the document this page was generated from.
# Durably request void communication (RA) dispatch (/docs/api/summaries/requestVoidedCommunicationSubmission)
Builds the VoidedDocuments UBL from the stored canonical payload, signs it, packages it and dispatches it with sendSummary inside the same private durable Workflow as the daily summary, with the same ticket cycle and the same non-resendable ambiguity fence.
## POST /api/v2/voided/{id}/submissions
```json
{
"tags": [
"Summaries"
],
"operationId": "requestVoidedCommunicationSubmission",
"summary": "Durably request void communication (RA) dispatch",
"description": "Builds the VoidedDocuments UBL from the stored canonical payload, signs it, packages it and dispatches it with sendSummary inside the same private durable Workflow as the daily summary, with the same ticket cycle and the same non-resendable ambiguity fence.",
"security": [
{
"bearerAuth": []
}
],
"x-required-scope": "documents:submit",
"parameters": [
{
"name": "id",
"in": "path",
"required": true,
"schema": {
"type": "string",
"format": "uuid"
}
},
{
"name": "Idempotency-Key",
"in": "header",
"required": true,
"schema": {
"type": "string",
"minLength": 1,
"maxLength": 200
}
}
],
"responses": {
"200": {
"description": "Idempotent replay",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/SummaryOperationResponse"
}
}
}
},
"202": {
"description": "Dispatch request durably accepted",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/SummaryOperationResponse"
}
}
}
},
"400": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"401": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"403": {
"description": "Authenticated credential lacks documents:submit",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"404": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"409": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"429": {
"description": "Request blocked by the edge rate limit",
"content": {
"application/json": {
"schema": {
"type": "object",
"additionalProperties": false,
"required": [
"error"
],
"properties": {
"error": {
"type": "string",
"enum": [
"rate_limit_exceeded"
]
}
}
}
}
}
},
"500": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"503": {
"description": "The durable dispatch handle could not be started. Nothing reached SUNAT; retry with the same Idempotency-Key.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
}
}
}
```
Any `$ref` above resolves against `/openapi/apifact-v2.json`, the document this page was generated from.
# Validate and durably create a comprobante de retención (CRE, 20) (/docs/api/payment-regimes/createRetention)
A comprobante de retención declares the documents an agente de retención is settling, the payment made against each one, the amount withheld and the net handed over. Every monetary figure in the response is DERIVED from the payments and the declared rate; none may be supplied. Creation is capability-gated per tenant (retention_issuance_enabled, off by default) and does not contact SUNAT.
## POST /api/v2/retentions
```json
{
"tags": [
"Payment regimes"
],
"operationId": "createRetention",
"summary": "Validate and durably create a comprobante de retención (CRE, 20)",
"description": "A comprobante de retención declares the documents an agente de retención is settling, the payment made against each one, the amount withheld and the net handed over. Every monetary figure in the response is DERIVED from the payments and the declared rate; none may be supplied. Creation is capability-gated per tenant (retention_issuance_enabled, off by default) and does not contact SUNAT.",
"security": [
{
"bearerAuth": []
}
],
"x-required-scope": "documents:create",
"parameters": [
{
"name": "Idempotency-Key",
"in": "header",
"required": true,
"schema": {
"type": "string",
"minLength": 1,
"maxLength": 200
}
}
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/CreateRetentionRequest"
},
"examples": {
"default": {
"value": {
"schemaVersion": "1.0",
"documentType": "20",
"series": "R001",
"number": "56",
"issueDate": "2026-08-12",
"currency": "PEN",
"regimeCode": "01",
"regimePercent": "3",
"note": "Retencion del regimen general aplicada al pago de la factura del proveedor",
"agent": {
"documentType": "6",
"documentNumber": "20601030405",
"legalName": "MI EMPRESA EMISORA S.A.C.",
"address": "AV. JAVIER PRADO ESTE 1234",
"ubigeo": "150131",
"district": "SAN ISIDRO",
"province": "LIMA",
"department": "LIMA"
},
"receiver": {
"documentType": "6",
"documentNumber": "20512345678",
"legalName": "COMERCIAL LOS ANDES S.A.C.",
"address": "AV. AREQUIPA 4321",
"ubigeo": "150122",
"district": "MIRAFLORES",
"province": "LIMA",
"department": "LIMA"
},
"documents": [
{
"documentType": "01",
"series": "F500",
"number": "128",
"issueDate": "2026-08-12",
"totalAmount": "1180.00",
"currency": "PEN",
"payment": {
"id": "1",
"amount": "1180.00",
"date": "2026-08-12"
},
"regimeDate": "2026-08-12"
}
]
}
}
}
}
}
},
"responses": {
"200": {
"description": "Idempotent replay",
"headers": {
"X-Request-Id": {
"schema": {
"type": "string",
"format": "uuid"
}
}
},
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/PaymentRegimeOperationResponse"
}
}
}
},
"201": {
"description": "comprobante de retención (CRE, 20) created in VALIDATED state",
"headers": {
"X-Request-Id": {
"schema": {
"type": "string",
"format": "uuid"
}
}
},
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/PaymentRegimeOperationResponse"
}
}
}
},
"400": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"401": {
"description": "Missing, malformed, expired, revoked or invalid credential",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
},
"headers": {
"WWW-Authenticate": {
"schema": {
"type": "string"
}
}
}
},
"403": {
"description": "The credential lacks documents:create, or the declared agent is not the authenticated issuer. The agent of a CRE/CPE is its issuer, so declaring another RUC is an authorization failure rather than a payload error.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"409": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"413": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"415": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"422": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"429": {
"description": "Request blocked by the edge rate limit",
"content": {
"application/json": {
"schema": {
"type": "object",
"additionalProperties": false,
"required": [
"error"
],
"properties": {
"error": {
"type": "string",
"enum": [
"rate_limit_exceeded"
]
}
}
}
}
}
},
"500": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
}
}
}
```
Any `$ref` above resolves against `/openapi/apifact-v2.json`, the document this page was generated from.
# Get a tenant-owned comprobante de retención (CRE, 20) and its references (/docs/api/payment-regimes/getRetention)
Returns the stored header and every reference in the order the signed XML declares them, with the PEN base, the amount withheld and the net for each. netAmount = baseAmount - regimeAmount.
## GET /api/v2/retentions/{id}
```json
{
"tags": [
"Payment regimes"
],
"operationId": "getRetention",
"summary": "Get a tenant-owned comprobante de retención (CRE, 20) and its references",
"description": "Returns the stored header and every reference in the order the signed XML declares them, with the PEN base, the amount withheld and the net for each. netAmount = baseAmount - regimeAmount.",
"security": [
{
"bearerAuth": []
}
],
"x-required-scope": "documents:read",
"parameters": [
{
"name": "id",
"in": "path",
"required": true,
"schema": {
"type": "string",
"format": "uuid"
}
}
],
"responses": {
"200": {
"description": "Header and references, in declaration order",
"headers": {
"X-Request-Id": {
"schema": {
"type": "string",
"format": "uuid"
}
}
},
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/PaymentRegimeResponse"
}
}
}
},
"401": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"403": {
"description": "Authenticated credential lacks documents:read",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"404": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"429": {
"description": "Request blocked by the edge rate limit",
"content": {
"application/json": {
"schema": {
"type": "object",
"additionalProperties": false,
"required": [
"error"
],
"properties": {
"error": {
"type": "string",
"enum": [
"rate_limit_exceeded"
]
}
}
}
}
}
},
"500": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
}
}
}
```
Any `$ref` above resolves against `/openapi/apifact-v2.json`, the document this page was generated from.
# Durably request comprobante de retención (CRE, 20) dispatch (/docs/api/payment-regimes/submitRetention)
Durably requests dispatch through SUNAT's 'otros CPE' service, which is a different deployment from the one that carries facturas and boletas. Answers 202 with the operation id allocated at creation. Dispatch is gated by submission_publication_enabled AND retention_issuance_enabled, both re-read under a row lock immediately before the outbound call. Exactly one envelope can ever leave for a given document: the attempt is fenced in the database before the call, and a run that finds a fenced attempt with no CDR settles AMBIGUOUS for reconciliation rather than resending.
## POST /api/v2/retentions/{id}/submissions
```json
{
"tags": [
"Payment regimes"
],
"operationId": "submitRetention",
"summary": "Durably request comprobante de retención (CRE, 20) dispatch",
"description": "Durably requests dispatch through SUNAT's 'otros CPE' service, which is a different deployment from the one that carries facturas and boletas. Answers 202 with the operation id allocated at creation. Dispatch is gated by submission_publication_enabled AND retention_issuance_enabled, both re-read under a row lock immediately before the outbound call. Exactly one envelope can ever leave for a given document: the attempt is fenced in the database before the call, and a run that finds a fenced attempt with no CDR settles AMBIGUOUS for reconciliation rather than resending.",
"security": [
{
"bearerAuth": []
}
],
"x-required-scope": "documents:submit",
"parameters": [
{
"name": "id",
"in": "path",
"required": true,
"schema": {
"type": "string",
"format": "uuid"
}
},
{
"name": "Idempotency-Key",
"in": "header",
"required": true,
"schema": {
"type": "string",
"minLength": 1,
"maxLength": 200
}
}
],
"responses": {
"200": {
"description": "Idempotent replay",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/PaymentRegimeOperationResponse"
}
}
}
},
"202": {
"description": "Dispatch request durably accepted",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/PaymentRegimeOperationResponse"
}
}
}
},
"400": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"401": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"403": {
"description": "Authenticated credential lacks documents:submit",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"404": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"409": {
"description": "The tenant capability is off, a dispatch is already in flight, or the document has already settled. A settled document is never redispatched: an ambiguous submission is fenced for reconciliation, never resent.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"429": {
"description": "Request blocked by the edge rate limit",
"content": {
"application/json": {
"schema": {
"type": "object",
"additionalProperties": false,
"required": [
"error"
],
"properties": {
"error": {
"type": "string",
"enum": [
"rate_limit_exceeded"
]
}
}
}
}
}
},
"500": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"503": {
"description": "The durable dispatch handle could not be started. Nothing reached SUNAT; retry with the same Idempotency-Key.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
}
}
}
```
Any `$ref` above resolves against `/openapi/apifact-v2.json`, the document this page was generated from.
# Validate and durably create a comprobante de percepción (CPE, 40) (/docs/api/payment-regimes/createPerception)
A comprobante de percepción declares the documents an agente de percepción is collecting, the collection made against each one, the amount charged on top and the total collected. It is the mirror of the retention and the only arithmetic difference is the sign. Creation is capability-gated per tenant (perception_issuance_enabled, off by default) and does not contact SUNAT.
## POST /api/v2/perceptions
```json
{
"tags": [
"Payment regimes"
],
"operationId": "createPerception",
"summary": "Validate and durably create a comprobante de percepción (CPE, 40)",
"description": "A comprobante de percepción declares the documents an agente de percepción is collecting, the collection made against each one, the amount charged on top and the total collected. It is the mirror of the retention and the only arithmetic difference is the sign. Creation is capability-gated per tenant (perception_issuance_enabled, off by default) and does not contact SUNAT.",
"security": [
{
"bearerAuth": []
}
],
"x-required-scope": "documents:create",
"parameters": [
{
"name": "Idempotency-Key",
"in": "header",
"required": true,
"schema": {
"type": "string",
"minLength": 1,
"maxLength": 200
}
}
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/CreatePerceptionRequest"
},
"examples": {
"default": {
"value": {
"schemaVersion": "1.0",
"documentType": "40",
"series": "P001",
"number": "31",
"issueDate": "2026-08-12",
"currency": "PEN",
"regimeCode": "01",
"regimePercent": "2",
"note": "Percepcion por venta interna aplicada al cobro de la factura",
"agent": {
"documentType": "6",
"documentNumber": "20601030405",
"legalName": "MI EMPRESA EMISORA S.A.C.",
"address": "AV. JAVIER PRADO ESTE 1234",
"ubigeo": "150131",
"district": "SAN ISIDRO",
"province": "LIMA",
"department": "LIMA"
},
"receiver": {
"documentType": "6",
"documentNumber": "20512345678",
"legalName": "COMERCIAL LOS ANDES S.A.C.",
"address": "AV. AREQUIPA 4321",
"ubigeo": "150122",
"district": "MIRAFLORES",
"province": "LIMA",
"department": "LIMA"
},
"documents": [
{
"documentType": "01",
"series": "F001",
"number": "1024",
"issueDate": "2026-08-12",
"totalAmount": "1180.00",
"currency": "PEN",
"payment": {
"id": "1",
"amount": "1180.00",
"date": "2026-08-12"
},
"regimeDate": "2026-08-12"
}
]
}
}
}
}
}
},
"responses": {
"200": {
"description": "Idempotent replay",
"headers": {
"X-Request-Id": {
"schema": {
"type": "string",
"format": "uuid"
}
}
},
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/PaymentRegimeOperationResponse"
}
}
}
},
"201": {
"description": "comprobante de percepción (CPE, 40) created in VALIDATED state",
"headers": {
"X-Request-Id": {
"schema": {
"type": "string",
"format": "uuid"
}
}
},
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/PaymentRegimeOperationResponse"
}
}
}
},
"400": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"401": {
"description": "Missing, malformed, expired, revoked or invalid credential",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
},
"headers": {
"WWW-Authenticate": {
"schema": {
"type": "string"
}
}
}
},
"403": {
"description": "The credential lacks documents:create, or the declared agent is not the authenticated issuer. The agent of a CRE/CPE is its issuer, so declaring another RUC is an authorization failure rather than a payload error.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"409": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"413": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"415": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"422": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"429": {
"description": "Request blocked by the edge rate limit",
"content": {
"application/json": {
"schema": {
"type": "object",
"additionalProperties": false,
"required": [
"error"
],
"properties": {
"error": {
"type": "string",
"enum": [
"rate_limit_exceeded"
]
}
}
}
}
}
},
"500": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
}
}
}
```
Any `$ref` above resolves against `/openapi/apifact-v2.json`, the document this page was generated from.
# Get a tenant-owned comprobante de percepción (CPE, 40) and its references (/docs/api/payment-regimes/getPerception)
Returns the stored header and every reference in the order the signed XML declares them, with the PEN base, the amount perceived and the total for each. netAmount = baseAmount + regimeAmount.
## GET /api/v2/perceptions/{id}
```json
{
"tags": [
"Payment regimes"
],
"operationId": "getPerception",
"summary": "Get a tenant-owned comprobante de percepción (CPE, 40) and its references",
"description": "Returns the stored header and every reference in the order the signed XML declares them, with the PEN base, the amount perceived and the total for each. netAmount = baseAmount + regimeAmount.",
"security": [
{
"bearerAuth": []
}
],
"x-required-scope": "documents:read",
"parameters": [
{
"name": "id",
"in": "path",
"required": true,
"schema": {
"type": "string",
"format": "uuid"
}
}
],
"responses": {
"200": {
"description": "Header and references, in declaration order",
"headers": {
"X-Request-Id": {
"schema": {
"type": "string",
"format": "uuid"
}
}
},
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/PaymentRegimeResponse"
}
}
}
},
"401": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"403": {
"description": "Authenticated credential lacks documents:read",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"404": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"429": {
"description": "Request blocked by the edge rate limit",
"content": {
"application/json": {
"schema": {
"type": "object",
"additionalProperties": false,
"required": [
"error"
],
"properties": {
"error": {
"type": "string",
"enum": [
"rate_limit_exceeded"
]
}
}
}
}
}
},
"500": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
}
}
}
```
Any `$ref` above resolves against `/openapi/apifact-v2.json`, the document this page was generated from.
# Durably request comprobante de percepción (CPE, 40) dispatch (/docs/api/payment-regimes/submitPerception)
The perception mirror of submitRetention: same 'otros CPE' service, same single-envelope fence, same AMBIGUOUS outcome for a dispatch whose CDR never arrived. Gated by submission_publication_enabled AND perception_issuance_enabled.
## POST /api/v2/perceptions/{id}/submissions
```json
{
"tags": [
"Payment regimes"
],
"operationId": "submitPerception",
"summary": "Durably request comprobante de percepción (CPE, 40) dispatch",
"description": "The perception mirror of submitRetention: same 'otros CPE' service, same single-envelope fence, same AMBIGUOUS outcome for a dispatch whose CDR never arrived. Gated by submission_publication_enabled AND perception_issuance_enabled.",
"security": [
{
"bearerAuth": []
}
],
"x-required-scope": "documents:submit",
"parameters": [
{
"name": "id",
"in": "path",
"required": true,
"schema": {
"type": "string",
"format": "uuid"
}
},
{
"name": "Idempotency-Key",
"in": "header",
"required": true,
"schema": {
"type": "string",
"minLength": 1,
"maxLength": 200
}
}
],
"responses": {
"200": {
"description": "Idempotent replay",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/PaymentRegimeOperationResponse"
}
}
}
},
"202": {
"description": "Dispatch request durably accepted",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/PaymentRegimeOperationResponse"
}
}
}
},
"400": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"401": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"403": {
"description": "Authenticated credential lacks documents:submit",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"404": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"409": {
"description": "The tenant capability is off, a dispatch is already in flight, or the document has already settled. A settled document is never redispatched: an ambiguous submission is fenced for reconciliation, never resent.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"429": {
"description": "Request blocked by the edge rate limit",
"content": {
"application/json": {
"schema": {
"type": "object",
"additionalProperties": false,
"required": [
"error"
],
"properties": {
"error": {
"type": "string",
"enum": [
"rate_limit_exceeded"
]
}
}
}
}
}
},
"500": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"503": {
"description": "The durable dispatch handle could not be started. Nothing reached SUNAT; retry with the same Idempotency-Key.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
}
}
}
```
Any `$ref` above resolves against `/openapi/apifact-v2.json`, the document this page was generated from.
# List this tenant's webhook subscriptions (/docs/api/webhooks/listWebhookSubscriptions)
Returns every subscription of the authenticated tenant with its destination, subscribed events, status and current secret version. No signing secret is ever included.
## GET /api/v2/webhooks/subscriptions
```json
{
"tags": [
"Webhooks"
],
"operationId": "listWebhookSubscriptions",
"summary": "List this tenant's webhook subscriptions",
"description": "Returns every subscription of the authenticated tenant with its destination, subscribed events, status and current secret version. No signing secret is ever included.",
"security": [
{
"bearerAuth": []
}
],
"x-required-scope": "webhooks:manage",
"responses": {
"200": {
"description": "Subscriptions owned by the authenticated tenant",
"headers": {
"X-Request-Id": {
"schema": {
"type": "string",
"format": "uuid"
}
}
},
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/WebhookSubscriptionListResponse"
}
}
}
},
"400": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"401": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"403": {
"description": "Authenticated credential lacks webhooks:manage",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"404": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"409": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"413": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"415": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"422": {
"description": "Payload invalid, or the destination was refused by the anti-SSRF gate. The error code is INVALID_WEBHOOK_DESTINATION and details[0].reason names the rule that refused it.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"429": {
"description": "Request blocked by the edge rate limit",
"content": {
"application/json": {
"schema": {
"type": "object",
"additionalProperties": false,
"required": [
"error"
],
"properties": {
"error": {
"type": "string",
"enum": [
"rate_limit_exceeded"
]
}
}
}
}
}
},
"500": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
}
}
}
```
Any `$ref` above resolves against `/openapi/apifact-v2.json`, the document this page was generated from.
# Register a signed webhook destination for this tenant (/docs/api/webhooks/createWebhookSubscription)
Registers an HTTPS destination and the versioned events it should receive, and mints the HMAC signing secret. The secret is returned EXACTLY ONCE, in this response: the API's database role holds no read privilege on the column that stores it, so it cannot be retrieved later by any route. A replay of the same Idempotency-Key answers 200 with replay=true and no secret. Destinations are admitted by an anti-SSRF gate: https only, default port only, no credentials in the URL, no fragment, and the host must be a public DNS name whose last label is alphabetic, which refuses every IP literal in every base as well as localhost, .internal, .local and cloud metadata names.
## POST /api/v2/webhooks/subscriptions
```json
{
"tags": [
"Webhooks"
],
"operationId": "createWebhookSubscription",
"summary": "Register a signed webhook destination for this tenant",
"description": "Registers an HTTPS destination and the versioned events it should receive, and mints the HMAC signing secret. The secret is returned EXACTLY ONCE, in this response: the API's database role holds no read privilege on the column that stores it, so it cannot be retrieved later by any route. A replay of the same Idempotency-Key answers 200 with replay=true and no secret. Destinations are admitted by an anti-SSRF gate: https only, default port only, no credentials in the URL, no fragment, and the host must be a public DNS name whose last label is alphabetic, which refuses every IP literal in every base as well as localhost, .internal, .local and cloud metadata names.",
"security": [
{
"bearerAuth": []
}
],
"x-required-scope": "webhooks:manage",
"parameters": [
{
"name": "Idempotency-Key",
"in": "header",
"required": true,
"schema": {
"type": "string",
"minLength": 1,
"maxLength": 200,
"pattern": "^[!-~]+$"
}
}
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/CreateWebhookSubscriptionRequest"
},
"examples": {
"default": {
"value": {
"url": "https://webhooks.example.com/apifact",
"events": [
"document.validated",
"document.submitted",
"document.accepted",
"document.observed",
"document.rejected",
"document.ambiguous",
"document.failed",
"document.pdf_ready"
]
}
}
}
}
}
},
"responses": {
"200": {
"description": "Idempotent replay; the secret is NOT repeated",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/WebhookSubscriptionResponse"
}
}
}
},
"201": {
"description": "Subscription created; the signing secret is present exactly here",
"headers": {
"X-Request-Id": {
"schema": {
"type": "string",
"format": "uuid"
}
}
},
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/WebhookSubscriptionCreatedResponse"
}
}
}
},
"400": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"401": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"403": {
"description": "Authenticated credential lacks webhooks:manage",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"404": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"409": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"413": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"415": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"422": {
"description": "Payload invalid, or the destination was refused by the anti-SSRF gate. The error code is INVALID_WEBHOOK_DESTINATION and details[0].reason names the rule that refused it.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"429": {
"description": "Request blocked by the edge rate limit",
"content": {
"application/json": {
"schema": {
"type": "object",
"additionalProperties": false,
"required": [
"error"
],
"properties": {
"error": {
"type": "string",
"enum": [
"rate_limit_exceeded"
]
}
}
}
}
}
},
"500": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
}
}
}
```
Any `$ref` above resolves against `/openapi/apifact-v2.json`, the document this page was generated from.
# Change a subscription's destination, events or status (/docs/api/webhooks/updateWebhookSubscription)
Partial update. status=PAUSED stops the fan-out without deleting the subscription or its secret; paused subscriptions receive no new deliveries and are not selected when an event is published. A new url is revalidated by the same anti-SSRF gate as creation.
## PATCH /api/v2/webhooks/subscriptions/{id}
```json
{
"tags": [
"Webhooks"
],
"operationId": "updateWebhookSubscription",
"summary": "Change a subscription's destination, events or status",
"description": "Partial update. status=PAUSED stops the fan-out without deleting the subscription or its secret; paused subscriptions receive no new deliveries and are not selected when an event is published. A new url is revalidated by the same anti-SSRF gate as creation.",
"security": [
{
"bearerAuth": []
}
],
"x-required-scope": "webhooks:manage",
"parameters": [
{
"name": "id",
"in": "path",
"required": true,
"schema": {
"type": "string",
"format": "uuid"
}
},
{
"name": "Idempotency-Key",
"in": "header",
"required": true,
"schema": {
"type": "string",
"minLength": 1,
"maxLength": 200,
"pattern": "^[!-~]+$"
}
}
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/UpdateWebhookSubscriptionRequest"
},
"examples": {
"default": {
"value": {
"status": "PAUSED"
}
}
}
}
}
},
"responses": {
"200": {
"description": "Updated subscription",
"headers": {
"X-Request-Id": {
"schema": {
"type": "string",
"format": "uuid"
}
}
},
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/WebhookSubscriptionResponse"
}
}
}
},
"400": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"401": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"403": {
"description": "Authenticated credential lacks webhooks:manage",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"404": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"409": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"413": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"415": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"422": {
"description": "Payload invalid, or the destination was refused by the anti-SSRF gate. The error code is INVALID_WEBHOOK_DESTINATION and details[0].reason names the rule that refused it.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"429": {
"description": "Request blocked by the edge rate limit",
"content": {
"application/json": {
"schema": {
"type": "object",
"additionalProperties": false,
"required": [
"error"
],
"properties": {
"error": {
"type": "string",
"enum": [
"rate_limit_exceeded"
]
}
}
}
}
}
},
"500": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
}
}
}
```
Any `$ref` above resolves against `/openapi/apifact-v2.json`, the document this page was generated from.
# Delete a subscription and everything still aimed at it (/docs/api/webhooks/deleteWebhookSubscription)
Deletes the subscription, its secret versions and its pending and dead-lettered deliveries. Stored events survive, because an event may still be owed to another subscription.
## DELETE /api/v2/webhooks/subscriptions/{id}
```json
{
"tags": [
"Webhooks"
],
"operationId": "deleteWebhookSubscription",
"summary": "Delete a subscription and everything still aimed at it",
"description": "Deletes the subscription, its secret versions and its pending and dead-lettered deliveries. Stored events survive, because an event may still be owed to another subscription.",
"security": [
{
"bearerAuth": []
}
],
"x-required-scope": "webhooks:manage",
"parameters": [
{
"name": "id",
"in": "path",
"required": true,
"schema": {
"type": "string",
"format": "uuid"
}
},
{
"name": "Idempotency-Key",
"in": "header",
"required": true,
"schema": {
"type": "string",
"minLength": 1,
"maxLength": 200,
"pattern": "^[!-~]+$"
}
}
],
"responses": {
"200": {
"description": "Subscription deleted",
"headers": {
"X-Request-Id": {
"schema": {
"type": "string",
"format": "uuid"
}
}
},
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/WebhookSubscriptionDeletedResponse"
}
}
}
},
"400": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"401": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"403": {
"description": "Authenticated credential lacks webhooks:manage",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"404": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"409": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"413": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"415": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"422": {
"description": "Payload invalid, or the destination was refused by the anti-SSRF gate. The error code is INVALID_WEBHOOK_DESTINATION and details[0].reason names the rule that refused it.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"429": {
"description": "Request blocked by the edge rate limit",
"content": {
"application/json": {
"schema": {
"type": "object",
"additionalProperties": false,
"required": [
"error"
],
"properties": {
"error": {
"type": "string",
"enum": [
"rate_limit_exceeded"
]
}
}
}
}
}
},
"500": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
}
}
}
```
Any `$ref` above resolves against `/openapi/apifact-v2.json`, the document this page was generated from.
# Mint the next signing secret version (/docs/api/webhooks/rotateWebhookSecret)
Creates the next secret version and switches signing to it immediately; there is no overlap window, so install the new secret before rotating. The new secret is returned exactly once. A replay of the same Idempotency-Key answers 200 with replay=true and no secret; if the value was lost, rotate again with a new key.
## POST /api/v2/webhooks/subscriptions/{id}/secret-rotations
```json
{
"tags": [
"Webhooks"
],
"operationId": "rotateWebhookSecret",
"summary": "Mint the next signing secret version",
"description": "Creates the next secret version and switches signing to it immediately; there is no overlap window, so install the new secret before rotating. The new secret is returned exactly once. A replay of the same Idempotency-Key answers 200 with replay=true and no secret; if the value was lost, rotate again with a new key.",
"security": [
{
"bearerAuth": []
}
],
"x-required-scope": "webhooks:manage",
"parameters": [
{
"name": "id",
"in": "path",
"required": true,
"schema": {
"type": "string",
"format": "uuid"
}
},
{
"name": "Idempotency-Key",
"in": "header",
"required": true,
"schema": {
"type": "string",
"minLength": 1,
"maxLength": 200,
"pattern": "^[!-~]+$"
}
}
],
"responses": {
"200": {
"description": "Idempotent replay; the secret is NOT repeated",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/WebhookSecretRotationResponse"
}
}
}
},
"201": {
"description": "New secret version; the secret is present exactly here",
"headers": {
"X-Request-Id": {
"schema": {
"type": "string",
"format": "uuid"
}
}
},
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/WebhookSecretRotationResponse"
}
}
}
},
"400": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"401": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"403": {
"description": "Authenticated credential lacks webhooks:manage",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"404": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"409": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"413": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"415": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"422": {
"description": "Payload invalid, or the destination was refused by the anti-SSRF gate. The error code is INVALID_WEBHOOK_DESTINATION and details[0].reason names the rule that refused it.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"429": {
"description": "Request blocked by the edge rate limit",
"content": {
"application/json": {
"schema": {
"type": "object",
"additionalProperties": false,
"required": [
"error"
],
"properties": {
"error": {
"type": "string",
"enum": [
"rate_limit_exceeded"
]
}
}
}
}
}
},
"500": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
}
}
}
```
Any `$ref` above resolves against `/openapi/apifact-v2.json`, the document this page was generated from.
# List this tenant's certificate versions with their state and validity (/docs/api/certificates/listCertificates)
Returns every enrolled version of the authenticated tenant, newest first, with its status (DRAFT, ACTIVE or RETIRED), the certificate's identity and validity window, and currentlyValid, which answers whether the version could sign right now. Object keys and secret references are deliberately absent: they are the internal storage layout of the signing boundary and there is nothing a caller can do with them.
## GET /api/v2/certificates
```json
{
"tags": [
"Certificates"
],
"operationId": "listCertificates",
"summary": "List this tenant's certificate versions with their state and validity",
"description": "Returns every enrolled version of the authenticated tenant, newest first, with its status (DRAFT, ACTIVE or RETIRED), the certificate's identity and validity window, and currentlyValid, which answers whether the version could sign right now. Object keys and secret references are deliberately absent: they are the internal storage layout of the signing boundary and there is nothing a caller can do with them.",
"security": [
{
"bearerAuth": []
}
],
"x-required-scope": "certificates:manage",
"responses": {
"200": {
"description": "Certificate versions owned by the authenticated tenant",
"headers": {
"X-Request-Id": {
"schema": {
"type": "string",
"format": "uuid"
}
}
},
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/CertificateListResponse"
}
}
}
},
"400": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"401": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"403": {
"description": "Authenticated credential lacks certificates:manage",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"404": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"409": {
"description": "COMPANY_NOT_ENROLLABLE when the company is not ACTIVE, CERTIFICATE_VERSION_CONFLICT when another enrolment took the version, CERTIFICATE_NOT_ACTIVATABLE when the version is revoked or expired. CERTIFICATE_ALREADY_PROBED when this version has already been probed, whatever the verdict was: a probe consumes F000- at SUNAT and that number is never reissued, so a version that has been probed once cannot be probed again — enrol a new one. CERTIFICATE_ACTIVATION_IN_FLIGHT when another attempt on this version has not settled yet; poll GET /api/v2/certificates/{id}/activation rather than starting a second probe. CERTIFICATE_FIRST_ISSUE_OCCUPIED when a PRODUCTION version is asked to arm for activation by first issue and the company already holds one that is ACTIVE or already armed: at most one version per company and environment can be either, and this mechanism exists for a company that has none.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"413": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"415": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"422": {
"description": "INVALID_CERTIFICATE_ENROLLMENT for a malformed body, or CERTIFICATE_REJECTED when a validation refused the upload. details[0].field is one of environment, pfx, pfxPassword, solUser or solPassword and details[0].reason names the rule. No value from the upload or from the certificate is ever echoed.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"429": {
"description": "Request blocked by the edge rate limit",
"content": {
"application/json": {
"schema": {
"type": "object",
"additionalProperties": false,
"required": [
"error"
],
"properties": {
"error": {
"type": "string",
"enum": [
"rate_limit_exceeded"
]
}
}
}
}
}
},
"500": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"503": {
"description": "CERTIFICATE_ENROLLMENT_UNAVAILABLE: the private signing boundary is unreachable. CERTIFICATE_ACTIVATION_UNAVAILABLE: the durable probe could not be started, so nothing was sent to SUNAT and the version is untouched; retry with the same Idempotency-Key, which adopts the attempt already recorded instead of opening a second one.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
}
}
}
```
Any `$ref` above resolves against `/openapi/apifact-v2.json`, the document this page was generated from.
# Enrol a signing certificate and its SOL credentials as a DRAFT version (/docs/api/certificates/enrollCertificate)
Uploads the four secrets a Peruvian issuer needs: the PKCS#12 certificate (base64), its password, and the SOL user and password of the SUNAT secondary user. All four together, because a certificate signs the XML and the SOL credentials authenticate sendBill: one without the other cannot submit anything. The upload is validated BEFORE it is stored — the PFX must open with the supplied password, the certificate subject RUC must be this company's RUC, the validity window must cover now, and the declared environment must be the one the company is enrolled for — and only then sealed. The new version is created in DRAFT and signs NOTHING until POST /api/v2/certificates/{id}/activation. The certificate and the passwords are never stored in plaintext, never logged and never returned; only the sealed envelopes reach storage, and only their digests reach the database. A repeated Idempotency-Key answers 200 with replay=true and the version the first call produced, without re-sealing anything; the same key with a different certificate is a 409.
## POST /api/v2/certificates
```json
{
"tags": [
"Certificates"
],
"operationId": "enrollCertificate",
"summary": "Enrol a signing certificate and its SOL credentials as a DRAFT version",
"description": "Uploads the four secrets a Peruvian issuer needs: the PKCS#12 certificate (base64), its password, and the SOL user and password of the SUNAT secondary user. All four together, because a certificate signs the XML and the SOL credentials authenticate sendBill: one without the other cannot submit anything. The upload is validated BEFORE it is stored — the PFX must open with the supplied password, the certificate subject RUC must be this company's RUC, the validity window must cover now, and the declared environment must be the one the company is enrolled for — and only then sealed. The new version is created in DRAFT and signs NOTHING until POST /api/v2/certificates/{id}/activation. The certificate and the passwords are never stored in plaintext, never logged and never returned; only the sealed envelopes reach storage, and only their digests reach the database. A repeated Idempotency-Key answers 200 with replay=true and the version the first call produced, without re-sealing anything; the same key with a different certificate is a 409.",
"security": [
{
"bearerAuth": []
}
],
"x-required-scope": "certificates:manage",
"parameters": [
{
"name": "Idempotency-Key",
"in": "header",
"required": true,
"schema": {
"type": "string",
"minLength": 1,
"maxLength": 200,
"pattern": "^[!-~]+$"
}
}
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/EnrollCertificateRequest"
},
"examples": {
"default": {
"value": {
"environment": "BETA",
"pfxBase64": "MIIKlgIBAzCCClwGCSqGSIb3DQEHAaCCCk0EggpJMIIKRTCCBgYGCSqGSIb3DQEHAaCCBfcEggXzMIIF7zCCBesGCyqGSIb3DQEMCgECoIIE",
"pfxPassword": "la-clave-del-pkcs12",
"solUser": "MODDATOS",
"solPassword": "moddatos"
}
}
}
}
}
},
"responses": {
"200": {
"description": "Idempotent replay: the version this key already produced, in the listing shape",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/CertificateEnrollmentReplayResponse"
}
}
}
},
"201": {
"description": "Certificate enrolled as a DRAFT version; nothing signs with it yet",
"headers": {
"X-Request-Id": {
"schema": {
"type": "string",
"format": "uuid"
}
}
},
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/CertificateEnrolledResponse"
}
}
}
},
"400": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"401": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"403": {
"description": "Authenticated credential lacks certificates:manage",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"404": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"409": {
"description": "COMPANY_NOT_ENROLLABLE when the company is not ACTIVE, CERTIFICATE_VERSION_CONFLICT when another enrolment took the version, CERTIFICATE_NOT_ACTIVATABLE when the version is revoked or expired. CERTIFICATE_ALREADY_PROBED when this version has already been probed, whatever the verdict was: a probe consumes F000- at SUNAT and that number is never reissued, so a version that has been probed once cannot be probed again — enrol a new one. CERTIFICATE_ACTIVATION_IN_FLIGHT when another attempt on this version has not settled yet; poll GET /api/v2/certificates/{id}/activation rather than starting a second probe. CERTIFICATE_FIRST_ISSUE_OCCUPIED when a PRODUCTION version is asked to arm for activation by first issue and the company already holds one that is ACTIVE or already armed: at most one version per company and environment can be either, and this mechanism exists for a company that has none.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"413": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"415": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"422": {
"description": "INVALID_CERTIFICATE_ENROLLMENT for a malformed body, or CERTIFICATE_REJECTED when a validation refused the upload. details[0].field is one of environment, pfx, pfxPassword, solUser or solPassword and details[0].reason names the rule. No value from the upload or from the certificate is ever echoed.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"429": {
"description": "Request blocked by the edge rate limit",
"content": {
"application/json": {
"schema": {
"type": "object",
"additionalProperties": false,
"required": [
"error"
],
"properties": {
"error": {
"type": "string",
"enum": [
"rate_limit_exceeded"
]
}
}
}
}
}
},
"500": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"503": {
"description": "CERTIFICATE_ENROLLMENT_UNAVAILABLE: the private signing boundary is unreachable. CERTIFICATE_ACTIVATION_UNAVAILABLE: the durable probe could not be started, so nothing was sent to SUNAT and the version is untouched; retry with the same Idempotency-Key, which adopts the attempt already recorded instead of opening a second one.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
}
}
}
```
Any `$ref` above resolves against `/openapi/apifact-v2.json`, the document this page was generated from.
# Read the verdict of this version's activation probe (/docs/api/certificates/getCertificateActivation)
The poll target the 202 names. Reports the probe's own status, the certificate's status beside it — they are two different questions, and a probe that ended REJECTED leaves a version that is still DRAFT — and the SUNAT verdict verbatim, because a responseCode is what tells an operator which certificate to replace where 'activation failed' tells them nothing. Requires no Idempotency-Key: it is a read. A version that never requested activation answers 404 CERTIFICATE_ACTIVATION_NOT_FOUND, which is not the same as an unknown id: the version exists and simply has no attempt against it.
## GET /api/v2/certificates/{id}/activation
```json
{
"tags": [
"Certificates"
],
"operationId": "getCertificateActivation",
"summary": "Read the verdict of this version's activation probe",
"description": "The poll target the 202 names. Reports the probe's own status, the certificate's status beside it — they are two different questions, and a probe that ended REJECTED leaves a version that is still DRAFT — and the SUNAT verdict verbatim, because a responseCode is what tells an operator which certificate to replace where 'activation failed' tells them nothing. Requires no Idempotency-Key: it is a read. A version that never requested activation answers 404 CERTIFICATE_ACTIVATION_NOT_FOUND, which is not the same as an unknown id: the version exists and simply has no attempt against it.",
"security": [
{
"bearerAuth": []
}
],
"x-required-scope": "certificates:manage",
"parameters": [
{
"name": "id",
"in": "path",
"required": true,
"schema": {
"type": "string",
"format": "uuid"
}
}
],
"responses": {
"200": {
"description": "The latest activation attempt recorded for this version",
"headers": {
"X-Request-Id": {
"schema": {
"type": "string",
"format": "uuid"
}
}
},
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/CertificateActivationStatusResponse"
}
}
}
},
"400": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"401": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"403": {
"description": "Authenticated credential lacks certificates:manage",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"404": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"409": {
"description": "COMPANY_NOT_ENROLLABLE when the company is not ACTIVE, CERTIFICATE_VERSION_CONFLICT when another enrolment took the version, CERTIFICATE_NOT_ACTIVATABLE when the version is revoked or expired. CERTIFICATE_ALREADY_PROBED when this version has already been probed, whatever the verdict was: a probe consumes F000- at SUNAT and that number is never reissued, so a version that has been probed once cannot be probed again — enrol a new one. CERTIFICATE_ACTIVATION_IN_FLIGHT when another attempt on this version has not settled yet; poll GET /api/v2/certificates/{id}/activation rather than starting a second probe. CERTIFICATE_FIRST_ISSUE_OCCUPIED when a PRODUCTION version is asked to arm for activation by first issue and the company already holds one that is ACTIVE or already armed: at most one version per company and environment can be either, and this mechanism exists for a company that has none.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"413": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"415": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"422": {
"description": "INVALID_CERTIFICATE_ENROLLMENT for a malformed body, or CERTIFICATE_REJECTED when a validation refused the upload. details[0].field is one of environment, pfx, pfxPassword, solUser or solPassword and details[0].reason names the rule. No value from the upload or from the certificate is ever echoed.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"429": {
"description": "Request blocked by the edge rate limit",
"content": {
"application/json": {
"schema": {
"type": "object",
"additionalProperties": false,
"required": [
"error"
],
"properties": {
"error": {
"type": "string",
"enum": [
"rate_limit_exceeded"
]
}
}
}
}
}
},
"500": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"503": {
"description": "CERTIFICATE_ENROLLMENT_UNAVAILABLE: the private signing boundary is unreachable. CERTIFICATE_ACTIVATION_UNAVAILABLE: the durable probe could not be started, so nothing was sent to SUNAT and the version is untouched; retry with the same Idempotency-Key, which adopts the attempt already recorded instead of opening a second one.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
}
}
}
```
Any `$ref` above resolves against `/openapi/apifact-v2.json`, the document this page was generated from.
# Prove a DRAFT version against SUNAT and activate it only if SUNAT accepts (/docs/api/certificates/activateCertificate)
Requests activation of the named DRAFT. It does NOT promote it in the request, in either environment: a version becomes ACTIVE only when SUNAT has accepted a comprobante signed with it. What differs is WHICH comprobante, and the mechanism field in the 202 says which of the two you got.
BETA — mechanism PROBE. This API signs a real factura with the DRAFT certificate and sends it to SUNAT beta; the version becomes ACTIVE only if SUNAT accepts it. A certificate that cannot issue is therefore never activated, which is the whole point — the behaviour before this promoted on request, and the first document the tenant tried to issue was the one that discovered the key was unusable. The probe issues documentType 01 on the reserved series F000 with number equal to the enrolment version, so it is unique per version and never reused. It writes no row in the document collections: it is not a comprobante of the tenant's, it is not listed, it has no artifacts and it consumes no quota. Because a SUNAT round trip can take up to the dispatch timeout, this answers 202 with an operationId and a statusPath rather than blocking; poll GET /api/v2/certificates/{id}/activation for the verdict.
PRODUCTION — mechanism FIRST_ISSUE, and it is a different shape because the probe cannot exist here. A probe in production is a REAL comprobante: it enters the taxpayer's registro de ventas and is withdrawn only by an accepted comunicación de baja. Inventing a sale to prove a key is not this platform's decision to make, and refusing to activate at all — which is what this route did until now — left a taxpayer with a certificate that could never sign and a company that could never send. So nothing is sent. The version is ARMED: its status becomes PENDING_FIRST_ISSUE, it signs your documents from this response onwards, and the FIRST comprobante you issue that SUNAT accepts or observes promotes it to ACTIVE in the same transaction that records that CDR — along with the company capabilities that follow a first activation. Nothing is emitted that you were not going to emit anyway.
If SUNAT then refuses that first comprobante because of the CERTIFICATE OR THE SOL CREDENTIALS — a wrong password, an unknown or inactive SOL user, no CPE profile, a file whose RUC is not the user's — the version is RETIRED and you enrol another. If SUNAT refuses it for anything else, including every CDR rejection about the CONTENT of the document, the version STAYS ARMED and your next comprobante tries again with the same key. A rejection is a statement about the document, not about the certificate, and retiring a good key over one is worse than leaving it pending.
Arming is refused with 409 CERTIFICATE_FIRST_ISSUE_OCCUPIED when the company already has a PRODUCTION version that is ACTIVE or already armed; at most one version per company and environment can be either. That means this mechanism does not renew: it is for a company with no signing certificate at all.
Everything decidable without SUNAT is still refused synchronously and by name in both environments: not a DRAFT, revoked, expired, already probed, another attempt in flight. Idempotency-Key is required: a retry after a lost response adopts what was already recorded with replay=true, and re-sending the same key also repairs an attempt whose row committed but whose probe never started.
## POST /api/v2/certificates/{id}/activation
```json
{
"tags": [
"Certificates"
],
"operationId": "activateCertificate",
"summary": "Prove a DRAFT version against SUNAT and activate it only if SUNAT accepts",
"description": "Requests activation of the named DRAFT. It does NOT promote it in the request, in either environment: a version becomes ACTIVE only when SUNAT has accepted a comprobante signed with it. What differs is WHICH comprobante, and the mechanism field in the 202 says which of the two you got.\n\nBETA — mechanism PROBE. This API signs a real factura with the DRAFT certificate and sends it to SUNAT beta; the version becomes ACTIVE only if SUNAT accepts it. A certificate that cannot issue is therefore never activated, which is the whole point — the behaviour before this promoted on request, and the first document the tenant tried to issue was the one that discovered the key was unusable. The probe issues documentType 01 on the reserved series F000 with number equal to the enrolment version, so it is unique per version and never reused. It writes no row in the document collections: it is not a comprobante of the tenant's, it is not listed, it has no artifacts and it consumes no quota. Because a SUNAT round trip can take up to the dispatch timeout, this answers 202 with an operationId and a statusPath rather than blocking; poll GET /api/v2/certificates/{id}/activation for the verdict.\n\nPRODUCTION — mechanism FIRST_ISSUE, and it is a different shape because the probe cannot exist here. A probe in production is a REAL comprobante: it enters the taxpayer's registro de ventas and is withdrawn only by an accepted comunicación de baja. Inventing a sale to prove a key is not this platform's decision to make, and refusing to activate at all — which is what this route did until now — left a taxpayer with a certificate that could never sign and a company that could never send. So nothing is sent. The version is ARMED: its status becomes PENDING_FIRST_ISSUE, it signs your documents from this response onwards, and the FIRST comprobante you issue that SUNAT accepts or observes promotes it to ACTIVE in the same transaction that records that CDR — along with the company capabilities that follow a first activation. Nothing is emitted that you were not going to emit anyway.\n\nIf SUNAT then refuses that first comprobante because of the CERTIFICATE OR THE SOL CREDENTIALS — a wrong password, an unknown or inactive SOL user, no CPE profile, a file whose RUC is not the user's — the version is RETIRED and you enrol another. If SUNAT refuses it for anything else, including every CDR rejection about the CONTENT of the document, the version STAYS ARMED and your next comprobante tries again with the same key. A rejection is a statement about the document, not about the certificate, and retiring a good key over one is worse than leaving it pending.\n\nArming is refused with 409 CERTIFICATE_FIRST_ISSUE_OCCUPIED when the company already has a PRODUCTION version that is ACTIVE or already armed; at most one version per company and environment can be either. That means this mechanism does not renew: it is for a company with no signing certificate at all.\n\nEverything decidable without SUNAT is still refused synchronously and by name in both environments: not a DRAFT, revoked, expired, already probed, another attempt in flight. Idempotency-Key is required: a retry after a lost response adopts what was already recorded with replay=true, and re-sending the same key also repairs an attempt whose row committed but whose probe never started.",
"security": [
{
"bearerAuth": []
}
],
"x-required-scope": "certificates:manage",
"parameters": [
{
"name": "id",
"in": "path",
"required": true,
"schema": {
"type": "string",
"format": "uuid"
}
},
{
"name": "Idempotency-Key",
"in": "header",
"required": true,
"schema": {
"type": "string",
"minLength": 1,
"maxLength": 200,
"pattern": "^[!-~]+$"
}
}
],
"responses": {
"202": {
"description": "Accepted and durably committed. Under mechanism PROBE the version is still DRAFT and whatever signs this tenant's documents keeps signing them until SUNAT answers. Under mechanism FIRST_ISSUE the version is PENDING_FIRST_ISSUE and is signing already — the tenant had nothing signing before, which is the only situation in which arming is allowed.",
"headers": {
"X-Request-Id": {
"schema": {
"type": "string",
"format": "uuid"
}
},
"Location": {
"schema": {
"type": "string"
},
"description": "Same value as statusPath, for a client that follows headers."
}
},
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/CertificateActivationAcceptedResponse"
},
"examples": {
"betaProbe": {
"summary": "BETA — a factura on F000 is on its way to SUNAT",
"value": {
"schemaVersion": "2.0",
"requestId": "6f0b1a3e-9c2d-4f51-8f0a-2b7c4d5e6f70",
"mechanism": "PROBE",
"operationId": "1c9a7f52-3d84-4b16-9d0e-8a4f2c6b1e30",
"activationId": "1c9a7f52-3d84-4b16-9d0e-8a4f2c6b1e30",
"id": "b2d4f6a8-1c3e-4a5b-8d7f-9e0a1b2c3d4e",
"resourceId": "b2d4f6a8-1c3e-4a5b-8d7f-9e0a1b2c3d4e",
"version": 3,
"environment": "BETA",
"status": "PENDING",
"certificateStatus": "DRAFT",
"probe": {
"documentType": "01",
"series": "F000",
"number": "3"
},
"statusPath": "/api/v2/certificates/b2d4f6a8-1c3e-4a5b-8d7f-9e0a1b2c3d4e/activation",
"createdAt": "2026-08-14T15:04:05.000Z",
"replay": false
}
},
"productionFirstIssue": {
"summary": "PRODUCTION — armed, signing already, promoted by the next accepted comprobante",
"description": "Nothing was sent to SUNAT and there is no verdict coming, which is why operationId, activationId and probe are all null. Do not poll statusPath waiting for it to move: the next thing that moves this version is a comprobante you issue. Note certificateStatus: the key IS in use from here, and it is not ACTIVE yet.",
"value": {
"schemaVersion": "2.0",
"requestId": "0a1b2c3d-4e5f-4a6b-8c7d-9e0f1a2b3c4d",
"mechanism": "FIRST_ISSUE",
"operationId": null,
"activationId": null,
"id": "5f4e3d2c-1b0a-4998-8776-655443322110",
"resourceId": "5f4e3d2c-1b0a-4998-8776-655443322110",
"version": 1,
"environment": "PRODUCTION",
"status": "PENDING_FIRST_ISSUE",
"certificateStatus": "PENDING_FIRST_ISSUE",
"probe": null,
"statusPath": "/api/v2/certificates/5f4e3d2c-1b0a-4998-8776-655443322110/activation",
"createdAt": "2026-08-14T15:04:05.000Z",
"replay": false
}
}
}
}
}
},
"400": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"401": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"403": {
"description": "Authenticated credential lacks certificates:manage",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"404": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"409": {
"description": "COMPANY_NOT_ENROLLABLE when the company is not ACTIVE, CERTIFICATE_VERSION_CONFLICT when another enrolment took the version, CERTIFICATE_NOT_ACTIVATABLE when the version is revoked or expired. CERTIFICATE_ALREADY_PROBED when this version has already been probed, whatever the verdict was: a probe consumes F000- at SUNAT and that number is never reissued, so a version that has been probed once cannot be probed again — enrol a new one. CERTIFICATE_ACTIVATION_IN_FLIGHT when another attempt on this version has not settled yet; poll GET /api/v2/certificates/{id}/activation rather than starting a second probe. CERTIFICATE_FIRST_ISSUE_OCCUPIED when a PRODUCTION version is asked to arm for activation by first issue and the company already holds one that is ACTIVE or already armed: at most one version per company and environment can be either, and this mechanism exists for a company that has none.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"413": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"415": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"422": {
"description": "INVALID_CERTIFICATE_ENROLLMENT for a malformed body, or CERTIFICATE_REJECTED when a validation refused the upload. details[0].field is one of environment, pfx, pfxPassword, solUser or solPassword and details[0].reason names the rule. No value from the upload or from the certificate is ever echoed.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"429": {
"description": "Request blocked by the edge rate limit",
"content": {
"application/json": {
"schema": {
"type": "object",
"additionalProperties": false,
"required": [
"error"
],
"properties": {
"error": {
"type": "string",
"enum": [
"rate_limit_exceeded"
]
}
}
}
}
}
},
"500": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"503": {
"description": "CERTIFICATE_ENROLLMENT_UNAVAILABLE: the private signing boundary is unreachable. CERTIFICATE_ACTIVATION_UNAVAILABLE: the durable probe could not be started, so nothing was sent to SUNAT and the version is untouched; retry with the same Idempotency-Key, which adopts the attempt already recorded instead of opening a second one.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
}
}
}
```
Any `$ref` above resolves against `/openapi/apifact-v2.json`, the document this page was generated from.
# List the client companies of the calling organization (/docs/api/organizations/listOrganizationCompanies)
The read that makes this surface usable: every company, with the nine capability flags that decide what it may do and the state of the certificate that decides whether it may sign at all. Keyset paginated on the RUC, which is unique platform-wide, so a page cannot skip or repeat a company because another was registered mid-scan. INACTIVE companies are not listed. Requires organizations:read.
## GET /api/v2/organizations/companies
```json
{
"tags": [
"Organizations"
],
"operationId": "listOrganizationCompanies",
"summary": "List the client companies of the calling organization",
"description": "The read that makes this surface usable: every company, with the nine capability flags that decide what it may do and the state of the certificate that decides whether it may sign at all. Keyset paginated on the RUC, which is unique platform-wide, so a page cannot skip or repeat a company because another was registered mid-scan. INACTIVE companies are not listed. Requires organizations:read.",
"security": [
{
"bearerAuth": []
}
],
"x-required-scope": "organizations:read",
"parameters": [
{
"name": "limit",
"in": "query",
"required": false,
"schema": {
"type": "integer",
"minimum": 1,
"maximum": 200,
"default": 50
}
},
{
"name": "cursor",
"in": "query",
"required": false,
"description": "The RUC the previous page ended on; this page starts strictly after it.",
"schema": {
"type": "string",
"pattern": "^[0-9]{11}$"
}
}
],
"responses": {
"200": {
"description": "One page of client companies with their capability and certificate state",
"headers": {
"X-Request-Id": {
"schema": {
"type": "string",
"format": "uuid"
}
}
},
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/OrganizationCompanyListResponse"
}
}
}
},
"400": {
"description": "INVALID_PAGE_LIMIT when limit is not an integer in 1..200, or INVALID_PAGE_CURSOR when cursor is not an eleven-digit RUC.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"401": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"403": {
"description": "Authenticated credential lacks organizations:read, or is a company credential rather than an organization credential. A credential holding companies:manage satisfies organizations:read -- the implication runs that way and never the other.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"429": {
"description": "Request blocked by the edge rate limit",
"content": {
"application/json": {
"schema": {
"type": "object",
"additionalProperties": false,
"required": [
"error"
],
"properties": {
"error": {
"type": "string",
"enum": [
"rate_limit_exceeded"
]
}
}
}
}
}
},
"500": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"503": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
}
}
}
```
Any `$ref` above resolves against `/openapi/apifact-v2.json`, the document this page was generated from.
# Register a client company under the calling organization (/docs/api/organizations/createOrganizationCompany)
Creates the fiscal entity a platform will issue for. Requires an ORGANIZATION credential (apf_org_v2_...) carrying companies:manage; a company credential is refused by the token prefix before any lookup happens. The company is created ACTIVE but can sign nothing: it has no certificate, and since migration 000052 the only route to an ACTIVE certificate is a comprobante SUNAT accepted. Every call writes an ops.audit_events row naming the organization, the credential and the RUC, in the same transaction as the company.
## POST /api/v2/organizations/companies
```json
{
"tags": [
"Organizations"
],
"operationId": "createOrganizationCompany",
"summary": "Register a client company under the calling organization",
"description": "Creates the fiscal entity a platform will issue for. Requires an ORGANIZATION credential (apf_org_v2_...) carrying companies:manage; a company credential is refused by the token prefix before any lookup happens. The company is created ACTIVE but can sign nothing: it has no certificate, and since migration 000052 the only route to an ACTIVE certificate is a comprobante SUNAT accepted. Every call writes an ops.audit_events row naming the organization, the credential and the RUC, in the same transaction as the company.",
"security": [
{
"bearerAuth": []
}
],
"x-required-scope": "companies:manage",
"parameters": [
{
"name": "Idempotency-Key",
"in": "header",
"required": true,
"schema": {
"type": "string",
"minLength": 1,
"maxLength": 200,
"pattern": "^[!-~]+$"
}
}
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/CreateOrganizationCompanyRequest"
},
"examples": {
"default": {
"value": {
"ruc": "20698765432",
"legalName": "DISTRIBUIDORA SAN MARTIN S.A.C.",
"environment": "BETA"
}
}
}
}
}
},
"responses": {
"201": {
"description": "Company registered under this organization",
"headers": {
"X-Request-Id": {
"schema": {
"type": "string",
"format": "uuid"
}
}
},
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/OrganizationCompanyResponse"
}
}
}
},
"400": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"401": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"403": {
"description": "Authenticated credential lacks companies:manage, or is a company credential rather than an organization credential. A company credential cannot reach these routes at all: it is refused by the token prefix before it is refused by the scope.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"404": {
"description": "ORGANIZATION_COMPANY_NOT_FOUND. A company belonging to another organization is reported identically to one that does not exist, so this route cannot be used to discover that a RUC is registered.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"409": {
"description": "COMPANY_RUC_TAKEN when the RUC is already registered, by this organization or any other. IDEMPOTENCY_CONFLICT when the key was reused with a different payload.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"413": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"415": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"422": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"429": {
"description": "Request blocked by the edge rate limit",
"content": {
"application/json": {
"schema": {
"type": "object",
"additionalProperties": false,
"required": [
"error"
],
"properties": {
"error": {
"type": "string",
"enum": [
"rate_limit_exceeded"
]
}
}
}
}
}
},
"500": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"503": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
}
}
}
```
Any `$ref` above resolves against `/openapi/apifact-v2.json`, the document this page was generated from.
# Upload a client company's signing certificate and SOL credentials (/docs/api/organizations/enrollOrganizationCompanyCertificate)
The same body, the same validation, the same sealing boundary and the same DRAFT outcome as POST /api/v2/certificates -- it is that path, reached with the tenant established by auth.bind_organization_company after verifying the company belongs to the caller's organization. The PKCS#12 and the three passwords cross the signing service binding and reach nothing durable in between. Activation is deliberately NOT available here: promoting the DRAFT sends a real signed factura to SUNAT under the client's RUC, and that is performed with the company credential this organization minted.
## POST /api/v2/organizations/companies/{id}/certificate
```json
{
"tags": [
"Organizations"
],
"operationId": "enrollOrganizationCompanyCertificate",
"summary": "Upload a client company's signing certificate and SOL credentials",
"description": "The same body, the same validation, the same sealing boundary and the same DRAFT outcome as POST /api/v2/certificates -- it is that path, reached with the tenant established by auth.bind_organization_company after verifying the company belongs to the caller's organization. The PKCS#12 and the three passwords cross the signing service binding and reach nothing durable in between. Activation is deliberately NOT available here: promoting the DRAFT sends a real signed factura to SUNAT under the client's RUC, and that is performed with the company credential this organization minted.",
"security": [
{
"bearerAuth": []
}
],
"x-required-scope": "companies:manage",
"parameters": [
{
"name": "id",
"in": "path",
"required": true,
"schema": {
"type": "string",
"format": "uuid"
}
},
{
"name": "Idempotency-Key",
"in": "header",
"required": true,
"schema": {
"type": "string",
"minLength": 1,
"maxLength": 200,
"pattern": "^[!-~]+$"
}
}
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/EnrollCertificateRequest"
},
"examples": {
"default": {
"value": {
"environment": "BETA",
"pfxBase64": "MIIKlgIBAzCCClwGCSqGSIb3DQEHAaCCCk0EggpJMIIKRTCCBgYGCSqGSIb3DQEHAaCCBfcEggXzMIIF7zCCBesGCyqGSIb3DQEMCgECoIIE",
"pfxPassword": "la-clave-del-pkcs12",
"solUser": "MODDATOS",
"solPassword": "moddatos"
}
}
}
}
}
},
"responses": {
"200": {
"description": "Idempotent replay: the version this key already produced",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/CertificateEnrollmentReplayResponse"
}
}
}
},
"201": {
"description": "Certificate enrolled as a DRAFT version; nothing signs with it yet",
"headers": {
"X-Request-Id": {
"schema": {
"type": "string",
"format": "uuid"
}
}
},
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/CertificateEnrolledResponse"
}
}
}
},
"400": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"401": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"403": {
"description": "Authenticated credential lacks companies:manage, or is a company credential rather than an organization credential. A company credential cannot reach these routes at all: it is refused by the token prefix before it is refused by the scope.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"404": {
"description": "ORGANIZATION_COMPANY_NOT_FOUND. A company belonging to another organization is reported identically to one that does not exist, so this route cannot be used to discover that a RUC is registered.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"409": {
"description": "COMPANY_RUC_TAKEN when the RUC is already registered, by this organization or any other. IDEMPOTENCY_CONFLICT when the key was reused with a different payload.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"413": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"415": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"422": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"429": {
"description": "Request blocked by the edge rate limit",
"content": {
"application/json": {
"schema": {
"type": "object",
"additionalProperties": false,
"required": [
"error"
],
"properties": {
"error": {
"type": "string",
"enum": [
"rate_limit_exceeded"
]
}
}
}
}
}
},
"500": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"503": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
}
}
}
```
Any `$ref` above resolves against `/openapi/apifact-v2.json`, the document this page was generated from.
# Mint an API credential for a client company (/docs/api/organizations/mintCompanyCredential)
Creates a COMPANY credential, which can issue comprobantes. ADR 0016 point 3 names this a privilege escalation and does not pretend otherwise: the mitigation is that it is recorded. The credential row and its ops.audit_events row are written in one transaction, and a DEFERRABLE constraint trigger on auth.api_credentials refuses at COMMIT to admit a credential for an organization-owned company without one -- so an unaudited mint is not a policy violation, it is a failed transaction. The token is returned exactly once.
## POST /api/v2/organizations/companies/{id}/credentials
```json
{
"tags": [
"Organizations"
],
"operationId": "mintCompanyCredential",
"summary": "Mint an API credential for a client company",
"description": "Creates a COMPANY credential, which can issue comprobantes. ADR 0016 point 3 names this a privilege escalation and does not pretend otherwise: the mitigation is that it is recorded. The credential row and its ops.audit_events row are written in one transaction, and a DEFERRABLE constraint trigger on auth.api_credentials refuses at COMMIT to admit a credential for an organization-owned company without one -- so an unaudited mint is not a policy violation, it is a failed transaction. The token is returned exactly once.",
"security": [
{
"bearerAuth": []
}
],
"x-required-scope": "companies:manage",
"parameters": [
{
"name": "id",
"in": "path",
"required": true,
"schema": {
"type": "string",
"format": "uuid"
}
},
{
"name": "Idempotency-Key",
"in": "header",
"required": true,
"schema": {
"type": "string",
"minLength": 1,
"maxLength": 200,
"pattern": "^[!-~]+$"
}
}
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/MintCompanyCredentialRequest"
},
"examples": {
"default": {
"value": {
"name": "SaaS - facturacion 20698765432",
"scopes": [
"documents:create",
"documents:read",
"documents:submit",
"documents:pdf",
"operations:read",
"artifacts:read",
"webhooks:manage"
],
"expiresInDays": 90
}
}
}
}
}
},
"responses": {
"201": {
"description": "Credential minted; the token is in this response and nowhere else",
"headers": {
"X-Request-Id": {
"schema": {
"type": "string",
"format": "uuid"
}
}
},
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/MintedCompanyCredentialResponse"
}
}
}
},
"400": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"401": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"403": {
"description": "Authenticated credential lacks companies:manage, or is a company credential rather than an organization credential. A company credential cannot reach these routes at all: it is refused by the token prefix before it is refused by the scope.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"404": {
"description": "ORGANIZATION_COMPANY_NOT_FOUND. A company belonging to another organization is reported identically to one that does not exist, so this route cannot be used to discover that a RUC is registered.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"409": {
"description": "COMPANY_RUC_TAKEN when the RUC is already registered, by this organization or any other. IDEMPOTENCY_CONFLICT when the key was reused with a different payload.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"413": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"415": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"422": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"429": {
"description": "Request blocked by the edge rate limit",
"content": {
"application/json": {
"schema": {
"type": "object",
"additionalProperties": false,
"required": [
"error"
],
"properties": {
"error": {
"type": "string",
"enum": [
"rate_limit_exceeded"
]
}
}
}
}
}
},
"500": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"503": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
}
}
}
```
Any `$ref` above resolves against `/openapi/apifact-v2.json`, the document this page was generated from.
# Consolidated consumption across every company in the organization (/docs/api/organizations/getOrganizationUsage)
A read, and only a read. ADR 0016 point 6 keeps the quota holder on the company: a platform with fifty clients has fifty subscriptions and each taxpayer pays its own. usage.consume_quota is untouched. Each company is reported against its own plan period, so a mixed portfolio of DAY and MONTH plans is not flattened onto a window this route picked.
## GET /api/v2/organizations/usage
```json
{
"tags": [
"Organizations"
],
"operationId": "getOrganizationUsage",
"summary": "Consolidated consumption across every company in the organization",
"description": "A read, and only a read. ADR 0016 point 6 keeps the quota holder on the company: a platform with fifty clients has fifty subscriptions and each taxpayer pays its own. usage.consume_quota is untouched. Each company is reported against its own plan period, so a mixed portfolio of DAY and MONTH plans is not flattened onto a window this route picked.",
"security": [
{
"bearerAuth": []
}
],
"x-required-scope": "organizations:read",
"parameters": [
{
"name": "metric",
"in": "query",
"required": false,
"schema": {
"type": "string",
"maxLength": 60,
"default": "documents.accepted"
}
}
],
"responses": {
"200": {
"description": "Per-company consumption for the requested metric",
"headers": {
"X-Request-Id": {
"schema": {
"type": "string",
"format": "uuid"
}
}
},
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/OrganizationUsageResponse"
}
}
}
},
"400": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"401": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"403": {
"description": "Authenticated credential lacks companies:manage, or is a company credential rather than an organization credential. A company credential cannot reach these routes at all: it is refused by the token prefix before it is refused by the scope.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"404": {
"description": "ORGANIZATION_COMPANY_NOT_FOUND. A company belonging to another organization is reported identically to one that does not exist, so this route cannot be used to discover that a RUC is registered.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"409": {
"description": "COMPANY_RUC_TAKEN when the RUC is already registered, by this organization or any other. IDEMPOTENCY_CONFLICT when the key was reused with a different payload.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"413": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"415": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"422": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"429": {
"description": "Request blocked by the edge rate limit",
"content": {
"application/json": {
"schema": {
"type": "object",
"additionalProperties": false,
"required": [
"error"
],
"properties": {
"error": {
"type": "string",
"enum": [
"rate_limit_exceeded"
]
}
}
}
}
}
},
"500": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"503": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
}
}
}
```
Any `$ref` above resolves against `/openapi/apifact-v2.json`, the document this page was generated from.
# Read one client company, its capabilities, its certificate and its credentials (/docs/api/organizations/getOrganizationCompany)
Everything needed to answer 'can this client bill, and if not why not'. THE THREE FAILURES ARE ONE ANSWER: a company belonging to another organization, a company that does not exist and an INACTIVE company all return the identical 404, decided by a single SELECT rather than by three branches, so this route cannot be used to discover that a RUC is registered elsewhere. Requires organizations:read; a credential holding companies:manage satisfies it, and one holding only organizations:read can create nothing, mint nothing and enrol nothing.
## GET /api/v2/organizations/companies/{id}
```json
{
"tags": [
"Organizations"
],
"operationId": "getOrganizationCompany",
"summary": "Read one client company, its capabilities, its certificate and its credentials",
"description": "Everything needed to answer 'can this client bill, and if not why not'. THE THREE FAILURES ARE ONE ANSWER: a company belonging to another organization, a company that does not exist and an INACTIVE company all return the identical 404, decided by a single SELECT rather than by three branches, so this route cannot be used to discover that a RUC is registered elsewhere. Requires organizations:read; a credential holding companies:manage satisfies it, and one holding only organizations:read can create nothing, mint nothing and enrol nothing.",
"security": [
{
"bearerAuth": []
}
],
"x-required-scope": "organizations:read",
"parameters": [
{
"name": "id",
"in": "path",
"required": true,
"schema": {
"type": "string",
"format": "uuid"
}
}
],
"responses": {
"200": {
"description": "The company, its capability state, its certificate state and its credentials",
"headers": {
"X-Request-Id": {
"schema": {
"type": "string",
"format": "uuid"
}
}
},
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/OrganizationCompanyDetailResponse"
}
}
}
},
"400": {
"description": "INVALID_PAGE_LIMIT when limit is not an integer in 1..200, or INVALID_PAGE_CURSOR when cursor is not an eleven-digit RUC.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"401": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"403": {
"description": "Authenticated credential lacks organizations:read, or is a company credential rather than an organization credential. A credential holding companies:manage satisfies organizations:read -- the implication runs that way and never the other.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"404": {
"description": "ORGANIZATION_COMPANY_NOT_FOUND. Byte-identical, request id aside, for a company of another organization, a company that does not exist and an INACTIVE one.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"429": {
"description": "Request blocked by the edge rate limit",
"content": {
"application/json": {
"schema": {
"type": "object",
"additionalProperties": false,
"required": [
"error"
],
"properties": {
"error": {
"type": "string",
"enum": [
"rate_limit_exceeded"
]
}
}
}
}
}
},
"500": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"503": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
}
}
}
```
Any `$ref` above resolves against `/openapi/apifact-v2.json`, the document this page was generated from.
# Look a RUC up in the hosted SUNAT padrón — informational, never a validation (/docs/api/consultations/consultTaxpayer)
Answers what SUNAT's published padrón says about a RUC: the legal name, the literal estado del contribuyente and condición de domicilio, and a coarse classification of each. THIS DOES NOT VALIDATE ANYTHING AND DOES NOT AFFECT ISSUANCE. A RUC this route reports as not registered can still be invoiced, and doing so is neither an error nor a warning: no create, no trigger and no dispatch decision reads this catalogue. The copy is a snapshot and is therefore ALWAYS behind — a company registered since the snapshot was cut is absent from it while being entirely able to receive a comprobante — so treat the answer as information for a human, not as a gate. THERE ARE THREE OUTCOMES AND ONLY TWO OF THEM ARE ABOUT THE TAXPAYER: 200 with registered=true, 200 with registered=false (a snapshot is loaded and this RUC is not in it), and 503 TAXPAYER_REGISTRY_UNAVAILABLE (no snapshot is loaded at all, which is a statement about this service). A client that folds the 503 into registered=false reports an outage of ours as a fact about somebody's customer. Both the literal SUNAT value and the classification are returned: the literal is authoritative and the classification is ours and may be UNKNOWN, so a value SUNAT adds tomorrow is visible rather than silently mapped. registry.publishedOn dates the snapshot the answer came from and is SUNAT's own publication date, never the time this API ingested it. Requires consultations:read, which is disjoint from every documents:* capability: asking who a RUC belongs to must not require a credential that can issue a comprobante.
## GET /api/v2/consultations/ruc/{ruc}
```json
{
"tags": [
"Consultations"
],
"operationId": "consultTaxpayer",
"summary": "Look a RUC up in the hosted SUNAT padrón — informational, never a validation",
"description": "Answers what SUNAT's published padrón says about a RUC: the legal name, the literal estado del contribuyente and condición de domicilio, and a coarse classification of each. THIS DOES NOT VALIDATE ANYTHING AND DOES NOT AFFECT ISSUANCE. A RUC this route reports as not registered can still be invoiced, and doing so is neither an error nor a warning: no create, no trigger and no dispatch decision reads this catalogue. The copy is a snapshot and is therefore ALWAYS behind — a company registered since the snapshot was cut is absent from it while being entirely able to receive a comprobante — so treat the answer as information for a human, not as a gate. THERE ARE THREE OUTCOMES AND ONLY TWO OF THEM ARE ABOUT THE TAXPAYER: 200 with registered=true, 200 with registered=false (a snapshot is loaded and this RUC is not in it), and 503 TAXPAYER_REGISTRY_UNAVAILABLE (no snapshot is loaded at all, which is a statement about this service). A client that folds the 503 into registered=false reports an outage of ours as a fact about somebody's customer. Both the literal SUNAT value and the classification are returned: the literal is authoritative and the classification is ours and may be UNKNOWN, so a value SUNAT adds tomorrow is visible rather than silently mapped. registry.publishedOn dates the snapshot the answer came from and is SUNAT's own publication date, never the time this API ingested it. Requires consultations:read, which is disjoint from every documents:* capability: asking who a RUC belongs to must not require a credential that can issue a comprobante.",
"security": [
{
"bearerAuth": []
}
],
"x-required-scope": "consultations:read",
"parameters": [
{
"name": "ruc",
"in": "path",
"required": true,
"schema": {
"type": "string",
"pattern": "^[0-9]{11}$"
},
"description": "Exactly eleven digits. Anything else is 422 INVALID_RUC and is never reported as an unregistered taxpayer."
}
],
"responses": {
"200": {
"description": "The padrón's answer. registered=true and registered=false are BOTH this status: an unknown RUC is a successful answer, not an error.",
"headers": {
"X-Request-Id": {
"schema": {
"type": "string",
"format": "uuid"
}
}
},
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/TaxpayerConsultationResponse"
}
}
}
},
"401": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"403": {
"description": "Authenticated credential lacks consultations:read",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"422": {
"description": "INVALID_RUC when the path segment is not exactly eleven digits, INVALID_DATE when it is not a real calendar date in YYYY-MM-DD (2026-02-30 is refused rather than rolled over to March), INVALID_CURRENCY when the currency query parameter is not three letters. A malformed input is refused before the catalogue is read, and is never reported as an absent taxpayer or a missing rate.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"429": {
"description": "Request blocked by the edge rate limit",
"content": {
"application/json": {
"schema": {
"type": "object",
"additionalProperties": false,
"required": [
"error"
],
"properties": {
"error": {
"type": "string",
"enum": [
"rate_limit_exceeded"
]
}
}
}
}
}
},
"500": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"503": {
"description": "TAXPAYER_REGISTRY_UNAVAILABLE: no padrón snapshot is loaded, so this API cannot answer the question at all. THIS IS NOT registered=false. It says nothing about the RUC, and it does not affect document issuance — a comprobante may be issued while this route is answering 503.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
}
}
}
```
Any `$ref` above resolves against `/openapi/apifact-v2.json`, the document this page was generated from.
# Resolve the published exchange rate that applies on a given date (/docs/api/consultations/consultExchangeRate)
Returns the rate to apply to an operation on the requested date, following the rule in D.S. 29-94-EF artículo 5 numeral 17: the rate published on that date, or, on a day with no publication, EL ÚLTIMO PUBLICADO — the last one published on or before it. It never resolves forward and never interpolates. THE DATE IS A REQUIRED PATH SEGMENT WITH NO DEFAULT, on purpose: this service runs on UTC and Peru is UTC-05:00, so a server-side 'today' would resolve to tomorrow's date for every request sent after 19:00 in Lima, and the último-publicado rule would then hand back a rate labelled with a date the caller never asked about. The caller knows its comprobante's issue date; this service does not. BECAUSE THE ANSWER MAY COME FROM ANOTHER DAY, read rateDate, fallback and stalenessDays before displaying anything: fallback=true means rateDate differs from requestedOn, and stalenessDays is how far back the answer was taken from — 2 over a weekend is normal and correct under the rule, while 40 is not a market fact but a loader that stopped running. No ceiling is imposed here, because the norm has none; reporting is this API's half of the job and deciding is the caller's. Both sides are returned: for IGV the answer is sellRate ('promedio ponderado venta', for sales and purchases alike), while buyRate exists because the Impuesto a la Renta rule splits compra/venta by activo/pasivo. Rates are STRINGS with six decimals and must be parsed as decimals — routing a rate that gets multiplied into money through a binary float is the one place a fiscal figure can change value. Informational, like the padrón route: nothing here validates or blocks a document. Requires consultations:read.
## GET /api/v2/consultations/exchange-rate/{date}
```json
{
"tags": [
"Consultations"
],
"operationId": "consultExchangeRate",
"summary": "Resolve the published exchange rate that applies on a given date",
"description": "Returns the rate to apply to an operation on the requested date, following the rule in D.S. 29-94-EF artículo 5 numeral 17: the rate published on that date, or, on a day with no publication, EL ÚLTIMO PUBLICADO — the last one published on or before it. It never resolves forward and never interpolates. THE DATE IS A REQUIRED PATH SEGMENT WITH NO DEFAULT, on purpose: this service runs on UTC and Peru is UTC-05:00, so a server-side 'today' would resolve to tomorrow's date for every request sent after 19:00 in Lima, and the último-publicado rule would then hand back a rate labelled with a date the caller never asked about. The caller knows its comprobante's issue date; this service does not. BECAUSE THE ANSWER MAY COME FROM ANOTHER DAY, read rateDate, fallback and stalenessDays before displaying anything: fallback=true means rateDate differs from requestedOn, and stalenessDays is how far back the answer was taken from — 2 over a weekend is normal and correct under the rule, while 40 is not a market fact but a loader that stopped running. No ceiling is imposed here, because the norm has none; reporting is this API's half of the job and deciding is the caller's. Both sides are returned: for IGV the answer is sellRate ('promedio ponderado venta', for sales and purchases alike), while buyRate exists because the Impuesto a la Renta rule splits compra/venta by activo/pasivo. Rates are STRINGS with six decimals and must be parsed as decimals — routing a rate that gets multiplied into money through a binary float is the one place a fiscal figure can change value. Informational, like the padrón route: nothing here validates or blocks a document. Requires consultations:read.",
"security": [
{
"bearerAuth": []
}
],
"x-required-scope": "consultations:read",
"parameters": [
{
"name": "date",
"in": "path",
"required": true,
"schema": {
"type": "string",
"format": "date",
"pattern": "^\\d{4}-\\d{2}-\\d{2}$"
},
"description": "The date the rate is to APPLY to — normally the comprobante's issue date. A real calendar date in YYYY-MM-DD: 2026-02-30 is refused with 422 INVALID_DATE rather than rolled over to the 1st of March, which would attach a fiscal figure to a date nobody chose."
},
{
"name": "currency",
"in": "query",
"required": false,
"schema": {
"type": "string",
"pattern": "^[A-Za-z]{3}$",
"default": "USD"
},
"description": "Three-letter code, case-insensitive and echoed back upper-cased. Defaults to USD, the only series loaded today. A currency with no loaded series answers 503 rather than falling back to USD."
}
],
"responses": {
"200": {
"description": "The applicable rate. rateDate may differ from requestedOn; when it does, fallback is true and stalenessDays says by how many days.",
"headers": {
"X-Request-Id": {
"schema": {
"type": "string",
"format": "uuid"
}
}
},
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ExchangeRateConsultationResponse"
}
}
}
},
"401": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"403": {
"description": "Authenticated credential lacks consultations:read",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"422": {
"description": "INVALID_RUC when the path segment is not exactly eleven digits, INVALID_DATE when it is not a real calendar date in YYYY-MM-DD (2026-02-30 is refused rather than rolled over to March), INVALID_CURRENCY when the currency query parameter is not three letters. A malformed input is refused before the catalogue is read, and is never reported as an absent taxpayer or a missing rate.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"429": {
"description": "Request blocked by the edge rate limit",
"content": {
"application/json": {
"schema": {
"type": "object",
"additionalProperties": false,
"required": [
"error"
],
"properties": {
"error": {
"type": "string",
"enum": [
"rate_limit_exceeded"
]
}
}
}
}
}
},
"500": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"503": {
"description": "EXCHANGE_RATE_UNAVAILABLE: nothing has ever been published for this currency on or before the requested date — an unloaded currency, an empty series, or a date older than the first row loaded. Distinct from a resolved-but-stale answer, which is a 200 with fallback=true, and it does not affect document issuance.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
}
}
}
```
Any `$ref` above resolves against `/openapi/apifact-v2.json`, the document this page was generated from.
# Resolve a DNI to a person's NAME — informational, never a validation (/docs/api/consultations/consultDni)
Answers what RENIEC's registry holds for an eight-digit DNI: the person's names, and nothing else. Informational like the two routes above — no create, no trigger and no dispatch decision resolves a DNI, so a boleta whose acquirer name was typed by hand is issued exactly as before while this route is answering 503.
ONLY THE NAME IS RETURNED, AND THAT IS A DECISION RATHER THAN A GAP. The directory this API calls also publishes a person's date of birth and gender, on a different endpoint. That endpoint is never called. Neither datum has a fiscal purpose in this product — a boleta needs at most the acquirer's name — and under Ley 29733's minimisation principle obtaining data with no purpose is the wrong default, not a feature waiting to be finished. Asking for them would be a change to this published contract, not a field somebody appends to a mapper. AND NOTHING IS STORED: not the name, not the DNI, and not a hash of the DNI. An eight-digit space is exhaustible in seconds, so a digest of a DNI identifies the person exactly as well as the DNI does and is not a de-identification of anything. What is recorded is one metered row per consultation carrying the tenant, the verdict CLASS and the instant, because the platform pays a third party for each call and an unattributable spend is one nobody can bill or cap. If you need the name later, keep it yourself, and keep `consultedAt` beside it — this surface has no cache and will not serve you the same answer twice.
AN ABSENT ANSWER IS NOT AN ABSENT PERSON, and this is the expensive mistake the route is shaped to prevent. There are two answers and seven non-answers. `found: true` and `found: false` are both 200 and are both statements about the person: `false` means the registry was reached, answered, and holds no such record. Everything else is a 502 or a 503 with its own code and is a statement about the network or about this platform — never about the person. 502 DNI_DIRECTORY_AMBIGUOUS is the one to read twice: it means the directory answered 200 while declaring a failure WITHOUT saying which, so this API refuses to guess that the person does not exist. Treating any of the seven as `found: false` writes our outage into somebody's customer record and refuses a real person a real document. Branch on the STATUS CODE first and on `found` second.
Requires consultations:dni, which is NOT consultations:read and is not implied by it. The padrón and exchange-rate routes read free local catalogues about legal entities; this one spends a platform contract, writes a row under row-level security and returns personal data about a natural person. A credential handed to a form so it can prefill a company's legal name must not thereby become a credential that can enumerate people's names.
## GET /api/v2/consultations/dni/{dni}
```json
{
"tags": [
"Consultations"
],
"operationId": "consultDni",
"summary": "Resolve a DNI to a person's NAME — informational, never a validation",
"description": "Answers what RENIEC's registry holds for an eight-digit DNI: the person's names, and nothing else. Informational like the two routes above — no create, no trigger and no dispatch decision resolves a DNI, so a boleta whose acquirer name was typed by hand is issued exactly as before while this route is answering 503.\n\nONLY THE NAME IS RETURNED, AND THAT IS A DECISION RATHER THAN A GAP. The directory this API calls also publishes a person's date of birth and gender, on a different endpoint. That endpoint is never called. Neither datum has a fiscal purpose in this product — a boleta needs at most the acquirer's name — and under Ley 29733's minimisation principle obtaining data with no purpose is the wrong default, not a feature waiting to be finished. Asking for them would be a change to this published contract, not a field somebody appends to a mapper. AND NOTHING IS STORED: not the name, not the DNI, and not a hash of the DNI. An eight-digit space is exhaustible in seconds, so a digest of a DNI identifies the person exactly as well as the DNI does and is not a de-identification of anything. What is recorded is one metered row per consultation carrying the tenant, the verdict CLASS and the instant, because the platform pays a third party for each call and an unattributable spend is one nobody can bill or cap. If you need the name later, keep it yourself, and keep `consultedAt` beside it — this surface has no cache and will not serve you the same answer twice.\n\nAN ABSENT ANSWER IS NOT AN ABSENT PERSON, and this is the expensive mistake the route is shaped to prevent. There are two answers and seven non-answers. `found: true` and `found: false` are both 200 and are both statements about the person: `false` means the registry was reached, answered, and holds no such record. Everything else is a 502 or a 503 with its own code and is a statement about the network or about this platform — never about the person. 502 DNI_DIRECTORY_AMBIGUOUS is the one to read twice: it means the directory answered 200 while declaring a failure WITHOUT saying which, so this API refuses to guess that the person does not exist. Treating any of the seven as `found: false` writes our outage into somebody's customer record and refuses a real person a real document. Branch on the STATUS CODE first and on `found` second.\n\nRequires consultations:dni, which is NOT consultations:read and is not implied by it. The padrón and exchange-rate routes read free local catalogues about legal entities; this one spends a platform contract, writes a row under row-level security and returns personal data about a natural person. A credential handed to a form so it can prefill a company's legal name must not thereby become a credential that can enumerate people's names.",
"security": [
{
"bearerAuth": []
}
],
"x-required-scope": "consultations:dni",
"parameters": [
{
"name": "dni",
"in": "path",
"required": true,
"schema": {
"type": "string",
"pattern": "^[0-9]{8}$"
},
"description": "Exactly eight digits. Anything else is 422 INVALID_DNI, refused before a paid provider request is spent, and is never reported as a person who does not exist."
}
],
"responses": {
"200": {
"description": "The registry's answer. found=true and found=false are BOTH this status: a DNI the registry does not hold is a successful answer, not an error. The keys are the same either way, so branch on found and never on the response shape.",
"headers": {
"X-Request-Id": {
"schema": {
"type": "string",
"format": "uuid"
}
}
},
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/DniConsultationResponse"
}
}
}
},
"401": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"403": {
"description": "INSUFFICIENT_SCOPE: the authenticated credential lacks consultations:dni. A credential holding only consultations:read receives this, deliberately. The WWW-Authenticate challenge names the scope to grant, and nothing is spent: no provider request and no metered row.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"422": {
"description": "INVALID_DNI: the path segment is not exactly eight digits. Refused before the directory is called, so a malformed input never costs a paid request, and it is never reported as found=false. error.details is an ARRAY carrying the rejected candidate.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"429": {
"description": "Request blocked by the edge rate limit",
"content": {
"application/json": {
"schema": {
"type": "object",
"additionalProperties": false,
"required": [
"error"
],
"properties": {
"error": {
"type": "string",
"enum": [
"rate_limit_exceeded"
]
}
}
}
}
}
},
"500": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"502": {
"description": "The directory answered, and what it answered cannot be turned into a verdict about this person. NONE of these is found=false and none carries a Retry-After, because none is fixed by waiting. DNI_DIRECTORY_UNREADABLE: a 200 whose body this API cannot read as an identity. DNI_DIRECTORY_AMBIGUOUS: a 200 declaring a failure without saying which — the code that exists so an unrecognised negative is never guessed into 'this person does not exist'. DNI_DIRECTORY_REFUSED: the directory rejected the request for a reason about the request and not about the DNI, which is a bug on this side. None of the three affects document issuance.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"503": {
"description": "No answer was obtained at all, and again NONE of these is found=false. DNI_DIRECTORY_UNCONFIGURED: this deployment holds no directory key, so nothing was sent. DNI_DIRECTORY_UNAUTHORIZED: the directory refused THIS PLATFORM'S key — it is not about your credential, which is why it is not a 401, and rotating your token will not help. Both need an operator and therefore carry no Retry-After, because one would promise a recovery that never arrives on its own. DNI_DIRECTORY_THROTTLED: the directory is rate-limiting this platform, not you — it is not a 429 because 429 here means your own quota — and it carries Retry-After: 60. DNI_DIRECTORY_UNAVAILABLE: unreachable, timed out or 5xx, with Retry-After: 30. None of the four affects document issuance.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
},
"headers": {
"Retry-After": {
"schema": {
"type": "string",
"pattern": "^[0-9]+$"
},
"description": "Seconds to wait. Present on DNI_DIRECTORY_THROTTLED and DNI_DIRECTORY_UNAVAILABLE only. Absent on DNI_DIRECTORY_UNCONFIGURED and DNI_DIRECTORY_UNAUTHORIZED, because both need an operator and no amount of waiting resolves them."
}
}
}
}
}
```
Any `$ref` above resolves against `/openapi/apifact-v2.json`, the document this page was generated from.
# Get a tenant-owned v2 operation (/docs/api/operations/getOperation)
Resolves any operationId this API has issued, from all five collections: documents, summaries (RC), voided communications (RA), retentions (20) and perceptions (40). resourceKind names the collection the result is read from and resourceId the resource inside it; documentId is present only for comprobantes. An operationId belonging to another tenant is indistinguishable from one that never existed: both answer 404 OPERATION_NOT_FOUND.
## GET /api/v2/operations/{id}
```json
{
"tags": [
"Operations"
],
"operationId": "getOperation",
"summary": "Get a tenant-owned v2 operation",
"description": "Resolves any operationId this API has issued, from all five collections: documents, summaries (RC), voided communications (RA), retentions (20) and perceptions (40). resourceKind names the collection the result is read from and resourceId the resource inside it; documentId is present only for comprobantes. An operationId belonging to another tenant is indistinguishable from one that never existed: both answer 404 OPERATION_NOT_FOUND.",
"security": [
{
"bearerAuth": []
}
],
"x-required-scope": "operations:read",
"parameters": [
{
"name": "id",
"in": "path",
"required": true,
"schema": {
"type": "string",
"format": "uuid"
}
}
],
"responses": {
"200": {
"description": "Operation status",
"headers": {
"X-Request-Id": {
"schema": {
"type": "string",
"format": "uuid"
}
}
},
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/OperationResponse"
}
}
}
},
"401": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"403": {
"description": "Authenticated credential lacks operations:read",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"404": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"429": {
"description": "Request blocked by the edge rate limit",
"content": {
"application/json": {
"schema": {
"type": "object",
"additionalProperties": false,
"required": [
"error"
],
"properties": {
"error": {
"type": "string",
"enum": [
"rate_limit_exceeded"
]
}
}
}
}
}
},
"500": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
}
}
}
```
Any `$ref` above resolves against `/openapi/apifact-v2.json`, the document this page was generated from.
# This document (/docs/console/identity/getConsoleOpenApi)
Anonymous, like the machine API's own /api/v2/openapi.json: a client that cannot yet log in still has to be able to read the contract for logging in. It carries no tenant data and no session data — it is the same bytes for every caller.
## GET /console/v1/openapi.json
```json
{
"tags": [
"Identity"
],
"operationId": "getConsoleOpenApi",
"summary": "This document",
"description": "Anonymous, like the machine API's own /api/v2/openapi.json: a client that cannot yet log in still has to be able to read the contract for logging in. It carries no tenant data and no session data — it is the same bytes for every caller.",
"security": [],
"responses": {
"200": {
"description": "Bundled OpenAPI 3.0.3 document"
}
}
}
```
Any `$ref` above resolves against `/openapi/console-v1.json`, the document this page was generated from.
# Create an account and its first organization (/docs/console/identity/registerConsoleUser)
Answers 202 with the same body whether or not the address was already registered, and pays the same Argon2id cost either way, so neither the response nor its duration is an oracle for who has an account. The registrant becomes the OWNER of a new organization -- the only way to become an OWNER other than being promoted by one. The organization starts with no companies; the registrant attaches one through POST /console/v1/organizations/{organizationId}/companies, which migration 000065 added. This sentence used to say the opposite -- that attaching was deliberately impossible -- and it is the fourth copy of that claim found after the route shipped, which is what a fact repeated in prose costs when nothing compares prose to paths.
## POST /console/v1/registrations
```json
{
"tags": [
"Identity"
],
"operationId": "registerConsoleUser",
"summary": "Create an account and its first organization",
"description": "Answers 202 with the same body whether or not the address was already registered, and pays the same Argon2id cost either way, so neither the response nor its duration is an oracle for who has an account. The registrant becomes the OWNER of a new organization -- the only way to become an OWNER other than being promoted by one. The organization starts with no companies; the registrant attaches one through POST /console/v1/organizations/{organizationId}/companies, which migration 000065 added. This sentence used to say the opposite -- that attaching was deliberately impossible -- and it is the fourth copy of that claim found after the route shipped, which is what a fact repeated in prose costs when nothing compares prose to paths.",
"security": [],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleRegistrationRequest"
}
}
}
},
"responses": {
"202": {
"description": "Accepted. If the address was free, a verification link has been issued.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleRegistrationAccepted"
}
}
}
},
"400": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"401": {
"description": "No usable session cookie, or MFA_REQUIRED on the login route. A machine credential in an Authorization header reaches this too: console routes never read that header.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"403": {
"description": "CSRF_ORIGIN_REJECTED. Every unsafe method requires an Origin header naming a configured console origin; a deployment with none configured refuses them all.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"404": {
"description": "The resource does not exist, or is not visible to this session. The two are deliberately indistinguishable: a company in another organization answers exactly what an invented uuid answers.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"405": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"413": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"415": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"422": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"500": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"503": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
}
}
}
```
Any `$ref` above resolves against `/openapi/console-v1.json`, the document this page was generated from.
# Spend an email verification token (/docs/console/identity/verifyConsoleEmail)
Single use. Unknown, expired, already spent and belonging to a disabled account are one outcome and one status code: the consuming UPDATE is the check, so there is nothing to distinguish even if the handler wanted to.
## POST /console/v1/email-verifications
```json
{
"tags": [
"Identity"
],
"operationId": "verifyConsoleEmail",
"summary": "Spend an email verification token",
"description": "Single use. Unknown, expired, already spent and belonging to a disabled account are one outcome and one status code: the consuming UPDATE is the check, so there is nothing to distinguish even if the handler wanted to.",
"security": [],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleTokenRequest"
}
}
}
},
"responses": {
"204": {
"description": "Address verified"
},
"400": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"401": {
"description": "No usable session cookie, or MFA_REQUIRED on the login route. A machine credential in an Authorization header reaches this too: console routes never read that header.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"403": {
"description": "CSRF_ORIGIN_REJECTED. Every unsafe method requires an Origin header naming a configured console origin; a deployment with none configured refuses them all.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"404": {
"description": "The resource does not exist, or is not visible to this session. The two are deliberately indistinguishable: a company in another organization answers exactly what an invented uuid answers.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"405": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"413": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"415": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"422": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"500": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"503": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
}
}
}
```
Any `$ref` above resolves against `/openapi/console-v1.json`, the document this page was generated from.
# Log in (/docs/console/identity/openConsoleSession)
Sets the session cookie. When the account has TOTP enrolled and no totpCode was supplied -- or the code was replayed -- the answer is 401 MFA_REQUIRED and NO cookie; resend the same body with the code. There is no intermediate ticket, because a ticket is a second session-shaped secret with its own theft story. Every failure below a correct password is one indistinguishable 401 that costs one Argon2id verification, including for an address that was never registered.
## POST /console/v1/sessions
```json
{
"tags": [
"Identity"
],
"operationId": "openConsoleSession",
"summary": "Log in",
"description": "Sets the session cookie. When the account has TOTP enrolled and no totpCode was supplied -- or the code was replayed -- the answer is 401 MFA_REQUIRED and NO cookie; resend the same body with the code. There is no intermediate ticket, because a ticket is a second session-shaped secret with its own theft story. Every failure below a correct password is one indistinguishable 401 that costs one Argon2id verification, including for an address that was never registered.",
"security": [],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleLoginRequest"
}
}
}
},
"responses": {
"201": {
"description": "Session opened; the cookie is in Set-Cookie and nowhere else",
"headers": {
"Set-Cookie": {
"schema": {
"type": "string"
},
"description": "__Host-apf_console=...; HttpOnly; Secure; SameSite=Strict; Path=/; Max-Age=43200"
}
},
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleSessionResponse"
}
}
}
},
"400": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"401": {
"description": "No usable session cookie, or MFA_REQUIRED on the login route. A machine credential in an Authorization header reaches this too: console routes never read that header.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"403": {
"description": "CSRF_ORIGIN_REJECTED. Every unsafe method requires an Origin header naming a configured console origin; a deployment with none configured refuses them all.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"404": {
"description": "The resource does not exist, or is not visible to this session. The two are deliberately indistinguishable: a company in another organization answers exactly what an invented uuid answers.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"405": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"413": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"415": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"422": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"500": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"503": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
}
}
}
```
Any `$ref` above resolves against `/openapi/console-v1.json`, the document this page was generated from.
# Log out everywhere (/docs/console/identity/closeAllConsoleSessions)
Revokes every session of the caller, this one included. Needs a live session of its own, so it cannot be aimed at another user.
## DELETE /console/v1/sessions
```json
{
"tags": [
"Identity"
],
"operationId": "closeAllConsoleSessions",
"summary": "Log out everywhere",
"description": "Revokes every session of the caller, this one included. Needs a live session of its own, so it cannot be aimed at another user.",
"responses": {
"204": {
"description": "All sessions revoked"
},
"400": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"401": {
"description": "No usable session cookie, or MFA_REQUIRED on the login route. A machine credential in an Authorization header reaches this too: console routes never read that header.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"403": {
"description": "CSRF_ORIGIN_REJECTED. Every unsafe method requires an Origin header naming a configured console origin; a deployment with none configured refuses them all.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"404": {
"description": "The resource does not exist, or is not visible to this session. The two are deliberately indistinguishable: a company in another organization answers exactly what an invented uuid answers.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"405": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"413": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"415": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"422": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"500": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"503": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
}
}
}
```
Any `$ref` above resolves against `/openapi/console-v1.json`, the document this page was generated from.
# Log out (/docs/console/identity/closeConsoleSession)
Revokes this session server-side and clears the cookie. The token is opaque and server-side precisely so this can be instantaneous rather than a matter of waiting for an expiry.
## DELETE /console/v1/sessions/current
```json
{
"tags": [
"Identity"
],
"operationId": "closeConsoleSession",
"summary": "Log out",
"description": "Revokes this session server-side and clears the cookie. The token is opaque and server-side precisely so this can be instantaneous rather than a matter of waiting for an expiry.",
"responses": {
"204": {
"description": "Session revoked"
},
"400": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"401": {
"description": "No usable session cookie, or MFA_REQUIRED on the login route. A machine credential in an Authorization header reaches this too: console routes never read that header.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"403": {
"description": "CSRF_ORIGIN_REJECTED. Every unsafe method requires an Origin header naming a configured console origin; a deployment with none configured refuses them all.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"404": {
"description": "The resource does not exist, or is not visible to this session. The two are deliberately indistinguishable: a company in another organization answers exactly what an invented uuid answers.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"405": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"413": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"415": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"422": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"500": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"503": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
}
}
}
```
Any `$ref` above resolves against `/openapi/console-v1.json`, the document this page was generated from.
# Who am I, and what do I belong to (/docs/console/identity/getConsoleSession)
## GET /console/v1/session
```json
{
"tags": [
"Identity"
],
"operationId": "getConsoleSession",
"summary": "Who am I, and what do I belong to",
"responses": {
"200": {
"description": "The caller and their organizations, with the role in each",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleSessionResponse"
}
}
}
},
"400": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"401": {
"description": "No usable session cookie, or MFA_REQUIRED on the login route. A machine credential in an Authorization header reaches this too: console routes never read that header.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"403": {
"description": "CSRF_ORIGIN_REJECTED. Every unsafe method requires an Origin header naming a configured console origin; a deployment with none configured refuses them all.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"404": {
"description": "The resource does not exist, or is not visible to this session. The two are deliberately indistinguishable: a company in another organization answers exactly what an invented uuid answers.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"405": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"413": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"415": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"422": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"500": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"503": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
}
}
}
```
Any `$ref` above resolves against `/openapi/console-v1.json`, the document this page was generated from.
# Change the password (/docs/console/identity/changeConsolePassword)
Compare-and-swap against the hash just verified, so a change racing another loses instead of silently overwriting it. EVERY session dies, the caller's included: a password change cannot be distinguished from the response to a compromise, and the rule that is right in the second case is the one that applies to both.
## POST /console/v1/password
```json
{
"tags": [
"Identity"
],
"operationId": "changeConsolePassword",
"summary": "Change the password",
"description": "Compare-and-swap against the hash just verified, so a change racing another loses instead of silently overwriting it. EVERY session dies, the caller's included: a password change cannot be distinguished from the response to a compromise, and the rule that is right in the second case is the one that applies to both.",
"security": [],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleChangePasswordRequest"
}
}
}
},
"responses": {
"204": {
"description": "Password changed; log in again"
},
"400": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"401": {
"description": "No usable session cookie, or MFA_REQUIRED on the login route. A machine credential in an Authorization header reaches this too: console routes never read that header.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"403": {
"description": "CSRF_ORIGIN_REJECTED. Every unsafe method requires an Origin header naming a configured console origin; a deployment with none configured refuses them all.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"404": {
"description": "The resource does not exist, or is not visible to this session. The two are deliberately indistinguishable: a company in another organization answers exactly what an invented uuid answers.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"405": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"413": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"415": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"422": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"500": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"503": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
}
}
}
```
Any `$ref` above resolves against `/openapi/console-v1.json`, the document this page was generated from.
# Start TOTP enrolment (/docs/console/identity/beginConsoleMfaEnrolment)
## POST /console/v1/mfa/enrolment
```json
{
"tags": [
"Identity"
],
"operationId": "beginConsoleMfaEnrolment",
"summary": "Start TOTP enrolment",
"responses": {
"201": {
"description": "A secret that is stored nowhere until a code proves it works",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleMfaEnrolmentStarted"
}
}
}
},
"400": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"401": {
"description": "No usable session cookie, or MFA_REQUIRED on the login route. A machine credential in an Authorization header reaches this too: console routes never read that header.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"403": {
"description": "CSRF_ORIGIN_REJECTED. Every unsafe method requires an Origin header naming a configured console origin; a deployment with none configured refuses them all.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"404": {
"description": "The resource does not exist, or is not visible to this session. The two are deliberately indistinguishable: a company in another organization answers exactly what an invented uuid answers.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"405": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"413": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"415": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"422": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"500": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"503": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
}
}
}
```
Any `$ref` above resolves against `/openapi/console-v1.json`, the document this page was generated from.
# Finish TOTP enrolment (/docs/console/identity/completeConsoleMfaEnrolment)
The secret is sealed with AES-256-GCM under a versioned KEK, and the step the enrolling code used is recorded with it, so replaying that same code cannot also open a session.
## POST /console/v1/mfa
```json
{
"tags": [
"Identity"
],
"operationId": "completeConsoleMfaEnrolment",
"summary": "Finish TOTP enrolment",
"description": "The secret is sealed with AES-256-GCM under a versioned KEK, and the step the enrolling code used is recorded with it, so replaying that same code cannot also open a session.",
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleMfaEnrolmentRequest"
}
}
}
},
"responses": {
"204": {
"description": "Enrolled"
},
"400": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"401": {
"description": "No usable session cookie, or MFA_REQUIRED on the login route. A machine credential in an Authorization header reaches this too: console routes never read that header.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"403": {
"description": "CSRF_ORIGIN_REJECTED. Every unsafe method requires an Origin header naming a configured console origin; a deployment with none configured refuses them all.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"404": {
"description": "The resource does not exist, or is not visible to this session. The two are deliberately indistinguishable: a company in another organization answers exactly what an invented uuid answers.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"405": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"413": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"415": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"422": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"500": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"503": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
}
}
}
```
Any `$ref` above resolves against `/openapi/console-v1.json`, the document this page was generated from.
# Remove the second factor (/docs/console/identity/disableConsoleMfa)
## DELETE /console/v1/mfa
```json
{
"tags": [
"Identity"
],
"operationId": "disableConsoleMfa",
"summary": "Remove the second factor",
"responses": {
"204": {
"description": "Disabled"
},
"400": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"401": {
"description": "No usable session cookie, or MFA_REQUIRED on the login route. A machine credential in an Authorization header reaches this too: console routes never read that header.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"403": {
"description": "CSRF_ORIGIN_REJECTED. Every unsafe method requires an Origin header naming a configured console origin; a deployment with none configured refuses them all.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"404": {
"description": "The resource does not exist, or is not visible to this session. The two are deliberately indistinguishable: a company in another organization answers exactly what an invented uuid answers.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"405": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"413": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"415": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"422": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"500": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"503": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
}
}
}
```
Any `$ref` above resolves against `/openapi/console-v1.json`, the document this page was generated from.
# The companies this session can operate (/docs/console/companies/listConsoleCompanies)
A listing, not an authorisation: it resolves membership across organizations and binds nothing. Selecting one is GET /console/v1/companies/{companyId}, which is where the membership check and the assignment of app.tenant_id happen as one act.
## GET /console/v1/companies
```json
{
"tags": [
"Companies"
],
"operationId": "listConsoleCompanies",
"summary": "The companies this session can operate",
"description": "A listing, not an authorisation: it resolves membership across organizations and binds nothing. Selecting one is GET /console/v1/companies/{companyId}, which is where the membership check and the assignment of app.tenant_id happen as one act.",
"responses": {
"200": {
"description": "Every company of every organization the caller belongs to",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleCompanyList"
}
}
}
},
"400": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"401": {
"description": "No usable session cookie, or MFA_REQUIRED on the login route. A machine credential in an Authorization header reaches this too: console routes never read that header.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"403": {
"description": "CSRF_ORIGIN_REJECTED. Every unsafe method requires an Origin header naming a configured console origin; a deployment with none configured refuses them all.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"404": {
"description": "The resource does not exist, or is not visible to this session. The two are deliberately indistinguishable: a company in another organization answers exactly what an invented uuid answers.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"405": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"413": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"415": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"422": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"500": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"503": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
}
}
}
```
Any `$ref` above resolves against `/openapi/console-v1.json`, the document this page was generated from.
# Select a company to operate (/docs/console/companies/getConsoleCompany)
The tenant binding of ADR 0016 point 5. auth.bind_console_company verifies the membership and sets app.tenant_id in ONE statement inside ONE transaction, and mints a binding token derived from a secret the console database role cannot read; every table the console can read carries a restrictive policy that demands it. Setting app.tenant_id by hand therefore widens nothing. A company in another organization and a company that does not exist both answer 404.
## GET /console/v1/companies/{companyId}
```json
{
"tags": [
"Companies"
],
"operationId": "getConsoleCompany",
"summary": "Select a company to operate",
"description": "The tenant binding of ADR 0016 point 5. auth.bind_console_company verifies the membership and sets app.tenant_id in ONE statement inside ONE transaction, and mints a binding token derived from a secret the console database role cannot read; every table the console can read carries a restrictive policy that demands it. Setting app.tenant_id by hand therefore widens nothing. A company in another organization and a company that does not exist both answer 404.",
"parameters": [
{
"name": "companyId",
"in": "path",
"required": true,
"schema": {
"type": "string",
"format": "uuid"
}
}
],
"responses": {
"200": {
"description": "The bound company",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleCompanyResponse"
}
}
}
},
"400": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"401": {
"description": "No usable session cookie, or MFA_REQUIRED on the login route. A machine credential in an Authorization header reaches this too: console routes never read that header.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"403": {
"description": "CSRF_ORIGIN_REJECTED. Every unsafe method requires an Origin header naming a configured console origin; a deployment with none configured refuses them all.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"404": {
"description": "The resource does not exist, or is not visible to this session. The two are deliberately indistinguishable: a company in another organization answers exactly what an invented uuid answers.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"405": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"413": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"415": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"422": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"500": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"503": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
}
}
}
```
Any `$ref` above resolves against `/openapi/console-v1.json`, the document this page was generated from.
# Comprobantes with their state (/docs/console/companies/listConsoleDocuments)
## GET /console/v1/companies/{companyId}/documents
```json
{
"tags": [
"Companies"
],
"operationId": "listConsoleDocuments",
"summary": "Comprobantes with their state",
"parameters": [
{
"name": "companyId",
"in": "path",
"required": true,
"schema": {
"type": "string",
"format": "uuid"
}
},
{
"name": "cursor",
"in": "query",
"required": false,
"schema": {
"type": "string"
}
},
{
"name": "limit",
"in": "query",
"required": false,
"schema": {
"type": "integer",
"minimum": 1,
"maximum": 100,
"default": 25
}
},
{
"name": "state",
"in": "query",
"required": false,
"schema": {
"type": "string"
}
},
{
"name": "documentType",
"in": "query",
"required": false,
"schema": {
"type": "string",
"enum": [
"01",
"03",
"07",
"08"
]
}
}
],
"responses": {
"200": {
"description": "One keyset page, newest change first",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleDocumentPage"
}
}
}
},
"400": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"401": {
"description": "No usable session cookie, or MFA_REQUIRED on the login route. A machine credential in an Authorization header reaches this too: console routes never read that header.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"403": {
"description": "CSRF_ORIGIN_REJECTED. Every unsafe method requires an Origin header naming a configured console origin; a deployment with none configured refuses them all.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"404": {
"description": "The resource does not exist, or is not visible to this session. The two are deliberately indistinguishable: a company in another organization answers exactly what an invented uuid answers.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"405": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"413": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"415": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"422": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"500": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"503": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
}
}
}
```
Any `$ref` above resolves against `/openapi/console-v1.json`, the document this page was generated from.
# One comprobante, with the SUNAT verdict and what it means (/docs/console/companies/getConsoleDocument)
The response code and description are verbatim from the CDR. The severity/standing/action/guidance block is derived from the catalog in @apifact/sunat at read time and is not stored, so it cannot drift; catalogVersion, by contrast, reports the version recorded on the attempt, because claiming yesterday's rejection was judged by today's catalog would forge the audit trail.
## GET /console/v1/companies/{companyId}/documents/{documentId}
```json
{
"tags": [
"Companies"
],
"operationId": "getConsoleDocument",
"summary": "One comprobante, with the SUNAT verdict and what it means",
"description": "The response code and description are verbatim from the CDR. The severity/standing/action/guidance block is derived from the catalog in @apifact/sunat at read time and is not stored, so it cannot drift; catalogVersion, by contrast, reports the version recorded on the attempt, because claiming yesterday's rejection was judged by today's catalog would forge the audit trail.",
"parameters": [
{
"name": "companyId",
"in": "path",
"required": true,
"schema": {
"type": "string",
"format": "uuid"
}
},
{
"name": "documentId",
"in": "path",
"required": true,
"schema": {
"type": "string",
"format": "uuid"
}
}
],
"responses": {
"200": {
"description": "The comprobante",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleDocumentResponse"
}
}
}
},
"400": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"401": {
"description": "No usable session cookie, or MFA_REQUIRED on the login route. A machine credential in an Authorization header reaches this too: console routes never read that header.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"403": {
"description": "CSRF_ORIGIN_REJECTED. Every unsafe method requires an Origin header naming a configured console origin; a deployment with none configured refuses them all.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"404": {
"description": "The resource does not exist, or is not visible to this session. The two are deliberately indistinguishable: a company in another organization answers exactly what an invented uuid answers.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"405": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"413": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"415": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"422": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"500": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"503": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
}
}
}
```
Any `$ref` above resolves against `/openapi/console-v1.json`, the document this page was generated from.
# Everything the platform produced for one comprobante (/docs/console/companies/listConsoleArtifacts)
## GET /console/v1/companies/{companyId}/documents/{documentId}/artifacts
```json
{
"tags": [
"Companies"
],
"operationId": "listConsoleArtifacts",
"summary": "Everything the platform produced for one comprobante",
"parameters": [
{
"name": "companyId",
"in": "path",
"required": true,
"schema": {
"type": "string",
"format": "uuid"
}
},
{
"name": "documentId",
"in": "path",
"required": true,
"schema": {
"type": "string",
"format": "uuid"
}
}
],
"responses": {
"200": {
"description": "Versionless artifacts and versioned PDF renders, in one listing",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleArtifactList"
}
}
}
},
"400": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"401": {
"description": "No usable session cookie, or MFA_REQUIRED on the login route. A machine credential in an Authorization header reaches this too: console routes never read that header.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"403": {
"description": "CSRF_ORIGIN_REJECTED. Every unsafe method requires an Origin header naming a configured console origin; a deployment with none configured refuses them all.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"404": {
"description": "The resource does not exist, or is not visible to this session. The two are deliberately indistinguishable: a company in another organization answers exactly what an invented uuid answers.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"405": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"413": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"415": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"422": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"500": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"503": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
}
}
}
```
Any `$ref` above resolves against `/openapi/console-v1.json`, the document this page was generated from.
# Stream one artifact (/docs/console/companies/downloadConsoleArtifact)
The bytes come from apps/artifact-worker, which re-verifies the SHA-256 and the byte size of what it reads before any of it reaches the caller. The object key never leaves the server. Use versions/{representationId} as the kind to fetch a specific PDF render.
## GET /console/v1/companies/{companyId}/documents/{documentId}/artifacts/{kind}
```json
{
"tags": [
"Companies"
],
"operationId": "downloadConsoleArtifact",
"summary": "Stream one artifact",
"description": "The bytes come from apps/artifact-worker, which re-verifies the SHA-256 and the byte size of what it reads before any of it reaches the caller. The object key never leaves the server. Use versions/{representationId} as the kind to fetch a specific PDF render.",
"parameters": [
{
"name": "companyId",
"in": "path",
"required": true,
"schema": {
"type": "string",
"format": "uuid"
}
},
{
"name": "documentId",
"in": "path",
"required": true,
"schema": {
"type": "string",
"format": "uuid"
}
},
{
"name": "kind",
"in": "path",
"required": true,
"schema": {
"type": "string"
},
"description": "A public artifact kind (signed-xml, submission-zip, cdr-zip, cdr-xml, pdf-a4, pdf-ticket, qr-png), or versions/{representationId} for a specific PDF render."
}
],
"responses": {
"200": {
"description": "The artifact bytes",
"headers": {
"Content-Digest": {
"schema": {
"type": "string"
}
},
"Content-Disposition": {
"schema": {
"type": "string"
}
}
},
"content": {
"application/octet-stream": {}
}
},
"400": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"401": {
"description": "No usable session cookie, or MFA_REQUIRED on the login route. A machine credential in an Authorization header reaches this too: console routes never read that header.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"403": {
"description": "CSRF_ORIGIN_REJECTED. Every unsafe method requires an Origin header naming a configured console origin; a deployment with none configured refuses them all.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"404": {
"description": "The resource does not exist, or is not visible to this session. The two are deliberately indistinguishable: a company in another organization answers exactly what an invented uuid answers.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"405": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"409": {
"description": "ARTIFACT_NOT_READY: it exists but has not been produced yet.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"413": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"415": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"422": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"500": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"503": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
}
}
}
```
Any `$ref` above resolves against `/openapi/console-v1.json`, the document this page was generated from.
# The API credentials of this company (/docs/console/companies/listConsoleCredentials)
Any member may read it, VIEWER included. It carries no secret -- there is no column holding one -- and the public prefix it does carry is already reported for every operation by GET /console/v1/companies/{companyId}/activity, so hiding this list would be a rule with no content. Revoked and expired credentials stay in it.
## GET /console/v1/companies/{companyId}/credentials
```json
{
"tags": [
"Companies"
],
"operationId": "listConsoleCredentials",
"summary": "The API credentials of this company",
"description": "Any member may read it, VIEWER included. It carries no secret -- there is no column holding one -- and the public prefix it does carry is already reported for every operation by GET /console/v1/companies/{companyId}/activity, so hiding this list would be a rule with no content. Revoked and expired credentials stay in it.",
"parameters": [
{
"name": "companyId",
"in": "path",
"required": true,
"schema": {
"type": "string",
"format": "uuid"
}
}
],
"responses": {
"200": {
"description": "Every credential of the bound company, newest first",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleCredentialList"
}
}
}
},
"400": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"401": {
"description": "No usable session cookie, or MFA_REQUIRED on the login route. A machine credential in an Authorization header reaches this too: console routes never read that header.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"403": {
"description": "CSRF_ORIGIN_REJECTED. Every unsafe method requires an Origin header naming a configured console origin; a deployment with none configured refuses them all.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"404": {
"description": "The resource does not exist, or is not visible to this session. The two are deliberately indistinguishable: a company in another organization answers exactly what an invented uuid answers.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"405": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"413": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"415": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"422": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"500": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"503": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
}
}
}
```
Any `$ref` above resolves against `/openapi/console-v1.json`, the document this page was generated from.
# Mint an API credential (/docs/console/companies/mintConsoleCredential)
DEVELOPER and above, which is what ADR 0016 point 7 assigns to that role. The credential can issue comprobantes, so this is the privilege escalation the ADR names and does not pretend otherwise about: the mitigation is that it is recorded. The credential row and its ops.audit_events row are written in one transaction by the SAME function the provisioning API calls, and a DEFERRABLE constraint trigger on auth.api_credentials refuses at COMMIT to admit a credential for an organization-owned company without one -- so an unaudited mint is not a policy violation, it is a failed transaction. THE TOKEN IS IN THE 201 AND NOWHERE ELSE. A scope the caller's own role could not exercise is refused with 403 SCOPE_NOT_GRANTABLE rather than silently dropped.
## POST /console/v1/companies/{companyId}/credentials
```json
{
"tags": [
"Companies"
],
"operationId": "mintConsoleCredential",
"summary": "Mint an API credential",
"description": "DEVELOPER and above, which is what ADR 0016 point 7 assigns to that role. The credential can issue comprobantes, so this is the privilege escalation the ADR names and does not pretend otherwise about: the mitigation is that it is recorded. The credential row and its ops.audit_events row are written in one transaction by the SAME function the provisioning API calls, and a DEFERRABLE constraint trigger on auth.api_credentials refuses at COMMIT to admit a credential for an organization-owned company without one -- so an unaudited mint is not a policy violation, it is a failed transaction. THE TOKEN IS IN THE 201 AND NOWHERE ELSE. A scope the caller's own role could not exercise is refused with 403 SCOPE_NOT_GRANTABLE rather than silently dropped.",
"parameters": [
{
"name": "companyId",
"in": "path",
"required": true,
"schema": {
"type": "string",
"format": "uuid"
}
}
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleMintCredentialRequest"
}
}
}
},
"responses": {
"201": {
"description": "Minted; the token is in this response and is not recoverable after it",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleMintedCredential"
}
}
}
},
"400": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"401": {
"description": "No usable session cookie, or MFA_REQUIRED on the login route. A machine credential in an Authorization header reaches this too: console routes never read that header.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"403": {
"description": "CSRF_ORIGIN_REJECTED, or SCOPE_NOT_GRANTABLE when the caller's role may not grant a scope it asked for (certificates:manage needs ADMIN).",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"404": {
"description": "The resource does not exist, or is not visible to this session. The two are deliberately indistinguishable: a company in another organization answers exactly what an invented uuid answers.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"405": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"413": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"415": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"422": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"500": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"503": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
}
}
}
```
Any `$ref` above resolves against `/openapi/console-v1.json`, the document this page was generated from.
# Revoke an API credential (/docs/console/companies/revokeConsoleCredential)
Immediate, not at expiry: auth.lookup_api_credential re-reads revoked_at on every authenticated request, so the next one fails. Same DEVELOPER floor as minting, deliberately and never higher -- revocation is the emergency action, and a system where creating a credential is easier than killing it has the incentives backwards. Unknown, already revoked, and belonging to another company are one 404.
## DELETE /console/v1/companies/{companyId}/credentials/{credentialId}
```json
{
"tags": [
"Companies"
],
"operationId": "revokeConsoleCredential",
"summary": "Revoke an API credential",
"description": "Immediate, not at expiry: auth.lookup_api_credential re-reads revoked_at on every authenticated request, so the next one fails. Same DEVELOPER floor as minting, deliberately and never higher -- revocation is the emergency action, and a system where creating a credential is easier than killing it has the incentives backwards. Unknown, already revoked, and belonging to another company are one 404.",
"parameters": [
{
"name": "companyId",
"in": "path",
"required": true,
"schema": {
"type": "string",
"format": "uuid"
}
},
{
"name": "credentialId",
"in": "path",
"required": true,
"schema": {
"type": "string",
"format": "uuid"
}
}
],
"responses": {
"200": {
"description": "Revoked, as of the moment in the body",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleCredentialRevoked"
}
}
}
},
"400": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"401": {
"description": "No usable session cookie, or MFA_REQUIRED on the login route. A machine credential in an Authorization header reaches this too: console routes never read that header.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"403": {
"description": "CSRF_ORIGIN_REJECTED. Every unsafe method requires an Origin header naming a configured console origin; a deployment with none configured refuses them all.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"404": {
"description": "The resource does not exist, or is not visible to this session. The two are deliberately indistinguishable: a company in another organization answers exactly what an invented uuid answers.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"405": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"413": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"415": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"422": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"500": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"503": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
}
}
}
```
Any `$ref` above resolves against `/openapi/console-v1.json`, the document this page was generated from.
# Plan consumption for this company (/docs/console/companies/getConsoleUsage)
A read, and only a read. ADR 0016 point 6 keeps the quota holder on the company and usage.consume_quota is untouched. The period is this company's own plan period, not a window this route picked.
## GET /console/v1/companies/{companyId}/usage
```json
{
"tags": [
"Companies"
],
"operationId": "getConsoleUsage",
"summary": "Plan consumption for this company",
"description": "A read, and only a read. ADR 0016 point 6 keeps the quota holder on the company and usage.consume_quota is untouched. The period is this company's own plan period, not a window this route picked.",
"parameters": [
{
"name": "companyId",
"in": "path",
"required": true,
"schema": {
"type": "string",
"format": "uuid"
}
},
{
"name": "metric",
"in": "query",
"required": false,
"schema": {
"type": "string",
"maxLength": 60,
"default": "documents.accepted"
}
}
],
"responses": {
"200": {
"description": "Limit and consumption for the current period",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleUsageResponse"
}
}
}
},
"400": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"401": {
"description": "No usable session cookie, or MFA_REQUIRED on the login route. A machine credential in an Authorization header reaches this too: console routes never read that header.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"403": {
"description": "CSRF_ORIGIN_REJECTED. Every unsafe method requires an Origin header naming a configured console origin; a deployment with none configured refuses them all.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"404": {
"description": "The resource does not exist, or is not visible to this session. The two are deliberately indistinguishable: a company in another organization answers exactly what an invented uuid answers.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"405": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"413": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"415": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"422": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"500": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"503": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
}
}
}
```
Any `$ref` above resolves against `/openapi/console-v1.json`, the document this page was generated from.
# The request and error log (/docs/console/companies/getConsoleActivity)
The read that exists so a rejected comprobante does not become an email to support. One entry per SUNAT attempt -- retries are never collapsed -- plus operations that never reached SUNAT at all, each with the verbatim response code and description, the transport classification, and the PUBLIC prefix of the credential that started it. The free-form operations.result jsonb is deliberately NOT published: nothing bounds what a future workflow writes into it.
## GET /console/v1/companies/{companyId}/activity
```json
{
"tags": [
"Companies"
],
"operationId": "getConsoleActivity",
"summary": "The request and error log",
"description": "The read that exists so a rejected comprobante does not become an email to support. One entry per SUNAT attempt -- retries are never collapsed -- plus operations that never reached SUNAT at all, each with the verbatim response code and description, the transport classification, and the PUBLIC prefix of the credential that started it. The free-form operations.result jsonb is deliberately NOT published: nothing bounds what a future workflow writes into it.",
"parameters": [
{
"name": "companyId",
"in": "path",
"required": true,
"schema": {
"type": "string",
"format": "uuid"
}
},
{
"name": "documentId",
"in": "query",
"required": false,
"schema": {
"type": "string",
"format": "uuid"
}
},
{
"name": "before",
"in": "query",
"required": false,
"schema": {
"type": "string",
"format": "date-time"
}
},
{
"name": "limit",
"in": "query",
"required": false,
"schema": {
"type": "integer",
"minimum": 1,
"maximum": 200,
"default": 50
}
}
],
"responses": {
"200": {
"description": "Recent attempts and operations, newest first",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleActivityResponse"
}
}
}
},
"400": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"401": {
"description": "No usable session cookie, or MFA_REQUIRED on the login route. A machine credential in an Authorization header reaches this too: console routes never read that header.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"403": {
"description": "CSRF_ORIGIN_REJECTED. Every unsafe method requires an Origin header naming a configured console origin; a deployment with none configured refuses them all.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"404": {
"description": "The resource does not exist, or is not visible to this session. The two are deliberately indistinguishable: a company in another organization answers exactly what an invented uuid answers.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"405": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"413": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"415": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"422": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"500": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"503": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
}
}
}
```
Any `$ref` above resolves against `/openapi/console-v1.json`, the document this page was generated from.
# The certificate versions this company has enrolled (/docs/console/companies/listConsoleCertificates)
ADMIN and above. Status and validity only: no storage location and no secret, because this route reads through the same repository the machine surface uses and that repository cannot read an envelope's location at all.
## GET /console/v1/companies/{companyId}/certificates
```json
{
"tags": [
"Companies"
],
"operationId": "listConsoleCertificates",
"summary": "The certificate versions this company has enrolled",
"description": "ADMIN and above. Status and validity only: no storage location and no secret, because this route reads through the same repository the machine surface uses and that repository cannot read an envelope's location at all.",
"parameters": [
{
"name": "companyId",
"in": "path",
"required": true,
"schema": {
"type": "string",
"format": "uuid"
}
}
],
"responses": {
"200": {
"description": "Enrolled versions, newest first, with the latest activation attempt",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleCertificateList"
}
}
}
},
"400": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"401": {
"description": "No usable session cookie, or MFA_REQUIRED on the login route. A machine credential in an Authorization header reaches this too: console routes never read that header.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"403": {
"description": "CSRF_ORIGIN_REJECTED. Every unsafe method requires an Origin header naming a configured console origin; a deployment with none configured refuses them all.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"404": {
"description": "The resource does not exist, or is not visible to this session. The two are deliberately indistinguishable: a company in another organization answers exactly what an invented uuid answers.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"405": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"413": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"415": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"422": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"500": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"503": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
}
}
}
```
Any `$ref` above resolves against `/openapi/console-v1.json`, the document this page was generated from.
# Enrol a new certificate version (/docs/console/companies/enrolConsoleCertificate)
ADMIN and above, which is the floor ADR 0016 point 7 sets for companies and the one 000060 already applies to minting a credential that carries certificates:manage. Takes the four secrets SUNAT needs: the PKCS#12, its password, and the SOL user and password that authenticate the submission. None of them touches a database connection — they cross the signing service binding and are sealed there. The result is a DRAFT that signs nothing; whatever certificate is signing today keeps signing until an activation probe is accepted by SUNAT.
## POST /console/v1/companies/{companyId}/certificates
```json
{
"tags": [
"Companies"
],
"operationId": "enrolConsoleCertificate",
"summary": "Enrol a new certificate version",
"description": "ADMIN and above, which is the floor ADR 0016 point 7 sets for companies and the one 000060 already applies to minting a credential that carries certificates:manage. Takes the four secrets SUNAT needs: the PKCS#12, its password, and the SOL user and password that authenticate the submission. None of them touches a database connection — they cross the signing service binding and are sealed there. The result is a DRAFT that signs nothing; whatever certificate is signing today keeps signing until an activation probe is accepted by SUNAT.",
"parameters": [
{
"name": "companyId",
"in": "path",
"required": true,
"schema": {
"type": "string",
"format": "uuid"
}
}
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleEnrolCertificateRequest"
}
}
}
},
"responses": {
"201": {
"description": "A DRAFT version was sealed and recorded",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleCertificateEnrolled"
}
}
}
},
"400": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"401": {
"description": "No usable session cookie, or MFA_REQUIRED on the login route. A machine credential in an Authorization header reaches this too: console routes never read that header.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"403": {
"description": "CSRF_ORIGIN_REJECTED. Every unsafe method requires an Origin header naming a configured console origin; a deployment with none configured refuses them all.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"404": {
"description": "The resource does not exist, or is not visible to this session. The two are deliberately indistinguishable: a company in another organization answers exactly what an invented uuid answers.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"405": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"413": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"415": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"422": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"500": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"503": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
}
}
}
```
Any `$ref` above resolves against `/openapi/console-v1.json`, the document this page was generated from.
# How the activation probe went (/docs/console/companies/getConsoleCertificateActivation)
## GET /console/v1/companies/{companyId}/certificates/{configurationId}/activation
```json
{
"tags": [
"Companies"
],
"operationId": "getConsoleCertificateActivation",
"summary": "How the activation probe went",
"parameters": [
{
"name": "companyId",
"in": "path",
"required": true,
"schema": {
"type": "string",
"format": "uuid"
}
},
{
"name": "configurationId",
"in": "path",
"required": true,
"schema": {
"type": "string",
"format": "uuid"
}
}
],
"responses": {
"200": {
"description": "The attempt, and SUNAT's verdict if it has one yet",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleCertificateActivation"
}
}
}
},
"400": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"401": {
"description": "No usable session cookie, or MFA_REQUIRED on the login route. A machine credential in an Authorization header reaches this too: console routes never read that header.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"403": {
"description": "CSRF_ORIGIN_REJECTED. Every unsafe method requires an Origin header naming a configured console origin; a deployment with none configured refuses them all.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"404": {
"description": "The resource does not exist, or is not visible to this session. The two are deliberately indistinguishable: a company in another organization answers exactly what an invented uuid answers.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"405": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"413": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"415": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"422": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"500": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"503": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
}
}
}
```
Any `$ref` above resolves against `/openapi/console-v1.json`, the document this page was generated from.
# Ask SUNAT to prove this version (/docs/console/companies/activateConsoleCertificate)
Answers 202 and nothing is switched yet. A DRAFT reaches ACTIVE only after this platform has signed a factura on the reserved series F000 with that certificate and SUNAT beta has accepted it (000052) — proof of control checked by the party that can check it. PRODUCTION is refused, because a production certificate cannot be proved against beta and issuing a real comprobante to prove it is not the platform's call. This route takes no Idempotency-Key: the key is derived from the version, so a double-click replays instead of probing twice.
## POST /console/v1/companies/{companyId}/certificates/{configurationId}/activation
```json
{
"tags": [
"Companies"
],
"operationId": "activateConsoleCertificate",
"summary": "Ask SUNAT to prove this version",
"description": "Answers 202 and nothing is switched yet. A DRAFT reaches ACTIVE only after this platform has signed a factura on the reserved series F000 with that certificate and SUNAT beta has accepted it (000052) — proof of control checked by the party that can check it. PRODUCTION is refused, because a production certificate cannot be proved against beta and issuing a real comprobante to prove it is not the platform's call. This route takes no Idempotency-Key: the key is derived from the version, so a double-click replays instead of probing twice.",
"parameters": [
{
"name": "companyId",
"in": "path",
"required": true,
"schema": {
"type": "string",
"format": "uuid"
}
},
{
"name": "configurationId",
"in": "path",
"required": true,
"schema": {
"type": "string",
"format": "uuid"
}
}
],
"responses": {
"202": {
"description": "A probe was started; the version stays DRAFT until it is accepted",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleCertificateActivationStarted"
}
}
}
},
"400": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"401": {
"description": "No usable session cookie, or MFA_REQUIRED on the login route. A machine credential in an Authorization header reaches this too: console routes never read that header.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"403": {
"description": "CSRF_ORIGIN_REJECTED. Every unsafe method requires an Origin header naming a configured console origin; a deployment with none configured refuses them all.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"404": {
"description": "The resource does not exist, or is not visible to this session. The two are deliberately indistinguishable: a company in another organization answers exactly what an invented uuid answers.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"405": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"413": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"415": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"422": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"500": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"503": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
}
}
}
```
Any `$ref` above resolves against `/openapi/console-v1.json`, the document this page was generated from.
# Who is in this organization (/docs/console/organizations/listConsoleMembers)
## GET /console/v1/organizations/{organizationId}/members
```json
{
"tags": [
"Organizations"
],
"operationId": "listConsoleMembers",
"summary": "Who is in this organization",
"parameters": [
{
"name": "organizationId",
"in": "path",
"required": true,
"schema": {
"type": "string",
"format": "uuid"
}
}
],
"responses": {
"200": {
"description": "Members, highest role first",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleMemberList"
}
}
}
},
"400": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"401": {
"description": "No usable session cookie, or MFA_REQUIRED on the login route. A machine credential in an Authorization header reaches this too: console routes never read that header.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"403": {
"description": "CSRF_ORIGIN_REJECTED. Every unsafe method requires an Origin header naming a configured console origin; a deployment with none configured refuses them all.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"404": {
"description": "The resource does not exist, or is not visible to this session. The two are deliberately indistinguishable: a company in another organization answers exactly what an invented uuid answers.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"405": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"413": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"415": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"422": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"500": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"503": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
}
}
}
```
Any `$ref` above resolves against `/openapi/console-v1.json`, the document this page was generated from.
# Change a member's role (/docs/console/organizations/setConsoleMemberRole)
OWNER only, and the last OWNER can be neither demoted nor removed. The affected member's sessions are revoked -- theirs alone, since a role change concerns one person and logging the whole team out would turn administration into an outage.
## PATCH /console/v1/organizations/{organizationId}/members/{userId}
```json
{
"tags": [
"Organizations"
],
"operationId": "setConsoleMemberRole",
"summary": "Change a member's role",
"description": "OWNER only, and the last OWNER can be neither demoted nor removed. The affected member's sessions are revoked -- theirs alone, since a role change concerns one person and logging the whole team out would turn administration into an outage.",
"parameters": [
{
"name": "organizationId",
"in": "path",
"required": true,
"schema": {
"type": "string",
"format": "uuid"
}
},
{
"name": "userId",
"in": "path",
"required": true,
"schema": {
"type": "string",
"format": "uuid"
}
}
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleMemberRoleRequest"
}
}
}
},
"responses": {
"200": {
"description": "The new role",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleMemberRoleResponse"
}
}
}
},
"400": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"401": {
"description": "No usable session cookie, or MFA_REQUIRED on the login route. A machine credential in an Authorization header reaches this too: console routes never read that header.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"403": {
"description": "CSRF_ORIGIN_REJECTED. Every unsafe method requires an Origin header naming a configured console origin; a deployment with none configured refuses them all.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"404": {
"description": "The resource does not exist, or is not visible to this session. The two are deliberately indistinguishable: a company in another organization answers exactly what an invented uuid answers.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"405": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"413": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"415": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"422": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"500": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"503": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
}
}
}
```
Any `$ref` above resolves against `/openapi/console-v1.json`, the document this page was generated from.
# Remove a member (/docs/console/organizations/removeConsoleMember)
## DELETE /console/v1/organizations/{organizationId}/members/{userId}
```json
{
"tags": [
"Organizations"
],
"operationId": "removeConsoleMember",
"summary": "Remove a member",
"parameters": [
{
"name": "organizationId",
"in": "path",
"required": true,
"schema": {
"type": "string",
"format": "uuid"
}
},
{
"name": "userId",
"in": "path",
"required": true,
"schema": {
"type": "string",
"format": "uuid"
}
}
],
"responses": {
"200": {
"description": "Removed; memberRole is null",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleMemberRoleResponse"
}
}
}
},
"400": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"401": {
"description": "No usable session cookie, or MFA_REQUIRED on the login route. A machine credential in an Authorization header reaches this too: console routes never read that header.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"403": {
"description": "CSRF_ORIGIN_REJECTED. Every unsafe method requires an Origin header naming a configured console origin; a deployment with none configured refuses them all.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"404": {
"description": "The resource does not exist, or is not visible to this session. The two are deliberately indistinguishable: a company in another organization answers exactly what an invented uuid answers.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"405": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"413": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"415": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"422": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"500": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"503": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
}
}
}
```
Any `$ref` above resolves against `/openapi/console-v1.json`, the document this page was generated from.
# Register a company under this organization (/docs/console/organizations/createConsoleCompany)
ADMIN and above, which is ADR 0016 point 7's assignment of companies rather than this route's opinion. Nothing here proves the caller controls the RUC and nothing needs to: the claim confers nothing until a certificate this platform signed with has been accepted by SUNAT, which is proof of control checked by the party that can perform it. What the alta does guarantee is attribution -- it writes an ops.audit_events row naming the person, in the same transaction as the company -- so a squatted RUC is an operator action with evidence rather than an argument between two customers. Every refusal about WHO the caller is answers 409 MEMBERSHIP_CHANGE_REFUSED indistinguishably; only a RUC already registered answers something specific, and it says nothing about whose it is.
## POST /console/v1/organizations/{organizationId}/companies
```json
{
"tags": [
"Organizations"
],
"operationId": "createConsoleCompany",
"summary": "Register a company under this organization",
"description": "ADMIN and above, which is ADR 0016 point 7's assignment of companies rather than this route's opinion. Nothing here proves the caller controls the RUC and nothing needs to: the claim confers nothing until a certificate this platform signed with has been accepted by SUNAT, which is proof of control checked by the party that can perform it. What the alta does guarantee is attribution -- it writes an ops.audit_events row naming the person, in the same transaction as the company -- so a squatted RUC is an operator action with evidence rather than an argument between two customers. Every refusal about WHO the caller is answers 409 MEMBERSHIP_CHANGE_REFUSED indistinguishably; only a RUC already registered answers something specific, and it says nothing about whose it is.",
"parameters": [
{
"name": "organizationId",
"in": "path",
"required": true,
"schema": {
"type": "string",
"format": "uuid"
}
}
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleCreateCompanyRequest"
}
}
}
},
"responses": {
"201": {
"description": "Company registered and attributed",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleCompanyCreated"
}
}
}
},
"400": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"401": {
"description": "No usable session cookie, or MFA_REQUIRED on the login route. A machine credential in an Authorization header reaches this too: console routes never read that header.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"403": {
"description": "CSRF_ORIGIN_REJECTED. Every unsafe method requires an Origin header naming a configured console origin; a deployment with none configured refuses them all.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"404": {
"description": "The resource does not exist, or is not visible to this session. The two are deliberately indistinguishable: a company in another organization answers exactly what an invented uuid answers.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"405": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"413": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"415": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"422": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"500": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"503": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
}
}
}
```
Any `$ref` above resolves against `/openapi/console-v1.json`, the document this page was generated from.
# The provisioning credentials of this organization (/docs/console/organizations/listConsoleOrganizationCredentials)
OWNER, and NOT VIEWER as the company credential listing is. That one is VIEWER because GET /console/v1/companies/{companyId}/activity already publishes every company credential's public prefix to every VIEWER, so hiding the list would be a rule with no content; nothing publishes an organization credential's prefix, because it issues nothing and so reaches no activity row. What is left is that this listing is the input to the revocation decision -- you revoke the row you are looking at -- and a list readable by somebody who cannot act on it splits an emergency across two people. It carries no secret: only a SHA-256 is stored. Revoked and expired credentials stay in it.
## GET /console/v1/organizations/{organizationId}/credentials
```json
{
"tags": [
"Organizations"
],
"operationId": "listConsoleOrganizationCredentials",
"summary": "The provisioning credentials of this organization",
"description": "OWNER, and NOT VIEWER as the company credential listing is. That one is VIEWER because GET /console/v1/companies/{companyId}/activity already publishes every company credential's public prefix to every VIEWER, so hiding the list would be a rule with no content; nothing publishes an organization credential's prefix, because it issues nothing and so reaches no activity row. What is left is that this listing is the input to the revocation decision -- you revoke the row you are looking at -- and a list readable by somebody who cannot act on it splits an emergency across two people. It carries no secret: only a SHA-256 is stored. Revoked and expired credentials stay in it.",
"parameters": [
{
"name": "organizationId",
"in": "path",
"required": true,
"schema": {
"type": "string",
"format": "uuid"
}
}
],
"responses": {
"200": {
"description": "Every organization credential, newest first",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleOrganizationCredentialList"
}
}
}
},
"400": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"401": {
"description": "No usable session cookie, or MFA_REQUIRED on the login route. A machine credential in an Authorization header reaches this too: console routes never read that header.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"403": {
"description": "CSRF_ORIGIN_REJECTED. Every unsafe method requires an Origin header naming a configured console origin; a deployment with none configured refuses them all.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"404": {
"description": "The resource does not exist, or is not visible to this session. The two are deliberately indistinguishable: a company in another organization answers exactly what an invented uuid answers.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"405": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"413": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"415": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"422": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"500": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"503": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
}
}
}
```
Any `$ref` above resolves against `/openapi/console-v1.json`, the document this page was generated from.
# Mint an organization credential (/docs/console/organizations/mintConsoleOrganizationCredential)
OWNER, and an enrolled second factor. This is the credential a SaaS puts in a server to register its own client companies by API. THE FLOOR IS NOT ADMIN, and not by symmetry with anything: every other act an ADMIN performs lands inside their own organization, whereas companies:manage can register a company under ANY RUC in Peru, and core.companies.ruc is unique platform-wide, so claiming one DENIES it to whoever controls it -- a third party with no account here. SUNAT publishes no ownership oracle, so migration 000053's answer is attribution rather than prevention: every act writes an ops.audit_events row in the same transaction. A control whose whole mechanism is 'we can say who did it' belongs to the rank that can be held answerable. THE SECOND FACTOR IS CHECKED IN SQL, on auth.users.mfa_enrolled_at, because the failure this act invites is a stolen session -- which is by construction a caller of the correct rank, and would make the audit row name somebody who did nothing. Enrolment is self-service, so this strands nobody. THE TOKEN IS IN THE 201 AND NOWHERE ELSE.
## POST /console/v1/organizations/{organizationId}/credentials
```json
{
"tags": [
"Organizations"
],
"operationId": "mintConsoleOrganizationCredential",
"summary": "Mint an organization credential",
"description": "OWNER, and an enrolled second factor. This is the credential a SaaS puts in a server to register its own client companies by API. THE FLOOR IS NOT ADMIN, and not by symmetry with anything: every other act an ADMIN performs lands inside their own organization, whereas companies:manage can register a company under ANY RUC in Peru, and core.companies.ruc is unique platform-wide, so claiming one DENIES it to whoever controls it -- a third party with no account here. SUNAT publishes no ownership oracle, so migration 000053's answer is attribution rather than prevention: every act writes an ops.audit_events row in the same transaction. A control whose whole mechanism is 'we can say who did it' belongs to the rank that can be held answerable. THE SECOND FACTOR IS CHECKED IN SQL, on auth.users.mfa_enrolled_at, because the failure this act invites is a stolen session -- which is by construction a caller of the correct rank, and would make the audit row name somebody who did nothing. Enrolment is self-service, so this strands nobody. THE TOKEN IS IN THE 201 AND NOWHERE ELSE.",
"parameters": [
{
"name": "organizationId",
"in": "path",
"required": true,
"schema": {
"type": "string",
"format": "uuid"
}
}
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleMintOrganizationCredentialRequest"
}
}
}
},
"responses": {
"201": {
"description": "Minted; the token is in this response and is not recoverable after it",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleMintedOrganizationCredential"
}
}
}
},
"400": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"401": {
"description": "No usable session cookie, or MFA_REQUIRED on the login route. A machine credential in an Authorization header reaches this too: console routes never read that header.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"403": {
"description": "CSRF_ORIGIN_REJECTED, or SECOND_FACTOR_REQUIRED when the acting OWNER has no TOTP enrolled. The second is reachable only by somebody already confirmed to be an OWNER of this organization, so it discloses nothing they did not know about their own account -- and it is the only thing they can act on, which is why it is not fused into the silent 409 below.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"404": {
"description": "The resource does not exist, or is not visible to this session. The two are deliberately indistinguishable: a company in another organization answers exactly what an invented uuid answers.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"405": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"409": {
"description": "MEMBERSHIP_CHANGE_REFUSED -- the organization may not exist, may not be yours, may be suspended, or your role may be below OWNER. The four are deliberately indistinguishable: the SQL returns zero rows for all of them, and separating them here would hand out an oracle for which organization ids are real.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"413": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"415": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"422": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"500": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"503": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
}
}
}
```
Any `$ref` above resolves against `/openapi/console-v1.json`, the document this page was generated from.
# Revoke an organization credential (/docs/console/organizations/revokeConsoleOrganizationCredential)
Immediate, not at expiry: auth.assert_organization_credential re-reads revoked_at on every provisioning statement. Same OWNER floor as minting, never higher, AND DELIBERATELY WITHOUT THE SECOND FACTOR the mint requires. Revocation is the emergency action -- it is what somebody does at 2am with a secret in a public repository -- and the person reaching for the kill switch is the person whose phone may be the thing that was lost. Minting creates risk and carries the friction; revoking removes risk and carries none. Unknown, already revoked, and belonging to another organization are one 404.
## DELETE /console/v1/organizations/{organizationId}/credentials/{credentialId}
```json
{
"tags": [
"Organizations"
],
"operationId": "revokeConsoleOrganizationCredential",
"summary": "Revoke an organization credential",
"description": "Immediate, not at expiry: auth.assert_organization_credential re-reads revoked_at on every provisioning statement. Same OWNER floor as minting, never higher, AND DELIBERATELY WITHOUT THE SECOND FACTOR the mint requires. Revocation is the emergency action -- it is what somebody does at 2am with a secret in a public repository -- and the person reaching for the kill switch is the person whose phone may be the thing that was lost. Minting creates risk and carries the friction; revoking removes risk and carries none. Unknown, already revoked, and belonging to another organization are one 404.",
"parameters": [
{
"name": "organizationId",
"in": "path",
"required": true,
"schema": {
"type": "string",
"format": "uuid"
}
},
{
"name": "credentialId",
"in": "path",
"required": true,
"schema": {
"type": "string",
"format": "uuid"
}
}
],
"responses": {
"200": {
"description": "Revoked, as of the moment in the body",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleOrganizationCredentialRevoked"
}
}
}
},
"400": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"401": {
"description": "No usable session cookie, or MFA_REQUIRED on the login route. A machine credential in an Authorization header reaches this too: console routes never read that header.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"403": {
"description": "CSRF_ORIGIN_REJECTED. Every unsafe method requires an Origin header naming a configured console origin; a deployment with none configured refuses them all.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"404": {
"description": "The resource does not exist, or is not visible to this session. The two are deliberately indistinguishable: a company in another organization answers exactly what an invented uuid answers.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"405": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"413": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"415": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"422": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"500": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"503": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
}
}
}
```
Any `$ref` above resolves against `/openapi/console-v1.json`, the document this page was generated from.
# Invitations sent and not yet spent (/docs/console/organizations/listConsoleInvitations)
ADMIN and above, unlike the member list: a pending invitation names an address that belongs to nobody here yet.
## GET /console/v1/organizations/{organizationId}/invitations
```json
{
"tags": [
"Organizations"
],
"operationId": "listConsoleInvitations",
"summary": "Invitations sent and not yet spent",
"description": "ADMIN and above, unlike the member list: a pending invitation names an address that belongs to nobody here yet.",
"parameters": [
{
"name": "organizationId",
"in": "path",
"required": true,
"schema": {
"type": "string",
"format": "uuid"
}
}
],
"responses": {
"200": {
"description": "Live invitations",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleInvitationList"
}
}
}
},
"400": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"401": {
"description": "No usable session cookie, or MFA_REQUIRED on the login route. A machine credential in an Authorization header reaches this too: console routes never read that header.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"403": {
"description": "CSRF_ORIGIN_REJECTED. Every unsafe method requires an Origin header naming a configured console origin; a deployment with none configured refuses them all.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"404": {
"description": "The resource does not exist, or is not visible to this session. The two are deliberately indistinguishable: a company in another organization answers exactly what an invented uuid answers.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"405": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"413": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"415": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"422": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"500": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"503": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
}
}
}
```
Any `$ref` above resolves against `/openapi/console-v1.json`, the document this page was generated from.
# Invite somebody (/docs/console/organizations/inviteConsoleMember)
ADMIN and above, and nobody may invite above their own rank: an ADMIN who could mint an OWNER is an ADMIN who can promote themselves through a second account. The invitation is bound to the address it names, so a forwarded link is worth nothing to anybody else.
## POST /console/v1/organizations/{organizationId}/invitations
```json
{
"tags": [
"Organizations"
],
"operationId": "inviteConsoleMember",
"summary": "Invite somebody",
"description": "ADMIN and above, and nobody may invite above their own rank: an ADMIN who could mint an OWNER is an ADMIN who can promote themselves through a second account. The invitation is bound to the address it names, so a forwarded link is worth nothing to anybody else.",
"parameters": [
{
"name": "organizationId",
"in": "path",
"required": true,
"schema": {
"type": "string",
"format": "uuid"
}
}
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleInvitationRequest"
}
}
}
},
"responses": {
"201": {
"description": "Invitation issued; the token is in this response and nowhere else",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleInvitationCreated"
}
}
}
},
"400": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"401": {
"description": "No usable session cookie, or MFA_REQUIRED on the login route. A machine credential in an Authorization header reaches this too: console routes never read that header.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"403": {
"description": "CSRF_ORIGIN_REJECTED. Every unsafe method requires an Origin header naming a configured console origin; a deployment with none configured refuses them all.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"404": {
"description": "The resource does not exist, or is not visible to this session. The two are deliberately indistinguishable: a company in another organization answers exactly what an invented uuid answers.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"405": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"413": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"415": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"422": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"500": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"503": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
}
}
}
```
Any `$ref` above resolves against `/openapi/console-v1.json`, the document this page was generated from.
# Withdraw an invitation (/docs/console/organizations/revokeConsoleInvitation)
An invitation lives a week. Without this, an invitation sent to a mistyped address is live for all of it.
## DELETE /console/v1/organizations/{organizationId}/invitations/{invitationId}
```json
{
"tags": [
"Organizations"
],
"operationId": "revokeConsoleInvitation",
"summary": "Withdraw an invitation",
"description": "An invitation lives a week. Without this, an invitation sent to a mistyped address is live for all of it.",
"parameters": [
{
"name": "organizationId",
"in": "path",
"required": true,
"schema": {
"type": "string",
"format": "uuid"
}
},
{
"name": "invitationId",
"in": "path",
"required": true,
"schema": {
"type": "string",
"format": "uuid"
}
}
],
"responses": {
"204": {
"description": "Revoked"
},
"400": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"401": {
"description": "No usable session cookie, or MFA_REQUIRED on the login route. A machine credential in an Authorization header reaches this too: console routes never read that header.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"403": {
"description": "CSRF_ORIGIN_REJECTED. Every unsafe method requires an Origin header naming a configured console origin; a deployment with none configured refuses them all.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"404": {
"description": "The resource does not exist, or is not visible to this session. The two are deliberately indistinguishable: a company in another organization answers exactly what an invented uuid answers.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"405": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"413": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"415": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"422": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"500": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"503": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
}
}
}
```
Any `$ref` above resolves against `/openapi/console-v1.json`, the document this page was generated from.
# Accept an invitation (/docs/console/organizations/acceptConsoleInvitation)
Requires a live session whose verified address is the one the invitation was sent to. Accepting with a different account would turn a leaked link into a way into someone else's organization. An invitation for somebody who is already a member is spent without changing the role they already have.
## POST /console/v1/invitations/acceptance
```json
{
"tags": [
"Organizations"
],
"operationId": "acceptConsoleInvitation",
"summary": "Accept an invitation",
"description": "Requires a live session whose verified address is the one the invitation was sent to. Accepting with a different account would turn a leaked link into a way into someone else's organization. An invitation for somebody who is already a member is spent without changing the role they already have.",
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleTokenRequest"
}
}
}
},
"responses": {
"200": {
"description": "Joined",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleInvitationAccepted"
}
}
}
},
"400": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"401": {
"description": "No usable session cookie, or MFA_REQUIRED on the login route. A machine credential in an Authorization header reaches this too: console routes never read that header.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"403": {
"description": "CSRF_ORIGIN_REJECTED. Every unsafe method requires an Origin header naming a configured console origin; a deployment with none configured refuses them all.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"404": {
"description": "The resource does not exist, or is not visible to this session. The two are deliberately indistinguishable: a company in another organization answers exactly what an invented uuid answers.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"405": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"413": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"415": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"422": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"500": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
},
"503": {
"description": "Stable error envelope",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConsoleError"
}
}
}
}
}
}
```
Any `$ref` above resolves against `/openapi/console-v1.json`, the document this page was generated from.