# API JSON directa SUNAT

Base: `/api/peru/cpe`. El body nunca contiene XML, SOAP, credenciales, endpoint ni correlativo.

## Headers

```http
Content-Type: application/json
Authorization: Bearer JWT-o-bheed_sk_API_KEY
Idempotency-Key: clave estable del evento de negocio
```

`Authorization` acepta dos credenciales:

- JWT temporal: se verifica HS256, `iss`, `aud`, `exp` y `nbf`.
- API key servidor-a-servidor: comienza con `bheed_sk_`, no vence automáticamente y puede revocarse individualmente.

En ambos casos el tenant y el actor provienen de la credencial; se ignoran los headers elegidos por el cliente. La API key se muestra una sola vez y MongoDB conserva únicamente su hash SHA-256, prefijo, tenant, actor, último uso y estado de revocación. No debe incluirse en aplicaciones frontend, repositorios o logs. `X-Tenant-Id` y `X-Actor-Id` se admiten sólo durante desarrollo explícitamente inseguro.

## Borradores

```http
POST /drafts
```

El borrador recibe directamente el mismo JSON válido que `POST /invoices`, genera un folio aleatorio no tributario y no reserva una serie/correlativo de SUNAT. Tampoco genera XML, firma, ZIP ni realiza transmisiones. No envíe un envoltorio `documentType`/`payload` ni el identificador de una factura existente.

```json
{
  "series": "F001",
  "currency": "PEN",
  "operationType": "0101",
  "customer": {
    "documentType": "6",
    "documentNumber": "20123456789",
    "legalName": "CLIENTE SAC",
    "addressCode": "0000"
  },
  "payment": { "method": "CONTADO" },
  "items": [{
    "sku": "SAAS-001",
    "description": "Servicio SaaS mensual",
    "quantity": "1",
    "unitCode": "NIU",
    "unitValue": "100.00",
    "tax": { "type": "IGV", "affectationCode": "10", "rate": "18.00" }
  }]
}
```

La validación se realiza con el mismo esquema Zod de factura. El tipo se fija internamente como factura `01`. `purchaseOrder`, cuando se envía, se conserva como `externalReference` y como referencia UBL, pero no se imprime automáticamente. Para mostrar una referencia comercial en el PDF debe enviarse explícitamente dentro de `references[]`.

Respuesta `201`:

```json
{
  "id": "49e8d90a-ff2a-4fc9-8a02-ed8a7afb4f09",
  "folio": "BOR-01-5A67D449C1",
  "documentType": "invoice",
  "documentTypeCode": "01",
  "status": "DRAFT",
  "isDraft": true,
  "tributaryValidity": false,
  "sentToSunat": false,
  "externalReference": null,
  "payload": {
    "series": "F001",
    "currency": "PEN",
    "customer": {
      "documentType": "6",
      "documentNumber": "20123456789",
      "legalName": "CLIENTE SAC"
    },
    "items": [{ "sku": "SAAS-001", "description": "Servicio SaaS mensual" }]
  },
  "createdAt": "2026-09-08T12:00:00.000Z",
  "updatedAt": "2026-09-08T12:00:00.000Z",
  "message": "Borrador creado. No fue emitido ni enviado a SUNAT."
}
```

El prefijo `BOR-` impide confundir este identificador con una serie y correlativo fiscal. Cada solicitud crea un folio nuevo; `Idempotency-Key` sólo es obligatorio al emitir documentos tributarios.

### PDF temporal del borrador

```http
GET /drafts/:id/pdf
Authorization: Bearer JWT-o-bheed_sk_API_KEY
```

El `:id` es el valor retornado por `POST /drafts`. La respuesta es `application/pdf`, se abre en línea y usa como nombre de archivo el folio del borrador.

```bash
curl "https://facturador-peru.bheed.net/api/peru/cpe/drafts/ID_DEL_BORRADOR/pdf" \
  -H "Authorization: Bearer $BHEED_SUNAT_TOKEN" \
  --output borrador.pdf
```

El PDF usa el JSON de factura validado y lleva la marca visible `BORRADOR - SIN VALIDEZ TRIBUTARIA - NO ENVIADO A SUNAT`. No contiene QR SUNAT, XML, firma digital ni CDR. El borrador sólo puede ser descargado por el mismo tenant que lo creó. La sección `REFERENCIAS COMERCIALES` sólo aparece cuando el JSON contiene al menos un elemento en `references[]`; `purchaseOrder` por sí solo no la hace visible.

Cuando todas las líneas usan `affectationCode: "20"`, el título del borrador y del PDF emitido indica `FACTURA ELECTRONICA EXONERADA`. En documentos con líneas mixtas se mantiene `FACTURA ELECTRONICA` y los importes se separan en los totales tributarios.

