Inicio rápido
Emitir un comprobante son tres llamadas: abrir una sesión con la Clave SOL del emisor, enviar la venta y, si lo necesitas, descargar el PDF para imprimirlo o enviarlo por WhatsApp.
- Abre una sesión
POST /v1/sessionsCon el DNI y la Clave SOL del emisor. Recibes un token válido por 1 hora.
- Emite
POST /v1/receiptsCon la venta en JSON y un
Idempotency-Keyúnico. SUNAT asigna la serie y el número. - Descarga
GET /v1/filesEl PDF o el XML del comprobante, en base64.
# 1. Sesión: guarda el token (vale 1 hora)
TOKEN=$(curl -s https://facturador.innobyte.pe/v1/sessions \
-H "x-api-key: $FACTURADOR_API_KEY" \
-H "content-type: application/json" \
-d '{"mode":"dni","dni":"45678901","password":"'"$CLAVE_SOL"'"}' | jq -r .accessToken)
# 2. Emitir una boleta
curl -s https://facturador.innobyte.pe/v1/receipts \
-H "x-api-key: $FACTURADOR_API_KEY" \
-H "authorization: Bearer $TOKEN" \
-H "idempotency-key: venta-000123" \
-H "content-type: application/json" \
-d @boleta.json
# 3. Descargar el PDF
curl -s "https://facturador.innobyte.pe/v1/files?ruc=10456789012&codCpe=03&series=B001&number=1&date=2026-11-01&format=PDF" \
-H "x-api-key: $FACTURADOR_API_KEY" \
-H "authorization: Bearer $TOKEN" | jq -r .base64 | base64 -d > boleta.pdfconst BASE = 'https://facturador.innobyte.pe';
const apiKey = process.env.FACTURADOR_API_KEY;
// 1. Sesión
const session = await fetch(`${BASE}/v1/sessions`, {
method: 'POST',
headers: { 'x-api-key': apiKey, 'content-type': 'application/json' },
body: JSON.stringify({ mode: 'dni', dni: '45678901', password: claveSol }),
}).then((r) => r.json());
// 2. Emitir
const res = await fetch(`${BASE}/v1/receipts`, {
method: 'POST',
headers: {
'x-api-key': apiKey,
authorization: `Bearer ${session.accessToken}`,
'idempotency-key': `venta-${venta.id}`,
'content-type': 'application/json',
},
body: JSON.stringify(boleta),
});
const { series, number } = await res.json(); // { series: 'B001', number: '1' }Antes de empezar
Necesitas tres cosas:
- Una API key del Facturador. Se envía en el header
x-api-keyen todas las llamadas. Solo la usa tu backend: nunca la pongas en una app móvil ni en el navegador. - La Clave SOL del emisor. Es el usuario y la contraseña con los que el emisor entra a SUNAT Operaciones en Línea. El Facturador la usa para abrir la sesión y no la guarda.
- Un RUC habilitado para emitir comprobantes electrónicos desde SEE-SOL. Puedes comprobarlo con condiciones de emisión.
Todas las peticiones y respuestas son JSON en UTF-8. Los montos van en soles como números decimales (2.5, no "2.50").
No existe un modo de prueba: todo lo que emites queda registrado en SUNAT con efecto tributario y no se puede borrar. Para corregir un comprobante se emite una nota de crédito. Mientras integras, usa los endpoints de consulta, que no emiten nada.
Autenticación
Hay dos credenciales, y cada una responde a una pregunta distinta:
| Header | Qué demuestra | Dónde se usa |
|---|---|---|
x-api-key | Que tu sistema puede usar el Facturador. | Todas las llamadas. |
authorization: Bearer … | Que el emisor inició sesión en SUNAT. Define el RUC con el que se emite. | Todo, excepto /v1/sessions. |
Abrir una sesión
POST/v1/sessions
Requiere x-api-key.
| Campo | Tipo | Descripción | |
|---|---|---|---|
mode | "dni" | "ruc" | obligatorio | Cómo entra el emisor a SUNAT. Hoy se recomienda dni. |
dni | string | si mode=dni | 8 dígitos. |
ruc | string | si mode=ruc | 11 dígitos. |
solUser | string | si mode=ruc | Usuario SOL secundario. |
password | string | obligatorio | La Clave SOL. Se usa una vez para abrir la sesión y no se guarda. |
{
"accessToken": "eyJraWQiOiJhcGkuc3VuYXQuZ29iLnBlLmtpZDAwMSIs…",
"tokenType": "JWT",
"expiresIn": 3600
}Reutiliza el token durante su hora de vida: abrir una sesión por cada venta es lento y multiplica los intentos de login contra SUNAT. Cuando cualquier llamada responda 401, abre una sesión nueva y repite la llamada.
Con ella se puede emitir a nombre del contribuyente. Guárdala cifrada, no la escribas en logs y envíala solo a /v1/sessions. Si un login falla varias veces seguidas, SUNAT puede bloquear la clave: no reintentes logins fallidos en bucle.
Idempotencia: nunca emitir dos veces
Si una emisión se corta por un timeout, no sabes si SUNAT llegó a registrar el comprobante. Reintentar a ciegas podría crear una segunda boleta, real e imborrable. Por eso, todas las rutas de emisión exigen el header Idempotency-Key.
- Usa un valor único por venta y estable entre reintentos, por ejemplo el id de la venta en tu sistema:
venta-000123. Admite hasta 128 caracteres. - Si repites la petición con la misma key y el mismo body, recibes la respuesta original con el header
Idempotent-Replayed: true, y no se emite nada nuevo. - Las keys se recuerdan durante 7 días.
Qué responde un reintento
Recibes la misma serie y número. No hagas nada más.
Espera unos segundos y reintenta con la misma key.
Revisa en SUNAT si el comprobante existe antes de usar una key nueva.
Es un error de tu sistema: cada venta necesita su propia key.
Si la emisión falla por una razón que garantiza que no se emitió nada (datos inválidos o rechazo de SUNAT), la key se libera y puedes reintentar con ella después de corregir el problema.
Fechas y montos
Fechas en hora de Perú
Las fechas se envían en ISO 8601. Si no indicas zona horaria, se interpretan en hora de Perú (UTC−5), igual que las muestra SUNAT:
| Envías | Se emite como |
|---|---|
2026-11-01T09:30:00 | 1 de noviembre, 09:30 en Lima |
2026-11-01T14:30:00Z | 1 de noviembre, 09:30 en Lima |
2026-11-01 | 1 de noviembre, 00:00 en Lima |
Cómo se calculan los montos
Envías el valor unitario sin IGV (unitValue) y la cantidad. El Facturador calcula el resto con el mismo redondeo que SUNAT, a dos decimales:
importeVenta = redondear(unitValue × quantity − discount)
igv = redondear(importeVenta × igvRate / 100) // solo si es "gravado"
precioUnitario = redondear(importeVenta × (1 + igvRate / 100) / quantity)
total = suma de importeVenta + suma de igvEjemplo con IGV 18%: 2 unidades a S/ 2.50 dan un importe de 5.00, un IGV de 0.90 y un total de S/ 5.90. Un emisor del Nuevo RUS no cobra IGV: usa igvRate: 0, tipoOperacion: "0113" y líneas exonerado.
Errores
Todos los errores devuelven JSON con un mensaje en error. Los de validación incluyen la lista completa de problemas en issues, para que los corrijas de una sola vez:
{
"error": "invalid comprobante: lines[0].quantity must be a number; customer.document must be a non-empty string",
"issues": [
"lines[0].quantity must be a number",
"customer.document must be a non-empty string"
]
}| Código | Significa | Qué hacer |
|---|---|---|
| 200 201 | Correcto. 201 = comprobante emitido. | Guarda la serie y el número. |
| 400 | La petición está mal formada o no cumple las reglas (detalle en issues). | Corrige los datos. No reintentes igual. |
| 401 | Falta el token, venció, o la Clave SOL es incorrecta. | Abre una sesión nueva y repite. |
| 403 | API key ausente o incorrecta. | Revisa x-api-key. |
| 404 | Ruta inexistente, o documento no encontrado en SUNAT. | Revisa la URL o el documento. |
| 409 | Conflicto de idempotencia (en curso o incierta). | Ver idempotencia. |
| 422 | SUNAT rechazó el comprobante (su respuesta va en sunat), o la key se usó con otro body. | Corrige según el mensaje de SUNAT. |
| 429 | Demasiadas peticiones. | Espera y reintenta con pausa creciente. |
| 502 | SUNAT no está disponible o devolvió un error interno. | Reintenta con la misma key. |
| 504 | SUNAT no respondió a tiempo. | Reintenta con la misma key. |
| 500 | Error del Facturador. | Reintenta. Si persiste, avísanos. |
Reintentos
Una política simple y segura para las emisiones:
- Genera el
Idempotency-Keyuna vez por venta, antes del primer intento, y guárdalo con la venta. - Ante un error de red, 502, 504 o 429, reintenta con la misma key tras 2, 4 y 8 segundos.
- Ante un 401, abre una sesión nueva y reintenta una vez con la misma key.
- Ante un 400 o 422, no reintentes: muestra el error para corregir la venta.
- Si llegas a un 409 incierto, marca la venta para revisión manual.
Configura en tu cliente HTTP un timeout de al menos 30 segundos para las emisiones: SUNAT a veces tarda.
Límites
Emitir una boleta
POST/v1/receipts
Requiere x-api-key, authorization e Idempotency-Key. Responde 201.
La boleta de venta (código 03) es el comprobante para consumidores finales. SUNAT asigna la serie y el número: no los envíes.
Parámetros de consulta
| Parámetro | Tipo | Descripción |
|---|---|---|
autofill | boolean | Por defecto true: si falta el nombre o el ubigeo del emisor o del cliente, se completan con los datos de SUNAT. Envía false para usar solo lo que mandas. |
Body
| Campo | Tipo | Descripción | |
|---|---|---|---|
solUser | string | obligatorio | Usuario SOL del emisor (el DNI cuando entra con DNI). |
issuer.document | string | obligatorio | RUC del emisor, 11 dígitos. Debe ser el RUC de la sesión. |
issuer.name | string | opcional | Razón social. Se completa con autofill. |
customer.document | string | obligatorio | DNI (8), RUC (11), otro documento, o "-" para un cliente sin identificar. |
customer.documentTypeCode | string | opcional | 1 DNI, 4 carné de extranjería, 6 RUC, 7 pasaporte, - sin documento. Si falta, se deduce de la longitud. |
customer.name | string | opcional | Nombre del cliente. Usa "VARIOS" si no está identificado. |
currency | string | obligatorio | Moneda: PEN o USD. |
issueDate | string | obligatorio | Fecha y hora de emisión. Hora de Perú si no lleva zona. |
igvRate | number | obligatorio | Tasa de IGV en %: 18 en régimen general o MYPE, 0 en Nuevo RUS. |
tipoOperacion | string | obligatorio | 0101 venta interna, 0113 emisor del Nuevo RUS. |
lines | array | obligatorio | Al menos una línea (ver abajo). |
globalDiscount | number | opcional | Descuento global en soles. |
itinerant | object | opcional | Lugar de una venta itinerante (ambulante): ubigeoCode, department, province, district, line. |
Cada línea
| Campo | Tipo | Descripción | |
|---|---|---|---|
code | string | obligatorio | Código del producto: de 3 a 15 letras o números. |
description | string | obligatorio | De 3 a 124 caracteres, sin emojis. |
unitCode | string | obligatorio | Unidad de medida: NIU unidad, KGM kilo, ZZ servicio… (ver catálogos). |
itemTypeCode | string | obligatorio | Tipo de ítem del catálogo del emisor, por ejemplo Z. |
quantity | number | obligatorio | Mayor que 0. |
unitValue | number | obligatorio | Valor unitario sin IGV, en la moneda del comprobante. |
affectation | string | obligatorio | gravado (lleva IGV), exonerado o inafecto. |
discount | number | opcional | Descuento de la línea; menor que el valor de la línea. |
bag | object | opcional | Impuesto a las bolsas plásticas (ICBPER): { "count": 2, "taxTotal": 0.4 }. |
{
"solUser": "45678901",
"issuer": { "document": "10456789012" },
"customer": { "documentTypeCode": "-", "document": "-", "name": "VARIOS" },
"currency": "PEN",
"issueDate": "2026-11-01T09:30:00",
"igvRate": 0,
"tipoOperacion": "0113",
"lines": [
{ "code": "PAN001", "description": "PAN FRANCES", "unitCode": "NIU", "itemTypeCode": "Z",
"quantity": 10, "unitValue": 0.3, "affectation": "exonerado" },
{ "code": "PAN002", "description": "PAN DE YEMA", "unitCode": "NIU", "itemTypeCode": "Z",
"quantity": 4, "unitValue": 0.5, "affectation": "exonerado" }
]
}{
"ruc": "10456789012",
"kind": "receipt",
"codCpe": "03",
"series": "B001",
"number": "1"
}Reglas que se validan antes de enviar a SUNAT
- El cliente no puede ser el mismo emisor.
- Si el cliente tiene DNI, son 8 dígitos; si tiene RUC, son 11.
- Un cliente con carné de extranjería o pasaporte necesita nombre.
- La cantidad y el valor unitario son mayores que 0, y el valor es menor que 9 999 999 999.99.
SUNAT exige identificar al comprador cuando el total de la boleta supera cierto monto (S/ 700 a la fecha de esta guía). Si no lo identificas, SUNAT rechaza la boleta con un 422. Confirma el monto vigente con tu contador.
Emitir una factura
POST/v1/invoices
Requiere x-api-key, authorization e Idempotency-Key. Responde 201.
La factura (código 01) usa el mismo body que la boleta, con estas diferencias:
- El cliente es siempre una empresa o negocio con RUC de 11 dígitos y nombre.
- Admite
payment({ "onCredit": true }para una venta al crédito) ydueDate(fecha de vencimiento). - En régimen general o MYPE las líneas suelen ser
gravadoconigvRate: 18.
{
"solUser": "45678901",
"issuer": { "document": "20601234567" },
"customer": { "document": "20509876543", "name": "DISTRIBUIDORA EL HORNO SAC" },
"currency": "PEN",
"issueDate": "2026-11-03T16:00:00",
"igvRate": 18,
"tipoOperacion": "0101",
"payment": { "onCredit": false },
"lines": [
{ "code": "HAR050", "description": "HARINA PREPARADA 50 KG", "unitCode": "NIU", "itemTypeCode": "Z",
"quantity": 2, "unitValue": 2.5, "affectation": "gravado" }
]
}Esta línea da un importe de S/ 5.00, un IGV de S/ 0.90 y un total de S/ 5.90.
Notas de crédito y débito
POST/v1/credit-notes
POST/v1/debit-notes
Requieren x-api-key, authorization e Idempotency-Key. Responden 201.
Una nota de crédito (07) reduce o anula el monto de un comprobante ya emitido; una nota de débito (08) lo aumenta, por ejemplo por intereses. Así se corrige un comprobante, porque no se pueden borrar.
| Campo | Tipo | Descripción | |
|---|---|---|---|
solUser | string | obligatorio | Usuario SOL del emisor. |
issuer.document | string | obligatorio | RUC del emisor. |
reference.series | string | obligatorio | Serie del comprobante que se modifica, por ejemplo B001. |
reference.number | string | obligatorio | Su número. |
reference.comprobanteTypeCode | string | obligatorio | 03 si es una boleta, 01 si es una factura. |
reasonCode | string | obligatorio | Motivo según el catálogo de SUNAT; por ejemplo 04 descuento global, 11 corrección de error, 13 ajuste de montos o fecha de pago. |
reasonText | string | obligatorio | Sustento, de 5 caracteres como mínimo. |
issueDate | string | obligatorio | Fecha de la nota. |
amount | number | obligatorio | Monto del ajuste, mayor que 0. |
itemDescription | string | opcional | Descripción de la línea; por defecto, el sustento. |
{
"solUser": "45678901",
"issuer": { "document": "10456789012" },
"reference": { "series": "B001", "number": "1", "comprobanteTypeCode": "03" },
"reasonCode": "04",
"reasonText": "Devolucion de producto",
"issueDate": "2026-11-02T10:00:00",
"amount": 1
}Descargar el PDF o el XML
GET/v1/files
Requiere x-api-key y authorization.
| Parámetro | Formato | Descripción |
|---|---|---|
ruc | 11 dígitos | RUC del emisor. |
codCpe | 01 | 03 | 07 | 08 | Tipo de comprobante. |
series | 4 caracteres | Serie devuelta al emitir, por ejemplo B001. |
number | hasta 8 dígitos | Número devuelto al emitir. |
date | AAAA-MM-DD | Fecha de emisión. |
format | PDF | XML | CDR | Por defecto PDF. |
{ "base64": "JVBERi0xLjQKJcfsj6IKNSAwIG9iago8PC9MZW5n…", "fileType": "PDF" }Buscar un cliente
GET/v1/parties?document=20509876543
Requiere x-api-key y authorization.
Devuelve el nombre y la ubicación de un DNI (8 dígitos) o un RUC (11) según SUNAT, para mostrarlos en tu caja antes de emitir. Responde 404 si SUNAT no conoce el documento.
{
"document": "20509876543",
"name": "DISTRIBUIDORA EL HORNO SAC",
"address": { "ubigeoCode": "150103", "department": "LIMA", "province": "LIMA", "district": "ATE" }
}Condiciones de emisión
GET/v1/emission-conditions?ruc=10456789012&comprobante=03
Requiere x-api-key y authorization.
Indica si el RUC puede emitir ese tipo de comprobante. Es la mejor forma de comprobar que la integración funciona sin emitir nada.
{
"indValidoEmision": true,
"indPrimeraEmision": true,
"indEmisionIcbper": false,
"indPermiteEmitirADni": false
}| Campo | Significa cuando es true |
|---|---|
indValidoEmision | El RUC puede emitir este comprobante. |
indPrimeraEmision | Todavía no emitió ninguno de este tipo. |
indEmisionIcbper | Puede cobrar el impuesto a las bolsas plásticas. |
indPermiteEmitirADni | Puede emitir a un cliente identificado con DNI. Si es false, emite con cliente "-". |
Productos
GET/v1/products?ruc=10456789012
Requiere x-api-key y authorization.
El catálogo de productos que el emisor registró en SUNAT. Devuelve [] si está vacío.
| Parámetro | Formato | Descripción |
|---|---|---|
ruc | 11 dígitos | RUC del emisor. |
page | entero ≥ 1 | Página, por defecto 1. |
per_page | 1 a 100 | Resultados por página, por defecto 20. |
indIcbper | boolean | Solo productos con impuesto a las bolsas. |
Catálogos SUNAT
GET/v1/parameters?numParametro=1002
Requiere x-api-key y authorization.
Los catálogos vigentes que publica SUNAT: tipos de documento, monedas, unidades de medida, motivos de nota y otros. Sin numParametro devuelve el conjunto completo; con él, solo los códigos indicados, separados por comas.
[
{ "numParametro": "1002", "lstParam": [
{ "codParametro": "1", "desParametro": "DOCUMENTO NACIONAL DE IDENTIDAD", "estado": "1" },
{ "codParametro": "4", "desParametro": "CARNET DE EXTRANJERÍA", "estado": "1" },
{ "codParametro": "6", "desParametro": "REGISTRO ÚNICO DE CONTRIBUYENTES", "estado": "1" },
{ "codParametro": "7", "desParametro": "PASAPORTE", "estado": "1" }
] }
]Códigos y catálogos
| Concepto | Códigos |
|---|---|
| Tipo de comprobante | 01 factura, 03 boleta, 07 nota de crédito, 08 nota de débito |
| Tipo de operación | 0101 venta interna, 0113 emisor del Nuevo RUS (solo boletas) |
| Afectación del IGV | gravado (código 10), exonerado (20), inafecto (30) |
| Documento de identidad | 1 DNI, 4 carné de extranjería, 6 RUC, 7 pasaporte, - sin documento |
| Motivos de nota frecuentes | 04 descuento global, 11 corrección de error, 13 ajuste de montos o fecha de pago |
| Monedas y unidades | Catálogos vivos: consúltalos con /v1/parameters |
Cliente completo en JavaScript
Un cliente para Node.js 18 o superior que reutiliza la sesión, renueva el token ante un 401 y reintenta las emisiones de forma segura. Puedes copiarlo tal cual a tu backend.
const BASE = 'https://facturador.innobyte.pe';
const RETRYABLE = new Set([429, 502, 504]);
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
export class Facturador {
constructor({ apiKey, dni, claveSol }) {
this.apiKey = apiKey;
this.credentials = { mode: 'dni', dni, password: claveSol };
this.token = null;
this.expiresAt = 0;
}
// Reutiliza el token hasta 5 minutos antes de que venza.
async session() {
if (this.token && Date.now() < this.expiresAt) return this.token;
const res = await this.call('POST', '/v1/sessions', { body: this.credentials });
this.token = res.accessToken;
this.expiresAt = Date.now() + (res.expiresIn - 300) * 1000;
return this.token;
}
// ventaId identifica la venta en tu sistema: se usa como Idempotency-Key.
emitirBoleta(ventaId, boleta) { return this.emitir('/v1/receipts', ventaId, boleta); }
emitirFactura(ventaId, factura) { return this.emitir('/v1/invoices', ventaId, factura); }
async emitir(path, ventaId, body) {
const key = `venta-${ventaId}`;
for (let intento = 0; ; intento++) {
try {
return await this.call('POST', path, { body, key, auth: true });
} catch (err) {
if (err.status === 401 && intento === 0) { this.token = null; continue; }
const transitorio = !err.status || RETRYABLE.has(err.status);
if (!transitorio || intento >= 3) throw err; // 400, 409, 422: no reintentar
await sleep(2000 * 2 ** intento);
}
}
}
async call(method, path, { body, key, auth } = {}) {
const headers = { 'x-api-key': this.apiKey, 'content-type': 'application/json' };
if (auth) headers.authorization = `Bearer ${await this.session()}`;
if (key) headers['idempotency-key'] = key;
const res = await fetch(BASE + path, {
method, headers,
body: body ? JSON.stringify(body) : undefined,
signal: AbortSignal.timeout(35_000),
});
const data = await res.json();
if (!res.ok) throw Object.assign(new Error(data.error), { status: res.status, data });
return data;
}
}
// Uso
const facturador = new Facturador({ apiKey: process.env.FACTURADOR_API_KEY, dni: '45678901', claveSol });
const { series, number } = await facturador.emitirBoleta(venta.id, boleta);Especificación OpenAPI
El contrato completo está en formato OpenAPI 3.1. Impórtalo en Postman, Insomnia o tu generador de clientes: