factsimil
Todo lo que incluyeCómo funcionaPreciosBlogPreguntas frecuentesNosotrosContacto
Ingresar
Crear cuenta
Desarrolladores

Documentación de la API

Última actualización: 17 de agosto de 2026

En esta página
  • Autenticación
  • Crear un comprobante
  • Firmar y enviar a SUNAT
  • Consultar el estado
  • XML, PDF y enlaces compartibles
  • Multiempresa (header x-tenant-id)
  • Activar producción (salir de beta)
  • Siguientes pasos

factsimil es, antes que nada, una API REST. El panel web que ves en módulos es un cliente más de esa misma API — si estás integrando facturación electrónica en un marketplace, un ERP vertical o cualquier plataforma propia, esto es lo que necesitás saber para empezar.

Esta página cubre el flujo real de comprobantes. Para la referencia completa de cada endpoint (parámetros opcionales, códigos de error, catálogos SUNAT completos), consultá la documentación OpenAPI pública.

1. Autenticación

Cada request autenticado necesita un header Authorization: Bearer <token> — pero hay dos formas distintas de conseguir ese token, según qué estés construyendo.

Credenciales de integración — para conectar tu plataforma (marketplace, ERP, sistema propio)

Es el flujo que probablemente necesitás si llegaste a esta página buscando integrar facturación electrónica en tu propio producto. Cada sistema externo que hable con factsimil tiene su propia credencial, generada desde Mi Empresa → Integraciones (no hace falta pedírnosla) — le ponés un nombre (ej. "mi ERP de ventas") y se muestra en pantalla exactamente una vez, con un aviso de que no se puede volver a consultar después.

Authorization: Bearer <client_id>.<secret>

El token completo (client_id + secret, unidos por un punto) es lo que usás como Bearer en cada llamada — no expira, y da acceso completo a la empresa que lo generó (facturas, notas, guías, proformas, clientes). El client_id (la parte antes del punto) identifica a esa integración puntual en tu bitácora de auditoría, así que si conectás más de un sistema conviene crear una credencial separada para cada uno.

Podés rotarla o revocarla vos mismo en cualquier momento desde Mi Empresa → Integraciones. Al rotar, la credencial anterior sigue funcionando 48 horas más mientras actualizás la integración — no hay corte inmediato.

Login por sesión — para un usuario humano dentro de tu producto

Si en cambio tu integración necesita actuar como una persona (por ejemplo, mostrar el panel de factsimil embebido, o representar distintos usuarios con sus propios permisos), usás las credenciales de esa cuenta:

POST /api/auth/login
Content-Type: application/json

{ "email": "tu-cuenta@ejemplo.com", "password": "..." }

HTTP/1.1 200 OK
{ "token": "eyJhbGciOiJIUzI1NiIs...", "requiresMfa": false }

Ese token es un JWT de vida limitada, a diferencia de la API key. Guardalo del lado del servidor de tu plataforma —nunca en un cliente móvil o web público— y reenvialo en cada llamada.

2. Crear un comprobante

Un comprobante empieza como borrador. customerId y seriesId son los únicos campos que dependen de datos ya creados en tu cuenta (cliente y serie); el resto describe las líneas del comprobante:

POST /api/invoices
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...
Content-Type: application/json

{
  "customerId": "3fa2...",
  "seriesId": "9c1b...",
  "moneda": "PEN",
  "formaPago": "Contado",
  "items": [
    {
      "descripcion": "Servicio de consultoría",
      "cantidad": 1,
      "precioUnitario": 250.00,
      "tipoAfectacionIgv": "10"
    }
  ]
}

HTTP/1.1 201 Created
{ "id": "6f1a...", "estado": "BORRADOR", "numero": null, "total": 295.00 }

moneda acepta PEN, USD, EUR, GBP, CAD, JPY, SEK o CHF. tipoAfectacionIgv sigue el Catálogo 07 de SUNAT (10 Gravado, 20 Exonerado, 30 Inafecto, 40 Exportación) — si no lo mandás, asume Gravado con IGV general.

Si tu integración puede reintentar un POST tras un timeout de red (sin saber si el primero llegó a crear el comprobante o no), mandá un header Idempotency-Key con un valor único por operación (ej. el ID de tu propio pedido, o un UUID que generes vos):

POST /api/invoices
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...
Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000
Content-Type: application/json

{ ... }