En los PDFs de borrador y sandbox la columna de cantidad se presenta siempre con dos decimales (`1.00`, `2.50`). Esta regla es sólo visual: el JSON, los cálculos y el XML UBL conservan la cantidad decimal validada originalmente.

## Emisión

```http
POST /invoices
POST /receipts
POST /credit-notes
POST /debit-notes
```

Factura/boleta:

```json
{
  "series": "F001",
  "issuedAt": "2026-08-25T10:00:00-05:00",
  "currency": "PEN",
  "operationType": "0101",
  "customer": {
    "documentType": "6",
    "documentNumber": "20123456789",
    "legalName": "CLIENTE SAC",
    "addressCode": "0000"
  },
  "payment": { "method": "CONTADO" },
  "items": [
    {
      "sku": "SAAS-001",
      "description": "Servicio SaaS",
      "quantity": "1",
      "unitCode": "NIU",
      "unitValue": "100.00",
      "discount": { "percent": "5", "reasonCode": "00" },
      "tax": { "type": "IGV", "affectationCode": "10", "rate": "18" }
    }
  ],
  "globalDiscount": { "amount": "5.00" },
  "legends": [{ "code": "1000", "value": "SON ..." }]
}
```

Para crédito:

```json
{
  "payment": {
    "method": "CREDITO",
    "installments": [
      { "amount": "118.00", "dueDate": "2026-09-25" }
    ]
  }
}
```

La suma de cuotas debe ser igual al total calculado. Los decimales se validan como texto para no introducir floats.

### Impuestos admitidos en cada ítem

Actualmente `tax.type` admite únicamente `"IGV"` y también puede omitirse porque su valor predeterminado es `"IGV"`. El régimen tributario no se selecciona cambiando `tax.type`; se determina con `tax.affectationCode`:

| Operación | `tax.type` | `tax.affectationCode` | `tax.rate` habitual | UBL `ID / Name / TaxTypeCode` |
|---|---|---|---|---|
| Gravada | `IGV` u omitido | `10` | `18` | `1000 / IGV / VAT` |
| Exonerada | `IGV` u omitido | `20` | `0` | `9997 / EXO / VAT` |
| Inafecta | `IGV` u omitido | `30` | `0` | `9998 / INA / FRE` |

No envíe `"EXO"` ni `"INA"` en `tax.type`: esos valores corresponden al nombre tributario generado internamente en el XML UBL, no al contrato JSON público.

### Glosa, detalle y referencias comerciales

Cada ítem conserva `description` como descripción tributaria requerida. Opcionalmente puede enviar `gloss` y `detail`; la API los combina, sin duplicados, en la descripción UBL y los presenta por separado en los PDF de borrador y emitido.

```json
{
  "items": [{
    "sku": "BC-34-1",
    "gloss": "Gasto común",
    "description": "Cuota de mantenimiento septiembre 2026",
    "detail": "Departamento 678",
    "quantity": "1",
    "unitCode": "NIU",
    "unitValue": "1000.00",
    "tax": { "type": "IGV", "affectationCode": "20", "rate": "0" }
  }],
  "purchaseOrder": "OC-1234",
  "references": [{
    "date": "2026-09-14",
    "documentType": "Orden de compra",
    "folio": "1234",
    "code": "Corrige texto",
    "reason": "Adjunta referencia"
  }]
}
```

`references` admite hasta 20 referencias comerciales. Se muestran en ambos PDF, pero no sustituyen la referencia tributaria obligatoria `reference` de una nota de crédito o débito.

### Factura con detracción general

Use `POST /invoices` con `operationType: "1001"`. La factura puede emitirse en PEN o USD; el monto de la detracción siempre se calcula y expresa en PEN. No se admiten aún los casos especializados `1002` (recursos hidrobiológicos) ni `1004` (transporte de carga), que requieren información adicional.

```json
{
  "series": "F001",
  "issuedAt": "2026-09-14T10:00:00-05:00",
  "currency": "USD",
  "operationType": "1001",
  "customer": {
    "documentType": "6",
    "documentNumber": "20123456789",
    "legalName": "CLIENTE SAC",
    "addressCode": "0000"
  },
  "payment": { "method": "CONTADO" },
  "detraction": {
    "goodsServiceCode": "037",
    "bankAccount": "0012345678901",
    "paymentMeansCode": "001",
    "percent": "12.00",
    "legend": "Operación sujeta al Sistema de Pago de Obligaciones Tributarias con el Gobierno Central",
    "exchangeRate": {
      "rate": "3.7500",
      "date": "2026-09-14",
      "source": "SBS"
    }
  },
  "items": [{
    "sku": "SERV-001",
    "description": "Servicio gravado sujeto a detracción",
    "quantity": "1",
    "unitCode": "NIU",
    "unitValue": "100.00",
    "tax": { "type": "IGV", "affectationCode": "10", "rate": "18.00" }
  }]
}
```

