API Facturación SUNAT
OpenAPI

API Facturación SUNAT

API SaaS de facturación electrónica multi-tenant para Perú. Integración con SUNAT completamente asíncrona: cada emisión retorna 202 Accepted en menos de 200ms mientras el documento se procesa en segundo plano.

Async-first

202 inmediato, workers Redis en segundo plano.

Multi-tenant

Cada empresa totalmente aislada en BD y storage.

Reintentos

Backoff exponencial automático si SUNAT está caída.

Base URL
https://api.tudominio.com/api/v1

Autenticación

Cada empresa tiene su propio API Key vinculado a un RUC. Inclúyelo en todas las peticiones.

HTTP Headers
Authorization: Bearer sk_live_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
Content-Type: application/json
Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000   ← recomendado en emisiones
Idempotencia: El header Idempotency-Key previene duplicados ante reintentos de red. El sistema cachea la respuesta 24h. Máximo 255 caracteres. Usa un UUID por operación.

Headers de Respuesta

Header Descripción Ejemplo
X-RateLimit-LimitMáx. peticiones/minuto60
X-RateLimit-RemainingPeticiones restantes45
Retry-AfterSegundos a esperar (solo en 429)60
X-Idempotency-CacheHIT = respuesta cacheada, MISS = nuevo procesoMISS

Flujo Asíncrono

Secuencia
Tu sistema         API REST          Redis Queue       SUNAT
    │                  │                   │               │
    ├── POST /facturas ─▶│                  │               │
    │                  ├─ Valida + firma XML │               │
    │◀── 202 Accepted ──┤                  │               │
    │    (< 200ms)     ├─ Encola job ──────▶│               │
    │                  │             Worker procesa         │
    │                  │                   ├── Envía XML ──▶│
    │                  │                   │◀── CDR ────────┤
    │                  ├─ Actualiza estado ─┤               │
    │◀═══ Webhook ═══════════════════════════              │

Tabla de Estados

Estado Significado Acción
EN_PROCESOEn cola Redis, aún no enviado a SUNATEsperar webhook o polling
ACEPTADOSUNAT aceptó. CDR disponibleDescargar CDR, notificar usuario
RECHAZADOError de datos. No se reintentaVer sunat.codigo, corregir y reemitir
ERRORAgotados 5 reintentos automáticosUsar POST /comprobantes/{id}/reenviar
ANULADOAnulado vía Comunicación de BajaNo reemitir con el mismo número

Reintentos automáticos (errores transitorios)

2 min 4 min 8 min 16 min 32 min ERROR definitivo

Solo errores de SUNAT temporalmente caída o timeout de red. Errores de datos (RUC inválido, XML malformado) se marcan RECHAZADO inmediatamente.

Validaciones SUNAT

Estas reglas se aplican antes de encolar el documento. Incumplirlas devuelve 422.

1. Prefijo de serie obligatorio por tipo

TipoDocumentoSerie válidaEjemplo
01FacturaEmpieza con FF001
03BoletaEmpieza con BB001
07 NC Fact.Nota de Crédito a FacturaEmpieza con FFC01
07 NC Bol.Nota de Crédito a BoletaEmpieza con BBC01
08 ND Fact.Nota de Débito a FacturaEmpieza con FFD01
08 ND Bol.Nota de Débito a BoletaEmpieza con BBD01
09Guía de RemisiónEmpieza con TT001
20RetenciónEmpieza con RR001
40PercepciónEmpieza con PP001

Código de error: INVALID_SERIE

2. Plazo máximo de emisión (R.S. 003-2023/SUNAT)

Facturas: máx. 3 días calendario retroactivos. Boletas: máx. 7 días calendario. Si se omite fecha_emision, usa la fecha actual (siempre válida).

Código de error: FECHA_EMISION_VENCIDA

3. Identificación del receptor en boletas

Para ventas ≥ S/700, el receptor debe estar identificado con DNI (tipo_documento: "1") o RUC ("6"). No se permite tipo_documento: "-" en este caso.

Código de error: RECEPTOR_REQUERIDO

4. Notas de Crédito y Débito sobre ACEPTADO

