SWSPERU FACT - Facturador SUNAT API — Referencia de la API
Esta es la referencia completa de la API externa de SWSPERU FACT: la superficie HTTP que tu sistema puede usar para emitir Facturas, Boletas, Notas de Crédito/Débito y Guías de Remisión electrónicas ante SUNAT (Perú) a través de nuestra plataforma.
Si es tu primera vez integrando, empieza por la Guía de inicio rápido — este documento es la referencia detallada para cuando ya tengas el flujo básico funcionando.
Todos los ejemplos de este documento (requests y responses) fueron ejecutados en vivo contra el ambiente beta de SUNAT el 2026-08-13, no son esquemas inventados.
1. Conceptos básicos
- Base URL:
https://<tu-dominio-facturador>/api/v1/external— sustituye<tu-dominio-facturador>por la URL real que te entregue tu proveedor. Todas las rutas de este documento son relativas a esa base. - Formato: JSON en ambas direcciones. Envía
Content-Type: application/jsonen cada request con body. - Autenticación: API key por header, ver sección 2.
- Un tenant = una cuenta: cada API key pertenece a una sola organización
(“tenant”) dentro de la plataforma. Nunca vas a ver ni podrás referenciar
datos de otra organización con tu key — cualquier intento de acceder a un
recurso ajeno responde
404, igual que si no existiera (ver sección 5). - El campo
ides un contador propio de tu cuenta, no un id global de la plataforma: eninvoices,notes,voidydispatch-guides,ides “tu documento número 1, 2, 3…” dentro de tu cuenta y ambiente (beta y producción llevan cada uno su propio contador, arrancando en 1) — nunca el identificador interno real de la fila en la base de datos de la plataforma. Sigue siendo un entero que puedes usar tal cual en la URL de los demás endpoints (GET /invoices/{id}, etc.), simplemente no refleja ni permite inferir cuántos documentos ha emitido el resto de la plataforma.
2. Autenticación
Cada request a /api/v1/external/* debe incluir:
Authorization: Bearer <tu_api_key>
Cómo se obtiene la API key
Hay dos caminos:
- Auto-registro en
https://portal.swsperu-fact.com— te registras con los datos de tu empresa (nombre, RUC, email, contraseña). Tu cuenta (tenant) queda en estadopendientehasta que el equipo de SWSPERU FACT la revisa y aprueba; recibes una notificación por correo (y por WhatsApp, si dejaste un teléfono) en cuanto se aprueba. Una vez aprobada, generas tu API key tú mismo desde el propio portal — no necesitas pedírsela a nadie. - Alta asistida por el equipo de SWSPERU FACT — si prefieres que te den de alta directamente (por ejemplo, para una integración empresarial con varios pasos de configuración), contacta a tu proveedor y te crean el tenant con tu primera key ya generada.
En ambos casos, la key en texto plano se muestra una sola vez en el momento de crearla — si la pierdes, genera una nueva (la anterior puede revocarse sin afectar el resto de tu integración, ya que puedes tener varias keys activas a la vez, incluso una por ambiente — ver sección 10).
Toda cuenta nueva empieza en el ambiente beta de SUNAT (pruebas, sin validez tributaria real) — ver sección 10 para cómo y cuándo se pasa a producción.
Formato de la key
sak_ seguido de 48 caracteres alfanuméricos aleatorios, por ejemplo:
sak_g1zIgfuxddKvyshHbhCoqTH0yFfCo2bQvOL1lNEghRBTZJ7m
Errores de autenticación
| Situación | HTTP | error.code |
|---|---|---|
Falta el header Authorization |
401 | unauthenticated |
| Key inexistente, revocada, o el tenant está suspendido | 401 | invalid_api_key |
El mensaje de invalid_api_key es intencionalmente genérico — no distingue
“tu key es inválida” de “tu cuenta está suspendida”, por seguridad. Si
sospechas que tu cuenta fue suspendida, contacta a tu proveedor.
Verifica tu key en cualquier momento con GET /me (ver sección 8.1) —
es el endpoint más simple, no crea ni modifica nada.
3. Formato de error único
Cualquier error en /api/v1/external/* (autenticación, validación, límite de
tasa, o un error de negocio) responde en el mismo formato:
{
"error": {
"code": "sunat_not_configured",
"message": "SUNAT no está configurado. Configúralo en Administración > Configuración.",
"details": { "campo": ["mensaje de validación"] }
}
}
details solo aparece cuando code es validation_failed — trae, campo por
campo, la lista de mensajes de validación que falló.
Catálogo completo de error.code
error.code |
HTTP | Cuándo ocurre |
|---|---|---|
unauthenticated |
401 | Falta el header Authorization: Bearer |
invalid_api_key |
401 | Key inválida/revocada, o tenant suspendido |
rate_limit_exceeded |
429 | Superaste el límite de 60 req/min (sección 4) |
validation_failed |
422 | El body de tu request no pasa las validaciones — revisa details |
not_found |
404 | El recurso no existe, o pertenece a otro tenant (nunca vas a ver un 403 por esto, ver sección 1) |
sunat_not_configured |
422 | Tu cuenta todavía no tiene credenciales SUNAT (RUC/usuario SOL/certificado) configuradas — contacta a tu proveedor |
sunat_gre_not_configured |
422 | Igual que arriba, pero específico de las credenciales de la API de Guía de Remisión (GRE) |
invoice_not_accepted |
422 | Intentaste emitir una nota o una baja sobre un comprobante que SUNAT no aceptó (estado_sunat distinto de aceptado) |
invoice_already_voided |
409 | Ya existe una comunicación de baja para ese comprobante — no se puede anular dos veces |
invoice_note_exceeds_original |
422 | La cantidad de la nota de crédito/débito excede la del comprobante original |
document_series_not_configured |
422 | Tu cuenta no tiene una serie configurada para ese tipo de comprobante — contacta a tu proveedor |
document_not_ready |
404 | Pediste el XML/CDR/PDF de un documento (sección 8.5, 8.6, 8.7, etc.) cuyo archivo todavía no existe — típicamente el CDR de una Guía de Remisión antes de que resuelva el ticket, o cuando SUNAT nunca llegó a procesar el documento (falla de comunicación). No repolea SUNAT ni genera nada al vuelo: vuelve a pedirlo más tarde |
invalid_request |
422 | Caso puntual: el invoice_item_id que enviaste no pertenece al comprobante de la URL |
lookup_provider_not_configured |
422 | El servicio de consultas (sección 8.20 en adelante) todavía no está configurado en tu cuenta — contacta a tu proveedor |
lookup_provider_unavailable |
422 | El servicio de consultas no pudo resolver lo que pediste (no encontrado, error temporal, etc.) |
lookup_quota_exceeded |
429 | Superaste tu cupo mensual de consultas RUC+DNI (sección 8.20-8.21) — distinto del límite de 60 req/min de la sección 4, se reinicia recién el primer día del mes siguiente |
lookups_disabled |
422 | El servicio de consultas está desactivado para tu cuenta específicamente — distinto de lookup_provider_not_configured (ese es a nivel de toda la plataforma). Contacta a tu proveedor |
whatsapp_disabled |
422 | Las notificaciones por WhatsApp están desactivadas para tu cuenta específicamente. Contacta a tu proveedor |
branch_is_default |
422 | Intentaste desactivar (sección 8.34) la sucursal marcada como is_default — marca otra como predeterminada primero |
internal_error |
500 | Error inesperado del lado de la plataforma — si persiste, contacta a soporte |
method_not_allowed |
405 | Verbo HTTP no soportado en esa ruta |
http_error |
Variable | Catch-all genérico para cualquier otro error HTTP no cubierto por los códigos de arriba |
Importante — cómo NO se reporta un rechazo de SUNAT: si tu comprobante
llegó a procesarse y SUNAT lo observó o rechazó, la respuesta HTTP
sigue siendo 201 Created (éxito de la operación de la API), con el
resultado real en el campo estado_sunat del body — ver sección 6. Un
4xx/5xx de esta tabla solo ocurre si la solicitud nunca llegó a
intentarse contra SUNAT.
4. Límites de uso (rate limiting)
60 requests por minuto por tenant (tu cuenta, no tu IP — si tienes varios
servidores llamando con la misma key, comparten el mismo cupo). Aplica a toda
la superficie /api/v1/external/* como un solo cupo compartido, no un límite
distinto por endpoint.
Cada respuesta trae estos headers para que monitorees tu consumo:
| Header | Significado |
|---|---|
X-RateLimit-Limit |
Límite total (60) |
X-RateLimit-Remaining |
Requests que te quedan en la ventana actual |
X-RateLimit-Reset |
Timestamp Unix en que el contador se reinicia |
Retry-After |
Solo presente en la respuesta 429 — segundos que debes esperar |
Al exceder el límite:
HTTP/1.1 429 Too Many Requests
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 0
Retry-After: 42
{
"error": {
"code": "rate_limit_exceeded",
"message": "Has excedido el límite de solicitudes permitidas (60 por minuto). Intenta de nuevo en unos segundos."
}
}
5. Aislamiento entre cuentas (tenants)
Tu API key solo puede ver y crear datos de tu propia organización. Si tu
request referencia el id de un comprobante, guía, transportista, vehículo,
conductor o sucursal que no te pertenece (sea en la URL o dentro del body,
por ejemplo vehicle_id o branch_id), la API responde:
404 not_foundsi el id va en la URL (GET /invoices/{id}, etc.) — indistinguible de un id que simplemente no existe, nunca vas a recibir un403.422 validation_failedsi el id va en el body de unPOST(por ejemplovehicle_iden una guía de remisión) — el campo específico queda señalado enerror.details.
6. Estados de un documento SUNAT (estado_sunat)
Todo comprobante, nota o guía que emites pasa por uno de estos estados. Es el campo más importante para tu lógica de negocio — no lo confundas con el código HTTP de la respuesta (ver la nota en la sección 3):
| Estado | Significado | Qué hacer |
|---|---|---|
pendiente |
SUNAT confirmó la recepción pero todavía está procesando (aplica a Guías de Remisión y a Comunicaciones de Baja, que son asíncronas). | Vuelve a pedir el recurso con GET — la API reconsulta el ticket automáticamente y actualiza el estado antes de responder. |
aceptado |
SUNAT registró el documento sin ninguna objeción. | Es el resultado final — el documento es válido tributariamente. |
observado |
SUNAT registró el documento, pero con una observación de forma (un dato con formato distinto al esperado, por ejemplo). El documento SÍ quedó registrado en SUNAT y es válido — no es lo mismo que rechazado. |
Revisa codigo_respuesta_sunat/descripcion_sunat para entender la observación y corregirla en tu próxima emisión; no necesitas reemitir este documento. |
rechazado |
SUNAT no registró el documento — no es válido tributariamente. | Corrige el error indicado en descripcion_sunat y vuelve a emitir (con un correlativo nuevo; un documento rechazado no se puede reintentar con el mismo número). |
error |
No se pudo determinar el resultado (problema de comunicación con SUNAT en el momento de la consulta). No confundir con rechazado. |
Vuelve a pedir el recurso con GET más tarde para reintentar la consulta. |
codigo_respuesta_sunat y descripcion_sunat siempre traen el detalle
exacto que devolvió SUNAT (código y mensaje de su Constancia de Recepción, o
del error puntual), útil para logging y para decidir si un observado
requiere corrección humana.
6.1 Documentos generados (XML, CDR y PDF)
Cada Factura/Boleta, Nota de Crédito/Débito y Guía de Remisión que emites queda con tres archivos disponibles para descarga (ver sección 8):
- XML — el comprobante UBL 2.1 firmado que se envió a SUNAT.
- CDR (
Constancia de Recepción) — el ZIP oficial que SUNAT devuelve como respuesta, tal cual lo emitió (no una reconstrucción ni solo el XML extraído). Solo existe una vez que SUNAT terminó de procesar el documento — en una Guía de Remisión (asíncrona), eso puede ser unos segundos después de la respuesta dePOST, no en el mismo request. - PDF — la representación impresa, generada por la plataforma en el momento de emitir (no es un documento oficial de SUNAT, es para uso interno/impresión), con código QR y, si tu cuenta tiene branding configurado, tu logo/color/datos de contacto.
Cada respuesta de emisión/consulta trae un bloque documents que indica cuál
de los tres ya está listo para descargar:
"documents": { "xml_available": true, "cdr_available": true, "pdf_available": true }
Si cdr_available es false (típicamente justo después de crear una Guía
de Remisión, o si SUNAT nunca llegó a procesar el documento por un problema
de comunicación), descargar el CDR responde 404 document_not_ready — no es
un error tuyo, solo significa que todavía no existe. Vuelve a consultar el
recurso con GET (que reconsulta el ticket automáticamente mientras siga
pendiente, sección 9) y reintenta la descarga después.
Retención: conservamos estos archivos indefinidamente — no hay ningún mecanismo de purga automática por ahora.
7. Catálogos
Estos son los catálogos oficiales de SUNAT que necesitas para armar tus requests correctamente. Se confirmaron contra el comportamiento real de la plataforma (no son una transcripción de memoria de la normativa SUNAT).
7.1 Tipo de comprobante (tipo_comprobante / catálogo 01 SUNAT)
| Código | Documento |
|---|---|
01 |
Factura |
03 |
Boleta de venta |
07 |
Nota de Crédito |
08 |
Nota de Débito |
09 |
Guía de Remisión Remitente |
31 |
Guía de Remisión Transportista |
En POST /invoices, tipo_comprobante es opcional: si no lo envías, la
API lo infiere del cliente (01 si el tax_id tiene 11 dígitos o
document_type es 6 — es decir, tiene RUC; 03 en cualquier otro caso).
7.2 Tipo de documento de identidad (catálogo 06 SUNAT)
Campo customer.document_type (en /invoices) y
destinatario_tipo_doc/remitente_tipo_doc (en /dispatch-guides*).
La API no valida este campo contra una lista cerrada — solo exige que sea
un único carácter (string|max:1) y lo reenvía tal cual a SUNAT, que sí lo
valida contra su catálogo 06 oficial al procesar el documento (un código
incorrecto puede resultar en observado o rechazado, no en un 422 de
esta API). Si no envías document_type en customer, la API lo infiere
automáticamente:
| Código | Documento | Cuándo se infiere automáticamente |
|---|---|---|
1 |
DNI | tax_id de 8 dígitos |
6 |
RUC | tax_id de 11 dígitos |
0 |
Otros / sin documento | Cualquier otro caso, o tax_id vacío |
Para otros documentos de identidad (Carné de Extranjería, Pasaporte, etc.), consulta con tu proveedor el código exacto del catálogo 06 vigente de SUNAT antes de enviarlo — esta API no los infiere ni los valida por ti.
7.3 Código de afectación al IGV (items.*.tax_affectation_code / catálogo 07 SUNAT)
Solo estos tres valores tienen cálculo de impuestos implementado en la API (cualquier otro código se acepta en la validación pero no calculará el IGV correctamente):
| Código | Afectación | Efecto |
|---|---|---|
10 |
Gravado (Operación Onerosa) — valor por defecto si no envías el campo | Se calcula IGV 18% sobre el valor de venta |
20 |
Exonerado | Sin IGV |
30 |
Inafecto | Sin IGV |
7.4 Unidad de medida (unit_measure / catálogo 03 SUNAT, UN/ECE Rec. 20)
Valor por defecto: NIU (Unidad) si no envías el campo. La API no valida
este campo contra una lista cerrada (string|max:3) — se reenvía tal cual al
XML UBL; usa los códigos UN/ECE estándar (NIU unidad, KGM kilogramo,
MTR metro, etc.) según lo que corresponda a tu producto.
7.5 Motivo de Nota de Crédito (cod_tipo_motivo cuando tipo_comprobante=07 / catálogo 09 SUNAT)
| Código | Motivo |
|---|---|
01 |
Anulación de la operación |
02 |
Anulación por error en el RUC |
03 |
Corrección por error en la descripción |
04 |
Descuento global |
05 |
Descuento por ítem |
06 |
Devolución total |
07 |
Devolución por ítem |
08 |
Bonificación |
09 |
Disminución en el valor |
10 |
Otros conceptos |
11 |
Ajuste de operaciones de exportación |
12 |
Ajustes - montos y/o fechas de pago IVAP |
7.6 Motivo de Nota de Débito (cod_tipo_motivo cuando tipo_comprobante=08 / catálogo 10 SUNAT)
| Código | Motivo |
|---|---|
01 |
Intereses por mora |
02 |
Aumento en el valor |
03 |
Penalidades / otros conceptos |
7.7 Motivo de traslado (motivo_traslado_code / catálogo 20 SUNAT)
| Código | Motivo |
|---|---|
01 |
Venta |
14 |
Venta sujeta a confirmación del comprador |
02 |
Compra |
04 |
Traslado entre establecimientos de la misma empresa |
18 |
Traslado emisor itinerante CP |
08 |
Importación |
09 |
Exportación |
19 |
Traslado a zona primaria |
13 |
Otros — requiere que también envíes motivo_traslado_descripcion con el sustento |
No existe un código propio para “Consignación” en el catálogo oficial de
SUNAT — usa 13 (Otros) con el sustento en motivo_traslado_descripcion.
7.8 Modalidad de traslado (modalidad_traslado, solo en POST /dispatch-guides)
| Código | Modalidad | Requiere en el body |
|---|---|---|
01 |
Transporte público (un transportista externo) | carrier_id |
02 |
Transporte privado (vehículo propio) | vehicle_id y driver_id |
POST /dispatch-guides/transportista no usa este campo — siempre opera en
modalidad privada (02) con el vehículo/conductor del transportista mismo.
Nota importante: carrier_id/vehicle_id/driver_id deben pertenecer a
tu cuenta (tenant). Hoy no existe un endpoint público para dar de alta tus
propios transportistas/vehículos/conductores — pídele a tu proveedor que te
los registre antes de emitir guías con esta modalidad.
8. Endpoints
Todos bajo {BASE_URL} = https://<tu-dominio-facturador>/api/v1/external.
8.1 GET /me
Verifica que tu API key es válida y te devuelve los datos de tu cuenta. No crea ni modifica nada — es el endpoint recomendado para probar tu integración.
Headers: Authorization: Bearer <api_key>
Respuesta 200:
{
"tenant": { "id": 15, "name": "Tu Empresa S.A.C.", "slug": "tu-empresa", "status": "active" },
"environment": "beta"
}
environment ("beta" o "produccion") es el ambiente de la key que usaste
para el request, no del tenant — un mismo tenant puede tener keys de ambos
ambientes activas a la vez (ver sección 10). Es la forma más simple de
confirmar contra qué ambiente está apuntando tu integración en cualquier
momento.
8.2 POST /invoices — Emitir Factura o Boleta
Headers: Authorization: Bearer <api_key>, Content-Type: application/json
Body:
| Campo | Tipo | Requerido | Notas |
|---|---|---|---|
tipo_comprobante |
string | No | 01 o 03 — ver 7.1, se infiere si se omite |
fecha_emision |
string (YYYY-MM-DD) |
No | Hoy por defecto. SUNAT solo acepta hasta 3 días calendario atrás |
external_reference |
string | No | Referencia libre tuya (tu propio id de venta/pedido) — la API no la interpreta, solo la guarda y te la devuelve tal cual |
branch_id |
integer | No | id de una de tus sucursales (ver sección 8.30) — si la omites, usa tu sucursal marcada is_default (si tienes una, sección 8.31); si no tienes ninguna marcada, comportamiento idéntico al de una cuenta sin sucursales configuradas |
customer.tax_id |
string | No | RUC/DNI del cliente |
customer.name |
string | No | Razón social / nombre |
customer.address |
string | No | Dirección |
customer.document_type |
string | No | Ver 7.2 |
items[].description |
string | Sí | |
items[].quantity |
number | Sí | |
items[].unit_price |
number | Sí | Precio unitario con IGV incluido |
items[].sku |
string | No | Tu propio código de producto |
items[].unit_measure |
string | No | Ver 7.4, NIU por defecto |
items[].tax_affectation_code |
string | No | Ver 7.3, 10 por defecto |
Ejemplo de request (Boleta, probado en vivo):
{
"tipo_comprobante": "03",
"external_reference": "PEDIDO-00123",
"customer": {
"tax_id": "10412345678",
"name": "CLIENTE DE PRUEBA",
"address": "AV. SIEMPRE VIVA 123",
"document_type": "1"
},
"items": [
{ "sku": "DEMO-1", "description": "Producto de prueba", "quantity": 2, "unit_price": 15.90 }
]
}
Respuesta 201 (capturada en vivo, estado_sunat: "aceptado"):
{
"id": 7,
"environment": "beta",
"external_reference": "PEDIDO-00123",
"tipo_comprobante": "03",
"serie": "B001",
"numero": "00000003",
"fecha_emision": "2026-08-13T00:00:00.000000Z",
"moneda": "PEN",
"subtotal": "26.95",
"total_igv": "4.85",
"total": "31.80",
"customer": {
"tax_id": "10412345678",
"name": "CLIENTE DE PRUEBA",
"address": "AV. SIEMPRE VIVA 123",
"document_type": "1"
},
"estado_sunat": "aceptado",
"codigo_respuesta_sunat": "0",
"descripcion_sunat": "La Boleta numero B001-00000003, ha sido aceptada",
"documents": { "xml_available": true, "cdr_available": true, "pdf_available": true },
"items": [
{ "id": 7, "sku": "DEMO-1", "description": "Producto de prueba", "unit_measure": "NIU", "quantity": 2, "unit_price": "15.90", "tax_affectation_code": "10", "igv_amount": "4.85", "subtotal": "26.95" }
],
"branch": null,
"created_at": "2026-08-13T19:40:16.000000Z"
}
branch es null si no enviaste branch_id, o el objeto completo de la
sucursal (mismo shape que GET /branches, sección 8.30) si lo enviaste.
environment refleja el ambiente de la key que emitió el documento — queda
grabado en el documento mismo, así que si más adelante consultas historial o
haces analítica sobre tus propios registros, cada uno sabe a qué ambiente
perteneció aunque hayas rotado de key. documents indica si el XML/CDR/PDF
ya están listos para descargar — ver sección 6.1 y 8.5-8.7.
Guarda items[].id de la respuesta — lo vas a necesitar como
invoice_item_id si más adelante emites una nota de crédito/débito sobre
esta línea (sección 8.8). No es un id de producto tuyo.
¿Quieres avisarle a tu cliente por WhatsApp que este comprobante ya está listo? No pasa automáticamente al emitir — es una acción aparte, opcional, que disparas cuando tú decidas (por ejemplo, cuando tu propio usuario hace clic en un botón “Enviar por WhatsApp” en tu sistema). Ver sección 8.27.
Errores posibles: 422 validation_failed (incluye branch_id que no
pertenece a tu cuenta), 422 sunat_not_configured,
422 document_series_not_configured, 401, 429 (ver sección 3).
8.3 GET /invoices — Buscar/listar comprobantes
Headers: Authorization: Bearer <api_key>
Parámetros de query (todos opcionales):
| Parámetro | Tipo | Notas |
|---|---|---|
fecha_desde / fecha_hasta |
string (YYYY-MM-DD) |
Filtra por fecha_emision. fecha_hasta debe ser igual o posterior a fecha_desde |
estado_sunat |
string | Uno de los valores de la sección 6 |
tipo_comprobante |
string | 01 o 03 — ver 7.1 |
cliente |
string | Busca coincidencia parcial contra customer.tax_id o customer.name |
page |
integer | Página, arranca en 1 |
per_page |
integer | Tamaño de página, máx. 100 (15 por defecto) |
Ejemplo de request (probado en vivo):
GET /invoices?estado_sunat=aceptado&fecha_desde=2026-08-01&fecha_hasta=2026-08-31
Respuesta 200 — mismo envelope de paginación en toda la plataforma:
{
"data": [
{ "id": 9, "environment": "beta", "...": "... (mismo shape que 8.2) ..." }
],
"pagination": { "current_page": 1, "last_page": 1, "per_page": 15, "total": 1 }
}
Solo ves comprobantes de tu propia cuenta — nunca hay que filtrar por tenant tú mismo (ver sección 1/5).
Errores posibles: 422 validation_failed (parámetro inválido, ej.
estado_sunat fuera del catálogo), 401, 429.
8.4 GET /invoices/{id} — Consultar un comprobante
Headers: Authorization: Bearer <api_key>
Si el comprobante tiene una comunicación de baja (void) con
estado_sunat: "pendiente", la API reconsulta el ticket ante SUNAT
automáticamente antes de responder — no necesitas un endpoint de “check”
aparte.
Respuesta 200: mismo shape que la respuesta de POST /invoices, más:
{
"...": "... (mismos campos de arriba) ...",
"notes": [],
"void": null
}
notes es un array (puede estar vacío) con cada nota de crédito/débito
emitida sobre este comprobante. void es null o el objeto de la
comunicación de baja (mismo shape que la sección 8.12).
Errores posibles: 404 not_found (id inexistente o de otra cuenta),
401, 429.
8.5 GET /invoices/{id}/xml — Descargar el XML
8.6 GET /invoices/{id}/cdr — Descargar el CDR
8.7 GET /invoices/{id}/pdf — Descargar el PDF
Headers: Authorization: Bearer <api_key>
Los tres devuelven el archivo tal cual (no un JSON) con el Content-Type
correspondiente (application/xml, application/zip, application/pdf) y
Content-Disposition: attachment. Nunca repollan SUNAT ni generan nada al
vuelo — son los archivos ya persistidos al emitir (ver sección 6.1).
Probado en vivo contra un comprobante aceptado — los tres devuelven
200 con contenido real (XML UBL 2.1 firmado, ZIP del CDR oficial de SUNAT
con hash SHA verificable, y un PDF de una página con QR y el hash del CDR en
el pie).
Errores posibles: 404 not_found (id inexistente o de otra cuenta),
404 document_not_ready (el archivo pedido todavía no existe — ver 6.1),
401, 429.
8.8 POST /invoices/{id}/notes — Emitir Nota de Crédito o Débito
Headers: Authorization: Bearer <api_key>, Content-Type: application/json
Body:
| Campo | Tipo | Requerido | Notas |
|---|---|---|---|
tipo_comprobante |
string | Sí | 07 (crédito) u 08 (débito) |
cod_tipo_motivo |
string | Sí | Ver 7.5/7.6 según el tipo elegido |
descripcion_motivo |
string | Sí | Máx. 255 caracteres |
items[].invoice_item_id |
integer | Sí | El id que devolvió POST /invoices en items[].id — debe pertenecer al comprobante {id} de la URL |
items[].quantity |
integer | Sí | No puede exceder la cantidad del ítem original |
items[].unit_price |
number | No | Si se omite, usa el precio del comprobante original |
Ejemplo de request (probado en vivo):
{
"tipo_comprobante": "07",
"cod_tipo_motivo": "01",
"descripcion_motivo": "Anulacion de la operacion",
"items": [
{ "invoice_item_id": 4, "quantity": 1 }
]
}
Respuesta 201 — nota: este ejemplo real quedó estado_sunat: "rechazado"
(el valor de venta del ítem no coincidía exactamente en la validación de
SUNAT), justamente para mostrar cómo se ve un rechazo real — sigue siendo
201 porque la operación de la API se completó (ver sección 3 y 6):
{
"id": 2,
"environment": "beta",
"invoice_id": 8,
"tipo_comprobante": "07",
"serie": "FC01",
"numero": "00000001",
"cod_tipo_motivo": "01",
"descripcion_motivo": "Anulacion de la operacion",
"fecha_emision": "2026-08-13T19:40:45.000000Z",
"moneda": "PEN",
"subtotal": "100.00",
"total_igv": "18.00",
"total": "118.00",
"estado_sunat": "rechazado",
"codigo_respuesta_sunat": "soap-env:Client.3271",
"descripcion_sunat": "El valor de venta por ítem difiere de los importes consignados...",
"documents": { "xml_available": true, "cdr_available": false, "pdf_available": true },
"items": [
{ "id": 2, "invoice_item_id": 8, "sku": "DEMO-2", "description": "Producto para nota de credito", "unit_measure": "NIU", "quantity": 1, "unit_price": "118.00", "tax_affectation_code": "10", "igv_amount": "18.00", "subtotal": "100.00" }
],
"created_at": "2026-08-13T19:40:45.000000Z"
}
Nota real de esta captura: cdr_available: false — cuando SUNAT rechaza por
una falla SOAP (sin llegar a emitir un CDR), el XML y el PDF sí quedan
persistidos (útiles como evidencia de lo que se intentó enviar), pero el CDR
nunca existió del lado de SUNAT — pedirlo responde 404 document_not_ready
(sección 6.1), no es un bug.
Errores posibles: 422 validation_failed (incluye el caso de
invoice_item_id que no pertenece a este comprobante),
422 invoice_not_accepted (el comprobante original no está aceptado),
422 invoice_note_exceeds_original, 404, 401, 429.
¿Quieres avisarle a tu cliente por WhatsApp que esta nota ya está lista?
Mismo criterio que en POST /invoices — acción aparte, opcional, ver
sección 8.27.
8.9 GET /invoices/{id}/notes/{note_id}/xml — Descargar el XML de una nota
8.10 GET /invoices/{id}/notes/{note_id}/cdr — Descargar el CDR de una nota
8.11 GET /invoices/{id}/notes/{note_id}/pdf — Descargar el PDF de una nota
Headers: Authorization: Bearer <api_key>
Mismo comportamiento que 8.5-8.7, pero para una nota de crédito/débito. La
nota debe pertenecer al comprobante {id} de la URL — si el note_id existe
pero pertenece a otro comprobante (o a otra cuenta), responde
404 not_found igual que cualquier id ajeno.
Errores posibles: 404 not_found, 404 document_not_ready, 401, 429.
8.12 POST /invoices/{id}/void — Comunicar la baja de un comprobante
Solicita a SUNAT anular un comprobante ya aceptado. Es asíncrono del lado
de SUNAT — el resultado puede tardar en confirmarse (por eso GET /invoices/{id} reconsulta automáticamente mientras esté pendiente).
Headers: Authorization: Bearer <api_key>, Content-Type: application/json
Body:
{ "motivo": "Error en el RUC del cliente" }
motivo: string, requerido, máx. 255 caracteres.
Respuesta 201 (probado en vivo, estado_sunat: "aceptado"):
{
"id": 2,
"environment": "beta",
"invoice_id": 8,
"motivo": "Error en el RUC del cliente",
"estado_sunat": "aceptado",
"codigo_respuesta_sunat": "0",
"descripcion_sunat": "La Comunicacion de baja RA-20260813-00001, ha sido aceptada",
"fecha_solicitud": "2026-08-13T19:42:22.000000Z"
}
Errores posibles:
409 invoice_already_voidedsi ya se solicitó una baja para este comprobante (probado en vivo):{"error":{"code":"invoice_already_voided","message":"Ya existe una comunicación de baja para este comprobante."}}422 invoice_not_acceptedsi el comprobante no estáaceptado.422 validation_failed,404,401,429.
Este endpoint no genera XML/CDR/PDF descargables — la comunicación de baja es un flujo distinto (resumen/ticket) sin representación impresa propia, fuera del alcance de la sección 6.1.
8.13 POST /dispatch-guides — Guía de Remisión Remitente (código 09)
Úsala cuando tú eres quien traslada la mercadería (como remitente),
usando transporte público (un carrier externo) o privado (tu propio
vehicle/driver).
Headers: Authorization: Bearer <api_key>, Content-Type: application/json
Body:
| Campo | Tipo | Requerido | Notas |
|---|---|---|---|
motivo_traslado_code |
string | Sí | Ver 7.7 |
motivo_traslado_descripcion |
string | Solo si motivo_traslado_code=13 |
|
modalidad_traslado |
string | Sí | 01 o 02, ver 7.8 |
fecha_traslado |
string (YYYY-MM-DD) |
Sí | |
llegada_ubigeo |
string (6 dígitos) | Sí | |
llegada_direccion |
string | Sí | |
partida_ubigeo / partida_direccion |
string | No | Si se omiten, usa el domicilio fiscal registrado de tu cuenta |
peso_bruto_total |
number | No | Si se omite, se suma items[].weight × quantity |
numero_bultos |
integer | No | |
carrier_id |
integer | Si modalidad_traslado=01 |
Debe pertenecer a tu cuenta |
vehicle_id / driver_id |
integer | Si modalidad_traslado=02 |
Deben pertenecer a tu cuenta |
destinatario_tipo_doc |
string | Sí | Ver 7.2 |
destinatario_num_doc |
string | Sí | |
destinatario_razon_social |
string | Sí | |
invoice_id |
integer | No | El comprobante relacionado, si aplica — debe pertenecer a tu cuenta |
branch_id |
integer | No | id de una de tus sucursales (sección 8.30) — si la omites, usa tu sucursal marcada is_default si tienes una (sección 8.31); si no, comportamiento idéntico al de una cuenta sin sucursales configuradas |
external_reference |
string | No | |
items[].description |
string | Sí | |
items[].quantity |
number | Sí | |
items[].sku / unit_measure / weight |
— | No | weight es el peso unitario, usado para sugerir peso_bruto_total |
Ejemplo de request (probado en vivo):
{
"motivo_traslado_code": "01",
"modalidad_traslado": "02",
"fecha_traslado": "2026-08-14",
"partida_ubigeo": "150101",
"partida_direccion": "Av. Industrial 500",
"llegada_ubigeo": "150102",
"llegada_direccion": "Av. Los Olivos 123",
"vehicle_id": 2,
"driver_id": 2,
"destinatario_tipo_doc": "6",
"destinatario_num_doc": "20512345678",
"destinatario_razon_social": "DISTRIBUIDORA DEMO S.A.C.",
"items": [
{ "sku": "POLO-1", "quantity": 10, "description": "Polo", "weight": 0.35 }
]
}
Respuesta 201 — este ejemplo real quedó estado_sunat: "observado"
(el RUC del destinatario que se usó en la prueba no está registrado como
habido ante SUNAT), mostrado a propósito para ilustrar el caso (sigue siendo
201, ver sección 6):
{
"id": 3,
"environment": "beta",
"external_reference": null,
"invoice_id": null,
"serie": "T900",
"numero": "00000001",
"document_type_code": "09",
"fecha_emision": "2026-08-13T00:00:00.000000Z",
"fecha_traslado": "2026-08-14T00:00:00.000000Z",
"motivo_traslado_code": "01",
"motivo_traslado_descripcion": null,
"modalidad_traslado": "02",
"peso_bruto_total": "3.50",
"numero_bultos": null,
"partida_ubigeo": "150101",
"partida_direccion": "Av. Industrial 500",
"llegada_ubigeo": "150102",
"llegada_direccion": "Av. Los Olivos 123",
"destinatario_tipo_doc": "6",
"destinatario_num_doc": "20512345678",
"destinatario_razon_social": "DISTRIBUIDORA DEMO S.A.C.",
"remitente_tipo_doc": null,
"remitente_num_doc": null,
"remitente_razon_social": null,
"related_dispatch_guide_id": null,
"carrier": null,
"vehicle": { "id": 2, "placa": "ABC123", "marca": "Toyota", "modelo": "Hilux", "categoria": "M1", "carrier_id": null, "is_active": true, "created_at": "...", "updated_at": "..." },
"driver": { "id": 2, "dni": "12345678", "nombres": "Carlos", "apellidos": "Rojas Mendez", "licencia": "Q12345678", "carrier_id": null, "is_active": true, "created_at": "...", "updated_at": "..." },
"branch": null,
"estado_sunat": "observado",
"codigo_respuesta_sunat": "2011",
"descripcion_sunat": "El contribuyente no está habido",
"documents": { "xml_available": true, "cdr_available": true, "pdf_available": true },
"items": [
{ "id": 3, "sku": "POLO-1", "quantity": "10.00", "unit_measure": "NIU", "description": "Polo" }
],
"created_at": "2026-08-13T19:44:39.000000Z"
}
Nota real de esta captura: aunque el resultado es observado, cdr_available
es true — un observado sigue siendo una respuesta definitiva de
SUNAT, con su propio CDR (a diferencia del rechazado sin CDR del ejemplo de
notas en 8.8, que fue una falla de comunicación SOAP, no una respuesta de
SUNAT). El XML y el PDF quedan disponibles de inmediato al crear la guía —
antes incluso de que el ticket resuelva (la GRE es asíncrona, ver sección
6.1); el CDR aparece cuando SUNAT termina de procesarlo.
Nota: si omites partida_ubigeo/partida_direccion, la API usa el domicilio
fiscal registrado de tu cuenta — en este ejemplo se enviaron explícitos solo
para que el request fuera autocontenido.
Nota: SUNAT exige la placa del vehículo sin guiones en el XML — la API la limpia automáticamente al armar el documento, tú puedes registrarla con o sin guion.
Errores posibles: 422 validation_failed (incluye carrier_id/
vehicle_id/driver_id/invoice_id/branch_id que no pertenecen a tu
cuenta), 422 sunat_gre_not_configured, 404, 401, 429.
¿Quieres avisarle a tu cliente por WhatsApp que esta guía ya está lista?
Mismo criterio que en POST /invoices — acción aparte, opcional, ver
sección 8.27 (aplica igual a guías Remitente y Transportista).
8.14 POST /dispatch-guides/transportista — Guía de Remisión Transportista (código 31)
Úsala cuando tu cuenta actúa como transportista de mercadería de un tercero (el remitente no es el emisor de este documento).
Headers: Authorization: Bearer <api_key>, Content-Type: application/json
Body: igual a POST /dispatch-guides (sección 8.13), salvo:
| Campo | Tipo | Requerido |
|---|---|---|
remitente_tipo_doc |
string | Sí |
remitente_num_doc |
string | Sí |
remitente_razon_social |
string | Sí |
vehicle_id |
integer | Sí (siempre, no depende de modalidad_traslado — este endpoint no usa ese campo) |
driver_id |
integer | Sí |
related_dispatch_guide_id |
integer | No — debe pertenecer a tu cuenta |
No lleva modalidad_traslado: siempre opera con vehículo/conductor propio.
Respuesta 201: mismo shape que 8.13, con remitente_* poblado.
Errores posibles: mismos que 8.13.
8.15 GET /dispatch-guides — Buscar/listar guías
Headers: Authorization: Bearer <api_key>
Parámetros de query (todos opcionales): mismos que GET /invoices
(sección 8.3), con dos diferencias:
| Parámetro | Notas |
|---|---|
tipo_comprobante |
Acá filtra por document_type_code — usa 09 o 31 (ver 7.1), mismo nombre de parámetro por uniformidad con /invoices |
cliente |
Compara contra destinatario y remitente (cubre también las guías de transportista, donde el remitente es el dueño real de la carga) |
Respuesta 200: mismo envelope de paginación que 8.3.
Errores posibles: 422 validation_failed, 401, 429.
8.16 GET /dispatch-guides/{id} — Consultar una guía
Headers: Authorization: Bearer <api_key>
Si estado_sunat sigue pendiente, la API reconsulta el ticket ante SUNAT
automáticamente antes de responder.
Respuesta 200: mismo shape que la respuesta de POST /dispatch-guides.
Errores posibles: 404 not_found, 401, 429.
8.17 GET /dispatch-guides/{id}/xml — Descargar el XML
8.18 GET /dispatch-guides/{id}/cdr — Descargar el CDR
8.19 GET /dispatch-guides/{id}/pdf — Descargar el PDF
Headers: Authorization: Bearer <api_key>
Mismo comportamiento que 8.5-8.7 (archivo real, Content-Disposition: attachment, nunca repolea SUNAT). Probado en vivo: el XML y el PDF están
disponibles desde el mismo POST que crea la guía; el CDR recién aparece
cuando el ticket resuelve — pedirlo antes responde 404 document_not_ready
(ver la nota real de 8.13 y la sección 6.1).
Errores posibles: 404 not_found, 404 document_not_ready, 401, 429.
8.20 GET /lookups/ruc/{ruc} — Consultar un RUC
Consulta datos de un RUC ante SUNAT para que autocompletes customer antes
de armar una Factura/Boleta — es un endpoint de consulta independiente, la
API nunca autocompleta customer por ti al emitir. Internamente
respondemos desde nuestra propia base de datos cuando ya tenemos el RUC
consultado y no está vencido (ver nota de caché debajo) — no siempre se
llama en vivo al proveedor externo.
Headers: Authorization: Bearer <api_key>
Respuesta 200 (capturada en vivo, RUC de la propia SUNAT):
{
"ruc": "20131312955",
"name": "SUPERINTENDENCIA NACIONAL DE ADUANAS Y DE ADMINISTRACION TRIBUTARIA - SUNAT",
"business_line": null,
"status": "ACTIVO",
"domicile_conditions": "HABIDO",
"ubigeo": "150101",
"department": "LIMA",
"province": "LIMA",
"district": "LIMA",
"address": "AV. GARCILASO DE LA VEGA NRO. 1472 - LIMA LIMA LIMA",
"person_type_code": null,
"person_type": null,
"zone": null,
"company_type": null,
"es_buen_contribuyente": false,
"es_agente_de_retencion": true,
"date_creation": null,
"date_update": "2026-08-17",
"updated_at": "2026-08-17T15:28:43.000000Z"
}
business_line/person_type_code/person_type/zone/company_type/
date_creation no siempre vienen informados — el proveedor externo los
devuelve como null cuando no tiene el dato para ese RUC en particular (no
es un error nuestro). updated_at es la fecha en que nosotros
consultamos este RUC por última vez — útil para que sepas qué tan reciente
es el dato (ver nota de caché debajo).
Errores posibles: 422 validation_failed (el RUC no tiene 11 dígitos),
422 lookup_provider_not_configured, 422 lookup_provider_unavailable (RUC
no encontrado), 429 lookup_quota_exceeded, 401, 429 rate_limit_exceeded.
8.21 GET /lookups/dni/{dni} — Consultar un DNI
Mismo propósito que 8.20, para clientes persona natural.
Headers: Authorization: Bearer <api_key>
Respuesta 200 (capturada en vivo):
{
"dni": "46027897",
"full_name": "ROXANA KARINA DELGADO HUAMANI",
"name": "ROXANA KARINA",
"surname": "DELGADO HUAMANI",
"first_last_name": "DELGADO",
"second_last_name": "HUAMANI",
"gender": null,
"date_of_birth": null,
"department": null,
"province": null,
"district": null,
"address": null,
"ubigeo": null,
"updated_at": "2026-09-08T15:28:44.000000Z"
}
gender, date_of_birth, department, province, district, address
y ubigeo no vienen informados por el proveedor de este servicio — no es
un caso puntual, ocurre siempre. No los uses para autocompletar esos datos;
sí están confiables para RUC (sección 8.20).
Errores posibles: 422 validation_failed (el DNI no tiene 8 dígitos),
422 lookup_provider_not_configured, 422 lookup_provider_unavailable, 429 lookup_quota_exceeded, 401, 429 rate_limit_exceeded.
8.22 GET /lookups/ruc/{ruc}/establecimientos — Establecimientos anexos de un RUC
Locales/sucursales registrados a un RUC ante SUNAT.
Headers: Authorization: Bearer <api_key>
Respuesta 200 (capturada en vivo):
[
{
"codigo": "0195",
"tipo_establecimiento": "AG. AGENCIA",
"actividad_economica": null,
"direccion": "CAL. PROLONGACION TACNA, LA LIBERTAD - ASCOPE - RAZURI",
"departamento": "LA LIBERTAD",
"provincia": "ASCOPE",
"distrito": "RAZURI",
"ubigeo_sunat": "130206"
}
]
Puede devolver una lista vacía [] si el RUC no tiene anexos registrados
(no es un error). Errores posibles: 422 validation_failed, 422 lookup_provider_not_configured, 422 lookup_provider_unavailable, 429 lookup_quota_exceeded, 401, 429 rate_limit_exceeded.
8.23 GET /lookups/ruc/{ruc}/representantes — Representantes legales de un RUC
Headers: Authorization: Bearer <api_key>
Respuesta 200 (capturada en vivo):
[
{
"tipo_documento": "DNI",
"numero_documento": "02808305",
"nombres": "RAMIREZ GARCIA CARLOS MARCELINO",
"cargo": "GERENTE DE FINANZAS",
"fecha_desde": "2025-07-25"
}
]
Errores posibles: igual que 8.22.
8.24 GET /lookups/tipo-cambio — Tipo de cambio
Headers: Authorization: Bearer <api_key>
Parámetros de query:
| Parámetro | Tipo | Requerido | Notas |
|---|---|---|---|
moneda |
string | Sí | USD, EUR o GBP |
fecha |
string (YYYY-MM-DD) |
No | Hoy por defecto. No acepta fechas futuras |
Respuesta 200 (capturada en vivo):
{
"currency": "USD",
"date": "2026-08-16",
"buy": "3.3580",
"sale": "3.3680",
"updated_at": "2026-08-17T20:59:32.000000Z"
}
A diferencia de RUC/DNI, el tipo de cambio de una fecha ya pasada nunca
vence en nuestra caché — es un dato histórico que no cambia. Si pides
una fecha para la que todavía no hay tipo de cambio publicado, recibes
422 lookup_provider_unavailable.
Errores posibles: 422 validation_failed (moneda no soportada o fecha
futura), 422 lookup_provider_not_configured, 422 lookup_provider_unavailable,
429 lookup_quota_exceeded, 401, 429 rate_limit_exceeded.
8.25 GET /lookups/afp-comisiones — Comisiones AFP por periodo
Headers: Authorization: Bearer <api_key>
Parámetros de query:
| Parámetro | Tipo | Requerido | Notas |
|---|---|---|---|
periodo |
string (YYYY-MM) |
No | Mes calendario actual por defecto |
Respuesta 200 (capturada en vivo):
[
{
"periodo": "2026-07",
"afp": "HABITAT",
"comision_fija": "0.0000",
"comision_sobre_flujo": "1.4700",
"comision_mixta_flujo": "0.0000",
"comision_mixta_sobre_saldo": "1.2500",
"prima_de_seguro": "1.3700",
"aporte_obligatorio": "10.0000",
"remuneracion_maxima_asegurable": "12672.65"
}
]
Igual que el tipo de cambio, un periodo ya publicado no vence en nuestra
caché. Errores posibles: 422 validation_failed, 422 lookup_provider_not_configured, 422 lookup_provider_unavailable, 429 lookup_quota_exceeded, 401, 429 rate_limit_exceeded.
8.26 POST /lookups/cpe — Validar un comprobante de un tercero ante SUNAT
Verifica si un comprobante (no necesariamente emitido por ti) es válido ante SUNAT — útil para confirmar antes de aceptar un comprobante de un proveedor tuyo, por ejemplo.
Headers: Authorization: Bearer <api_key>, Content-Type: application/json
Body:
| Campo | Tipo | Requerido | Notas |
|---|---|---|---|
ruc_emisor |
string | Sí | RUC de quien emitió el comprobante (11 dígitos) |
tipo_comprobante |
string | Sí | Ver 7.1 |
serie |
string | Sí | |
numero |
string | Sí | |
fecha_emision |
string (YYYY-MM-DD) |
Sí | |
total |
number | Sí | Monto total del comprobante |
tipo_documento_cliente |
string | No | Tipo de documento del receptor |
numero_documento_cliente |
string | No | Número de documento del receptor |
Respuesta 200:
{
"ruc_emisor": "20131312955",
"tipo_comprobante": "01",
"serie": "F001",
"numero": "111",
"fecha_emision": "2024-08-26",
"total": "411.82",
"cpe_code": "1",
"status_cpe_description": "ACEPTADO",
"ruc_code": "00",
"status_ruc_description": "ACTIVO",
"address_ruc_code": "00",
"status_address_ruc": "HABIDO",
"updated_at": "2026-08-17T20:59:48.000000Z"
}
status_cpe_description es lo que te interesa validar (ACEPTADO,
ANULADO, etc. — el catálogo exacto depende de SUNAT, no lo
reinterpretamos). A diferencia de todos los demás endpoints de esta
sección, este cachea por solo 1 día (no 30/365) — el estado de un
comprobante puede cambiar (por ejemplo, pasar a anulado), así que no
conviene servir un resultado viejo por mucho tiempo.
Errores posibles: 422 validation_failed, 422 lookup_provider_not_configured, 422 lookup_provider_unavailable, 429 lookup_quota_exceeded, 401, 429 rate_limit_exceeded.
Caché propia — por qué una consulta puede no llegar al proveedor externo: guardamos en nuestra base de datos el resultado de cada consulta de esta sección, compartido entre todos nuestros clientes (si otro cliente ya consultó el mismo RUC/DNI/periodo, tú también te beneficias de esa caché). Cada endpoint tiene su propia vigencia: RUC 30 días, DNI 365 días, establecimientos/representantes 30 días, tipo de cambio y comisiones AFP no vencen (son hechos históricos), validación de comprobante (8.26) 1 día. No hay forma de forzar una reconsulta antes de ese plazo por ahora.
Cupo mensual de /lookups/*: además del límite de 60 req/min de la
sección 4 (compartido con el resto de la API), todas las consultas de
esta sección comparten un cupo mensual propio (todas suman al mismo
contador, sin importar el endpoint). A diferencia de todo lo demás en
esta API, superarlo sí bloquea la consulta — cada vez que necesitamos
consultar al proveedor externo (no cuando respondemos desde nuestra
caché) tiene un costo directo para la plataforma. El cupo lo define tu
proveedor por cuenta; contáctalo si necesitas subirlo. Un cache hit
nunca descuenta tu cupo — solo se descuenta cuando la consulta no
estaba en nuestra base de datos o el registro ya venció.
8.27 POST /invoices/{id}/whatsapp — Notificar por WhatsApp al cliente final
Avisa por WhatsApp que la Factura o Boleta {id} ya está lista. Nunca
ocurre automáticamente al emitir — es una acción explícita que disparas
tú, cuando quieras (por ejemplo, cuando tu propio usuario hace clic en un
botón “Enviar por WhatsApp” en tu sistema). Puedes llamarla las veces que
quieras sobre el mismo comprobante — no hay límite de “una sola vez”.
Headers: Authorization: Bearer <api_key>, Content-Type: application/json
Body:
| Campo | Tipo | Requerido | Notas |
|---|---|---|---|
phone |
string | Sí | Teléfono del cliente al que se le envía el aviso. Formato libre (con o sin código de país) — lo normalizamos nosotros |
Respuesta 202 (aceptado, encolado):
{ "message": "Notificación en camino." }
202, no 200: el envío real ocurre en segundo plano (cola), así que esta
respuesta confirma que lo aceptamos, no que ya llegó al teléfono. El
mensaje identifica a tu negocio por su nombre (no el nuestro) y se envía
desde el número/gateway compartido de la plataforma.
Errores posibles: 422 validation_failed (falta phone), 404
(el comprobante no existe o no es tuyo), 401, 429.
8.28 POST /invoices/{id}/notes/{note_id}/whatsapp — Notificar por WhatsApp una nota
Mismo comportamiento que 8.27, para una Nota de Crédito/Débito ya emitida.
Headers/Body: iguales a 8.27.
Errores posibles: iguales a 8.27 (el 404 también cubre una note_id
que no pertenece al {id} de la URL).
8.29 POST /dispatch-guides/{id}/whatsapp — Notificar por WhatsApp una guía
Mismo comportamiento que 8.27, para una Guía de Remisión ya emitida (Remitente o Transportista, sin distinción).
Headers/Body: iguales a 8.27.
Errores posibles: iguales a 8.27.
Cupo mensual de /*/whatsapp: a diferencia del cupo de /lookups/*
(sección 8.20, que sí bloquea), este cupo nunca bloquea el envío — si
tu cuenta lo supera, solo se lo avisamos a tu proveedor para que gestione
el cobro del excedente o ajuste tu plan; tu llamada sigue respondiendo
202 igual.
8.30 GET /branches — Listar tus sucursales
Catálogo de las sucursales activas de tu cuenta. Este endpoint existe para
que tu sistema pueda mapear sus propias sucursales al id que después vas
a enviar como branch_id en POST /invoices (8.2) o POST /dispatch-guides
(8.13).
Headers: Authorization: Bearer <api_key>
Respuesta 200:
[
{ "id": 1, "codigo": "SUC01", "nombre": "Sucursal Miraflores", "direccion": "Av. Larco 123", "ubigeo": "150122", "is_active": true, "is_default": false, "created_at": "...", "updated_at": "..." },
{ "id": 2, "codigo": "SUC02", "nombre": "Sucursal Surco", "direccion": "Av. Benavides 456", "ubigeo": "150140", "is_active": true, "is_default": true, "created_at": "...", "updated_at": "..." }
]
No paginado (el volumen esperado de sucursales por cuenta es bajo). Solo
lista sucursales activas (is_active: true) — si desactivas una, deja de
aparecer acá, pero los documentos ya emitidos con esa sucursal conservan la
referencia.
Errores posibles: 401, 429.
8.31 POST /branches — Crear una sucursal
Alta 100% self-service, sin coordinar con soporte.
Headers: Authorization: Bearer <api_key>, Content-Type: application/json
Body:
| Campo | Tipo | Requerido | Notas |
|---|---|---|---|
codigo |
string | No | Código de establecimiento. Si lo envías, se valida contra los establecimientos anexos reales que SUNAT tiene registrados para tu RUC (mismo dato que devuelve GET /lookups/ruc/{ruc}/establecimientos, sección 8.22) — ver nota abajo |
nombre |
string | Sí | Nombre descriptivo para ti, no aparece en el comprobante |
direccion |
string | No | Se autocompleta desde SUNAT si mandaste codigo y la omites |
ubigeo |
string (6 dígitos) | No | Idem |
is_active |
boolean | No | true por defecto |
is_default |
boolean | No | Si es true, cualquier POST /invoices/dispatch-guides que omita branch_id usa esta sucursal (antes: sin sucursal). Marcar una nueva como is_default desmarca automáticamente la anterior — nunca hay dos a la vez |
Validación contra SUNAT del campo codigo: si el código no corresponde a
ningún establecimiento anexo activo de tu RUC, la API responde 422 en vez
de crear la sucursal — evita que termines emitiendo comprobantes desde un
establecimiento que SUNAT ni siquiera reconoce. Usa "0000" para tu
domicilio fiscal (casa matriz) — se acepta siempre, sin llamar a ningún
proveedor externo, porque no aparece en el listado de anexos (que solo
lista establecimientos adicionales). Esta validación no consume tu
cupo mensual de /lookups/* (sección 8, nota de cupo) ni requiere que
tengas el servicio de consultas habilitado — es parte del alta de
sucursal, no el producto opcional de consultas RUC/DNI.
Respuesta 201: mismo shape que las filas de GET /branches.
Errores posibles: 422 validation_failed (incluye codigo no
registrado en SUNAT, codigo duplicado en tu cuenta, o cualquier otro
campo inválido — revisa error.details), 401, 429.
8.32 GET /branches/{id} — Detalle de una sucursal
8.33 PUT /branches/{id} — Editar una sucursal
Headers: Authorization: Bearer <api_key>, Content-Type: application/json (solo para PUT)
Mismos campos que el alta. La validación contra SUNAT del codigo (8.31)
solo se repite si realmente cambias el valor de codigo respecto al que
ya tenía la sucursal — editar solo nombre/direccion no depende de que
el proveedor externo esté disponible en ese momento.
Errores posibles: 404 not_found (id inexistente o de otra cuenta),
422 validation_failed, 401, 429.
8.34 POST /branches/{id}/deactivate — Desactivar una sucursal
Baja lógica, no física — a diferencia de otros recursos, nunca se borra
una sucursal: los documentos ya emitidos desde ella conservan la
referencia intacta para siempre (GET /invoices/{id} sigue funcionando
igual). Una vez desactivada, deja de aparecer en GET /branches y ya no
se puede usar como branch_id en nuevas emisiones.
Headers: Authorization: Bearer <api_key>
Respuesta 200: la sucursal con is_active: false.
Errores posibles: 422 branch_is_default (es la sucursal
predeterminada — marca otra como is_default primero), 404 not_found,
401, 429.
Nota sobre series/correlativos por sucursal: cada sucursal tiene su
propio contador independiente por tipo de comprobante — dos sucursales
con series configuradas para Boletas, por ejemplo, avanzan sus correlativos
en paralelo sin interferirse (B001-1, B001-2... en una y B002-1, B002-2... en la otra, cada una a su propio ritmo). Si nunca configuras
sucursales, o emites sin branch_id, el comportamiento es el de siempre:
un único contador por tipo de comprobante para toda la cuenta.
9. Notas generales para tu integración
external_referenceen/invoicesy/dispatch-guideses tuyo: úsalo para guardar tu propio id de pedido/venta y así hacer el mapeo de vuelta en tu sistema — la API no lo valida ni lo usa para nada más.- Los precios de línea (
unit_price) van con IGV incluido — la API calcula el desglose de IGV/subtotal por ti. fecha_emision(en/invoices) solo acepta fechas entre hoy y hasta 3 días calendario atrás — es el límite que impone SUNAT, no una regla propia de esta API.- No hay endpoints “de consulta” (
check) separados — el estado se refresca automáticamente en cadaGETmientras sigapendiente. - No hay CRUD externo de transportistas/vehículos/conductores todavía —
si tu integración necesita
modalidad_traslado=01o02, pide a tu proveedor que registre esos recursos para tu cuenta antes de emitir. - Búsqueda y descarga (sección 6.1 y 8.3/8.5-8.7/8.9-8.11/8.15/8.17-8.19):
no necesitas guardar cada
idque devuelve la API para recuperar tus documentos después —GET /invoices/GET /dispatch-guideste permiten reconstruir tu historial por fecha/estado/cliente, y cada documento conserva su XML/CDR/PDF descargables indefinidamente.
10. Ambientes: beta (pruebas) vs. producción
SUNAT opera dos ambientes completamente separados, con URLs de Web Service distintas:
| Ambiente | Uso |
|---|---|
| Beta | Ambiente de pruebas de SUNAT — los comprobantes que emites ahí no tienen validez tributaria real, es donde debes probar tu integración de punta a punta. Todos los ejemplos de este documento se ejecutaron contra el ambiente beta. |
| Producción | Ambiente real de SUNAT — los comprobantes emitidos ahí sí tienen validez tributaria. Úsalo solo cuando tu integración ya esté probada y tu empresa esté lista para emitir comprobantes reales. |
Cómo se determina en qué ambiente opera cada request
El ambiente no es un parámetro de la request ni de la URL — es una
propiedad fija de la API key que usas, no de tu cuenta como un todo: cada
key se emite para un solo ambiente (beta o produccion) y ya no cambia.
Un mismo tenant puede tener keys de ambos ambientes activas simultáneamente
(por ejemplo, una key beta para tu entorno de pruebas/staging y una key
produccion para tu sistema en vivo, corriendo en paralelo) — cada una opera
sobre sus propias credenciales SUNAT, series de comprobantes y correlativos,
completamente aislados del otro ambiente aunque sean el mismo tenant.
Confírmalo con GET /me (sección 8.1): la respuesta incluye
environment con el ambiente de la key que usaste en ese request — es la
forma más simple de verificar contra qué ambiente está apuntando tu
integración en cualquier momento, sin tener que inspeccionar la key misma.
Cada documento que emites también queda etiquetado con el environment
de la key que lo generó — visible en el campo environment de la respuesta
de POST /invoices, POST /invoices/{id}/notes, POST /invoices/{id}/void,
POST /dispatch-guides y POST /dispatch-guides/transportista (y en sus
GET correspondientes). Útil si tu sistema consolida documentos de ambas
keys en un mismo lugar y necesitas distinguir cuáles son de prueba y cuáles
tienen validez tributaria real.
Toda cuenta nueva empieza con una key en modo beta. Para obtener una key de
produccion, coordina con tu proveedor la carga de tus credenciales SUNAT y
certificado digital reales — típicamente, una vez que ya validaste tu
integración completa (emisión, consulta, notas, guías) contra beta.