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 token sk_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:

ScopePermite
*Todos los alcances (acceso total de API).
documents:readListar y leer documentos, descargar PDF/XML/CDR, exportar CSV.
documents:writeEmitir documentos.
quotes:readListar y leer cotizaciones, descargar su PDF.
quotes:writeCrear, actualizar y eliminar cotizaciones.
products:readListar y leer productos.
products:writeCrear, actualizar y desactivar productos.
customers:readListar y buscar clientes.
customers:writeCrear/actualizar clientes.
expenses:writeRegistrar 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 (118 o 118.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

POST/api/v1/documentsdocuments:write

Crea 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

CampoTipoRequeridoDescripción
typestring"factura" (01, exige cliente con RUC) o "boleta" (03).
seriesstringNoSerie de 4 caracteres (F001/B001…). Prefijo F para facturas, B para boletas. Por defecto, la serie activa del tipo.
cash_register_idstringNoUUID 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_datestringNoYYYY-MM-DD. Por defecto hoy (Lima). Máximo 7 días atrás; nunca futura.
currencystringNo"PEN" (por defecto) o "USD".
customerobjectFactura: 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.
itemsarray1 a 100 ítems. Ver tabla de ítems.
paymentobjectNoForma de pago. Por defecto contado. Ver tabla de pago.
notesstringNoHasta 1000 caracteres. Texto libre impreso en el PDF (no forma parte del XML).
send_emailbooleanNoEnviar PDF + XML al email del cliente. Por defecto, la configuración de la empresa.
recargo_consumoobjectNoRecargo 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

CampoTipoRequeridoDescripción
idstring (uuid)NoCliente existente del directorio. Excluyente con los campos en línea.
doc_typestringCon datos en línea"6" RUC · "1" DNI · "4" carnet de extranjería · "7" pasaporte · "0" sin documento.
doc_numberstringCon datos en líneaMáx. 15. RUC: 11 dígitos con dígito verificador. DNI: 8 dígitos. Para "0" debe ser "0".
namestringCon datos en líneaRazón social o nombre. Máx. 500.
emailstringNoDestino del PDF + XML.
addressstringNoDirecció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).

CampoTipoRequeridoDescripción
product_idstring (uuid)No*Producto del catálogo por id.
codestringNo*Producto del catálogo por tu código.
descriptionstringNo*Descripción de línea libre (exige unit_price) o reemplazo de la descripción del producto. Máx. 500.
quantitynumberNo> 0, hasta 3 decimales. Por defecto 1.
unit_pricenumberNo*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_codestringNoCatálogo 03 SUNAT: NIU unidad (por defecto), ZZ servicio, KGM kg, HUR hora, etc.
affectationstringNoIGV: "10" gravado (por defecto) · "20" exonerado · "30" inafecto.
isc_ratenumberNoISC 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_ratenumberNoDescuento 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).
icbperbooleanNoAfecto a ICBPER (bolsa plástica): suma S/ 0.50 por unidad sobre el precio. Sobrescribe al producto.

Objeto payment

CampoTipoRequeridoDescripción
typestringNo"contado" (por defecto) o "credito".
installmentsarrayCon creditoHasta 36 cuotas { "amount": número, "due_date": "YYYY-MM-DD" }. Deben sumar exactamente el total del documento (tolerancia ±0.01).

Ejemplo

Petición
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"
  }'