El comprobante referenciado en comprobante_referencia debe tener estado ACEPTADO en el sistema. Si está EN_PROCESO o RECHAZADO, SUNAT también lo rechazaría.

Código de error: REFERENCE_NOT_ACCEPTED

5. Receptor en Facturas de Exportación

Para tipo_operacion: "0401" (Exportación), el receptor no puede identificarse con DNI ("1") ni Sin Documento ("-"). Usar: "7" Pasaporte, "4" Carnet de extranjería, "6" Tax ID extranjero, o "0" sin doc. internacional.

Código de error: RECEPTOR_INVALIDO_EXPORTACION

POST

/api/v1/facturas

Emite una Factura Electrónica (Tipo 01). El IGV se calcula automáticamente desde precio_unitario. Para exportaciones usar tipo_operacion: "0401" — ver sección Exportación.

JSON Request
{
  "serie_solicitada": "F001",          // ← debe empezar con F
  "tipo_operacion": "0101",            // 0101=Venta interna, 0401=Exportación
  "fecha_emision": "2025-04-14",       // ← máx. 3 días retroactivos
  "moneda": "PEN",
  "forma_pago": "Contado",
  "emisor": {
    "ubigueo": "150101",
    "departamento": "LIMA",
    "provincia": "LIMA",
    "distrito": "MIRAFLORES",
    "direccion": "Av. Principal 100"
  },
  "cliente": {
    "tipo_documento": "6",             // 6=RUC, 1=DNI, 7=Pasaporte, 4=CE
    "numero_documento": "20100000001",
    "razon_social": "EMPRESA SAC"
  },
  "items": [
    {
      "codigo": "PROD-001",
      "descripcion": "Servicio de Consultoría",
      "unidad_medida": "NIU",
      "cantidad": 2,
      "precio_unitario": 59.00,        // ← precio CON IGV incluido
      "tipo_afectacion_igv": "10"      // 10=Gravado, 20=Exonerado, 30=Inafecto, 40=Exportación
    }
  ]
}
Crédito: Para facturas a crédito, agrega "forma_pago": "Credito", "monto_pendiente": 118.00 y el array "cuotas": [{"monto": 59.00, "fecha_pago": "2025-05-14"}].
POST

/api/v1/facturas — Exportación

Cuando el comprador es una entidad o persona extranjera no domiciliada en Perú, se emite una Factura de Exportación usando tipo_operacion: "0401" (Catálogo 51 SUNAT).

IGV automático = 0%

El sistema asigna tipo_afectacion_igv: "40" e IGV 0.00 a todos los ítems automáticamente. No necesitas especificarlo.

precio_unitario sin IGV

En exportación, precio_unitario es el valor de exportación (sin IGV). No se divide por 1.18.

Receptor: no DNI ni "-"

El receptor debe identificarse con Pasaporte ("7"), CE ("4"), Tax ID ("6") o sin doc. ("0").

JSON Request — Exportación
{
  "serie_solicitada": "F001",
  "tipo_operacion": "0401",            // ← clave: activa modo exportación
  "fecha_emision": "2025-04-14",
  "moneda": "USD",                     // recomendado: moneda extranjera
  "emisor": {
    "ubigueo": "150101",
    "departamento": "LIMA",
    "provincia": "LIMA",
    "distrito": "MIRAFLORES",
    "direccion": "Av. Principal 100"
  },
  "cliente": {
    "tipo_documento": "7",             // 7=Pasaporte (no puede ser "1" DNI ni "-")
    "numero_documento": "P123456789",
    "razon_social": "ACME CORP USA LLC",
    "direccion": "123 Main St, New York, NY 10001"
  },
  "items": [
    {
      "codigo": "SRV-001",
      "descripcion": "Servicio de Consultoría de Software",
      "unidad_medida": "ZZ",           // ZZ = Servicio
      "cantidad": 1,
      "precio_unitario": 1500.00       // ← valor SIN IGV (= monto de exportación)
                                       // IGV 0% se asigna automáticamente
    }
  ]
}

Tipos de documento válidos para receptor extranjero

