APIs Peru
← Volver a la documentación

Guía de integración

Referencia técnica completa de la API v2, pensada para leerse de punta a punta. Base: https://api.apisperu.pro

Este documento reúne en un solo lugar el contrato completo de cada endpoint de la API v2 — autenticación, empresas, emisión de comprobantes, Resumen Diario y Guía de Remisión Electrónica. Pensado para integrarse de una sola pasada, sin saltar entre pestañas. Para una exploración interactiva con ejemplos en varios lenguajes, la documentación principal sigue siendo el punto de partida más cómodo.

Introducción y conceptos base

URL base: https://api.apisperu.pro. Todas las rutas de este documento cuelgan de /api/v2/... salvo donde se indique lo contrario. No existe un ambiente de pruebas separado para la API en sí (SUNAT beta se selecciona por Empresa, ver abajo).

Dos credenciales, nunca intercambiables. Cada una responde 403 en las rutas de la otra.

Credencial Qué controla Rutas que acepta
Cuenta (scope=account) Listar/crear Empresas, consultas públicas (RUC/DNI/tipo de cambio/establecimientos) GET/POST /companies, GET /account/context, GET /consultas/*
Empresa (scope=company) Operar UNA empresa específica: su ficha, sus credenciales SUNAT, sus comprobantes GET/PATCH /company, PUT /company/sunat-credentials, PUT /company/gre-credentials, POST /company/digital-certificate, POST/GET /cpe, POST/GET /daily-summaries, POST /dispatch-guides

La Empresa que emite se resuelve siempre desde la credencial autenticada (ApiClient.team_id), nunca desde un company_code en la URL o el cuerpo: quien tiene la credencial de Empresa no puede elegir sobre cuál Empresa opera.

Formato de respuesta estándar en todos los endpoints v2:

{ "success": true, "message": "...", "data": { ... } }

Un error de validación (422) reemplaza data por errors (mapa de campo → lista de mensajes). Los endpoints de emisión (/cpe, /dispatch-guides) agregan además stored, retryable y, en éxito, replay.

Los montos y cantidades se envían siempre como STRING, nunca como número JSON ("100.000000", no 100): preserva la precisión decimal exacta que exige un comprobante tributario.

Autenticación

Tokens Sanctum, emitidos con client_id/client_secret propios de cada credencial (Cuenta o Empresa, generados desde el panel). No hay OAuth ni flujo de contraseña de usuario: la integración guarda el client_id/client_secret como un secreto de servidor.

curl -X POST https://api.apisperu.pro/api/v1/auth/token \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -d '{"client_id": "CLI_XXXXXXXXXXXX", "client_secret": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"}'
{ "success": true, "data": { "access_token": "1|xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", "token_type": "Bearer" } }

El token resultante se envía en cada llamada como Authorization: Bearer <access_token>. Un token de credencial de Cuenta y uno de credencial de Empresa son objetos distintos con abilities distintas: no son intercambiables entre sí (ver tabla de la sección anterior).

Un token cuyo client_id/client_secret ya no existe, o cuya integración fue desactivada/revocada, responde 401 en cualquier ruta protegida.

Cuenta: contexto y consultas

Credencial de Cuenta. Ninguna de estas rutas exige que la Cuenta tenga un plan activo (a diferencia de las consultas, ver abajo): una Cuenta nueva sin Empresas todavía debe poder usarlas.

Método Ruta Para qué
GET /api/v2/account/context Datos de la Cuenta autenticada y resumen de sus Empresas (total/activas/inactivas/borrador)
GET /api/v2/companies Listado paginado de las Empresas de la Cuenta
POST /api/v2/companies Registrar una Empresa nueva (puede quedar en borrador, campos opcionales)

Consultas (exigen plan activo, EnsureAccountHasActivePlan):

Método Ruta Para qué
GET /api/consultas/v1/ruc/{ruc} Ficha de un contribuyente por RUC
GET /api/consultas/v1/dni/{dni} Datos de una persona por DNI
GET /api/consultas/v1/establecimientos/{ruc} Locales anexos declarados para un RUC
GET /api/consultas/v1/tipo-cambio?fecha=YYYY-MM-DD Tipo de cambio SBS de una fecha (por defecto, hoy); informativo — no interviene en la emisión, el emisor sigue enviando el suyo

GET /consultas/v1/ruc/{ruc} y GET /consultas/v1/dni/{dni} devuelven la respuesta plana (sin envolver en data, a diferencia del resto de la API); un RUC/DNI inexistente responde 404 con el mismo formato de error. Ambos comparten la misma estructura de campos: los que no aplican al tipo de documento vienen en null.

{
  "success": true,
  "message": "RUC consultado correctamente.",
  "document_type": "6",
  "document_number": "20131312955",
  "entity_type": "company",
  "display_name": "SUPERINTENDENCIA NACIONAL DE ADUANAS Y DE ADMINISTRACION TRIBUTARIA - SUNAT",
  "business_name": "SUPERINTENDENCIA NACIONAL DE ADUANAS Y DE ADMINISTRACION TRIBUTARIA - SUNAT",
  "status": "ACTIVO",
  "condition": "HABIDO",
  "ubigeo_code": "150101",
  "department": "LIMA",
  "province": "LIMA",
  "district": "LIMA",
  "address": "AV. GARCILASO DE LA VEGA NRO. 1472",
  "fiscal_address": "AV. GARCILASO DE LA VEGA NRO. 1472 LIMA - LIMA - LIMA",
  "location_text": "LIMA - LIMA - LIMA"
}

address se compone a partir de las piezas sueltas del padrón SUNAT (vía, número, interior, manzana, lote...); cuando el padrón no las declara para ese contribuyente, viene en null — dato disperso real, no un campo ausente de la API.

{
  "success": true,
  "message": "DNI consultado correctamente.",
  "document_type": "1",
  "document_number": "41375341",
  "entity_type": "person",
  "display_name": "MARITZA ABANTO ABANTO",
  "full_name": "MARITZA ABANTO ABANTO",
  "first_names": "MARITZA",
  "last_names": "ABANTO ABANTO",
  "paternal_surname": "ABANTO",
  "maternal_surname": "ABANTO",
  "ubigeo_code": "010101",
  "department": "AMAZONAS",
  "province": "CHACHAPOYAS",
  "district": "CHACHAPOYAS",
  "location_text": "AMAZONAS - CHACHAPOYAS - CHACHAPOYAS",
  "voting_group": "000001"
}

Un DNI nunca trae address, status ni condition; un RUC nunca trae first_names, last_names ni voting_group.

Empresas: ficha y aprovisionamiento

GET/POST /api/v2/companies (credencial de Cuenta) listan/crean Empresas. Todo lo demás —ficha, edición y las tres credenciales que una Empresa necesita para emitir— exige la credencial de esa propia Empresa, sin company_code: la Empresa se resuelve desde la credencial.

Método Ruta Para qué
GET /api/v2/company Ficha de la Empresa (datos fiscales + indicadores de configuración)
PATCH /api/v2/company Actualizar datos fiscales (JSON Merge Patch: campo ausente conserva, presente actualiza)
PUT /api/v2/company/sunat-credentials Usuario y Clave SOL
PUT /api/v2/company/gre-credentials client_id/client_secret de la API REST GRE (ver sección Guía de Remisión)
POST /api/v2/company/digital-certificate Certificado digital PKCS#12 (.pfx/.p12), requisito para emitir en producción

Las tres rutas de credenciales comparten el mismo patrón: en el primer guardado ambos campos son obligatorios; en guardados posteriores, la clave/secreto es opcional —si se omite, se conserva la actual—. Ninguna respuesta incluye nunca la clave, el secreto ni la contraseña del certificado: solo un indicador connected/is_configured y metadatos públicos.

Emisión de comprobantes: POST /api/v2/cpe

Un solo endpoint para Factura (01), Boleta (03) y Notas de Crédito/Débito (07/08). Credencial de Empresa. Síncrono: prepara el XML, lo firma, lo transmite a SUNAT y responde en la misma llamada.

{
  "external_reference": "erp-venta-2026-000456",
  "document_type_code": "01",
  "series": "F001",
  "number": "123",
  "issue_date": "2026-09-24",
  "currency_code": "PEN",
  "country_code": "PE",
  "sunat_establishment_code": "0000",
  "operation_type_code": "0101",
  "issuer": { "ruc": "20600000001" },
  "customer": {
    "document_type_code": "6",
    "document_number": "20600000001",
    "name": "Cliente de Prueba SAC",
    "address": "AV. CENTENARIO NRO. 156"
  },
  "items": [{
    "code": "P001",
    "description": "Producto de prueba",
    "quantity": "2.000000",
    "unit_code": "NIU",
    "unit_value": "100.000000",
    "line_value": "200.000000",
    "igv_type_code": "10",
    "igv_percentage": "18.000000",
    "igv_amount": "36.000000",
    "line_total": "236.000000"
  }],
  "totals": {
    "taxable_amount": "200.000000",
    "exonerated_amount": "0.000000",
    "unaffected_amount": "0.000000",
    "igv_amount": "36.000000",
    "total_amount": "236.000000"
  },
  "payment": { "type": "contado" }
}

Series por tipo: Factura ^F[A-Z0-9]{3}$, Boleta ^B[A-Z0-9]{3}$, Nota debe empezar con el prefijo del documento que corrige (F o B).

customer.address es opcional. Cuando se envía, se mapea al bloque de dirección del cliente en el XML UBL; omitido o vacío, el XML se genera sin ese bloque.

Pago a crédito: con payment.type = "contado" no se envía nada más. Con payment.type = "credito", payment.installments es obligatorio: de 1 a 36 cuotas, cada una con amount (string) y due_date (YYYY-MM-DD). La suma de installments[].amount debe cuadrar con totals.total_amount.

{
  "payment": {
    "type": "credito",
    "installments": [
      { "amount": "2950.000000", "due_date": "2026-09-15" },
      { "amount": "2950.000000", "due_date": "2026-10-15" }
    ]
  }
}

Nota de Crédito/Débito agrega reference_document (document_type_code/series/number del comprobante que corrige) y reason_code (catálogo SUNAT 09 o 10).

Idempotencia: external_reference (identificador propio del consumidor) + identidad tributaria (document_type_code+series+number) son las dos dimensiones. Un reintento exacto es un replay (200, replay: true, mismo resultado ya almacenado); una divergencia entre ambas es un conflicto 409. La primera emisión responde 201, replay: false.

Resultado: data.document.status es accepted, accepted_with_observations o rejected — un rechazo de SUNAT también responde 201, es un desenlace válido, no un error HTTP. data.artifacts expone XML sin firmar, XML firmado, ZIP y CDR (available, filename, sha256 de cada uno).

Consulta y descarga de comprobantes

Credencial de Empresa, acotado siempre a los comprobantes de esa Empresa.

Método Ruta Para qué
GET /api/v2/cpe Catálogo paginado, con filtros
GET /api/v2/cpe/{uuid} Detalle (incluye flags available de cada artifact, sin los bytes)
GET /api/v2/cpe/{uuid}/xml XML UBL 2.1 firmado, bytes reales (Content-Type: application/xml)
GET /api/v2/cpe/{uuid}/cdr CDR oficial de SUNAT, bytes reales (Content-Type: application/zip)

Un uuid inexistente o de otra Empresa responde 404 genérico. Un comprobante rechazado sin CDR (excepción de servicio SUNAT, sin XML de respuesta) responde 404 en /cdr con un mensaje distinto ("este comprobante no tiene un CDR disponible").

Resumen Diario de Boletas

Vía alternativa para agrupar Boletas (no obligatoria: POST /api/v2/cpe con document_type_code: "03" ya emite una Boleta individual de forma síncrona). Credencial de Empresa. Asíncrono: SUNAT devuelve un ticket, y el estado final se consulta después.

Método Ruta Para qué
POST /api/v2/daily-summaries Generar y transmitir el Resumen Diario de un conjunto de Boletas
GET /api/v2/daily-summaries Catálogo paginado
GET /api/v2/daily-summaries/{uuid} Detalle
POST /api/v2/daily-summaries/{uuid}/status-check Consultar el estado del ticket ya transmitido (getStatus); no reenvía, no hace polling automático

El ticket puede quedar processing varias consultas antes de resolver a un resultado final; el consumidor debe volver a llamar a status-check periódicamente (no hay webhooks).

Guía de Remisión Electrónica (GRE)

Emisión de una Guía de Remisión Remitente (09). Usa el servicio REST nuevo de SUNAT (no el SOAP de Factura/Boleta) y es asíncrona: una respuesta exitosa entrega un ticket para procesamiento, no un resultado tributario final.

Alcance actual:

  • Motivo de traslado libre (catálogo SUNAT 20).
  • Transporte privado (vehículo y conductor propios de la Empresa) o público (transportista tercero), según transport_mode_code (catálogo SUNAT 18).
  • Solo Guía Remitente (09). Guía Transportista (31): no soportada todavía.
  • No existe host de pruebas separado documentado por SUNAT para este servicio REST: toda emisión es contra producción real, protegida por un interruptor de servidor que el operador de apis.admin activa explícitamente.

Requisitos previos

Dos contenedores de credenciales de la Empresa deben estar configurados antes de poder emitir, o la respuesta es 409:

  1. Credenciales SOL (PUT /api/v2/company/sunat-credentials) — las mismas de Factura/Boleta.
  2. Credenciales GRE (PUT /api/v2/company/gre-credentials) — client_id/client_secret que la Empresa genera en el menú SOL específicamente para el servicio API SUNAT, distintas del Usuario/Clave SOL.

Quién habla con SUNAT

El sistema que integra con apis.admin nunca llama a SUNAT directamente ni maneja el client_id/client_secret GRE en su propio código: eso es interno de apis.admin, que autentica contra SUNAT por su cuenta usando las credenciales ya guardadas de la Empresa. El sistema externo solo necesita su token Sanctum de siempre (credencial de Empresa, ver sección Autenticación) para llamar a POST /api/v2/dispatch-guides.

PUT /api/v2/company/gre-credentials

{ "client_id": "client-abc", "client_secret": "secret-123" }

Primer guardado: ambos campos obligatorios. Guardados posteriores: client_secret opcional —si se omite, se conserva el actual—.

{
  "success": true,
  "message": "Credenciales GRE guardadas correctamente.",
  "data": { "connected": true, "is_configured": true, "configured_at": "2026-09-23T10:00:00+00:00" }
}

Nunca incluye client_id ni client_secret en la respuesta.

POST /api/v2/dispatch-guides

{
  "external_reference": "erp-guia-2026-000456",
  "series": "T001",
  "number": "123",
  "issue_date": "2026-09-24",
  "country_code": "PE",
  "sunat_establishment_code": "0000",
  "transfer_reason_code": "01",
  "transfer_description": null,
  "gross_weight": "150.500",
  "weight_unit_code": "KGM",
  "total_packages": 3,
  "transfer_start_date": "2026-09-24",
  "recipient": {
    "document_type_code": "6",
    "document_number": "20100047218",
    "name": "BANCO DE CREDITO DEL PERU"
  },
  "departure": { "ubigeo": "150101", "address": "Av. Siempre Viva 123" },
  "arrival": { "ubigeo": "150102", "address": "Jr. Destino 456" },
  "transport_mode_code": "02",
  "vehicle": { "plate": "ABC-123", "circulation_permit": null },
  "drivers": [{
    "type": "Principal",
    "document_type_code": "1",
    "document_number": "45678912",
    "first_name": "Juan",
    "last_name": "Perez Quispe",
    "license": "Q12345678"
  }],
  "items": [{
    "code": "PROD-001",
    "description": "Producto de prueba",
    "quantity": "10.000000",
    "unit_code": "NIU",
    "sunat_product_code": null
  }]
}

Transporte privado o público — transport_mode_code (catálogo SUNAT 18) es opcional; si se omite, se asume 02 (privado). En 02, vehicle y drivers son obligatorios y carrier se rechaza con 422 si se envía. En 01 (público) es al revés: carrier obligatorio (document_type_code, document_number, name — razón social del transportista tercero —, mtc_registration_number opcional) y vehicle/drivers se rechazan si se envían.

{
  "transport_mode_code": "01",
  "carrier": {
    "document_type_code": "6",
    "document_number": "20600000001",
    "name": "Transportes El Rápido SAC",
    "mtc_registration_number": null
  }
}

Campos clave:

Campo Notas
series Prefijo T fijo, 4 caracteres (^T[A-Z0-9]{3}$) — convención oficial SUNAT para Guía Remitente
transfer_reason_code Catálogo SUNAT 20 (motivo de traslado); libre en este alcance
drivers Al menos un conductor; obligatorio solo con transporte privado (transport_mode_code = 02)
vehicle.plate Siempre obligatorio

No se envía company_code (422 si se incluye) ni bloque issuer: a diferencia de /cpe, no hay ningún dato declarado por el consumidor que deba coincidir con la Empresa —se resuelve íntegramente desde la credencial—.

Respuesta exitosa (201, o 200 en replay):

{
  "success": true,
  "message": "Guía de remisión enviada a SUNAT correctamente.",
  "stored": true,
  "retryable": false,
  "replay": false,
  "data": {
    "dispatch_guide": {
      "uuid": "290a3111-0d71-4c48-89a1-0f93367bcb47",
      "external_reference": "erp-guia-2026-000456",
      "company_code": "EMP-000123",
      "document_type_code": "09",
      "series": "T001",
      "number": 123,
      "status": "ticket_received",
      "accepted": true
    },
    "provider": {
      "name": "sunat",
      "environment": "production",
      "ticket": "1234567890",
      "response_code": null,
      "description": null
    }
  }
}

status es ticket_received (SUNAT aceptó el envío para su cola de proceso —no es la decisión tributaria final—) o rejected (rechazo real de validación, sin ticket; accepted: false).

Idempotencia

Mismo criterio que /cpe: un reintento exacto (mismo external_reference, misma identidad series+number, mismo contenido) es un replay (200, replay: true); una divergencia es un conflicto 409 (external_reference_conflict o document_identity_conflict).

A diferencia de /cpe, este endpoint no usa un candado distribuido entre servidores: la protección contra duplicados sigue siendo fuerte a nivel de base de datos, pero dos solicitudes idénticas verdaderamente concurrentes podrían, en un caso extremo, disparar dos llamadas a SUNAT antes de que la segunda falle al guardar. Para un sistema que integra: evitar reintentos automáticos concurrentes de la misma external_reference mientras la primera respuesta aún no vuelve.

Errores de POST /api/v2/dispatch-guides

Código Caso code
401 Sin token, o token cuyo registro ya no existe —
403 Credencial de Cuenta en vez de Empresa —
409 Credenciales GRE/SOL sin configurar, o conflicto de idempotencia external_reference_conflict / document_identity_conflict
422 Validación de forma fallida (serie sin prefijo T, sin conductores, campo reservado enviado) —
429 Límite de solicitudes excedido —
502 SUNAT respondió pero no de forma confiable (autenticación OAuth2 rechazada) gre_provider_invalid_response
503 SUNAT no responde, o el envío a producción está deshabilitado en el servidor gre_provider_unavailable
500 SUNAT respondió pero el resultado no pudo guardarse (nunca se reintenta el envío) gre_finalization_failed

Ejemplo completo (curl)

# 1. Configurar credenciales GRE (una sola vez por Empresa)
curl -X PUT https://api.apisperu.pro/api/v2/company/gre-credentials \
  -H "Authorization: Bearer $TOKEN_EMPRESA" \
  -H "Content-Type: application/json" \
  -d '{"client_id": "client-abc", "client_secret": "secret-123"}'

# 2. Emitir la guia
curl -X POST https://api.apisperu.pro/api/v2/dispatch-guides \
  -H "Authorization: Bearer $TOKEN_EMPRESA" \
  -H "Content-Type: application/json" \
  -H "Accept: application/json" \
  -d '{
    "external_reference": "erp-guia-2026-000456",
    "series": "T001",
    "number": "123",
    "issue_date": "2026-09-24",
    "country_code": "PE",
    "sunat_establishment_code": "0000",
    "transfer_reason_code": "01",
    "gross_weight": "150.500",
    "weight_unit_code": "KGM",
    "total_packages": 3,
    "transfer_start_date": "2026-09-24",
    "recipient": {"document_type_code": "6", "document_number": "20100047218", "name": "BANCO DE CREDITO DEL PERU"},
    "departure": {"ubigeo": "150101", "address": "Av. Siempre Viva 123"},
    "arrival": {"ubigeo": "150102", "address": "Jr. Destino 456"},
    "vehicle": {"plate": "ABC-123"},
    "drivers": [{"type": "Principal", "document_type_code": "1", "document_number": "45678912", "first_name": "Juan", "last_name": "Perez Quispe", "license": "Q12345678"}],
    "items": [{"code": "PROD-001", "description": "Producto de prueba", "quantity": "10.000000", "unit_code": "NIU"}]
  }'

Consulta de estado del ticket

POST /api/v2/dispatch-guides/{uuid}/status-check. Credencial de Empresa. Solo aplica a una guía en ticket_received; en cualquier otro estado responde 409 gre_status_check_not_available.

El sistema llama a consultarEnvio(ticket) de SUNAT y traduce el cod_respuesta recibido:

cod_respuesta Efecto HTTP
98 (procesando) No cambia status; incrementa status_check_count y actualiza last_status_checked_at 202
0 (aceptado) status pasa a accepted (resultado final) 200
99 (rechazado) status pasa a ticket_rejected (resultado final, distinto del rejected de validación al emitir) 200

Una guía ya en estado final (accepted o ticket_rejected) nunca vuelve a llamar a SUNAT: la respuesta es un replay con el resultado ya almacenado.

Si SUNAT adjunta el CDR (arc_cdr, base64) junto con un resultado final, se decodifica y persiste; queda disponible en GET .../cdr (ver abajo). ind_cdr_generado puede venir en true sin que SUNAT adjunte arc_cdr en esa respuesta puntual; en ese caso no queda nada que descargar todavía.

Como con el resto de la API, el cliente es responsable de invocar este endpoint cuando lo necesite — no hay webhook ni reintento automático del lado del servidor.

Descarga de XML y CDR

GET /api/v2/dispatch-guides/{uuid}/xml — XML UBL 2.1 tal como se transmitió a SUNAT (bytes reales, Content-Type: application/xml). Siempre disponible tras la emisión, en ambos desenlaces deterministas (ticket_received y rejected): SUNAT ya lo recibió de todas formas.

GET /api/v2/dispatch-guides/{uuid}/cdr — CDR tal como lo devolvió SUNAT (bytes reales, Content-Type: application/zip). Es el único artifact opcional: responde 404 con gre_cdr_not_available hasta que una consulta de estado lo adjunte. Ambas rutas comparten el mismo ownership que el resto: un uuid inexistente o de otra Empresa responde 404 genérico.

curl https://api.apisperu.pro/api/v2/dispatch-guides/{uuid}/xml \
  -H "Authorization: Bearer $TOKEN_EMPRESA" \
  -o guia.xml

curl https://api.apisperu.pro/api/v2/dispatch-guides/{uuid}/cdr \
  -H "Authorization: Bearer $TOKEN_EMPRESA" \
  -o cdr.zip

Catálogo y detalle propio

GET /api/v2/dispatch-guides — catálogo paginado, propiedad de la Empresa de la credencial (nunca de todas las Empresas de la Cuenta, a diferencia de GET /api/v2/cpe). Filtros: status (ticket_received/rejected/accepted/ticket_rejected), search (UUID, serie-número o external_reference exactos), issued_from/issued_to, received_from/received_to (rango máx. 366 días), sort/direction, page/per_page (máx. 100).

GET /api/v2/dispatch-guides/{uuid} — detalle de una única Guía. En ambos, artifacts.xml es siempre true; artifacts.cdr en el detalle confirma la existencia física del archivo (un solo golpe a disco), mientras que en el listado se deriva de una columna, sin tocar disco por fila.

curl "https://api.apisperu.pro/api/v2/dispatch-guides?status=ticket_received&per_page=10" \
  -H "Authorization: Bearer $TOKEN_EMPRESA"

curl https://api.apisperu.pro/api/v2/dispatch-guides/{uuid} \
  -H "Authorization: Bearer $TOKEN_EMPRESA"

Fuera de alcance (hoy)

Área Qué falta
Guía de Remisión Guía Transportista (31)
General Webhooks (todo es sincrónico o por consulta explícita de estado, nunca hay push del servidor)
General Retry automático del lado del servidor (un fallo de proveedor/timeout nunca se reintenta solo; el consumidor decide)

Cualquiera de estas áreas puede ampliarse; este documento se actualiza cuando eso ocurra.