Respuesta 201 (emitido y aceptado)
{
  "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

HTTPSignificado
201Documento emitido. status es accepted o rejected (el veredicto de SUNAT viene en sunat.cdr_description). Un rechazo también responde 201: el documento existe.
200Replay idempotente — la clave ya se usó; se devuelve el documento original con la cabecera Idempotent-Replay: true.
502El 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

HTTPCódigoCuándo
400invalid_jsonEl cuerpo no es JSON válido.
400validationDatos inválidos (con details por campo): fechas fuera de rango, cuotas que no suman el total, cantidad con más de 3 decimales, etc.
400customer_invalidCliente inconsistente: factura sin RUC, boleta con RUC, documento de identidad inválido o cliente inexistente.
402plan_limitLímite mensual del plan alcanzado. details trae used, limit y plan.
409company_not_readyLa empresa aún no completó el onboarding con el PSE.
422series_not_foundLa serie no existe, está inactiva o su prefijo no corresponde al tipo.
422product_not_foundUn ítem referencia un product_id o code inexistente o inactivo.
502provider_errorError del PSE antes de crear el documento.
500internalError interno inesperado.
401invalid_tokenToken ausente, mal formado, revocado o de una empresa eliminada.
403insufficient_scopeEl token no tiene el alcance (scope) que exige el endpoint.
403module_not_enabledTu empresa no tiene habilitado el módulo del endpoint (p. ej. Cotizaciones).
429rate_limitedMás de 120 solicitudes por minuto con el mismo token.
GET/api/v1/documentsdocuments:read

Lista documentos, los más recientes primero. Sin los ítems de línea (pídelos por id).

ParámetroDescripción
fromFecha de emisión mínima, YYYY-MM-DD (inclusive).
toFecha de emisión máxima, YYYY-MM-DD (inclusive).
typefactura o boleta.
statusaccepted · rejected · error · processing.
qBúsqueda libre por número completo, nombre o documento del cliente (máx. 100 caracteres).
pagePágina, desde 1.
per_pagePor defecto 25, máximo 100.
Petición
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_..."
Respuesta 200
{
  "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
}
HTTPCódigoCuándo
401invalid_tokenToken ausente, mal formado, revocado o de una empresa eliminada.
403insufficient_scopeEl token no tiene el alcance (scope) que exige el endpoint.
403module_not_enabledTu empresa no tiene habilitado el módulo del endpoint (p. ej. Cotizaciones).
429rate_limitedMás de 120 solicitudes por minuto con el mismo token.
GET/api/v1/documents/{id}documents:read

Devuelve un documento por su UUID, incluyendo el arreglo items (misma forma que la respuesta de creación).

Petición
curl https://bloques.example.com/api/v1/documents/9f1b2c3d-4e5f-4a7b-8c9d-0e1f2a3b4c5d \
  -H "Authorization: Bearer sk_live_..."
HTTPCódigoCuándo
404not_foundEl documento no existe o pertenece a otra empresa.
401invalid_tokenToken ausente, mal formado, revocado o de una empresa eliminada.
403insufficient_scopeEl token no tiene el alcance (scope) que exige el endpoint.
403module_not_enabledTu empresa no tiene habilitado el módulo del endpoint (p. ej. Cotizaciones).
429rate_limitedMás de 120 solicitudes por minuto con el mismo token.
GET/api/v1/documents/{id}/pdfdocuments:read

Descarga el PDF imprimible (application/pdf, adjunto {file_name}.pdf).

ParámetroDescripción
templateOpcional: 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).
Petición
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_..."
HTTPCódigoCuándo
404not_foundEl documento no existe o pertenece a otra empresa.
500pdf_failedNo se pudo generar el PDF.
401invalid_tokenToken ausente, mal formado, revocado o de una empresa eliminada.
403insufficient_scopeEl token no tiene el alcance (scope) que exige el endpoint.
403module_not_enabledTu empresa no tiene habilitado el módulo del endpoint (p. ej. Cotizaciones).
429rate_limitedMás de 120 solicitudes por minuto con el mismo token.
GET/api/v1/documents/{id}/xmldocuments:read

Descarga 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ámetroDescripción
unsignedtrue devuelve el XML UBL crudo sin firmar (application/xml, {file_name}-sin-firmar.xml), útil para depurar.
HTTPCódigoCuándo
404not_foundEl documento no existe o pertenece a otra empresa.
404file_not_foundEl archivo no está disponible (p. ej. el documento nunca llegó a firmarse).
401invalid_tokenToken ausente, mal formado, revocado o de una empresa eliminada.
403insufficient_scopeEl token no tiene el alcance (scope) que exige el endpoint.
403module_not_enabledTu empresa no tiene habilitado el módulo del endpoint (p. ej. Cotizaciones).
429rate_limitedMás de 120 solicitudes por minuto con el mismo token.
GET/api/v1/documents/{id}/cdrdocuments:read

Descarga 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.