tipo_documento Descripción Ejemplo
"7"PasaporteP123456789
"4"Carnet de extranjeríaCE123456
"6"Tax ID / RUC del país de origenUS-XX-1234567890
"0"Sin documento internacional00000000
"1" ✗DNI — NO permitido en exportación
"-" ✗Sin documento — NO permitido en exportación

Diferencias clave: Factura Normal vs Exportación

Aspecto Factura Normal Factura Exportación
tipo_operacion01010401
IGV18%0% (automático)
precio_unitarioCon IGV incluidoSin IGV (valor exportación)
MonedaPEN (típico)USD / EUR recomendado
tipo_documento receptor6, 1, -Solo 7, 4, 6, 0
Nota contable: La factura de exportación se registra en BD con es_exportacion = true y monto_exportacion separado del monto gravado, lo que facilita los reportes de comercio exterior.
POST

/api/v1/boletas

Emite una Boleta de Venta (Tipo 03). Para ventas ≥ S/700 el receptor debe estar identificado.

JSON Request
{
  "serie_solicitada": "B001",          // ← debe empezar con B
  "tipo_operacion": "0101",
  "moneda": "PEN",
  "cliente": {
    "tipo_documento": "1",             // 1=DNI, -=Sin identificar (solo < S/700)
    "numero_documento": "12345678",
    "razon_social": "JUAN PEREZ"
  },
  "items": [
    {
      "descripcion": "Producto A",
      "cantidad": 2,
      "precio_unitario": 50.00,
      "tipo_afectacion_igv": "10"
    }
  ]
}

// Venta sin identificar (solo si monto total < S/700):
{
  "cliente": {
    "tipo_documento": "-",
    "numero_documento": "00000000",
    "razon_social": "CLIENTE VARIOS"
  }
}
POST

/api/v1/notas-credito

POST

/api/v1/notas-debito

Requisito SUNAT: El comprobante en comprobante_referencia debe tener estado ACEPTADO. Si está EN_PROCESO, esperar a que SUNAT lo procese.
JSON Request — Nota de Crédito
{
  "serie_solicitada": "FC01",          // F+xxx para NC de Factura, B+xxx para NC de Boleta
  "comprobante_referencia": {
    "tipo": "01",                      // 01=Factura, 03=Boleta
    "serie": "F001",
    "correlativo": "123",
    "fecha": "2025-04-14"
  },
  "motivo_nota": "02",                 // 01=Anulación, 02=Error RUC, 03=Error descripción
  "sustento_motivo": "Anulación por error en el RUC del receptor",
  "moneda": "PEN",
  "emisor": { "ubigueo": "150101", "direccion": "Av. Principal 100", ... },
  "cliente": { "tipo_documento": "6", "numero_documento": "20100000001", ... },
  "items": [
    {
      "descripcion": "Servicio de Consultoría",
      "cantidad": 1,
      "precio_unitario": 118.00,
      "tipo_afectacion_igv": "10"
    }
  ]
}

Bajas y Resúmenes Diarios

POST /api/v1/bajas

Anula comprobantes ya enviados. SUNAT devuelve un ticket asíncrono. Consultar con GET /tickets/{ticket}.

JSON
{
  "fecha": "2025-04-14",
  "comprobantes": [{
    "tipo": "01",
    "serie": "F001",
    "correlativo": "123",
    "motivo": "Error en datos"
  }]
}
POST /api/v1/resumenes

Consolida boletas de un día. Si comprobantes está vacío, el sistema las busca automáticamente.

JSON
{
  "fecha": "2025-04-14",
  "comprobantes": []
  // Si vacío → busca automáticamente
  // las boletas ACEPTADAS del día
}
POST

/api/v1/guias

JSON Request — Guía de Remisión (09)
{
  "serie_solicitada": "T001",         // ← debe empezar con T
  "fecha_traslado": "2025-04-15",
  "motivo_traslado": "01",            // 01=Venta, 02=Compra, 04=Entre establecimientos
  "modalidad_traslado": "01",         // 01=Transporte público, 02=Privado
  "destinatario": {
    "tipo_documento": "6",
    "numero_documento": "20200000001",
    "razon_social": "EMPRESA DESTINO SAC"
  },
  "punto_partida": "Av. Lima 100, Lima",
  "punto_llegada": "Av. Arequipa 200, Arequipa",
  "items": [
    { "descripcion": "Mercadería", "cantidad": 10, "precio_unitario": 0 }
  ]
}