Reintentar con la MISMA key y el MISMO body te devuelve el comprobante que ya se creó, sin duplicarlo. Reusar la key con un body distinto es un 422 (probablemente un bug de tu lado, no un caso válido). Es opcional -- sin el header, el comportamiento es el de siempre.

3. Firmar y enviar a SUNAT

Firmar es async: la firma XML, la numeración correlativa y el envío a SUNAT ocurren en cola, no en el mismo request. Vas a recibir un 202 con un jobId, no el resultado final:

POST /api/invoices/6f1a.../sign
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...

HTTP/1.1 202 Accepted
{ "queued": true, "jobId": "42" }

Esto es deliberado: la numeración correlativa tiene que resolverse sin condiciones de carrera aunque dos comprobantes se firmen en el mismo instante, y SUNAT no siempre responde al instante. Por eso el siguiente paso es consultar el estado, no asumir que ya está listo.

4. Consultar el estado

Podés consultar por polling, o -- mejor -- configurar un webhook y que factsimil te avise. Por polling, el patrón es un GET corto sobre el mismo recurso hasta que estado deja de ser PROCESANDO:

GET /api/invoices/6f1a...
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...

HTTP/1.1 200 OK
{
  "id": "6f1a...",
  "estado": "ACEPTADO",
  "numero": 1042,
  "hash": "8f3a2c...",
  "xmlPath": "invoices/.../signed.xml",
  "cdrPath": "invoices/.../cdr.zip"
}

Los valores reales de estado que vas a ver son BORRADOR, PROCESANDO, ACEPTADO, OBSERVADO y RECHAZADO —OBSERVADO significa que SUNAT lo aceptó con una observación (el comprobante es válido), no que falló.

Webhooks (recomendado en vez de polling)

Configurá una URL desde Mi Empresa → Integraciones (por credencial -- cada integración puede apuntar a un endpoint distinto) y factsimil te va a avisar por POST cada vez que el comprobante cambie de estado, en vez de que tengas que preguntar vos. Al configurarla te mostramos un secret una única vez -- usalo para verificar que el webhook realmente vino de factsimil:

POST https://tu-servidor.com/webhooks/factsimil
X-Timestamp: 1755000000000
X-Signature: 7a3f9c...
Content-Type: application/json

{
  "event": "invoice.estado_changed",
  "invoiceId": "6f1a...",
  "tenantId": "9c1b...",
  "seriesId": "...", "numero": 1042,
  "estadoAnterior": "FIRMADO", "estadoNuevo": "ACEPTADO",
  "total": 295.00, "moneda": "PEN",
  "sunatMensaje": "La Factura numero F001-1042, ha sido aceptada",
  "timestamp": 1755000000000
}

Se dispara en cada transición real (GENERADO, FIRMADO, ACEPTADO/OBSERVADO/RECHAZADO, ANULADO), no solo en el veredicto final. Verificá la firma igual que lo hacemos nosotros del lado de cualquier partner que nos llama:

const crypto = require('crypto');

function isValid(timestamp, rawBody, signature, secret) {
  const expected = crypto.createHmac('sha256', secret)
    .update(`${timestamp}.${rawBody}`)
    .digest('hex');
  return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signature));
}

Si tu endpoint no responde 2xx (o tarda más de 10s), reintentamos con backoff creciente hasta 5 veces a lo largo de un par de horas antes de darnos por vencidos con esa notificación puntual.

5. XML, PDF y enlaces compartibles

Una vez ACEPTADO u OBSERVADO, el XML firmado y el PDF están disponibles directamente:

GET /api/invoices/6f1a.../xml   →  application/xml (el XML firmado, el mismo que valida SUNAT)
GET /api/invoices/6f1a.../pdf   →  application/pdf

Si tu plataforma necesita mandarle el comprobante al cliente final sin exponer tu token, generá un enlace compartible de un solo recurso:

POST /api/invoices/6f1a.../share-link
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...

HTTP/1.1 201 Created
{ "url": "https://factsimil.com/api/public/invoices/<token>/pdf" }

6. Multiempresa: el header x-tenant-id

Si tu cuenta tiene acceso a más de una empresa (por ejemplo, sos un contador con varios RUC), cada request opera sobre tu empresa principal salvo que mandes explícitamente x-tenant-id: <id-de-la-empresa>. Esto te deja resolver, en el mismo token, en qué empresa estás actuando en cada llamada — útil si tu plataforma administra facturación para varios clientes desde una sola integración.