HTTPCódigoCuándo
404not_foundEl documento no existe o pertenece a otra empresa.
404file_not_foundEste documento no tiene CDR.
401invalid_tokenToken ausente, mal formado, revocado o de una empresa eliminada.
403insufficient_scopeEl token no tiene el alcance (scope) que exige el endpoint.
403module_not_enabledTu empresa no tiene habilitado el módulo del endpoint (p. ej. Cotizaciones).
429rate_limitedMás de 120 solicitudes por minuto con el mismo token.
GET/api/v1/documents/{id}/zipdocuments:read

Descarga 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.

HTTPCódigoCuándo
404not_foundEl documento no existe o pertenece a otra empresa.
404file_not_foundEste documento no tiene XML firmado ni CDR.
401invalid_tokenToken ausente, mal formado, revocado o de una empresa eliminada.
403insufficient_scopeEl token no tiene el alcance (scope) que exige el endpoint.
403module_not_enabledTu empresa no tiene habilitado el módulo del endpoint (p. ej. Cotizaciones).
429rate_limitedMás de 120 solicitudes por minuto con el mismo token.
POST/api/v1/documents/{id}/resenddocuments:write

Corrige 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.

Petición
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 } ]
  }'
HTTPCódigoCuándo
400invalid_jsonEl cuerpo no es JSON válido.
400validationDatos inválidos (con details por campo).
400customer_invalidCliente inconsistente: factura sin RUC, boleta con RUC o documento inválido.
402plan_limitLímite mensual del plan alcanzado. details trae used, limit y plan.
404not_foundEl documento no existe o pertenece a otra empresa.
409not_resendableEl documento ya fue aceptado (SUNAT 1033) o está en proceso; su número no puede reusarse.
409company_not_readyLa empresa aún no completó el onboarding con el PSE.
422series_not_foundLa serie no existe, está inactiva o su prefijo no corresponde al tipo.
422product_not_foundUn ítem referencia un product_id o code inexistente o inactivo.
502provider_errorError del PSE durante el reenvío.
500internalError interno inesperado.
401invalid_tokenToken ausente, mal formado, revocado o de una empresa eliminada.
403insufficient_scopeEl token no tiene el alcance (scope) que exige el endpoint.
403module_not_enabledTu empresa no tiene habilitado el módulo del endpoint (p. ej. Cotizaciones).
429rate_limitedMás de 120 solicitudes por minuto con el mismo token.
GET/api/v1/documents/exportdocuments:read

Exporta 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, id
Petición
curl -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_..."
HTTPCódigoCuándo
401invalid_tokenToken ausente, mal formado, revocado o de una empresa eliminada.
403insufficient_scopeEl token no tiene el alcance (scope) que exige el endpoint.
403module_not_enabledTu empresa no tiene habilitado el módulo del endpoint (p. ej. Cotizaciones).
429rate_limitedMá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.

POST/api/v1/quotesquotes:write

Crea una cotización. Devuelve 201 con la cotización y sus ítems.

CampoTipoRequeridoDescripción
issue_datestringNoYYYY-MM-DD. Por defecto hoy (Lima). Puede ser futura o pasada.
valid_untilstringNoYYYY-MM-DD. Fecha límite de validez, impresa en el PDF.
currency"PEN" | "USD"NoPor defecto PEN.
customerobjectNoMismo objeto que en documentos. Vacío → CLIENTES VARIOS.
itemsitem[]1 a 100 líneas. Precios finales con IGV incluido.
paymentobjectNoMismo objeto payment que en documentos.
notesstringNoTexto libre impreso en el PDF.
recargo_consumoobjectNoRecargo al consumo (sin IGV, se suma al total).
Petición
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 }]
  }'
HTTPCódigoCuándo
401invalid_tokenToken ausente, mal formado, revocado o de una empresa eliminada.
403insufficient_scopeEl token no tiene el alcance (scope) que exige el endpoint.
403module_not_enabledTu empresa no tiene habilitado el módulo del endpoint (p. ej. Cotizaciones).
429rate_limitedMás de 120 solicitudes por minuto con el mismo token.
GET/api/v1/quotesquotes:read

Lista cotizaciones (más recientes primero) con filtros y paginación.

