SWSPERU FACT

← Documentación

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/json en 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 id es un contador propio de tu cuenta, no un id global de la plataforma: en invoices, notes, void y dispatch-guides, id es “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:

  1. 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 estado pendiente hasta 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.
  2. 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_found si el id va en la URL (GET /invoices/{id}, etc.) — indistinguible de un id que simplemente no existe, nunca vas a recibir un 403.
  • 422 validation_failed si el id va en el body de un POST (por ejemplo vehicle_id en una guía de remisión) — el campo específico queda señalado en error.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 de POST, 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
items[].quantity number
items[].unit_price number 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 07 (crédito) u 08 (débito)
cod_tipo_motivo string Ver 7.5/7.6 según el tipo elegido
descripcion_motivo string Máx. 255 caracteres
items[].invoice_item_id integer El id que devolvió POST /invoices en items[].iddebe pertenecer al comprobante {id} de la URL
items[].quantity integer 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_voided si 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_accepted si 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 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 Ver 7.7
motivo_traslado_descripcion string Solo si motivo_traslado_code=13
modalidad_traslado string 01 o 02, ver 7.8
fecha_traslado string (YYYY-MM-DD)
llegada_ubigeo string (6 dígitos)
llegada_direccion string
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 Ver 7.2
destinatario_num_doc string
destinatario_razon_social string
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
items[].quantity number
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
remitente_num_doc string
remitente_razon_social string
vehicle_id integer (siempre, no depende de modalidad_traslado — este endpoint no usa ese campo)
driver_id integer
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 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 RUC de quien emitió el comprobante (11 dígitos)
tipo_comprobante string Ver 7.1
serie string
numero string
fecha_emision string (YYYY-MM-DD)
total number 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 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 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_reference en /invoices y /dispatch-guides es 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 cada GET mientras siga pendiente.
  • No hay CRUD externo de transportistas/vehículos/conductores todavía — si tu integración necesita modalidad_traslado=01 o 02, 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 id que devuelve la API para recuperar tus documentos después — GET /invoices/GET /dispatch-guides te 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.