Facturador de Innobyte

Emite comprobantes SUNAT con una llamada HTTP

El Facturador emite boletas, facturas y notas de crédito o débito electrónicas a través de SEE-SOL, con la Clave SOL del propio emisor. Tu sistema envía la venta en JSON y recibe la serie y el número que asigna SUNAT.

URL base https://facturador.innobyte.pe
POST /v1/receipts responde con la serie y el número

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.

  1. Abre una sesiónPOST /v1/sessions

    Con el DNI y la Clave SOL del emisor. Recibes un token válido por 1 hora.

  2. EmitePOST /v1/receipts

    Con la venta en JSON y un Idempotency-Key único. SUNAT asigna la serie y el número.

  3. DescargaGET /v1/files

    El 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.pdf

Antes de empezar

Necesitas tres cosas:

  • Una API key del Facturador. Se envía en el header x-api-key en 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").

Cada comprobante emitido es real

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:

HeaderQué demuestraDónde se usa
x-api-keyQue 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.

CampoTipoDescripción
mode"dni" | "ruc"obligatorioCómo entra el emisor a SUNAT. Hoy se recomienda dni.
dnistringsi mode=dni8 dígitos.
rucstringsi mode=ruc11 dígitos.
solUserstringsi mode=rucUsuario SOL secundario.
passwordstringobligatorioLa Clave SOL. Se usa una vez para abrir la sesión y no se guarda.
Respuesta 200
{
  "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.

Protege la Clave SOL

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

201 RepetidaLa primera emisión terminó bien.

Recibes la misma serie y número. No hagas nada más.

409 En cursoLa primera petición aún se está procesando.

Espera unos segundos y reintenta con la misma key.

409 InciertaLa primera petición falló sin saber si SUNAT emitió.

Revisa en SUNAT si el comprobante existe antes de usar una key nueva.

422 Otro bodyLa key ya se usó con datos distintos.

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íasSe emite como
2026-11-01T09:30:001 de noviembre, 09:30 en Lima
2026-11-01T14:30:00Z1 de noviembre, 09:30 en Lima
2026-11-011 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:

Por línea
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 igv

Ejemplo 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:

Respuesta 400
{
  "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ódigoSignificaQué hacer
200 201Correcto. 201 = comprobante emitido.Guarda la serie y el número.
400La petición está mal formada o no cumple las reglas (detalle en issues).Corrige los datos. No reintentes igual.
401Falta el token, venció, o la Clave SOL es incorrecta.Abre una sesión nueva y repite.
403API key ausente o incorrecta.Revisa x-api-key.
404Ruta inexistente, o documento no encontrado en SUNAT.Revisa la URL o el documento.
409Conflicto de idempotencia (en curso o incierta).Ver idempotencia.
422SUNAT rechazó el comprobante (su respuesta va en sunat), o la key se usó con otro body.Corrige según el mensaje de SUNAT.
429Demasiadas peticiones.Espera y reintenta con pausa creciente.
502SUNAT no está disponible o devolvió un error interno.Reintenta con la misma key.
504SUNAT no respondió a tiempo.Reintenta con la misma key.
500Error del Facturador.Reintenta. Si persiste, avísanos.

Reintentos

Una política simple y segura para las emisiones:

  1. Genera el Idempotency-Key una vez por venta, antes del primer intento, y guárdalo con la venta.
  2. Ante un error de red, 502, 504 o 429, reintenta con la misma key tras 2, 4 y 8 segundos.
  3. Ante un 401, abre una sesión nueva y reintenta una vez con la misma key.
  4. Ante un 400 o 422, no reintentes: muestra el error para corregir la venta.
  5. 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

10/speticiones sostenidas, con picos de hasta 20
29 stiempo máximo por petición
1 horavida del token de sesión
7 díasmemoria de cada Idempotency-Key

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ámetroTipoDescripción
autofillbooleanPor 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

CampoTipoDescripción
solUserstringobligatorioUsuario SOL del emisor (el DNI cuando entra con DNI).
issuer.documentstringobligatorioRUC del emisor, 11 dígitos. Debe ser el RUC de la sesión.
issuer.namestringopcionalRazón social. Se completa con autofill.
customer.documentstringobligatorioDNI (8), RUC (11), otro documento, o "-" para un cliente sin identificar.
customer.documentTypeCodestringopcional1 DNI, 4 carné de extranjería, 6 RUC, 7 pasaporte, - sin documento. Si falta, se deduce de la longitud.
customer.namestringopcionalNombre del cliente. Usa "VARIOS" si no está identificado.
currencystringobligatorioMoneda: PEN o USD.
issueDatestringobligatorioFecha y hora de emisión. Hora de Perú si no lleva zona.
igvRatenumberobligatorioTasa de IGV en %: 18 en régimen general o MYPE, 0 en Nuevo RUS.
tipoOperacionstringobligatorio0101 venta interna, 0113 emisor del Nuevo RUS.
linesarrayobligatorioAl menos una línea (ver abajo).
globalDiscountnumberopcionalDescuento global en soles.
itinerantobjectopcionalLugar de una venta itinerante (ambulante): ubigeoCode, department, province, district, line.

Cada línea

CampoTipoDescripción
codestringobligatorioCódigo del producto: de 3 a 15 letras o números.
descriptionstringobligatorioDe 3 a 124 caracteres, sin emojis.
unitCodestringobligatorioUnidad de medida: NIU unidad, KGM kilo, ZZ servicio… (ver catálogos).
itemTypeCodestringobligatorioTipo de ítem del catálogo del emisor, por ejemplo Z.
quantitynumberobligatorioMayor que 0.
unitValuenumberobligatorioValor unitario sin IGV, en la moneda del comprobante.
affectationstringobligatoriogravado (lleva IGV), exonerado o inafecto.
discountnumberopcionalDescuento de la línea; menor que el valor de la línea.
bagobjectopcionalImpuesto a las bolsas plásticas (ICBPER): { "count": 2, "taxTotal": 0.4 }.
boleta.json · Nuevo RUS, cliente sin identificar
{
  "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" }
  ]
}
Respuesta 201
{
  "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.
Ventas grandes a clientes sin identificar

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) y dueDate (fecha de vencimiento).
  • En régimen general o MYPE las líneas suelen ser gravado con igvRate: 18.