ParámetroDescripción
from / toRango por issue_date (YYYY-MM-DD).
convertedtrue o false para filtrar por estado de conversión.
qBusca por referencia (COT-…), nombre o documento del cliente.
page / per_pagePaginación estándar (máx. 100).
HTTPCódigoCuándo
401invalid_tokenToken ausente, mal formado, revocado o de una empresa eliminada.
403insufficient_scopeEl token no tiene el alcance (scope) que exige el endpoint.
403module_not_enabledTu empresa no tiene habilitado el módulo del endpoint (p. ej. Cotizaciones).
429rate_limitedMás de 120 solicitudes por minuto con el mismo token.
GET/api/v1/quotes/{id}quotes:read

Devuelve una cotización con sus ítems, totales, estado derivado y la ruta del PDF.

HTTPCódigoCuándo
404not_foundLa cotización no existe o es de otra empresa.
401invalid_tokenToken ausente, mal formado, revocado o de una empresa eliminada.
403insufficient_scopeEl token no tiene el alcance (scope) que exige el endpoint.
403module_not_enabledTu empresa no tiene habilitado el módulo del endpoint (p. ej. Cotizaciones).
429rate_limitedMás de 120 solicitudes por minuto con el mismo token.
PUT/api/v1/quotes/{id}quotes:write

Reemplaza 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.

HTTPCódigoCuándo
404not_foundLa cotización no existe.
400validationYa convertida o datos inválidos.
401invalid_tokenToken ausente, mal formado, revocado o de una empresa eliminada.
403insufficient_scopeEl token no tiene el alcance (scope) que exige el endpoint.
403module_not_enabledTu empresa no tiene habilitado el módulo del endpoint (p. ej. Cotizaciones).
429rate_limitedMás de 120 solicitudes por minuto con el mismo token.
DELETE/api/v1/quotes/{id}quotes:write

Elimina una cotización. Falla con 409 si ya fue convertida en comprobante.

HTTPCódigoCuándo
404not_foundLa cotización no existe.
409validationYa convertida — no se puede eliminar.
401invalid_tokenToken ausente, mal formado, revocado o de una empresa eliminada.
403insufficient_scopeEl token no tiene el alcance (scope) que exige el endpoint.
403module_not_enabledTu empresa no tiene habilitado el módulo del endpoint (p. ej. Cotizaciones).
429rate_limitedMás de 120 solicitudes por minuto con el mismo token.
GET/api/v1/quotes/{id}/pdfquotes:read

Descarga el PDF de la cotización (se genera al vuelo, no se cachea). ?template=clasica (por defecto) o ?template=minimal.

Petición
curl -L -o COT-0001.pdf \
  "https://bloques.example.com/api/v1/quotes/{id}/pdf?template=minimal" \
  -H "Authorization: Bearer sk_live_..."
HTTPCódigoCuándo
404not_foundLa cotización no existe.
401invalid_tokenToken ausente, mal formado, revocado o de una empresa eliminada.
403insufficient_scopeEl token no tiene el alcance (scope) que exige el endpoint.
403module_not_enabledTu empresa no tiene habilitado el módulo del endpoint (p. ej. Cotizaciones).
429rate_limitedMá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.).

Petición
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

POST/api/v1/productsproducts:write

Crea un producto del catálogo.

CampoTipoRequeridoDescripción
codestringTu código interno, único por empresa. Máx. 50.
namestringMáx. 300.
descriptionstringNoMáx. 1000.
unit_codestringNoCatálogo 03. Por defecto NIU.
unit_pricenumberPrecio FINAL — lo que paga el cliente, con IGV incluido cuando es gravado.
currencystringNoPEN (por defecto) o USD.
affectationstringNo"10" (por defecto) · "20" · "30".
isc_ratenumberNoISC al valor por defecto como fracción 0–1 (ej. 0.10); solo productos gravados.
icbperbooleanNoMarca ICBPER por defecto (bolsa plástica) para las líneas que usen este producto.
track_stockbooleanNoSi el módulo de inventario (cuando está activo) controla stock de este producto. Por defecto true.
min_stocknumberNoUmbral de alerta de stock bajo (suma entre almacenes); omitido = sin alerta.
barcodestringNoCódigo de barras (EAN del fabricante o interno). Único por empresa.
costnumberNoÚltimo costo de compra, FINAL (IGV incluido). Informativo.
activebooleanNoPor defecto true.
Petición
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 }'
Respuesta 201
{
  "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"
}
HTTPCódigoCuándo
400invalid_jsonEl cuerpo no es JSON válido.
400validationCampos inválidos.
409duplicate_codeYa existe un producto con ese código.
409duplicate_barcodeYa existe un producto con ese código de barras.
401invalid_tokenToken ausente, mal formado, revocado o de una empresa eliminada.
403insufficient_scopeEl token no tiene el alcance (scope) que exige el endpoint.
403module_not_enabledTu empresa no tiene habilitado el módulo del endpoint (p. ej. Cotizaciones).
429rate_limitedMás de 120 solicitudes por minuto con el mismo token.
GET/api/v1/productsproducts:read

