# Facta — Documentación de integración (SUNAT · Perú) > Facta es un facturador electrónico SUNAT (SEE - Del Contribuyente) con una API REST > server-to-server. Tu sistema envía la venta a Facta; Facta firma el XML con tu > certificado, lo envía a SUNAT, interpreta la respuesta (CDR) y te devuelve el estado. > Este documento es completo y autocontenido: cubre autenticación, emisión, idempotencia, > estados, webhooks, anulaciones, manejo de errores, la referencia de todos los endpoints > y ejemplos en curl, Node.js y PHP. - Base URL (producción): `https://sunat.lexarsolutions.com` - Autenticación: header `Authorization: Bearer ` (formato `nf_live_…`) - Formato: JSON en request y response. Todas las rutas bajo `/api/v1/*` (salvo `/api/v1/health`) exigen API key. - Documentación legible por máquina (este archivo): `GET /llms.txt` - Documentación legible por humanos: `GET /docs` --- ## 1. Modelo mental Tu sistema **nunca** habla con SUNAT directamente. Habla con Facta: ``` Tu sistema ──HTTPS + API key──▸ Facta (API REST) ──firma + envío──▸ SUNAT (ecommerce, · cifra cert/SOL (beta o ERP, POS…) ◂──── webhook ──── · reintentos + estado producción) comprobante.updated ``` Conceptos que aparecen en toda la API: - **Tenant**: tu organización dentro de Facta. Todo lo que ves y operas queda aislado a tu tenant. - **Emisor**: la empresa que factura (RUC + certificado `.p12` + credenciales SOL). Puedes tener varios. Se identifica con `emisorId`. - **API key**: credencial `nf_live_…` de un "consumidor". Identifica a tu sistema y lo ata a un tenant. - **Comprobante**: una factura, boleta o nota. Tiene `id`, número (`F001-123`) y `estado`. - **externalRef**: tu identificador de la operación (p. ej. el `orderId`). Es la clave de idempotencia. La **emisión es síncrona**: el `POST` espera la respuesta de SUNAT. Los procesos que SUNAT resuelve por ticket (anulaciones) y los reintentos ante caídas son **asíncronos** — ahí entran los webhooks. --- ## 2. Requisitos previos 1. Un **emisor configurado** en el panel (RUC, certificado `.p12`, usuario/clave SOL y series). En beta el SOL universal es `MODDATOS` / `moddatos`. 2. Una **API key** de consumidor (créala en `/dashboard/consumidores`). Se muestra una sola vez; guárdala como secreto en tu backend. 3. El **`emisorId`**, que obtienes listando tus emisores (`GET /api/v1/emisores`). Cada emisor tiene un ambiente: **BETA** (pruebas, sin validez fiscal) o **PRODUCCION**. Integra y prueba todo en BETA; pasar a producción exige SOL real, certificado vigente y confirmación explícita. La API es idéntica en ambos ambientes. --- ## 3. La empresa emisora: cómo tu sistema la descubre y la elige El API key te ata a tu **organización**, pero no elige por sí solo con cuál de tus RUCs facturar: eso lo decide tu sistema enviando `emisorId` en cada emisión. Tu sistema **no adivina** ese id — lo **descubre** con un endpoint. ### Descubre tus emisores `GET /api/v1/emisores` devuelve los emisores (empresas) de tu organización, cada uno con su `id`. El API key ya te identifica, así que solo ves los tuyos: ```json { "success": true, "data": [ { "id": "cmr7sneym00017gq167k2pxf5", "ruc": "20123456789", "razonSocial": "EMPRESA A SAC", "environment": "PRODUCCION", "active": true, "series": [{ "tipo": "FACTURA", "serie": "F001", "correlativo": 42 }] } ] } ``` Guarda el `id` del emisor con el que vas a facturar: es **estable** (no cambia). Luego lo mandas en cada `POST /api/v1/comprobantes`. ### Una sola empresa Pides la lista una vez, guardas el único `emisorId` en tu configuración y lo mandas siempre igual. (El emisor se da de alta desde el panel en `/dashboard/emisores` o con `POST /api/v1/emisores`.) ### Varias empresas (multiempresa) Tu organización puede tener varios emisores. El patrón es el de cualquier plugin de facturación: 1. El usuario pega su **API key** en tu sistema/CMS. 2. Tu sistema llama `GET /api/v1/emisores` y muestra un selector **"Empresa emisora"** (`razonSocial (RUC)`). 3. El usuario elige; guardas ese `emisorId` (uno por tienda/sucursal si aplica). 4. En cada venta mandas el `emisorId` correspondiente. Ciclo de vida de la integración: ``` pegar API key → GET /emisores (descubrir) → elegir empresa (guardar emisorId) → POST /comprobantes { emisorId, … } ``` **Aislamiento.** Dentro de una misma organización, cualquier API key puede emitir por **cualquiera** de sus emisores. Si quieres que dos empresas queden totalmente separadas (que cada key solo pueda facturar por la suya), crea una **organización por empresa**, cada una con su propio API key. --- ## 4. Autenticación Toda ruta `/api/v1/*` (salvo `/api/v1/health`) exige el API key como Bearer token: ``` Authorization: Bearer nf_live_xxxxxxxxxxxxxxxxxxxxxxxx ``` También se acepta el header `X-Api-Key: nf_live_…`. **Seguridad — solo server-to-server.** El API key permite emitir documentos fiscales con tu RUC. Nunca lo pongas en el navegador, una app móvil ni en frontend. Llama a Facta siempre desde tu backend. Si se filtra, regenéralo en el panel: el anterior queda inválido al instante. Cada consumidor pertenece a un tenant; solo ve y opera **sus** emisores y comprobantes. Un `id` de otro tenant responde `404`. --- ## 5. Formato de respuesta Éxito: ```json { "success": true, "data": { ... } } ``` Error (4xx / 5xx): ```json { "success": false, "error": "mensaje legible", "detail": { ... } } ``` `detail` aparece en errores de validación de esquema (HTTP 422) con el desglose de campos. --- ## 6. Emisión de comprobantes, paso a paso ### Paso 1 — Emite el comprobante `POST /api/v1/comprobantes` con **tu `externalRef`** (el id de la orden en tu sistema). Reglas de negocio: - **Factura** → cliente con **RUC** válido (11 dígitos). - **Boleta** → DNI / CE / pasaporte / sin documento. Una boleta > S/ 700 sin documento exige DNI. Cuerpo del request: ```json { "emisorId": "cmr7sneym00017gq167k2pxf5", "tipo": "FACTURA", "externalRef": "orden-1024", "moneda": "PEN", "preciosIncluyenIgv": true, "cliente": { "tipoDoc": "RUC", "numDoc": "20123456789", "razonSocial": "CLIENTE SAC", "direccion": "Av. Ejemplo 123", "email": "cliente@correo.pe" }, "items": [ { "descripcion": "Servicio de consultoría", "cantidad": 1, "precio": 100.00, "afectacion": "GRAVADO", "descuento": 0, "unidad": "NIU", "codigo": "SERV-01" } ], "observaciones": "Texto libre opcional" } ``` Campos: | Campo | Tipo | Req. | Notas | |---|---|---|---| | `emisorId` | string | sí | Id del emisor (de `GET /api/v1/emisores`). | | `tipo` | enum | sí | `FACTURA` o `BOLETA`. | | `externalRef` | string | no* | Tu id de operación. **Recomendado**: es la clave de idempotencia. | | `moneda` | string | no | Default `PEN`. También `USD`, `EUR`. | | `preciosIncluyenIgv` | bool | no | Default `true`. Si `true`, `precio` ya incluye IGV. | | `cliente.tipoDoc` | enum | sí | `DNI` \| `RUC` \| `CE` \| `PASAPORTE` \| `SIN_DOC`. | | `cliente.numDoc` | string | según tipo | Requerido salvo `SIN_DOC`. | | `cliente.razonSocial` | string | sí | Nombre/razón social. | | `cliente.direccion` | string | no | | | `cliente.email` | string | no | Para el envío por correo. | | `items[].descripcion` | string | sí | | | `items[].cantidad` | number | sí | > 0. | | `items[].precio` | number | sí | Precio unitario (≥ 0). | | `items[].afectacion` | enum | no | `GRAVADO` (default) \| `EXONERADO` \| `INAFECTO`. | | `items[].descuento` | number | no | Descuento del ítem (≥ 0). | | `items[].unidad` | string | no | Unidad SUNAT (default `NIU`). | | `items[].codigo` | string | no | Tu SKU/código de producto. | | `serie` | string | no | 4 caracteres `[A-Z0-9]`. Si se omite, usa la serie por defecto del tipo. | Respuesta `201`: ```json { "success": true, "data": { "id": "cmr8l6ygt00017g5ru6f9wrl8", "tipo": "FACTURA", "serie": "F001", "correlativo": 123, "numero": "F001-123", "estado": "ACEPTADO", "moneda": "PEN", "total": "118.00", "igv": "18.00", "externalRef": "orden-1024", "sunatMessage": "La Factura numero F001-123 ha sido aceptada", "xmlUrl": "https://…/F001-123.xml", "cdrUrl": "https://…/F001-123-cdr.zip", "hashCpe": "aA1b2C…", "emitidoAt": "2026-07-06T02:14:19.134Z", "createdAt": "2026-07-06T02:14:18.900Z" } } ``` ### Paso 2 — Interpreta el `estado` - **ACEPTADO** — SUNAT lo aceptó. Guarda `id`, `numero`, `cdrUrl`. Listo. - **OBSERVADO** — **aceptado** con observaciones. Es válido; revisa `sunatMessage`. - **RECHAZADO** — SUNAT lo rechazó (dato inválido). Corrige y reemite: un `externalRef` rechazado puede reusarse. - **PENDIENTE** — fallo transitorio (red / SUNAT caído). No se perdió: Facta reintenta solo. ### Paso 3 — Si quedó PENDIENTE, deja que el reconciliador trabaje No martilles con reintentos manuales. Facta reintenta el **mismo número** con backoff exponencial + jitter y un circuit breaker cuando SUNAT está caído. Entérate del desenlace por **webhook** (recomendado) o por **polling**. Para forzar un reintento puntual: `POST /api/v1/comprobantes/:id/retry`. ### Paso 4 — Persiste el enlace Guarda el `id` de Facta y el `numero` junto a tu orden. Con eso reconcilias, reimprimes y consultas después sin depender de tu request original. ### Paso 5 — Entrega el comprobante `GET /api/v1/comprobantes/:id/pdf` (A4) o `?formato=ticket` (80 mm). Lleva el QR oficial de SUNAT. También puedes enviarlo por correo (`POST /api/v1/comprobantes/:id/email`). --- ## 7. Idempotencia Manda siempre un `externalRef` igual al identificador de la operación en tu sistema. Con él, Facta garantiza que **una orden = un comprobante**, aunque tu request se repita: - Un reintento de red con el mismo `externalRef` devuelve el **mismo** comprobante, no uno nuevo. - Vale ante **requests concurrentes**: un lock por `(emisor, externalRef)` los serializa; solo se crea uno. - Si el comprobante previo quedó **RECHAZADO**, se permite reemitir con el mismo `externalRef`. **Patrón recomendado.** La emisión es síncrona y puede tardar. Usa un timeout generoso (~40 s). Si tu request expira sin respuesta, **no asumas fracaso**: consulta por `externalRef` (`GET /api/v1/comprobantes?externalRef=…`) antes de decidir. Reintentar el `POST` también es seguro. --- ## 8. Máquina de estados ``` SUNAT responde PENDIENTE ──────────────────────────────▸ ACEPTADO │ ▲ OBSERVADO ──anular──▸ ANULADO │ │ reintento (backoff) RECHAZADO │ └───────────────────────────┐ │ └── agota reintentos ─▸ requiereAtencion └── reemitir (mismo externalRef) ``` - **PENDIENTE**: aún no confirmado por SUNAT (transitorio). Reintentable, nunca se pierde. Si agota reintentos queda con `requiereAtencion=true` (visible en el panel). - **ACEPTADO**: válido y aceptado. Estado final feliz. - **OBSERVADO**: aceptado con observaciones — **es válido**. Revisa el mensaje. - **RECHAZADO**: rechazado por dato inválido. Corrige y reemite. - **ANULADO**: dado de baja tras una anulación aceptada por SUNAT (proceso por ticket). Solo un comprobante **ACEPTADO / OBSERVADO** puede anularse o recibir una nota. --- ## 9. Webhooks (recomendado para el estado asíncrono) Configura una URL de webhook para tu consumidor en el panel (`/dashboard/consumidores`). Facta te enviará un `POST` firmado cuando un comprobante cambie a un estado resuelto (ACEPTADO/OBSERVADO/RECHAZADO), quede marcado para atención, o pase a ANULADO. Payload (`Content-Type: application/json`): ```json { "event": "comprobante.updated", "id": "cmr8l6ygt00017g5ru6f9wrl8", "ruc": "20123456789", "tipo": "FACTURA", "numero": "F001-123", "estado": "ACEPTADO", "requiereAtencion": false, "sunatCode": "0", "sunatMessage": "…", "externalRef": "orden-1024", "total": "118.00", "ts": "2026-07-06T02:14:19.134Z" } ``` **Verifica la firma.** Cada entrega trae el header `X-Facta-Signature`: un HMAC-SHA256 del cuerpo **crudo** (raw body) usando el secreto `whsec_…` que te dio el panel. Compáralo en tiempo constante antes de confiar en el payload. **Contrato del receptor:** - Responde `2xx` rápido (encola el trabajo pesado). - Un no-2xx o un timeout se reintenta con backoff hasta 10 veces; luego la entrega queda `FALLIDO` y visible en `/dashboard/webhooks` para reintento manual. - Haz tu handler **idempotente**: una entrega puede repetirse (at-least-once). Deduplica por `id` + `estado`. --- ## 10. Polling (alternativa / respaldo) Si no puedes exponer un webhook, consulta el estado. Ideal tras un `PENDIENTE` o para reconciliar: - `GET /api/v1/comprobantes/:id` — detalle por id. - `GET /api/v1/comprobantes?externalRef=orden-1024` — buscar por tu referencia. Consulta con un intervalo razonable (30–60 s) hasta ver un estado terminal. El reconciliador interno ya empuja el comprobante hacia su resolución; no hace falta un cron agresivo. --- ## 11. Correcciones y anulaciones | Caso | Qué usar | Endpoint | |---|---|---| | Anular una **factura** | Nota de crédito (cat. 09) o baja (RA) | `POST /api/v1/comprobantes/:id/nota-credito` | | Anular una **boleta** | Baja por Resumen Diario (RC estado 3). **No** admite NC individual | `POST /api/v1/comprobantes/:id/anular` | | Aumentar el importe (mora, penalidad) | Nota de débito (cat. 10) | `POST /api/v1/comprobantes/:id/nota-debito` | | Consultar el ticket de una baja | Consulta el Resumen (o espera el reconciliador) | `POST /api/v1/resumenes/:id/consultar` | **La anulación es asíncrona.** `POST /api/v1/comprobantes/:id/anular` devuelve un **Resumen** con un `ticket` en estado `EN_PROCESO`. SUNAT confirma la baja después; cuando lo hace, el comprobante pasa a `ANULADO` y —si tienes webhook— recibes el aviso. El reconciliador interno consulta los tickets pendientes por ti. Nota de crédito / débito — motivos: ```json { "codigoMotivo": "01", "motivo": "Anulación de la operación" } ``` - Cat. 09 (nota de crédito): `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… - Cat. 10 (nota de débito): `01` intereses por mora, `02` aumento en el valor, `03` penalidad u otros conceptos. --- ## 12. Manejo de errores | HTTP | Significado | Qué hacer | |---|---|---| | 200 / 201 | OK | Continúa. | | 400 | Regla de negocio (p. ej. factura sin RUC) | Corrige el dato. No reintentar igual. | | 401 | API key faltante o inválida | Revisa el header `Authorization`. | | 403 | Sin permiso / límite del plan | Revisa el plan o los permisos del consumidor. | | 404 | No encontrado (o de otro tenant) | Verifica el `id` / `emisorId`. | | 409 | Conflicto (ya existe una nota / baja en curso) | Ya está en curso; consulta el estado. | | 422 | Esquema inválido | Corrige según `detail`. | | 5xx | Error interno / gateway | Reintentable. En emisión suele reflejarse como `PENDIENTE`. | Regla práctica: **4xx = arregla tú el request** (salvo 409/timeout); **5xx y fallos de red = transitorios**, y en emisión Facta ya los convierte en `PENDIENTE` y los reintenta. --- ## 13. Referencia de endpoints Base: `https://sunat.lexarsolutions.com`. Todos exigen `Authorization: Bearer ` salvo donde se indica. ### Salud - `GET /api/v1/health` — liveness de la app (sin auth). `{ success, service, ts }`. - `GET /api/v1/health/gateway` — sondea el gateway PHP interno y expone el estado de los circuit breakers de SUNAT. `200` si responde y Greenter cargó; `503` si no. ### Emisores - `GET /api/v1/emisores` — lista tus emisores (credenciales enmascaradas, series). - `POST /api/v1/emisores` — crea un emisor (valida el `.p12` antes de guardar; respeta el límite del plan). - `POST /api/v1/emisores/:id/validate-cert` — revalida el certificado. `{ valid, message, validTo, subject }`. - `POST /api/v1/emisores/:id/environment` — cambia el ambiente. A producción: `{ "environment": "PRODUCCION", "confirm": true }` (exige SOL real y cert vigente). Alta de emisor: ```json { "ruc": "20123456789", "razonSocial": "MI EMPRESA SAC", "nombreComercial": "MI EMPRESA", "direccionFiscal": "Av. Siempre Viva 123", "ubigeo": "150101", "environment": "BETA", "certificate": { "pfxBase64": "<.p12 en base64>", "password": "clave-cert" }, "sol": { "usuario": "MODDATOS", "clave": "moddatos" }, "series": [ { "tipo": "FACTURA", "serie": "F001", "correlativoInicial": 0 }, { "tipo": "BOLETA", "serie": "B001", "correlativoInicial": 0 }, { "tipo": "NOTA_CREDITO", "serie": "FC01", "correlativoInicial": 0 }, { "tipo": "NOTA_DEBITO", "serie": "FD01", "correlativoInicial": 0 } ] } ``` ### Comprobantes - `POST /api/v1/comprobantes` — emite factura/boleta (síncrono; idempotente por `externalRef`). - `GET /api/v1/comprobantes` — lista (máx 100). Filtros: `?emisorId=`, `?estado=`, `?externalRef=`. - `GET /api/v1/comprobantes/:id` — detalle. - `GET /api/v1/comprobantes/:id/pdf[?formato=ticket]` — PDF con QR (A4 por defecto; `ticket` = 80 mm). Devuelve `application/pdf`. - `POST /api/v1/comprobantes/:id/nota-credito` — `{ codigoMotivo, motivo? }` (cat. 09). - `POST /api/v1/comprobantes/:id/nota-debito` — `{ codigoMotivo, motivo? }` (cat. 10). - `POST /api/v1/comprobantes/:id/retry` — reintenta un `PENDIENTE`/`RECHAZADO` (mismo serie-número). - `POST /api/v1/comprobantes/:id/anular` — da de baja un aceptado (asíncrono; devuelve un Resumen con `ticket`). Body: `{ "motivo": "…" }`. - `POST /api/v1/comprobantes/:id/email` — envía el PDF (+XML si está archivado) al cliente. `{ "to": "correo@…" }` opcional. Requiere SMTP configurado. ### Resúmenes / bajas (asíncrono por ticket) - `GET /api/v1/resumenes` — lista RC (resúmenes) y RA (bajas). Filtros: `?emisorId=`, `?estado=`. - `POST /api/v1/resumenes/:id/consultar` — consulta el ticket en SUNAT. Al aceptarse una baja, el comprobante pasa a `ANULADO`. --- ## 14. Catálogos SUNAT usados - **Tipo de comprobante**: `01` factura · `03` boleta · `07` nota de crédito · `08` nota de débito. - **Documento de identidad del cliente** (cat. 06): `DNI`=1 · `CE`=4 · `RUC`=6 · `PASAPORTE`=7 · `SIN_DOC`=0. - **Afectación IGV** (cat. 07): `GRAVADO`=10 · `EXONERADO`=20 · `INAFECTO`=30. - **Moneda**: `PEN`, `USD`, `EUR`. - IGV = 18%. Las fechas de emisión se calculan en zona horaria `America/Lima`. --- ## 15. Ejemplos completos ### Emitir (curl) ```bash curl -X POST https://sunat.lexarsolutions.com/api/v1/comprobantes \ -H "Authorization: Bearer $FACTA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "emisorId": "cmr7sneym00017gq167k2pxf5", "tipo": "BOLETA", "externalRef": "orden-1024", "cliente": { "tipoDoc": "DNI", "numDoc": "44556677", "razonSocial": "JUAN PEREZ" }, "items": [{ "descripcion": "Polo algodón", "cantidad": 2, "precio": 59.90 }] }' ``` ### Emitir (Node.js / TypeScript) ```ts const res = await fetch("https://sunat.lexarsolutions.com/api/v1/comprobantes", { method: "POST", headers: { Authorization: `Bearer ${process.env.FACTA_API_KEY}`, "Content-Type": "application/json", }, signal: AbortSignal.timeout(40_000), // emisión síncrona: timeout generoso body: JSON.stringify({ emisorId: "cmr7sneym00017gq167k2pxf5", tipo: "FACTURA", externalRef: order.id, // idempotencia = tu orderId cliente: { tipoDoc: "RUC", numDoc: "20123456789", razonSocial: "CLIENTE SAC" }, items: order.items.map((i) => ({ descripcion: i.name, cantidad: i.qty, precio: i.price })), }), }); const { success, data, error } = await res.json(); if (!success) throw new Error(error); switch (data.estado) { case "ACEPTADO": case "OBSERVADO": await saveInvoice(order, data); // válido break; case "RECHAZADO": await flagForReview(order, data.sunatMessage); break; case "PENDIENTE": await saveInvoice(order, data); // espera el webhook break; } ``` ### Emitir (PHP) ```php $ch = curl_init("https://sunat.lexarsolutions.com/api/v1/comprobantes"); curl_setopt_array($ch, [ CURLOPT_POST => true, CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 40, CURLOPT_HTTPHEADER => [ "Authorization: Bearer " . getenv("FACTA_API_KEY"), "Content-Type: application/json", ], CURLOPT_POSTFIELDS => json_encode([ "emisorId" => "cmr7sneym00017gq167k2pxf5", "tipo" => "BOLETA", "externalRef" => $order->id, "cliente" => ["tipoDoc" => "DNI", "numDoc" => "44556677", "razonSocial" => "JUAN PEREZ"], "items" => [["descripcion" => "Polo", "cantidad" => 2, "precio" => 59.90]], ]), ]); $out = json_decode(curl_exec($ch), true); if (empty($out["success"])) throw new Exception($out["error"] ?? "error"); $comprobante = $out["data"]; // ["estado"], ["numero"], ["id"] … ``` ### Verificar la firma de un webhook (Node.js / Express) ```ts import { createHmac, timingSafeEqual } from "node:crypto"; // Usa el cuerpo CRUDO (raw), no el JSON ya parseado. app.post("/webhooks/facta", express.raw({ type: "*/*" }), (req, res) => { const firma = req.header("X-Facta-Signature") ?? ""; const esperado = createHmac("sha256", process.env.FACTA_WEBHOOK_SECRET) .update(req.body) // req.body es un Buffer (raw) .digest("hex"); const a = Buffer.from(firma), b = Buffer.from(esperado); if (a.length !== b.length || !timingSafeEqual(a, b)) return res.status(401).end(); const evento = JSON.parse(req.body.toString()); enqueue(evento); // idempotente: encola por evento.id y responde ya res.status(200).end(); }); ``` ### Verificar la firma de un webhook (PHP) ```php $raw = file_get_contents("php://input"); $sig = $_SERVER["HTTP_X_FACTA_SIGNATURE"] ?? ""; $calc = hash_hmac("sha256", $raw, getenv("FACTA_WEBHOOK_SECRET")); if (!hash_equals($calc, $sig)) { http_response_code(401); exit; } $evento = json_decode($raw, true); // procesa $evento["id"], $evento["estado"], $evento["externalRef"] … http_response_code(200); ``` --- ## 16. Checklist de producción - [ ] Integra en **BETA** primero (emite, anula, prueba webhooks). Recién luego pasa el emisor a PRODUCCION (SOL real + cert vigente + confirmación). - [ ] El **API key vive en el backend**. Nunca en frontend/móvil. Rota si se filtra. - [ ] **Siempre `externalRef`** igual a tu orderId — tu red de seguridad ante reintentos y timeouts. - [ ] Guarda **`id` + `numero` + `estado`** junto a tu orden apenas emites. - [ ] Trata **OBSERVADO como válido** — no lo confundas con RECHAZADO. - [ ] Configura un **webhook y verifica su firma**. Deja el polling por `externalRef` como respaldo. - [ ] Handler de webhook **idempotente** y con respuesta `2xx` rápida. - [ ] **No reintentes `PENDIENTE` en bucle** — el reconciliador lo hace con backoff; espera o consulta. - [ ] Monitorea la salud con `GET /api/v1/health/gateway` (incluye el circuit breaker de SUNAT). --- _Facta — facturación electrónica SUNAT (SEE - Del Contribuyente). Esta documentación se sirve como texto plano en `/llms.txt` y renderizada en `/docs`._