Factura con IGV 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.

CampoTipoDescripción
solUserstringobligatorioUsuario SOL del emisor.
issuer.documentstringobligatorioRUC del emisor.
reference.seriesstringobligatorioSerie del comprobante que se modifica, por ejemplo B001.
reference.numberstringobligatorioSu número.
reference.comprobanteTypeCodestringobligatorio03 si es una boleta, 01 si es una factura.
reasonCodestringobligatorioMotivo 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.
reasonTextstringobligatorioSustento, de 5 caracteres como mínimo.
issueDatestringobligatorioFecha de la nota.
amountnumberobligatorioMonto del ajuste, mayor que 0.
itemDescriptionstringopcionalDescripción de la línea; por defecto, el sustento.
Nota de crédito por devolución
{
  "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ámetroFormatoDescripción
ruc11 dígitosRUC del emisor.
codCpe01 | 03 | 07 | 08Tipo de comprobante.
series4 caracteresSerie devuelta al emitir, por ejemplo B001.
numberhasta 8 dígitosNúmero devuelto al emitir.
dateAAAA-MM-DDFecha de emisión.
formatPDF | XML | CDRPor defecto PDF.
Respuesta 200
{ "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.

Respuesta 200
{
  "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.

Respuesta 200
{
  "indValidoEmision": true,
  "indPrimeraEmision": true,
  "indEmisionIcbper": false,
  "indPermiteEmitirADni": false
}
CampoSignifica cuando es true
indValidoEmisionEl RUC puede emitir este comprobante.
indPrimeraEmisionTodavía no emitió ninguno de este tipo.
indEmisionIcbperPuede cobrar el impuesto a las bolsas plásticas.
indPermiteEmitirADniPuede 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ámetroFormatoDescripción
ruc11 dígitosRUC del emisor.
pageentero ≥ 1Página, por defecto 1.
per_page1 a 100Resultados por página, por defecto 20.
indIcbperbooleanSolo 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.

Respuesta 200 · numParametro=1002
[
  { "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

ConceptoCódigos
Tipo de comprobante01 factura, 03 boleta, 07 nota de crédito, 08 nota de débito
Tipo de operación0101 venta interna, 0113 emisor del Nuevo RUS (solo boletas)
Afectación del IGVgravado (código 10), exonerado (20), inafecto (30)
Documento de identidad1 DNI, 4 carné de extranjería, 6 RUC, 7 pasaporte, - sin documento
Motivos de nota frecuentes04 descuento global, 11 corrección de error, 13 ajuste de montos o fecha de pago
Monedas y unidadesCatá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.

facturador.js
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:

Descargar openapi.yaml