Retenciones y Percepciones

POST /api/v1/retenciones

Serie empieza con R. Usa el endpoint SOAP de retenciones de SUNAT (diferente al de facturas).

JSON
{
  "serie_solicitada": "R001",
  "proveedor": {
    "tipo_documento": "6",
    "numero_documento": "20300000001",
    "razon_social": "PROVEEDOR SAC"
  },
  "total_retencion": 30.00,
  "total_pagado": 970.00,
  "regimen": "01",
  "tasa": 3,
  "detalles": [{
    "tipo_documento": "01",
    "serie_numero": "F001-00000123",
    "fecha_emision": "2025-04-10",
    "fecha_pago": "2025-04-14",
    "total_invoice": 1000.00,
    "moneda": "PEN",
    "monto_retencion": 30.00,
    "monto_pagado": 970.00
  }]
}
POST /api/v1/percepciones

Serie empieza con P.

JSON
{
  "serie_solicitada": "P001",
  "cliente": {
    "tipo_documento": "6",
    "numero_documento": "20400000001",
    "razon_social": "CLIENTE SAC"
  },
  "comprobantes": [{
    "tipo": "03",
    "serie": "B001",
    "correlativo": "456",
    "fecha_emision": "2025-04-14",
    "importe": 500.00,
    "importe_percepcion": 10.00
  }]
}

Gestión de Comprobantes

GET /api/v1/comprobantes

Lista comprobantes con filtros. Parámetros: estado (EN_PROCESO/ACEPTADO/RECHAZADO/ERROR/ANULADO), tipo, fecha_desde, fecha_hasta, receptor, limit (máx.100), offset.

GET /api/v1/comprobantes/{id}/estado

Consulta el estado directamente en SUNAT y actualiza el local. Para polling después de emitir.

MétodoRutaDescripción
GET/comprobantes/{id}Detalle completo
GET/comprobantes/{id}/xmlDescargar XML firmado UBL 2.1
GET/comprobantes/{id}/pdfDescargar representación impresa
GET/comprobantes/{id}/cdrDescargar CDR (ZIP) de SUNAT
POST/comprobantes/{id}/reenviarRe-encolar si estado = ERROR

Webhooks

Cada empresa tiene su propio webhook_secret para firmar los eventos. No usar el secreto global del sistema.

Configurar URL

PATCH /api/v1/config/webhook
{ "webhook_url": "https://mi-sistema.com/sunat/eventos" }

Eventos disponibles

comprobante.aceptado comprobante.rechazado comprobante.error comprobante.anulado

Validar firma HMAC-SHA256

// PHP — usar el webhook_secret de TU empresa (panel admin)
$body   = file_get_contents('php://input');
$recibida  = $_SERVER['HTTP_X_SIGNATURE'] ?? '';
$calculada = hash_hmac('sha256', $body, 'TU_WEBHOOK_SECRET_EMPRESA');

if (!hash_equals($calculada, $recibida)) {
    http_response_code(401);
    exit('Firma inválida');
}
$data = json_decode($body, true);
// procesar $data['event'] y $data['data']

Payload de webhook

{
  "event": "comprobante.aceptado",
  "timestamp": "2025-04-14T23:05:00+00:00",
  "data": {
    "id": 123,
    "comprobante": "20100000000-01-F001-00000123",
    "estado": "ACEPTADO",
    "sunat": { "codigo": "0", "mensaje": "La Factura F001-123 ha sido aceptada" },
    "enlace": { "xml": "...", "pdf": "...", "cdr": "..." }
  }
}

Reintentos automáticos

2 min 4 min 8 min 16 min 32 min
POST

/api/v1/config/credenciales

Configura credenciales SOL y certificado P12 sin intervención del administrador. El P12 se valida criptográficamente antes de guardarse.