Lista el catálogo (solo activos por defecto), ordenado por código.

ParámetroDescripción
qBúsqueda por nombre o código.
include_inactivetrue incluye productos desactivados.
pagePágina, desde 1.
per_pagePor defecto 50, máximo 100.
Respuesta 200
{ "data": [ { "id": "…", "code": "CAFE-250", … } ], "page": 1, "per_page": 50, "total": 12 }
HTTPCódigoCuándo
401invalid_tokenToken ausente, mal formado, revocado o de una empresa eliminada.
403insufficient_scopeEl token no tiene el alcance (scope) que exige el endpoint.
403module_not_enabledTu empresa no tiene habilitado el módulo del endpoint (p. ej. Cotizaciones).
429rate_limitedMás de 120 solicitudes por minuto con el mismo token.
GET/api/v1/products/{id}products:read

Devuelve un producto por UUID.

HTTPCódigoCuándo
404not_foundEl producto no existe o pertenece a otra empresa.
401invalid_tokenToken ausente, mal formado, revocado o de una empresa eliminada.
403insufficient_scopeEl token no tiene el alcance (scope) que exige el endpoint.
403module_not_enabledTu empresa no tiene habilitado el módulo del endpoint (p. ej. Cotizaciones).
429rate_limitedMás de 120 solicitudes por minuto con el mismo token.
PUT/api/v1/products/{id}products:write

Actualizació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.

Petición
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 }'
HTTPCódigoCuándo
400invalid_jsonEl cuerpo no es JSON válido.
400validationCampos inválidos.
404not_foundEl producto no existe o pertenece a otra empresa.
409duplicate_codeEl nuevo código ya pertenece a otro producto.
409duplicate_barcodeEl nuevo código de barras ya pertenece a otro producto.
401invalid_tokenToken ausente, mal formado, revocado o de una empresa eliminada.
403insufficient_scopeEl token no tiene el alcance (scope) que exige el endpoint.
403module_not_enabledTu empresa no tiene habilitado el módulo del endpoint (p. ej. Cotizaciones).
429rate_limitedMás de 120 solicitudes por minuto con el mismo token.
DELETE/api/v1/products/{id}products:write

Borrado 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 }.

HTTPCódigoCuándo
404not_foundEl producto no existe o pertenece a otra empresa.
401invalid_tokenToken ausente, mal formado, revocado o de una empresa eliminada.
403insufficient_scopeEl token no tiene el alcance (scope) que exige el endpoint.
403module_not_enabledTu empresa no tiene habilitado el módulo del endpoint (p. ej. Cotizaciones).
429rate_limitedMás de 120 solicitudes por minuto con el mismo token.

Clientes

POST/api/v1/customerscustomers:write

Crea 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.

CampoTipoRequeridoDescripción
doc_typestring"1" DNI · "4" CE · "6" RUC · "7" pasaporte. ("0" no se cataloga.)
doc_numberstringValidado según el tipo (RUC: dígito verificador; DNI: 8 dígitos).
namestringMáx. 500.
emailstringNoEmail válido, máx. 320.
phonestringNoDígitos, +, (), espacios y guiones; 6–20 caracteres.
addressstringNoMáx. 500.
custom_fieldsobjectNoCampos 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.
Petición
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" }'
Respuesta 201
{
  "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"
}
HTTPCódigoCuándo
400invalid_jsonEl cuerpo no es JSON válido.
400validationCampos o documento de identidad inválidos.
400custom_fields_invalidCampos personalizados inválidos (detalle por clave en details).
403module_not_enabledTu plan no incluye el módulo Campos personalizados (solo si envías custom_fields).
401invalid_tokenToken ausente, mal formado, revocado o de una empresa eliminada.
403insufficient_scopeEl token no tiene el alcance (scope) que exige el endpoint.
403module_not_enabledTu empresa no tiene habilitado el módulo del endpoint (p. ej. Cotizaciones).
429rate_limitedMás de 120 solicitudes por minuto con el mismo token.
GET/api/v1/customerscustomers:read

