Documentación
Guía para agentes IA
(In English — this page is written for the agents themselves.)
This is the integration recipe for AI agents and coding assistants adding Peruvian electronic invoicing (SUNAT) to any system — an online store, a POS, an ERP, a chatbot. Bloques exposes a REST API and an MCP server; both use the same tokens and the same rules. The complete machine-readable reference lives at /llms-full.txt.
Safety rules (read first)
- Prices are FINAL, IGV (18%) included.
unit_priceis what the customer actually pays. Bloques back-calculates the tax base (base = total / 1.18). Never send pre-tax prices — that would overcharge by 18%. - factura ↔ RUC. A
facturarequires a customer with RUC (doc_type: "6", 11 digits). Aboletamust NOT have a RUC customer; use DNI ("1") or omitcustomerentirely for walk-in sales. - This is production. Every accepted document is fiscally bindingand reported to the tax authority. There is no sandbox. Confirm with the human before each real emission, and never emit “test” documents.
- v1 cannot void or credit-note documents. Mistakes on accepted documents must be fixed with a credit note outside Bloques. Prevention (confirmation + idempotency) is your only tool.
- Always send an idempotency key derived from your business event (e.g. order ID), so retries can never double-invoice.
The 7-step recipe
- 1
Get a token
Ask the human to create an API token in Configuración → API (token management is session-only by design — you cannot create tokens via the API). Request minimal scopes: an invoicing agent needs
documents:write+documents:read; addproducts:read/products:writeif you manage the catalog. Send it on every request asAuthorization: Bearer sk_live_…. Rate limit: 120 req/min. - 2
Read /llms-full.txt
Fetch
https://<host>/llms-full.txtonce. It is the complete, self-contained API reference (every endpoint, field, error code, and worked examples). The OpenAPI spec is at/openapi.json. Then verify connectivity withGET /api/v1/companyandGET /api/v1/usage. - 3
Create products — or use free-form items
Two ways to describe what you sell. If the system has a stable catalog, create products once and emit by
code:POST /api/v1/products { "code": "PLAN-PRO", "name": "Suscripción Pro mensual", "unit_price": 40.00, "unit_code": "ZZ" }Then items are just
{ "code": "PLAN-PRO", "quantity": 1 }. For ad-hoc lines, skip the catalog and senddescription+unit_pricedirectly. Both can be mixed in one document. - 4
Emit with an idempotency key
POST /api/v1/documentswith headerIdempotency-Key: <your-order-id>. The call is synchronous (typically 2–8 s) and returns the SUNAT verdict. If you repeat the key, you get HTTP 200, headerIdempotent-Replay: true, and the original document — no duplicate emission, ever.Retry policy: network error / timeout → retry with the same key. Response with
status: "error"(HTTP 502) → retry with a new key (the same key would just replay the failed document). - 5
Handle accepted / rejected / error
switch (doc.status) { case "accepted": // Done. Legally valid. Store doc.id + full_number, deliver files. case "rejected": // SUNAT said no. Read sunat.cdr_description + sunat.observations, // fix the data (often the customer RUC), emit a NEW document. // The number is consumed; the rejected doc cannot be edited. case "error": // PSE/transport failure before a SUNAT verdict. Not counted // against the plan. Re-emit with a NEW idempotency key. case "processing": // Rare (sync API). Poll GET /api/v1/documents/{id}. } - 6
Download files
Same Bearer token:
GET /api/v1/documents/{id}/pdf(optionally?template=clasica|moderna|minimal|ticket80),/xml(signed XML;?unsigned=truefor the raw UBL),/cdr(SUNAT receipt XML),/zip(both XMLs in one archive). The document JSON already ships these paths underfiles.*. Bulk reporting:GET /api/v1/documents/export(CSV, same filters as the list, max 5000 rows). - 7
Monitor usage
GET /api/v1/usagereturns{ used, limit, remaining }for the current America/Lima month (free: 10 docs, pro: 3000). Whenremainingis low, warn the human; at 0, emission fails with402 plan_limit. Listing this month:GET /api/v1/documents?from=2026-06-01&status=accepted.
Worked example: store checkout emits a boleta
Scenario: an online store charges a consumer S/ 147.80 (two products, final prices) and must emit a boleta on payment confirmation. The buyer gave DNI and email; the order ID is ORD-2026-5512.
POST https://bloques.example.com/api/v1/documents
Authorization: Bearer sk_live_...
Content-Type: application/json
Idempotency-Key: ORD-2026-5512
{
"type": "boleta",
"customer": {
"doc_type": "1",
"doc_number": "44556677",
"name": "MARIA FERNANDA QUISPE ROJAS",
"email": "maria@example.com"
},
"items": [
{ "code": "CAFE-250", "quantity": 3 },
{ "description": "Taza térmica Bloques", "quantity": 1, "unit_price": 42.80 }
],
"payment": { "type": "contado" },
"notes": "Pedido ORD-2026-5512",
"send_email": true
}{
"id": "5e6f7a8b-9c0d-4e1f-8a2b-3c4d5e6f7a8b",
"type": "boleta",
"doc_type": "03",
"series": "B001",
"number": 118,
"full_number": "B001-118",
"file_name": "20123456789-03-B001-118",
"status": "accepted",
"issue_date": "2026-06-09",
"issue_time": "16:45:12",
"due_date": null,
"currency": "PEN",
"customer": {
"docType": "1",
"docNumber": "44556677",
"name": "MARIA FERNANDA QUISPE ROJAS",
"email": "maria@example.com"
},
"totals": {
"gravado": "125.25",
"exonerado": "0.00",
"inafecto": "0.00",
"igv": "22.55",
"total_value": "125.25",
"total": "147.80"
},
"amount_in_words": "CIENTO CUARENTA Y SIETE CON 80/100 SOLES",
"payment": { "type": "Contado" },
"notes": "Pedido ORD-2026-5512",
"sunat": {
"hash": "tU8vW9xY0zA1bC2dE3fG4hI5jK6=",
"cdr_description": "La Boleta numero B001-118, ha sido aceptada",
"observations": null,
"error_message": null
},
"files": {
"pdf": "/api/v1/documents/5e6f7a8b-9c0d-4e1f-8a2b-3c4d5e6f7a8b/pdf",
"xml": "/api/v1/documents/5e6f7a8b-9c0d-4e1f-8a2b-3c4d5e6f7a8b/xml",
"cdr": "/api/v1/documents/5e6f7a8b-9c0d-4e1f-8a2b-3c4d5e6f7a8b/cdr",
"zip": "/api/v1/documents/5e6f7a8b-9c0d-4e1f-8a2b-3c4d5e6f7a8b/zip"
},
"items": [
{
"position": 1,
"product_id": "1a2b3c4d-0000-4000-8000-000000000001",
"code": "CAFE-250",
"description": "Café molido 250g",
"unit_code": "NIU",
"quantity": "3",
"unit_price": "35.00",
"unit_value": "29.6610169492",
"line_base": "88.98",
"line_igv": "16.02",
"line_total": "105.00",
"affectation": "10"
},
{
"position": 2,
"product_id": null,
"code": null,
"description": "Taza térmica Bloques",
"unit_code": "NIU",
"quantity": "1",
"unit_price": "42.80",
"unit_value": "36.2711864407",
"line_base": "36.27",
"line_igv": "6.53",
"line_total": "42.80",
"affectation": "10"
}
],
"source": "api",
"emailed_to": "maria@example.com",
"emailed_at": "2026-06-09T21:45:15.882Z",
"created_at": "2026-06-09T21:45:13.001Z"
}Note how total (147.80) equals exactly what was charged: 3 × 35.00 + 42.80. The base and IGV were derived per line, never altering the amount the customer paid. The checkout flow then stores id and full_number with the order; the customer already got the PDF + XML by email (emailed_to is set).
Pseudo-code for the integration
async function onPaymentConfirmed(order) {
const res = await fetch("https://bloques.example.com/api/v1/documents", {
method: "POST",
headers: {
"Authorization": "Bearer " + BLOQUES_TOKEN,
"Content-Type": "application/json",
"Idempotency-Key": order.id, // safe retries
},
body: JSON.stringify({
type: order.customerRuc ? "factura" : "boleta",
customer: order.customerRuc
? { doc_type: "6", doc_number: order.customerRuc, name: order.businessName, email: order.email }
: order.dni
? { doc_type: "1", doc_number: order.dni, name: order.fullName, email: order.email }
: undefined, // boleta to CLIENTES VARIOS
items: order.lines.map(l => ({
description: l.title,
quantity: l.qty,
unit_price: l.finalUnitPrice, // IGV INCLUDED — what was charged
})),
notes: "Pedido " + order.id,
}),
});
const doc = await res.json();
if (res.status === 502 || doc.status === "error") {
return scheduleRetryWithNewKey(order); // PSE down — new idempotency key
}
if (doc.status === "rejected") {
return alertHuman(order, doc.sunat.cdr_description); // fix data, emit new doc
}
await saveInvoice(order.id, doc.id, doc.full_number); // accepted
}Prefer MCP?
If you operate inside an MCP-capable client, skip the HTTP plumbing: connect to POST https://<host>/api/mcp with the same Bearer header and use the tools create_document, get_document, list_documents, get_document_files, list_products, create_product, search_customers and get_usage — same semantics, same idempotency (idempotency_key argument). Details on the MCP page.
Pre-flight checklist
- Token has the minimal scopes needed, nothing more.
- Every
unit_priceis the final, IGV-inclusive amount actually charged. - Document type matches the customer: RUC →
factura; DNI/none →boleta. Idempotency-Keyset from the business event ID on every emission.- The human confirmed that real fiscal documents should be emitted.
- Handlers exist for
rejected,error,402 plan_limitand429 rate_limited. usage.remainingis monitored before high-volume runs.