JSON Request
{
  "usuario_sol": "MODDATOS",
  "clave_sol": "moddatos",
  "certificado_p12_base64": "MIINWAIBAz...",   // base64 del archivo .p12
  "certificado_pass": "contraseña_del_p12"
}

// Convertir a base64 en terminal:
// base64 -w 0 mi_certificado.p12
Proceso de validación: (1) Decodifica base64 → (2) Valida P12 + contraseña con OpenSSL → (3) Guarda en storage/certs/{cliente_id}/cert_*.p12 → (4) Cifra clave_sol y contraseña con AES-256-CBC en BD.

Consultas Externas

Endpoint Descripción
GET /consultar/ruc/{ruc}Datos de empresa en SUNAT (11 dígitos)
GET /consultar/dni/{dni}Datos de persona en RENIEC (8 dígitos)
POST /consultar/comprobanteVerificar validez de CPE externo en SUNAT
GET /tickets/{ticket}Resultado de ticket de Baja o Resumen Diario
GET /dashboard/statsEstadísticas del día y cuota mensual

Clasificación de Errores SUNAT

Cuando SUNAT rechaza un documento, el campo sunat.codigo indica la causa. El sistema clasifica automáticamente los errores para decidir si reintentar o no.

Rango Tipo ¿Reintento? Acción
0109, 0111–0130Servidor SUNAT caídoSí — automáticoEl worker reintenta hasta 5 veces
0100–0108Credenciales inválidasNoActualizar usuario/clave SOL
0131–0199Archivo ZIP inválidoNoError interno — contactar soporte
1000–1999XML malformadoNoRevisar campos del request
2000–2999Datos de negocio incorrectosNoCorregir RUC, fechas, montos
3000–3999Cálculos/totales incorrectosNoVerificar IGV y totales
4000+Observación (válido)N/AComprobante ACEPTADO, revisar observación

Errores frecuentes

CódigoDescripciónSolución
0Aceptado sin observacionesOK
0100–0108Credenciales SOL incorrectasActualizar con POST /config/credenciales
2010Contribuyente no activo en SUNATVerificar estado del RUC en SUNAT
2104RUC del emisor no existeVerificar RUC configurado
2325–2328Certificado digital inválido o vencidoRenovar P12 con POST /config/credenciales
2336Error en validación de firma digitalVerificar contraseña del P12
3100Fecha de emisión vencidaEl documento no puede enviarse retroactivamente

Códigos Internos del Sistema

codigo_interno HTTP Descripción
INVALID_SERIE422Serie no cumple el prefijo requerido por tipo
FECHA_EMISION_VENCIDA422Fecha supera el plazo SUNAT (3/7 días)
RECEPTOR_REQUERIDO422Boleta ≥ S/700 requiere receptor identificado
RECEPTOR_INVALIDO_EXPORTACION422Receptor con DNI o Sin Documento no es válido en facturas de exportación (tipo_operacion: "0401")
REFERENCE_NOT_ACCEPTED422Documento referenciado en NC/ND no está ACEPTADO
MISSING_REFERENCE422Falta comprobante_referencia en NC/ND
DOC_NOT_FOUND404Comprobante no encontrado o no pertenece al cliente
QUOTA_EXCEEDED402Límite mensual del plan agotado
ALREADY_ACCEPTED422No se puede reenviar un comprobante ya ACEPTADO
DUPLICATE_REQUEST200Petición bloqueada por idempotencia (devuelve respuesta original)
SUNAT_ERROR422Error reportado por SUNAT — ver campo codigo_sunat

Entorno BETA (Pruebas)

Cada empresa tiene un campo entorno que determina a qué servidores SUNAT se envían los documentos.

BETA — Homologación

Documentos sin validez legal. Ideal para desarrollo e integración.

Usuario SOL: MODDATOS

Clave SOL: moddatos

PRODUCCIÓN

Documentos con validez tributaria. Usar credenciales SOL reales.

El administrador cambia el campo en BD cuando la integración está lista.

Ciclo recomendado: (1) Crear empresa en BETA → (2) Configurar con MODDATOS → (3) Emitir comprobantes de prueba → (4) Verificar CDR y webhooks → (5) Pase a PRODUCCIÓN con credenciales reales.