Documentación
API REST
Referencia completa de la API de Bloques. Base URL: https://<tu-host>/api/v1. Todas las peticiones y respuestas son JSON UTF-8 (salvo las descargas de archivos y el CSV). También disponible como OpenAPI 3.1 y como referencia para LLMs.
Autenticación
Cada petición lleva un token Bearer creado en Configuración → API:
Authorization: Bearer sk_live_...- Los tokens tienen la forma
sk_live_…y se muestran una sola vez al crearlos; Bloques guarda solo su hash. - La gestión de tokens (crear, listar, revocar) es solo por sesión web, por diseño: no existe endpoint de API para crear tokens, de modo que un token filtrado nunca pueda fabricar más tokens ni escalar sus permisos.
- Cada token pertenece a una empresa y solo ve los datos de esa empresa.
- Límite de tasa: 120 solicitudes por minuto por token. Al excederlo recibes
429 rate_limited. - CORS habilitado (
Access-Control-Allow-Origin: *): puedes llamar a la API desde un navegador. Cabeceras permitidas:Authorization,Content-Type,Idempotency-Key. Aun así, nunca incrustes un tokensk_live_…en código que llegue al navegador de terceros.
Alcances (scopes)
Al crear un token eliges sus alcances. Un token solo puede usar los endpoints de sus alcances:
| Scope | Permite |
|---|---|
* | Todos los alcances (acceso total de API). |
documents:read | Listar y leer documentos, descargar PDF/XML/CDR, exportar CSV. |
documents:write | Emitir documentos. |
quotes:read | Listar y leer cotizaciones, descargar su PDF. |
quotes:write | Crear, actualizar y eliminar cotizaciones. |
products:read | Listar y leer productos. |
products:write | Crear, actualizar y desactivar productos. |
customers:read | Listar y buscar clientes. |
customers:write | Crear/actualizar clientes. |
expenses:write | Registrar gastos. Solo por MCP (herramienta create_expense): la API REST no expone gastos. |
GET /series, GET /usage y GET /company aceptan cualquier token válido (no exigen scope).
expenses:write no habilita ningún endpoint REST: los gastos se registran desde la app o con la herramienta create_expense del servidor MCP. No existe lectura de gastos ni de dinero por token — es una decisión de producto, no una omisión.
Formato de error
Todos los errores comparten el mismo sobre:
{
"error": {
"code": "plan_limit",
"message": "Límite mensual alcanzado (10/10 documentos).",
"details": { "used": 10, "limit": 10, "plan": "free" }
}
}details es opcional. En errores validation de cuerpo JSON es una lista [{ "path": "items.0.unit_price", "message": "…" }].
Paginación
Los listados aceptan page (desde 1) y per_page (máx. 100; por defecto 25 en documentos y 50 en productos/clientes) y responden:
{ "data": [ ... ], "page": 1, "per_page": 25, "total": 137 }Dinero, fechas y zona horaria
- Los montos en las respuestas son strings con 2 decimales (
"118.00") para evitar errores de coma flotante. En las peticiones envías números (118o118.00). - Todos los precios que envías son finales, con IGV (18%) incluido. Bloques calcula la base (total ÷ 1.18) y el IGV por ti.
- Fechas
YYYY-MM-DD; timestamps ISO 8601 UTC. El calendario operativo (fecha por defecto, meses del plan) es America/Lima.
Documentos
/api/v1/documentsdocuments:writeCrea y emite un comprobante (firma + envío a SUNAT, síncrono — típicamente 2–8 s). En producción es una emisión fiscal real.
Cabecera Idempotency-Key
Envía Idempotency-Key: <clave única> (máx. 100 caracteres, p. ej. el ID de tu orden) en cada emisión. Si repites una clave ya usada por tu empresa, Bloques no emite de nuevo: responde 200 con el documento original y la cabecera Idempotent-Replay: true. Así un timeout o doble clic jamás genera dos comprobantes. Ojo: el replay devuelve el documento original aunque haya quedado en estado error — para reintentar de verdad usa una clave nueva.
Cuerpo
| Campo | Tipo | Requerido | Descripción |
|---|---|---|---|
type | string | Sí | "factura" (01, exige cliente con RUC) o "boleta" (03). |
series | string | No | Serie de 4 caracteres (F001/B001…). Prefijo F para facturas, B para boletas. Por defecto, la serie activa del tipo. |
cash_register_id | string | No | UUID de la caja desde la que emites. La serie se resuelve según el modo de series de la empresa y la caja + su sucursal quedan registradas en el documento. Si el modo exige una serie asignada a la caja/sucursal y no existe, devuelve 422 series_not_found. |
issue_date | string | No | YYYY-MM-DD. Por defecto hoy (Lima). Máximo 7 días atrás; nunca futura. |
currency | string | No | "PEN" (por defecto) o "USD". |
customer | object | Factura: sí | Ver tabla siguiente. Opcional en boletas: si se omite, se emite a CLIENTES VARIOS — salvo que el total en soles supere S/ 700, en cuyo caso SUNAT exige identificar al cliente. |
items | array | Sí | 1 a 100 ítems. Ver tabla de ítems. |
payment | object | No | Forma de pago. Por defecto contado. Ver tabla de pago. |
notes | string | No | Hasta 1000 caracteres. Texto libre impreso en el PDF (no forma parte del XML). |
send_email | boolean | No | Enviar PDF + XML al email del cliente. Por defecto, la configuración de la empresa. |
recargo_consumo | object | No | Recargo al consumo (restaurantes/bares): sin IGV, se suma al total. apply (boolean) lo habilita/deshabilita y rate (fracción 0–0.13, p. ej. 0.05 = 5%) fija la tasa. Por defecto, la configuración de la empresa. |
Objeto customer
| Campo | Tipo | Requerido | Descripción |
|---|---|---|---|
id | string (uuid) | No | Cliente existente del directorio. Excluyente con los campos en línea. |
doc_type | string | Con datos en línea | "6" RUC · "1" DNI · "4" carnet de extranjería · "7" pasaporte · "0" sin documento. |
doc_number | string | Con datos en línea | Máx. 15. RUC: 11 dígitos con dígito verificador. DNI: 8 dígitos. Para "0" debe ser "0". |
name | string | Con datos en línea | Razón social o nombre. Máx. 500. |
email | string | No | Destino del PDF + XML. |
address | string | No | Dirección impresa en el PDF. Máx. 500. |
Objeto item
Cada ítem referencia un producto del catálogo (product_id o code) o es una línea libre (description + unit_price).
| Campo | Tipo | Requerido | Descripción |
|---|---|---|---|
product_id | string (uuid) | No* | Producto del catálogo por id. |
code | string | No* | Producto del catálogo por tu código. |
description | string | No* | Descripción de línea libre (exige unit_price) o reemplazo de la descripción del producto. Máx. 500. |
quantity | number | No | > 0, hasta 3 decimales. Por defecto 1. |
unit_price | number | No* | Precio unitario FINAL, con IGV incluido. Sobrescribe el precio del producto. Obligatorio en líneas libres y cuando la moneda del producto no coincide con la del documento. |
unit_code | string | No | Catálogo 03 SUNAT: NIU unidad (por defecto), ZZ servicio, KGM kg, HUR hora, etc. |
affectation | string | No | IGV: "10" gravado (por defecto) · "20" exonerado · "30" inafecto. |
isc_rate | number | No | ISC al valor como fracción 0–1 (ej. 0.10 = 10%); solo líneas gravadas. Sobrescribe al producto. El precio sigue siendo final: la base, el ISC y el IGV se derivan. |
disc_rate | number | No | Descuento por línea (catálogo 53 código 00) como fracción 0 ≤ d < 1 (ej. 0.10 = 10%); solo líneas gravadas. Baja la base imponible y el IGV de la línea; se descuenta dentro del valor de venta (no es un descuento global). |
icbper | boolean | No | Afecto a ICBPER (bolsa plástica): suma S/ 0.50 por unidad sobre el precio. Sobrescribe al producto. |
Objeto payment
| Campo | Tipo | Requerido | Descripción |
|---|---|---|---|
type | string | No | "contado" (por defecto) o "credito". |
installments | array | Con credito | Hasta 36 cuotas { "amount": número, "due_date": "YYYY-MM-DD" }. Deben sumar exactamente el total del documento (tolerancia ±0.01). |
Ejemplo
curl -X POST https://bloques.example.com/api/v1/documents \
-H "Authorization: Bearer sk_live_..." \
-H "Content-Type: application/json" \
-H "Idempotency-Key: orden-8842" \
-d '{
"type": "factura",
"customer": {
"doc_type": "6",
"doc_number": "20512345678",
"name": "ACME PERU S.A.C.",
"email": "facturas@acme.pe"
},
"items": [
{ "code": "CONSULT-HR", "quantity": 10 },
{ "description": "Bolsa ecológica", "quantity": 2, "unit_price": 5.90, "unit_code": "NIU" }
],
"notes": "Orden de compra OC-2026-118"
}'{
"id": "9f1b2c3d-4e5f-4a7b-8c9d-0e1f2a3b4c5d",
"type": "factura",
"doc_type": "01",
"series": "F001",
"number": 42,
"full_number": "F001-42",
"file_name": "20123456789-01-F001-42",
"status": "accepted",
"issue_date": "2026-06-09",
"issue_time": "14:32:05",
"due_date": null,
"currency": "PEN",
"customer": {
"docType": "6",
"docNumber": "20512345678",
"name": "ACME PERU S.A.C.",
"email": "facturas@acme.pe"
},
"totals": {
"gravado": "1010.00",
"exonerado": "0.00",
"inafecto": "0.00",
"igv": "181.80",
"isc": "0.00",
"icbper": "0.00",
"descuento": "0.00",
"total_value": "1010.00",
"recargo_consumo": "0.00",
"recargo_rate": null,
"total": "1191.80"
},
"amount_in_words": "MIL CIENTO NOVENTA Y UNO CON 80/100 SOLES",
"payment": { "type": "Contado" },
"notes": "Orden de compra OC-2026-118",
"sunat": {
"hash": "kA1bC2dE3fG4hI5jK6lM7nO8pQ=",
"cdr_description": "La Factura numero F001-42, ha sido aceptada",
"observations": null,
"error_message": null
},
"files": {
"pdf": "/api/v1/documents/9f1b2c3d-4e5f-4a7b-8c9d-0e1f2a3b4c5d/pdf",
"xml": "/api/v1/documents/9f1b2c3d-4e5f-4a7b-8c9d-0e1f2a3b4c5d/xml",
"cdr": "/api/v1/documents/9f1b2c3d-4e5f-4a7b-8c9d-0e1f2a3b4c5d/cdr",
"zip": "/api/v1/documents/9f1b2c3d-4e5f-4a7b-8c9d-0e1f2a3b4c5d/zip"
},
"items": [
{
"position": 1,
"product_id": "1a2b3c4d-0000-4000-8000-000000000001",
"code": "CONSULT-HR",
"description": "Consultoría por hora",
"unit_code": "HUR",
"quantity": "10",
"unit_price": "118.00",
"unit_value": "100.0000000000",
"line_base": "1000.00",
"line_discount": "0.00",
"disc_rate": null,
"line_igv": "180.00",
"line_isc": "0.00",
"isc_rate": null,
"line_icbper": "0.00",
"line_total": "1180.00",
"affectation": "10"
},
{
"position": 2,
"product_id": null,
"code": null,
"description": "Bolsa ecológica",
"unit_code": "NIU",
"quantity": "2",
"unit_price": "5.90",
"unit_value": "5.0000000000",
"line_base": "10.00",
"line_discount": "0.00",
"disc_rate": null,
"line_igv": "1.80",
"line_isc": "0.00",
"isc_rate": null,
"line_icbper": "0.00",
"line_total": "11.80",
"affectation": "10"
}
],
"source": "api",
"emailed_to": "facturas@acme.pe",
"emailed_at": "2026-06-09T19:32:08.412Z",
"created_at": "2026-06-09T19:32:06.120Z"
}Códigos de estado
| HTTP | Significado |
|---|---|
| 201 | Documento emitido. status es accepted o rejected (el veredicto de SUNAT viene en sunat.cdr_description). Un rechazo también responde 201: el documento existe. |
| 200 | Replay idempotente — la clave ya se usó; se devuelve el documento original con la cabecera Idempotent-Replay: true. |
| 502 | El cuerpo es el documento con status: "error": falla del PSE/red antes del veredicto de SUNAT. No consume tu plan. Reintenta con una clave de idempotencia nueva. |
Errores
| HTTP | Código | Cuándo |
|---|---|---|
| 400 | invalid_json | El cuerpo no es JSON válido. |
| 400 | validation | Datos inválidos (con details por campo): fechas fuera de rango, cuotas que no suman el total, cantidad con más de 3 decimales, etc. |
| 400 | customer_invalid | Cliente inconsistente: factura sin RUC, boleta con RUC, documento de identidad inválido o cliente inexistente. |
| 402 | plan_limit | Límite mensual del plan alcanzado. details trae used, limit y plan. |
| 409 | company_not_ready | La empresa aún no completó el onboarding con el PSE. |
| 422 | series_not_found | La serie no existe, está inactiva o su prefijo no corresponde al tipo. |
| 422 | product_not_found | Un ítem referencia un product_id o code inexistente o inactivo. |
| 502 | provider_error | Error del PSE antes de crear el documento. |
| 500 | internal | Error interno inesperado. |
| 401 | invalid_token | Token ausente, mal formado, revocado o de una empresa eliminada. |
| 403 | insufficient_scope | El token no tiene el alcance (scope) que exige el endpoint. |
| 403 | module_not_enabled | Tu empresa no tiene habilitado el módulo del endpoint (p. ej. Cotizaciones). |
| 429 | rate_limited | Más de 120 solicitudes por minuto con el mismo token. |
/api/v1/documentsdocuments:readLista documentos, los más recientes primero. Sin los ítems de línea (pídelos por id).
| Parámetro | Descripción |
|---|---|
from | Fecha de emisión mínima, YYYY-MM-DD (inclusive). |
to | Fecha de emisión máxima, YYYY-MM-DD (inclusive). |
type | factura o boleta. |
status | accepted · rejected · error · processing. |
q | Búsqueda libre por número completo, nombre o documento del cliente (máx. 100 caracteres). |
page | Página, desde 1. |
per_page | Por defecto 25, máximo 100. |
curl "https://bloques.example.com/api/v1/documents?from=2026-06-01&to=2026-06-30&type=boleta&status=accepted&per_page=50" \
-H "Authorization: Bearer sk_live_..."{
"data": [
{
"id": "9f1b2c3d-4e5f-4a7b-8c9d-0e1f2a3b4c5d",
"type": "boleta",
"doc_type": "03",
"series": "B001",
"number": 117,
"full_number": "B001-117",
"file_name": "20123456789-03-B001-117",
"status": "accepted",
"issue_date": "2026-06-09",
"issue_time": "11:02:44",
"due_date": null,
"currency": "PEN",
"customer": { "docType": "0", "docNumber": "0", "name": "CLIENTES VARIOS" },
"totals": { "gravado": "100.00", "exonerado": "0.00", "inafecto": "0.00",
"igv": "18.00", "isc": "0.00", "icbper": "0.00", "descuento": "0.00", "total_value": "100.00",
"recargo_consumo": "0.00", "recargo_rate": null, "total": "118.00" },
"amount_in_words": "CIENTO DIECIOCHO CON 00/100 SOLES",
"payment": { "type": "Contado" },
"notes": null,
"sunat": { "hash": "xY9z...=", "cdr_description": "La Boleta numero B001-117, ha sido aceptada",
"observations": null, "error_message": null },
"files": { "pdf": "/api/v1/documents/9f1b.../pdf", "xml": "/api/v1/documents/9f1b.../xml",
"cdr": "/api/v1/documents/9f1b.../cdr", "zip": "/api/v1/documents/9f1b.../zip" },
"source": "api",
"emailed_to": null,
"emailed_at": null,
"created_at": "2026-06-09T16:02:45.001Z"
}
],
"page": 1,
"per_page": 50,
"total": 1
}| HTTP | Código | Cuándo |
|---|---|---|
| 401 | invalid_token | Token ausente, mal formado, revocado o de una empresa eliminada. |
| 403 | insufficient_scope | El token no tiene el alcance (scope) que exige el endpoint. |
| 403 | module_not_enabled | Tu empresa no tiene habilitado el módulo del endpoint (p. ej. Cotizaciones). |
| 429 | rate_limited | Más de 120 solicitudes por minuto con el mismo token. |
/api/v1/documents/{id}documents:readDevuelve un documento por su UUID, incluyendo el arreglo items (misma forma que la respuesta de creación).
curl https://bloques.example.com/api/v1/documents/9f1b2c3d-4e5f-4a7b-8c9d-0e1f2a3b4c5d \
-H "Authorization: Bearer sk_live_..."| HTTP | Código | Cuándo |
|---|---|---|
| 404 | not_found | El documento no existe o pertenece a otra empresa. |
| 401 | invalid_token | Token ausente, mal formado, revocado o de una empresa eliminada. |
| 403 | insufficient_scope | El token no tiene el alcance (scope) que exige el endpoint. |
| 403 | module_not_enabled | Tu empresa no tiene habilitado el módulo del endpoint (p. ej. Cotizaciones). |
| 429 | rate_limited | Más de 120 solicitudes por minuto con el mismo token. |
/api/v1/documents/{id}/pdfdocuments:readDescarga el PDF imprimible (application/pdf, adjunto {file_name}.pdf).
| Parámetro | Descripción |
|---|---|
template | Opcional: re-renderiza al vuelo con otra plantilla — clasica, moderna, minimal o ticket80. Sin el parámetro se sirve el PDF de la plantilla por defecto de la empresa (cacheado). |
curl -L -o F001-42.pdf \
"https://bloques.example.com/api/v1/documents/9f1b2c3d-4e5f-4a7b-8c9d-0e1f2a3b4c5d/pdf?template=ticket80" \
-H "Authorization: Bearer sk_live_..."| HTTP | Código | Cuándo |
|---|---|---|
| 404 | not_found | El documento no existe o pertenece a otra empresa. |
| 500 | pdf_failed | No se pudo generar el PDF. |
| 401 | invalid_token | Token ausente, mal formado, revocado o de una empresa eliminada. |
| 403 | insufficient_scope | El token no tiene el alcance (scope) que exige el endpoint. |
| 403 | module_not_enabled | Tu empresa no tiene habilitado el módulo del endpoint (p. ej. Cotizaciones). |
| 429 | rate_limited | Más de 120 solicitudes por minuto con el mismo token. |
/api/v1/documents/{id}/xmldocuments:readDescarga el XML firmado (application/xml, {file_name}.xml) — el archivo con valor legal. Para bajar el XML firmado y el CDR en un solo archivo, usa /documents/{id}/zip.
| Parámetro | Descripción |
|---|---|
unsigned | true devuelve el XML UBL crudo sin firmar (application/xml, {file_name}-sin-firmar.xml), útil para depurar. |
| HTTP | Código | Cuándo |
|---|---|---|
| 404 | not_found | El documento no existe o pertenece a otra empresa. |
| 404 | file_not_found | El archivo no está disponible (p. ej. el documento nunca llegó a firmarse). |
| 401 | invalid_token | Token ausente, mal formado, revocado o de una empresa eliminada. |
| 403 | insufficient_scope | El token no tiene el alcance (scope) que exige el endpoint. |
| 403 | module_not_enabled | Tu empresa no tiene habilitado el módulo del endpoint (p. ej. Cotizaciones). |
| 429 | rate_limited | Más de 120 solicitudes por minuto con el mismo token. |
/api/v1/documents/{id}/cdrdocuments:readDescarga el CDR de SUNAT (constancia de recepción) como XML (application/xml, {file_name}-cdr.xml). Solo existe cuando SUNAT respondió (documentos accepted o rejected); en la respuesta JSON, files.cdr es null cuando no hay CDR.
| HTTP | Código | Cuándo |
|---|---|---|
| 404 | not_found | El documento no existe o pertenece a otra empresa. |
| 404 | file_not_found | Este documento no tiene CDR. |
| 401 | invalid_token | Token ausente, mal formado, revocado o de una empresa eliminada. |
| 403 | insufficient_scope | El token no tiene el alcance (scope) que exige el endpoint. |
| 403 | module_not_enabled | Tu empresa no tiene habilitado el módulo del endpoint (p. ej. Cotizaciones). |
| 429 | rate_limited | Más de 120 solicitudes por minuto con el mismo token. |
/api/v1/documents/{id}/zipdocuments:readDescarga el XML firmado y el CDR juntos en un ZIP (application/zip, {file_name}.zip), con {file_name}.xml y {file_name}-cdr.xml dentro. Si solo existe uno de los dos —un documento rechazado se firmó pero no tiene CDR— el ZIP trae ese. Es la descarga que ofrece la app.
| HTTP | Código | Cuándo |
|---|---|---|
| 404 | not_found | El documento no existe o pertenece a otra empresa. |
| 404 | file_not_found | Este documento no tiene XML firmado ni CDR. |
| 401 | invalid_token | Token ausente, mal formado, revocado o de una empresa eliminada. |
| 403 | insufficient_scope | El token no tiene el alcance (scope) que exige el endpoint. |
| 403 | module_not_enabled | Tu empresa no tiene habilitado el módulo del endpoint (p. ej. Cotizaciones). |
| 429 | rate_limited | Más de 120 solicitudes por minuto con el mismo token. |
/api/v1/documents/{id}/resenddocuments:writeCorrige y reenvía un documento que SUNAT rechazó (status:"rejected") o que no llegó a transmitirse (status:"error"), reutilizando el mismo serie-correlativo. Un comprobante rechazado no existe legalmente, así que su número puede reusarse; uno accepted es inmutable —reenviarlo daría el error 1033 de SUNAT («el comprobante ya fue informado»)— y se bloquea con 409 not_resendable.
El cuerpo es el documento corregido, con la misma forma que POST /documents; se ignoran type y series (la identidad del documento es fija por su id). Se vuelven a correr todas las validaciones. La cuota se comporta como en una primera emisión: un reenvío que termina accepted consume una unidad del plan y, si vuelve a rechazarse, la devuelve (solo los accepted cuentan). Devuelve el documento reemitido (200) con status accepted o rejected; un fallo de transporte deja status:"error" (502) y puede reenviarse de nuevo.
curl -X POST https://bloques.example.com/api/v1/documents/9f1b2c3d-4e5f-4a7b-8c9d-0e1f2a3b4c5d/resend \
-H "Authorization: Bearer sk_live_..." \
-H "Content-Type: application/json" \
-d '{
"type": "factura",
"currency": "PEN",
"customer": { "doc_type": "6", "doc_number": "20123456789", "name": "ACME S.A.C." },
"items": [ { "description": "Servicio de consultoría", "quantity": 1, "unit_price": 118.00 } ]
}'| HTTP | Código | Cuándo |
|---|---|---|
| 400 | invalid_json | El cuerpo no es JSON válido. |
| 400 | validation | Datos inválidos (con details por campo). |
| 400 | customer_invalid | Cliente inconsistente: factura sin RUC, boleta con RUC o documento inválido. |
| 402 | plan_limit | Límite mensual del plan alcanzado. details trae used, limit y plan. |
| 404 | not_found | El documento no existe o pertenece a otra empresa. |
| 409 | not_resendable | El documento ya fue aceptado (SUNAT 1033) o está en proceso; su número no puede reusarse. |
| 409 | company_not_ready | La empresa aún no completó el onboarding con el PSE. |
| 422 | series_not_found | La serie no existe, está inactiva o su prefijo no corresponde al tipo. |
| 422 | product_not_found | Un ítem referencia un product_id o code inexistente o inactivo. |
| 502 | provider_error | Error del PSE durante el reenvío. |
| 500 | internal | Error interno inesperado. |
| 401 | invalid_token | Token ausente, mal formado, revocado o de una empresa eliminada. |
| 403 | insufficient_scope | El token no tiene el alcance (scope) que exige el endpoint. |
| 403 | module_not_enabled | Tu empresa no tiene habilitado el módulo del endpoint (p. ej. Cotizaciones). |
| 429 | rate_limited | Más de 120 solicitudes por minuto con el mismo token. |
/api/v1/documents/exportdocuments:readExporta documentos a CSV (UTF-8 con BOM, separado por comas) con los mismos filtros del listado (from, to, type, status, q). Máximo 5000 filas, las más recientes primero. Ideal para conciliaciones o para tu contador.
Columnas:
full_number, type, status, issue_date, issue_time, due_date, currency,
customer_doc_type, customer_doc_number, customer_name,
total_gravado, total_exonerado, total_inafecto, total_igv, total_value, total,
amount_in_words, sunat_hash, cdr_description, source, emailed_to, created_at, idcurl -L -o documentos-junio.csv \
"https://bloques.example.com/api/v1/documents/export?from=2026-06-01&to=2026-06-30" \
-H "Authorization: Bearer sk_live_..."| HTTP | Código | Cuándo |
|---|---|---|
| 401 | invalid_token | Token ausente, mal formado, revocado o de una empresa eliminada. |
| 403 | insufficient_scope | El token no tiene el alcance (scope) que exige el endpoint. |
| 403 | module_not_enabled | Tu empresa no tiene habilitado el módulo del endpoint (p. ej. Cotizaciones). |
| 429 | rate_limited | Más de 120 solicitudes por minuto con el mismo token. |
Cotizaciones
Una cotización es un documento con precios que nunca se envía a SUNAT: sin UBL, sin firma, sin correlativo y no consume el límite del plan. Cada una recibe una referencia secuencial COT-0001 y puede convertirse luego en un comprobante real emitiendo un documento con from_quote_id. El cuerpo reutiliza los objetos customer, item y payment de los documentos, pero sin type, series ni send_email, y añade valid_until. El status derivado es open, expired (cuando valid_until ya pasó, calendario Lima) o converted.
/api/v1/quotesquotes:writeCrea una cotización. Devuelve 201 con la cotización y sus ítems.
| Campo | Tipo | Requerido | Descripción |
|---|---|---|---|
issue_date | string | No | YYYY-MM-DD. Por defecto hoy (Lima). Puede ser futura o pasada. |
valid_until | string | No | YYYY-MM-DD. Fecha límite de validez, impresa en el PDF. |
currency | "PEN" | "USD" | No | Por defecto PEN. |
customer | object | No | Mismo objeto que en documentos. Vacío → CLIENTES VARIOS. |
items | item[] | Sí | 1 a 100 líneas. Precios finales con IGV incluido. |
payment | object | No | Mismo objeto payment que en documentos. |
notes | string | No | Texto libre impreso en el PDF. |
recargo_consumo | object | No | Recargo al consumo (sin IGV, se suma al total). |
curl -X POST https://bloques.example.com/api/v1/quotes \
-H "Authorization: Bearer sk_live_..." \
-H "Content-Type: application/json" \
-d '{
"currency": "PEN",
"valid_until": "2026-07-15",
"customer": { "doc_type": "6", "doc_number": "20123456789", "name": "CLIENTE S.A.C." },
"items": [{ "description": "Consultoría", "quantity": 1, "unit_price": 1180 }]
}'| HTTP | Código | Cuándo |
|---|---|---|
| 401 | invalid_token | Token ausente, mal formado, revocado o de una empresa eliminada. |
| 403 | insufficient_scope | El token no tiene el alcance (scope) que exige el endpoint. |
| 403 | module_not_enabled | Tu empresa no tiene habilitado el módulo del endpoint (p. ej. Cotizaciones). |
| 429 | rate_limited | Más de 120 solicitudes por minuto con el mismo token. |
/api/v1/quotesquotes:readLista cotizaciones (más recientes primero) con filtros y paginación.
| Parámetro | Descripción |
|---|---|
from / to | Rango por issue_date (YYYY-MM-DD). |
converted | true o false para filtrar por estado de conversión. |
q | Busca por referencia (COT-…), nombre o documento del cliente. |
page / per_page | Paginación estándar (máx. 100). |
| HTTP | Código | Cuándo |
|---|---|---|
| 401 | invalid_token | Token ausente, mal formado, revocado o de una empresa eliminada. |
| 403 | insufficient_scope | El token no tiene el alcance (scope) que exige el endpoint. |
| 403 | module_not_enabled | Tu empresa no tiene habilitado el módulo del endpoint (p. ej. Cotizaciones). |
| 429 | rate_limited | Más de 120 solicitudes por minuto con el mismo token. |
/api/v1/quotes/{id}quotes:readDevuelve una cotización con sus ítems, totales, estado derivado y la ruta del PDF.
| HTTP | Código | Cuándo |
|---|---|---|
| 404 | not_found | La cotización no existe o es de otra empresa. |
| 401 | invalid_token | Token ausente, mal formado, revocado o de una empresa eliminada. |
| 403 | insufficient_scope | El token no tiene el alcance (scope) que exige el endpoint. |
| 403 | module_not_enabled | Tu empresa no tiene habilitado el módulo del endpoint (p. ej. Cotizaciones). |
| 429 | rate_limited | Más de 120 solicitudes por minuto con el mismo token. |
/api/v1/quotes/{id}quotes:writeReemplaza el contenido de una cotización editable (su code se conserva). El mismo cuerpo que POST /quotes. Falla si la cotización ya fue convertida.
| HTTP | Código | Cuándo |
|---|---|---|
| 404 | not_found | La cotización no existe. |
| 400 | validation | Ya convertida o datos inválidos. |
| 401 | invalid_token | Token ausente, mal formado, revocado o de una empresa eliminada. |
| 403 | insufficient_scope | El token no tiene el alcance (scope) que exige el endpoint. |
| 403 | module_not_enabled | Tu empresa no tiene habilitado el módulo del endpoint (p. ej. Cotizaciones). |
| 429 | rate_limited | Más de 120 solicitudes por minuto con el mismo token. |
/api/v1/quotes/{id}quotes:writeElimina una cotización. Falla con 409 si ya fue convertida en comprobante.
| HTTP | Código | Cuándo |
|---|---|---|
| 404 | not_found | La cotización no existe. |
| 409 | validation | Ya convertida — no se puede eliminar. |
| 401 | invalid_token | Token ausente, mal formado, revocado o de una empresa eliminada. |
| 403 | insufficient_scope | El token no tiene el alcance (scope) que exige el endpoint. |
| 403 | module_not_enabled | Tu empresa no tiene habilitado el módulo del endpoint (p. ej. Cotizaciones). |
| 429 | rate_limited | Más de 120 solicitudes por minuto con el mismo token. |
/api/v1/quotes/{id}/pdfquotes:readDescarga el PDF de la cotización (se genera al vuelo, no se cachea). ?template=clasica (por defecto) o ?template=minimal.
curl -L -o COT-0001.pdf \
"https://bloques.example.com/api/v1/quotes/{id}/pdf?template=minimal" \
-H "Authorization: Bearer sk_live_..."| HTTP | Código | Cuándo |
|---|---|---|
| 404 | not_found | La cotización no existe. |
| 401 | invalid_token | Token ausente, mal formado, revocado o de una empresa eliminada. |
| 403 | insufficient_scope | El token no tiene el alcance (scope) que exige el endpoint. |
| 403 | module_not_enabled | Tu empresa no tiene habilitado el módulo del endpoint (p. ej. Cotizaciones). |
| 429 | rate_limited | Más de 120 solicitudes por minuto con el mismo token. |
Convertir una cotización en comprobante
Para convertir, emite un documento normal (POST /api/v1/documents) incluyendo from_quote_id. Si la emisión es aceptada por SUNAT, la cotización se marca como converted (idempotente; una emisión rechazada o con error no la convierte). Reconstruye los items y el customer a partir de la cotización (ver GET /quotes/{id}) y añade lo propio de SUNAT (type, serie, etc.).
curl -X POST https://bloques.example.com/api/v1/documents \
-H "Authorization: Bearer sk_live_..." \
-H "Content-Type: application/json" \
-d '{
"type": "factura",
"from_quote_id": "9f1b2c3d-4e5f-4a7b-8c9d-0e1f2a3b4c5d",
"customer": { "doc_type": "6", "doc_number": "20123456789", "name": "CLIENTE S.A.C." },
"items": [{ "description": "Consultoría", "quantity": 1, "unit_price": 1180 }]
}'Productos
/api/v1/productsproducts:writeCrea un producto del catálogo.
| Campo | Tipo | Requerido | Descripción |
|---|---|---|---|
code | string | Sí | Tu código interno, único por empresa. Máx. 50. |
name | string | Sí | Máx. 300. |
description | string | No | Máx. 1000. |
unit_code | string | No | Catálogo 03. Por defecto NIU. |
unit_price | number | Sí | Precio FINAL — lo que paga el cliente, con IGV incluido cuando es gravado. |
currency | string | No | PEN (por defecto) o USD. |
affectation | string | No | "10" (por defecto) · "20" · "30". |
isc_rate | number | No | ISC al valor por defecto como fracción 0–1 (ej. 0.10); solo productos gravados. |
icbper | boolean | No | Marca ICBPER por defecto (bolsa plástica) para las líneas que usen este producto. |
track_stock | boolean | No | Si el módulo de inventario (cuando está activo) controla stock de este producto. Por defecto true. |
min_stock | number | No | Umbral de alerta de stock bajo (suma entre almacenes); omitido = sin alerta. |
barcode | string | No | Código de barras (EAN del fabricante o interno). Único por empresa. |
cost | number | No | Último costo de compra, FINAL (IGV incluido). Informativo. |
active | boolean | No | Por defecto true. |
curl -X POST https://bloques.example.com/api/v1/products \
-H "Authorization: Bearer sk_live_..." \
-H "Content-Type: application/json" \
-d '{ "code": "CAFE-250", "name": "Café molido 250g", "unit_price": 35.00 }'{
"id": "1a2b3c4d-0000-4000-8000-000000000001",
"code": "CAFE-250",
"name": "Café molido 250g",
"description": null,
"unit_code": "NIU",
"unit_price": "35.00",
"currency": "PEN",
"affectation": "10",
"isc_rate": null,
"icbper": false,
"track_stock": true,
"min_stock": null,
"barcode": null,
"cost": null,
"active": true,
"created_at": "2026-06-09T15:00:00.000Z",
"updated_at": "2026-06-09T15:00:00.000Z"
}| HTTP | Código | Cuándo |
|---|---|---|
| 400 | invalid_json | El cuerpo no es JSON válido. |
| 400 | validation | Campos inválidos. |
| 409 | duplicate_code | Ya existe un producto con ese código. |
| 409 | duplicate_barcode | Ya existe un producto con ese código de barras. |
| 401 | invalid_token | Token ausente, mal formado, revocado o de una empresa eliminada. |
| 403 | insufficient_scope | El token no tiene el alcance (scope) que exige el endpoint. |
| 403 | module_not_enabled | Tu empresa no tiene habilitado el módulo del endpoint (p. ej. Cotizaciones). |
| 429 | rate_limited | Más de 120 solicitudes por minuto con el mismo token. |
/api/v1/productsproducts:readLista el catálogo (solo activos por defecto), ordenado por código.
| Parámetro | Descripción |
|---|---|
q | Búsqueda por nombre o código. |
include_inactive | true incluye productos desactivados. |
page | Página, desde 1. |
per_page | Por defecto 50, máximo 100. |
{ "data": [ { "id": "…", "code": "CAFE-250", … } ], "page": 1, "per_page": 50, "total": 12 }| HTTP | Código | Cuándo |
|---|---|---|
| 401 | invalid_token | Token ausente, mal formado, revocado o de una empresa eliminada. |
| 403 | insufficient_scope | El token no tiene el alcance (scope) que exige el endpoint. |
| 403 | module_not_enabled | Tu empresa no tiene habilitado el módulo del endpoint (p. ej. Cotizaciones). |
| 429 | rate_limited | Más de 120 solicitudes por minuto con el mismo token. |
/api/v1/products/{id}products:readDevuelve un producto por UUID.
| HTTP | Código | Cuándo |
|---|---|---|
| 404 | not_found | El producto no existe o pertenece a otra empresa. |
| 401 | invalid_token | Token ausente, mal formado, revocado o de una empresa eliminada. |
| 403 | insufficient_scope | El token no tiene el alcance (scope) que exige el endpoint. |
| 403 | module_not_enabled | Tu empresa no tiene habilitado el módulo del endpoint (p. ej. Cotizaciones). |
| 429 | rate_limited | Más de 120 solicitudes por minuto con el mismo token. |
/api/v1/products/{id}products:writeActualización parcial: envía solo los campos a cambiar (mismos campos y reglas que la creación; todos opcionales; description admite null para limpiar). Los documentos ya emitidos conservan su snapshot.
curl -X PUT https://bloques.example.com/api/v1/products/1a2b3c4d-0000-4000-8000-000000000001 \
-H "Authorization: Bearer sk_live_..." \
-H "Content-Type: application/json" \
-d '{ "unit_price": 38.00 }'| HTTP | Código | Cuándo |
|---|---|---|
| 400 | invalid_json | El cuerpo no es JSON válido. |
| 400 | validation | Campos inválidos. |
| 404 | not_found | El producto no existe o pertenece a otra empresa. |
| 409 | duplicate_code | El nuevo código ya pertenece a otro producto. |
| 409 | duplicate_barcode | El nuevo código de barras ya pertenece a otro producto. |
| 401 | invalid_token | Token ausente, mal formado, revocado o de una empresa eliminada. |
| 403 | insufficient_scope | El token no tiene el alcance (scope) que exige el endpoint. |
| 403 | module_not_enabled | Tu empresa no tiene habilitado el módulo del endpoint (p. ej. Cotizaciones). |
| 429 | rate_limited | Más de 120 solicitudes por minuto con el mismo token. |
/api/v1/products/{id}products:writeBorrado suave: marca el producto como inactivo (active: false) y responde 200 con el producto actualizado. Deja de aparecer en listados y de poder usarse en emisiones; el historial no se toca. Reactívalo con PUT { "active": true }.
| HTTP | Código | Cuándo |
|---|---|---|
| 404 | not_found | El producto no existe o pertenece a otra empresa. |
| 401 | invalid_token | Token ausente, mal formado, revocado o de una empresa eliminada. |
| 403 | insufficient_scope | El token no tiene el alcance (scope) que exige el endpoint. |
| 403 | module_not_enabled | Tu empresa no tiene habilitado el módulo del endpoint (p. ej. Cotizaciones). |
| 429 | rate_limited | Más de 120 solicitudes por minuto con el mismo token. |
Clientes
/api/v1/customerscustomers:writeCrea un cliente en el directorio. Es un upsert: si ya existe uno con el mismo doc_type + doc_number, nombre, email, teléfono y dirección se sobrescriben con lo enviado (responde 201 igualmente). custom_fields es la excepción: ausente se preserva lo guardado; presente se valida contra las definiciones activas de tu empresa (Configuración → Campos) y reemplaza el objeto completo. Nota: emitir un documento con datos de cliente en línea también lo registra/actualiza — y nunca toca los campos personalizados.
| Campo | Tipo | Requerido | Descripción |
|---|---|---|---|
doc_type | string | Sí | "1" DNI · "4" CE · "6" RUC · "7" pasaporte. ("0" no se cataloga.) |
doc_number | string | Sí | Validado según el tipo (RUC: dígito verificador; DNI: 8 dígitos). |
name | string | Sí | Máx. 500. |
email | string | No | Email válido, máx. 320. |
phone | string | No | Dígitos, +, (), espacios y guiones; 6–20 caracteres. |
address | string | No | Máx. 500. |
custom_fields | object | No | Campos propios de tu empresa, por clave de definición. Valores escalares JSON (fechas como "AAAA-MM-DD"; null o "" borra la clave). Requiere el módulo Campos personalizados. |
curl -X POST https://bloques.example.com/api/v1/customers \
-H "Authorization: Bearer sk_live_..." \
-H "Content-Type: application/json" \
-d '{ "doc_type": "6", "doc_number": "20512345678", "name": "ACME PERU S.A.C.", "email": "compras@acme.pe" }'{
"id": "7a8b9c0d-0000-4000-8000-000000000002",
"doc_type": "6",
"doc_number": "20512345678",
"name": "ACME PERU S.A.C.",
"email": "compras@acme.pe",
"phone": "987654321",
"address": null,
"custom_fields": { "segmento": "Corporativo", "vip": true },
"created_at": "2026-06-09T15:10:00.000Z"
}| HTTP | Código | Cuándo |
|---|---|---|
| 400 | invalid_json | El cuerpo no es JSON válido. |
| 400 | validation | Campos o documento de identidad inválidos. |
| 400 | custom_fields_invalid | Campos personalizados inválidos (detalle por clave en details). |
| 403 | module_not_enabled | Tu plan no incluye el módulo Campos personalizados (solo si envías custom_fields). |
| 401 | invalid_token | Token ausente, mal formado, revocado o de una empresa eliminada. |
| 403 | insufficient_scope | El token no tiene el alcance (scope) que exige el endpoint. |
| 403 | module_not_enabled | Tu empresa no tiene habilitado el módulo del endpoint (p. ej. Cotizaciones). |
| 429 | rate_limited | Más de 120 solicitudes por minuto con el mismo token. |
/api/v1/customerscustomers:readLista o busca clientes, ordenados por nombre.
| Parámetro | Descripción |
|---|---|
q | Búsqueda por nombre o número de documento. |
page | Página, desde 1. |
per_page | Por defecto 50, máximo 100. |
{ "data": [ { "id": "…", "doc_type": "6", "doc_number": "20512345678", "name": "ACME PERU S.A.C.", … } ],
"page": 1, "per_page": 50, "total": 1 }| HTTP | Código | Cuándo |
|---|---|---|
| 401 | invalid_token | Token ausente, mal formado, revocado o de una empresa eliminada. |
| 403 | insufficient_scope | El token no tiene el alcance (scope) que exige el endpoint. |
| 403 | module_not_enabled | Tu empresa no tiene habilitado el módulo del endpoint (p. ej. Cotizaciones). |
| 429 | rate_limited | Más de 120 solicitudes por minuto con el mismo token. |
/api/v1/customers/{id}customers:readUn cliente por id, con la misma forma que la respuesta del POST (incluye phone y custom_fields).
| HTTP | Código | Cuándo |
|---|---|---|
| 404 | not_found | El cliente no existe o no es de tu empresa. |
| 401 | invalid_token | Token ausente, mal formado, revocado o de una empresa eliminada. |
| 403 | insufficient_scope | El token no tiene el alcance (scope) que exige el endpoint. |
| 403 | module_not_enabled | Tu empresa no tiene habilitado el módulo del endpoint (p. ej. Cotizaciones). |
| 429 | rate_limited | Más de 120 solicitudes por minuto con el mismo token. |
/api/v1/customers/{id}customers:writeEdita la ficha. Parche parcial: lo ausente no se toca; null limpia email, teléfono o dirección. La identidad (doc_type + doc_number) no es modificable — es la clave del directorio; otro documento es otro cliente. custom_fields presente se valida y reemplaza el objeto completo (igual que en el POST).
curl -X PATCH https://bloques.example.com/api/v1/customers/7a8b9c0d-… \
-H "Authorization: Bearer sk_live_..." \
-H "Content-Type: application/json" \
-d '{ "phone": "987654321", "custom_fields": { "segmento": "Retail", "vip": false } }'| HTTP | Código | Cuándo |
|---|---|---|
| 400 | validation | Campos inválidos. |
| 400 | custom_fields_invalid | Campos personalizados inválidos (detalle por clave en details). |
| 403 | module_not_enabled | Tu plan no incluye el módulo Campos personalizados (solo si envías custom_fields). |
| 404 | not_found | El cliente no existe o no es de tu empresa. |
| 401 | invalid_token | Token ausente, mal formado, revocado o de una empresa eliminada. |
| 403 | insufficient_scope | El token no tiene el alcance (scope) que exige el endpoint. |
| 403 | module_not_enabled | Tu empresa no tiene habilitado el módulo del endpoint (p. ej. Cotizaciones). |
| 429 | rate_limited | Más de 120 solicitudes por minuto con el mismo token. |
Series, consumo y empresa
Estos tres endpoints aceptan cualquier token válido de la empresa (no exigen scope).
/api/v1/seriescualquier tokenSeries de numeración disponibles por tipo de documento y el próximo correlativo de cada una. Cada serie incluye su owner (company / branch / register): el dueño decide qué serie se elige al emitir desde una caja.
{
"data": [
{ "type": "factura", "doc_type": "01", "code": "F001", "next_number": 43, "active": true,
"owner": "company", "branch_id": null, "cash_register_id": null },
{ "type": "boleta", "doc_type": "03", "code": "B002", "next_number": 118, "active": true,
"owner": "register", "branch_id": null, "cash_register_id": "…uuid…" }
]
}| HTTP | Código | Cuándo |
|---|---|---|
| 401 | invalid_token | Token ausente, mal formado, revocado o de una empresa eliminada. |
| 403 | insufficient_scope | El token no tiene el alcance (scope) que exige el endpoint. |
| 403 | module_not_enabled | Tu empresa no tiene habilitado el módulo del endpoint (p. ej. Cotizaciones). |
| 429 | rate_limited | Más de 120 solicitudes por minuto con el mismo token. |
/api/v1/usagecualquier tokenConsumo del mes en curso (calendario America/Lima) contra el límite del plan.
{
"plan": "pro",
"planStatus": "active",
"used": 137,
"limit": 3000,
"remaining": 2863,
"periodStart": "2026-06-01T05:00:00.000Z"
}| HTTP | Código | Cuándo |
|---|---|---|
| 401 | invalid_token | Token ausente, mal formado, revocado o de una empresa eliminada. |
| 403 | insufficient_scope | El token no tiene el alcance (scope) que exige el endpoint. |
| 403 | module_not_enabled | Tu empresa no tiene habilitado el módulo del endpoint (p. ej. Cotizaciones). |
| 429 | rate_limited | Más de 120 solicitudes por minuto con el mismo token. |
/api/v1/companycualquier tokenPerfil público de la empresa autenticada (nunca expone credenciales).
{
"id": "c0a80001-0000-4000-8000-000000000003",
"ruc": "20123456789",
"razon_social": "MI EMPRESA S.A.C.",
"email": "facturacion@miempresa.pe",
"direccion": "Av. Arequipa 1234, Lince",
"ubigeo": "150116",
"distrito": "Lince",
"provincia": "Lima",
"departamento": "Lima",
"environment": "produccion",
"plan": "pro",
"plan_status": "active",
"pdf_template": "moderna",
"email_enabled": true,
"created_at": "2026-01-15T14:00:00.000Z"
}| HTTP | Código | Cuándo |
|---|---|---|
| 401 | invalid_token | Token ausente, mal formado, revocado o de una empresa eliminada. |
| 403 | insufficient_scope | El token no tiene el alcance (scope) que exige el endpoint. |
| 403 | module_not_enabled | Tu empresa no tiene habilitado el módulo del endpoint (p. ej. Cotizaciones). |
| 429 | rate_limited | Más de 120 solicitudes por minuto con el mismo token. |