Fac-360
Documents

Validate and durably create a canonical invoice, receipt or fiscal note

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

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.

Authorization

bearerAuth
AuthorizationBearer <token>

Tenant-bound, scoped and expiring Apifact credential. Migrated legacy credentials are accepted only on deprecated v1 writes and tenant-scoped v2 read, poll and download routes. Each operation names the single scope it requires in x-required-scope; the scope array of the security requirement itself is empty because OpenAPI 3.0 requires it to be for a non-oauth2 scheme.

In: header

Header Parameters

Idempotency-Key*string
Length1 <= length <= 200

Request Body

application/json

TypeScript Definitions

Use the request body type in TypeScript.

Response Body

application/json

application/json

application/json

application/json

application/json

application/json

application/json

application/json

application/json

application/json

application/json

curl -X POST "https://example.com/api/v2/documents" \  -H "Idempotency-Key: b6a2f0e4-1c3d-4a5b-8e7f-9d0c1b2a3e4f" \  -H "Authorization: Bearer apf_v2_tu_credencial" \  -H "Content-Type: application/json" \  -d '{    "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"      }    ]  }'
{  "schemaVersion": "2.0",  "requestId": "d385ab22-0f51-4b97-9ecd-b8ff3fd4fcb6",  "operationId": "3051932a-fdd2-48fa-b330-7e7d41535969",  "resourceId": "026d60bb-63a8-407e-bf67-01dcfc6022e6",  "state": "RECEIVED",  "replay": true}

Durably request an immutable ticket representation POST POST

Safe asynchronous compatibility route. Requires documents:pdf; unsupported legacy fields are rejected.

Ask what POST /api/v2/documents would answer, without creating anything POST POST

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.