| Campo | Regla |
|---|---|
| `operationType` | Debe ser `1001` para la detracción general. |
| `currency` | Puede ser `PEN` o `USD`. El XML siempre expresa el monto de la detracción en `PEN`. |
| `detraction.goodsServiceCode` | Código de tres dígitos admitido por el catálogo SUNAT 54; `037` es “Demás servicios gravados con el IGV”. |
| `detraction.bankAccount` | Cuenta del Banco de la Nación del proveedor, hasta 100 caracteres alfanuméricos o guion. |
| `detraction.paymentMeansCode` | Catálogo SUNAT 59; normalmente `001`, depósito en cuenta. Si se omite usa `001`. |
| `detraction.percent` | Porcentaje mayor que `0` y hasta `100`, enviado como decimal. La tasa correcta depende del bien/servicio y de la normativa aplicable. |
| `detraction.legend` | Opcional. Si se omite usa la leyenda SPOT estándar y genera la leyenda SUNAT `2006`. |
| `detraction.exchangeRate` | Obligatorio sólo para una factura USD. Incluye `rate`, `date` y `source: "SBS"`. No se envía para PEN. La fecha y tasa deben corresponder al tipo de cambio promedio ponderado venta publicado por la SBS en la fecha de nacimiento de la obligación tributaria o en la fecha del depósito, la que ocurra primero; si no hubo publicación, se usa el último publicado. |

No envíe `detraction.amount`: la API lo calcula con Decimal y redondea el resultado a dos decimales. En PEN aplica el porcentaje al total final. En USD primero convierte el total a PEN con `exchangeRate.rate` y luego aplica el porcentaje. La respuesta incluye `detraction.sourceAmount`, `baseAmountPen`, `amount`, `currency: "PEN"` y los datos del tipo de cambio. También incorpora la leyenda SUNAT `2006`, el código del catálogo 54, el medio de pago, la cuenta y el monto en PEN en el XML UBL.

El PDF de borrador y el PDF emitido muestran por separado **Porcentaje de detracción** y **Monto de detracción**, además del código y nombre del bien/servicio, medio de pago, cuenta del Banco de la Nación, leyenda y conversión USD→PEN cuando corresponda. Debajo se presenta **Monto Neto a Pagar al Proveedor = Importe total - Monto de detracción** en la moneda de la factura. Para facturas USD, el bloque SPOT mantiene el monto oficial de detracción en PEN, pero el cuadro neto convierte esa detracción a USD con el mismo tipo de cambio y muestra la resta y el resultado final en USD. El borrador usa exactamente el mismo cálculo tributario, pero continúa sin firma, XML, CDR, correlativo SUNAT ni validez tributaria.

Cuando `payment.method` es `CREDITO`, el PDF añade debajo un **Cronograma de cuotas** con número de cuota, fecha de vencimiento y monto. Las filas se toman directamente de `payment.installments`; en pagos `CONTADO` no se muestra este cuadro.

La API valida estructura y coherencia técnica, pero la empresa emisora debe determinar si la operación supera el umbral legal, qué código del catálogo 54 y qué porcentaje corresponden según el SPOT vigente.

## Notas de crédito y débito

Las notas ya están implementadas. Sólo se aceptan cuando el comprobante referenciado existe en el mismo tenant y ambiente, corresponde al mismo cliente y moneda, y está aceptado o pendiente del mismo Resumen Diario en el caso de boletas.

### Nota de crédito

```http
POST /credit-notes
```

```json
{
  "series": "FC01",
  "currency": "PEN",
  "customer": {
    "documentType": "6",
    "documentNumber": "20123456789",
    "legalName": "CLIENTE SAC",
    "addressCode": "0000"
  },
  "reference": { "documentType": "01", "series": "F001", "number": 123 },
  "reasonCode": "01",
  "reason": "Anulación de la operación",
  "items": [{
    "sku": "SERV-001",
    "description": "Reverso del servicio",
    "quantity": "1",
    "unitCode": "NIU",
    "unitValue": "100.00",
    "tax": { "type": "IGV", "affectationCode": "10", "rate": "18.00" }
  }]
}
```

Motivos admitidos: `01` anulación de operación, `02` error en RUC, `03` corrección de descripción/código de producto, `04` descuento global, `05` descuento por ítem, `06` devolución total, `07` devolución por ítem, `08` bonificación, `09` disminución de valor, `10` otros conceptos, `11` ajustes de exportación, `12` ajustes IVAP y `13` modificación de cuotas pendientes.