7. Activar producción (salir de beta)

Tu cuenta arranca en modo beta, contra el ambiente de pruebas de SUNAT — todo lo de las secciones anteriores ya funciona ahí, pero los comprobantes no tienen validez real hasta que pasás a modo producción. Ese pasaje no es instantáneo ni autoservicio completo: son 3 pasos que hacés vos, seguidos de una aprobación manual de nuestro lado.

Importante para quien esté automatizando esto: los primeros dos pasos (certificado y credenciales SUNAT) exigen sesión de usuario humano (el JWT de POST /api/auth/login) — una API key recibe 401 Unauthorized ahí a propósito, son credenciales sensibles que no delegamos a un token de servidor-a-servidor de vida indefinida. El paso de pedir producción sí acepta API key igual que sesión.

Paso 1 — Subir el certificado digital (.p12)

POST /api/tenants/<tenantId>/certificates
Authorization: Bearer <JWT de sesión>
Content-Type: multipart/form-data

file: <tu-certificado>.p12
password: "..."

HTTP/1.1 201 Created
{ "id": "...", "filePath": "tenants/.../certificates/....p12.enc", "fechaVigencia": "2027-03-01", "estado": "activo" }

Paso 2 — Cargar usuario y clave SOL

PATCH /api/tenants/<tenantId>/sunat-credentials
Authorization: Bearer <JWT de sesión>
Content-Type: application/json

{ "usuarioSol": "...", "claveSol": "..." }

HTTP/1.1 200 OK
{ "ok": true }

Si además vas a emitir guías de remisión electrónica, las credenciales GRE se cargan aparte con el mismo patrón: PATCH /api/tenants/<tenantId>/gre-credentials con { "clientId": "...", "clientSecret": "..." } — también exige sesión, no API key.

Paso 3 — Pedir producción

Declarás, por cada serie, el último número que ya usaste emitiendo en producción antes (si nunca facturaste por ese RUC, mandá el correlativo actual tal cual, normalmente 0):

POST /api/tenants/<tenantId>/production-request
Authorization: Bearer <JWT de sesión o tu API key>
Content-Type: application/json

{ "declaredSeries": [ { "seriesId": "9c1b...", "ultimoNumero": 0 } ] }

HTTP/1.1 201 Created
{
  "id": "...",
  "estado": "pendiente",
  "declaredSeries": [ { "seriesId": "9c1b...", "codigo": "F001", "ultimoNumero": 0 } ],
  "motivoRechazo": null,
  "createdAt": "...",
  "resolvedAt": null
}

estado queda en pendiente hasta que alguien de nuestro equipo la revisa y la aprueba (o la rechaza, con un motivo) — no hay ningún camino automático. Mientras tanto seguís operando en beta con normalidad. Para consultar el estado sin volver a mandar el POST:

GET /api/tenants/<tenantId>/production-request
Authorization: Bearer <JWT de sesión o tu API key>

HTTP/1.1 200 OK
{ "id": "...", "estado": "aprobado", ... }   ← o null si todavía no mandaste ninguna solicitud

Errores esperables al pedir producción (todos 400 Bad Request salvo el último): sin certificado activo, sin usuario/clave SOL, sin ninguna serie creada, o un ultimoNumero menor al correlativo que la serie ya tiene — y 409 Conflict si ya hay una solicitud pendiente para ese tenant.

8. Códigos de error

Toda respuesta de error trae, además de statusCode y message, un code estable para tomar decisiones sin parsear el mensaje en español:

HTTP/1.1 409 Conflict
{ "statusCode": 409, "code": "invoice.duplicate", "message": "..." }

Facturas y boletas

  • invoice.not_found (404) — no existe o no pertenece a este tenant.
  • invoice.missing_exchange_rate (400) — moneda no es PEN y falta tipoCambio.
  • invoice.invalid_discount (400) — descuento inválido en un ítem.
  • invoice.credit_requires_installments (400) — formaPago: Credito sin cuotas, o la suma no coincide con el total.
  • invoice.installments_require_credit (400) — se enviaron cuotas sin formaPago: Credito.
  • invoice.detraccion_missing_amount (400) — sujetoDetraccion: true sin montoDetraccion.
  • invoice.detraccion_missing_code (400) — sujetoDetraccion: true sin codigoBienServicioDetraccion.
  • invoice.invalid_customer_or_series (400) — customerId/seriesId no existen o no son de este tenant.
  • invoice.wrong_series_type (400) — la serie no es de tipo factura/boleta.
  • invoice.factura_requires_ruc (400) — se pidió una factura para un cliente sin RUC.
  • invoice.invalid_state_transition (409) — ej. reintentar /sign sobre un comprobante ya procesado.
  • invoice.not_signed_yet (404) — XML/PDF pedidos antes de que termine de firmarse.
  • invoice.cdr_not_available (404) — CDR pedido antes de que SUNAT responda.