Lista o busca clientes, ordenados por nombre.

ParámetroDescripción
qBúsqueda por nombre o número de documento.
pagePágina, desde 1.
per_pagePor defecto 50, máximo 100.
Respuesta 200
{ "data": [ { "id": "…", "doc_type": "6", "doc_number": "20512345678", "name": "ACME PERU S.A.C.", … } ],
  "page": 1, "per_page": 50, "total": 1 }
HTTPCódigoCuándo
401invalid_tokenToken ausente, mal formado, revocado o de una empresa eliminada.
403insufficient_scopeEl token no tiene el alcance (scope) que exige el endpoint.
403module_not_enabledTu empresa no tiene habilitado el módulo del endpoint (p. ej. Cotizaciones).
429rate_limitedMás de 120 solicitudes por minuto con el mismo token.
GET/api/v1/customers/{id}customers:read

Un cliente por id, con la misma forma que la respuesta del POST (incluye phone y custom_fields).

HTTPCódigoCuándo
404not_foundEl cliente no existe o no es de tu empresa.
401invalid_tokenToken ausente, mal formado, revocado o de una empresa eliminada.
403insufficient_scopeEl token no tiene el alcance (scope) que exige el endpoint.
403module_not_enabledTu empresa no tiene habilitado el módulo del endpoint (p. ej. Cotizaciones).
429rate_limitedMás de 120 solicitudes por minuto con el mismo token.
PATCH/api/v1/customers/{id}customers:write

Edita 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).

Petición
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 } }'
HTTPCódigoCuándo
400validationCampos inválidos.
400custom_fields_invalidCampos personalizados inválidos (detalle por clave en details).
403module_not_enabledTu plan no incluye el módulo Campos personalizados (solo si envías custom_fields).
404not_foundEl cliente no existe o no es de tu empresa.
401invalid_tokenToken ausente, mal formado, revocado o de una empresa eliminada.
403insufficient_scopeEl token no tiene el alcance (scope) que exige el endpoint.
403module_not_enabledTu empresa no tiene habilitado el módulo del endpoint (p. ej. Cotizaciones).
429rate_limitedMá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).

GET/api/v1/seriescualquier token

Series 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.

Respuesta 200
{
  "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…" }
  ]
}
HTTPCódigoCuándo
401invalid_tokenToken ausente, mal formado, revocado o de una empresa eliminada.
403insufficient_scopeEl token no tiene el alcance (scope) que exige el endpoint.
403module_not_enabledTu empresa no tiene habilitado el módulo del endpoint (p. ej. Cotizaciones).
429rate_limitedMás de 120 solicitudes por minuto con el mismo token.
GET/api/v1/usagecualquier token

Consumo del mes en curso (calendario America/Lima) contra el límite del plan.

Respuesta 200
{
  "plan": "pro",
  "planStatus": "active",
  "used": 137,
  "limit": 3000,
  "remaining": 2863,
  "periodStart": "2026-06-01T05:00:00.000Z"
}
HTTPCódigoCuándo
401invalid_tokenToken ausente, mal formado, revocado o de una empresa eliminada.
403insufficient_scopeEl token no tiene el alcance (scope) que exige el endpoint.
403module_not_enabledTu empresa no tiene habilitado el módulo del endpoint (p. ej. Cotizaciones).
429rate_limitedMás de 120 solicitudes por minuto con el mismo token.
GET/api/v1/companycualquier token

Perfil público de la empresa autenticada (nunca expone credenciales).

Respuesta 200
{
  "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"
}
HTTPCódigoCuándo
401invalid_tokenToken ausente, mal formado, revocado o de una empresa eliminada.
403insufficient_scopeEl token no tiene el alcance (scope) que exige el endpoint.
403module_not_enabledTu empresa no tiene habilitado el módulo del endpoint (p. ej. Cotizaciones).
429rate_limitedMás de 120 solicitudes por minuto con el mismo token.