### Nota de débito

```http
POST /debit-notes
```

```json
{
  "series": "FD01",
  "currency": "PEN",
  "customer": {
    "documentType": "6",
    "documentNumber": "20123456789",
    "legalName": "CLIENTE SAC",
    "addressCode": "0000"
  },
  "reference": { "documentType": "01", "series": "F001", "number": 123 },
  "reasonCode": "01",
  "reason": "Intereses por mora",
  "items": [{
    "sku": "MORA-001",
    "description": "Intereses por mora",
    "quantity": "1",
    "unitCode": "NIU",
    "unitValue": "12.50",
    "tax": { "type": "IGV", "affectationCode": "10", "rate": "18.00" }
  }]
}
```

Motivos admitidos: `01` intereses por mora, `02` aumento en el valor, `03` penalidades/otros conceptos, `11` ajustes de exportación, `12` ajustes IVAP y `13` penalidades.

Para facturas use series `FCxx`/`FDxx` y referencia `documentType: "01"`. Para boletas use `BCxx`/`BDxx` y referencia `documentType: "03"`; estas últimas quedan `PENDING_SUMMARY` hasta incluirlas en el Resumen Diario. Cada nota requiere su propia `Idempotency-Key`.

La respuesta normalizada:

```json
{
  "id": "...",
  "documentNumber": "F001-123",
  "environment": "beta",
  "status": "ACCEPTED",
  "tributaryValidity": false,
  "sunat": { "code": "0", "description": "...", "notes": [] },
  "attempts": 1,
  "audit": {
    "createdBy": "integracion-erp",
    "issuedBy": "integracion-erp",
    "generatedAt": "2026-09-10T17:00:00.000Z",
    "signedAt": "2026-09-10T17:00:00.000Z",
    "sentAt": "2026-09-10T17:00:01.000Z",
    "cdrAt": "2026-09-10T17:00:01.000Z"
  },
  "history": [
    { "state": "DRAFT", "at": "2026-09-10T17:00:00.000Z" },
    { "state": "ACCEPTED", "at": "2026-09-10T17:00:01.000Z" }
  ],
  "artifacts": {}
}
```

`attempts`, `audit` e `history` se entregan sólo por la API autenticada para operación y backoffice. El portal del receptor no expone este historial interno.

En MOCK el estado es `MOCK_VALIDATED` y `tributaryValidity` siempre es `false`.

## Resumen Diario y bajas

```http
POST /daily-summaries
POST /voided-documents
GET  /async/:id
POST /async/:id/status
```

```json
{
  "referenceDate": "2026-08-25",
  "documentIds": ["id-boleta-1", "id-boleta-2"],
  "poll": true
}
```

La baja agrega `reason`. `poll: true` usa backoff acotado; con `false` devuelve `PROCESSING` y el ticket para consulta posterior.

Para anular una boleta ya informada se usa el mismo endpoint RC:

```json
{
  "referenceDate": "2026-08-25",
  "items": [{ "documentId": "id-boleta", "action": "VOID" }],
  "poll": true
}
```

`VOID` genera ConditionCode `3`; `VOID_SAME_DAY` genera `4`. Un worker con lease continúa consultando tickets `PROCESSING`/`UNKNOWN` después de reinicios.

## Consultas y artefactos

```http
GET  /:id
GET  /:id/xml
GET  /:id/cdr
GET  /:id/pdf
POST /:id/send
POST /:id/reconcile
POST /:id/access-tokens
DELETE /access-tokens/:token
```

`GET /:id/pdf` genera `application/pdf` desde el mismo modelo canónico que produjo el XML e incorpora QR SUNAT con `ds:DigestValue`. En `mock` y `beta` muestra una marca visible indicando que no posee validez tributaria de producción.

`POST /:id/access-tokens` crea por defecto un acceso privado de 366 días. El receptor usa `GET /cpe/PE/:token`, `/pdf` y `/xml`; sólo se expone `/cdr` si el despliegue habilita `SUNAT_PORTAL_EXPOSE_CDR=true`. El token es aleatorio y revocable; la base conserva únicamente su HMAC.

`send` no vuelve a emitir ciegamente. Para estados ambiguos intenta consulta CDR; una boleta obliga al flujo de Resumen Diario.

## Operación interna

```http
GET /internal/health/sunat
GET /internal/metrics
```

El health check valida tenant, configuración, certificado y ejecuta una generación UBL 2.1 interna en memoria. No reserva correlativos, no persiste artefactos y no transmite documentos a SUNAT. `xmlGeneration.status: "VALID"` confirma que el proceso pudo construir XML bien formado. Las métricas no incluyen credenciales ni RUC completo.