Notas de crédito/débito

  • note.not_found (404) — no existe o no pertenece a este tenant.
  • note.invoice_not_accepted (400) — la factura referenciada no está ACEPTADA/OBSERVADA.
  • note.invalid_discount (400) — descuento inválido en un ítem.
  • note.exceeds_invoice_icbper (400) — el ICBPER de la nota supera el de la factura.
  • note.exceeds_invoice_perception (400) — la percepción de la nota supera la de la factura.
  • note.invalid_series (400) — la serie no es del tipo crédito/débito esperado.
  • note.exceeds_invoice_total (400) — la nota acreditada supera el total de la factura.
  • note.not_signed_yet (404) — XML/PDF pedidos antes de que termine de firmarse.
  • note.cdr_not_available (404) — CDR pedido antes de que SUNAT responda.

Guías de remisión y proformas

  • guia.not_found (404) — no existe o no pertenece a este tenant.
  • guia.not_signed_yet (404) — XML/PDF pedidos antes de que termine de firmarse.
  • proforma.not_found (404) — no existe o no pertenece a este tenant.
  • proforma.missing_exchange_rate (400) — moneda no es PEN y falta tipoCambio.
  • proforma.already_converted (400) — ya fue convertida a factura.
  • proforma.voided (400) — está anulada.

Clientes, series y tenant

  • customer.duplicate_document (409) — ya existe un cliente con ese número de documento.
  • series.duplicate_code (409) — ya existe una serie con ese código.
  • series.not_found (404) — no existe o no pertenece a este tenant.
  • tenant.not_found (404) — el tenant no existe.

Autenticación, idempotencia y rate limiting

  • auth.missing_credentials (401) — no se envió cookie de sesión ni Authorization: Bearer.
  • auth.invalid_api_key (401) — el client_id/secret no es válido o fue revocado.
  • auth.tenant_mismatch (400) — el API key no pertenece al tenant de la URL.
  • idempotency.key_too_long (422) — el Idempotency-Key supera los 255 caracteres.
  • idempotency.request_in_progress (409) — ya hay un request en curso con esa key.
  • idempotency.key_reused (422) — la key ya se usó con un body diferente.
  • rate_limit.tenant_exceeded (429) — el tenant superó su límite de 300 requests/min.

Cualquier otro error responde con un code genérico derivado del status HTTP (error.bad_request, error.not_found, error.internal, etc.) — el campo code está siempre presente, aunque no todos los casos tengan todavía un código namespaced específico.

9. Siguientes pasos

  • Crear una cuenta gratis te da acceso inmediato al panel y a las mismas credenciales que usa esta API — no hay un "modo sandbox" separado con datos de prueba distintos.
  • ¿Vas a integrar esto para más de un cliente final (marketplace, ERP vertical)? Escribinos — es exactamente el caso de uso para el que se diseñó factsimil.
factsimil

Facturación electrónica SUNAT, lista para tu plataforma.

info@factsimil.com
WhatsApp

Producto

  • Precios
  • Todo lo que incluye
  • Cómo funciona
  • App móvil
  • Tienda online
  • Punto de venta
  • vs. facturar a mano
  • Desarrolladores

Soluciones

  • Para empresas y PYMES
  • Para contadores
  • Para tiendas
  • Para emprendedores
  • Para empresas con inventario

Recursos

  • Preguntas frecuentes
  • Contacto
  • Blog
  • Cómo elegir un software SUNAT
  • Nosotros

Cuenta y legal

  • Crear cuenta
  • Ingresar
  • Panel admin
  • Términos
  • Privacidad
  • Cookies
  • Libro de Reclamaciones
© 2026 factsimil. Todos los derechos reservados.
Operado por MULTISERVICIOS KADA EIRL — RUC 20612829579, Perú