FacNow API /v1
Referencia para integradores externos que consumen el API público /v1 de FacNow Core (el motor de facturación electrónica detrás de NORAC). Es la API pensada para que sistemas de terceros (ERPs, gestores de clínicas/gimnasios, etc.) emitan comprobantes SUNAT en nombre de las empresas de sus clientes.
https://norac-facturacion.onrender.com
/docs
·
/openapi.json
X-Company-Id), consulta API.md en el repositorio.
1. Introducción
FacNow Core sigue un modelo integrador → empresas:
- •Un integrador es el sistema que consume la API (identificado por su API key). Es dueño de una o varias empresas (RUCs) que factura en su nombre.
- •Cada
/v1/*request se autentica con la API key del integrador y todas las consultas se filtran automáticamente por ese integrador — nunca ves ni puedes tocar empresas o comprobantes de otro integrador. - •Modelo de trabajo típico: creas la empresa (
POST /v1/companies), le subes certificado digital y credenciales SOL, y luego emites comprobantes (POST /v1/invoices) para esa empresa. Opcionalmente configuras un webhook para que FacNow te avise cuando SUNAT resuelva cada comprobante.
2. Autenticación
Toda llamada a /v1/* requiere el header:
(o fn_test_... para pruebas). La key identifica al integrador; no hay concepto de usuario/contraseña en este API.
Cómo pedir una key
Las API keys de integrador no se autoprovisionan por la API pública — las emite el equipo de NORAC (proceso administrativo interno, hoy un script de alta que genera el integrador y su primera key). El secreto se muestra una sola vez en ese momento; en el servidor solo se guarda su hash. Contacta al equipo de NORAC para solicitar tu integrador y tu primera key.
Formato de errores
Todos los errores de /v1/* devuelven el mismo shape:
Códigos que puedes encontrar: invalid_api_key, expired_api_key, inactive_integrator (401); ruc_already_exists (409); missing_idempotency_key, missing_sunat_credentials, invalid_certificate, invalid_credentials (400); company_not_found, invoice_not_found, cdr_not_available (404).
404 = ajeno o inexistente
Si pides una empresa o un comprobante que no existe o que pertenece a otro integrador, la respuesta es siempre 404 (nunca 403). Es una decisión de diseño: no se revela la existencia de recursos ajenos.
3. Empresas
Crear una empresa
Campos de CompanyIn (todos excepto ruc y razon_social son opcionales, con los defaults mostrados):
| Campo | Tipo | Default | Notas |
|---|---|---|---|
| ruc | string | — | exactamente 11 dígitos |
| razon_social | string | — | 1–255 caracteres |
| nombre_comercial | string | "" | |
| ubigeo | string | "150101" | |
| direccion | string | "-" | |
| distrito | string | "" | |
| provincia | string | "" | |
| departamento | string | "" |
Respuesta 201 (CompanyOut):
409 ruc_already_exists si ya existe una empresa con ese RUC.
Listar y ver empresas
Subir certificado digital
Body: pfx_base64 (obligatorio, el .pfx/.p12 codificado en base64), password (obligatorio), filename (opcional, default "cert.pfx"). Respuesta (CertificateOut): {"id", "filename", "subject", "not_after"}. 400 invalid_certificate si el PFX o la contraseña son inválidos.
Credenciales SOL
sunat_mode es "beta" (homologación, default) o "production". Devuelve CompanyOut actualizado. 400 invalid_credentials si SUNAT las rechaza.
4. Emisión
Emitir un comprobante
Header obligatorio: Idempotency-Key: <clave única por operación>. Sin él, 400 missing_idempotency_key.
Comportamiento de replay: si repites la misma Idempotency-Key (para el mismo integrador), la API no vuelve a emitir — devuelve 200 con el comprobante original ya creado (en la primera emisión exitosa la respuesta es 201). Esto hace seguro reintentar una request que no sabes si llegó.
Ejemplo — boleta (tipo 03):
Ejemplo — factura (tipo 01), con dirección del receptor:
Campos de EmisionIn (cuerpo del POST):
| Campo | Tipo | Default | Notas |
|---|---|---|---|
| tipo | string | — | "01" Factura · "03" Boleta · "07" NC · "08" ND |
| serie | string | — | |
| correlativo | int | null | null | si se omite, se autoincrementa la serie |
| fecha_emision | date | — | YYYY-MM-DD |
| hora_emision | time | 00:00:00 | |
| moneda | string | "PEN" | |
| forma_pago | string | "Contado" | |
| receptor | ReceptorIn | — | ver abajo |
| lineas | LineaIn[] | — | al menos una |
| observaciones | string | "" | |
| motivo_codigo | string | "" | solo NC/ND |
| motivo_descripcion | string | "" | solo NC/ND |
| referencia | ReferenciaIn | null | null | solo NC/ND: {"tipo_doc", "serie_numero"} del documento afectado |
ReceptorIn: tipo_doc (default "6" = RUC; "1" = DNI), num_doc, razon_social, direccion (opcional, DireccionIn), email (default "").
LineaIn: codigo (default ""), descripcion, unidad (default "NIU"), cantidad, valor_unitario (sin IGV), afectacion_igv (default "10" = gravado; catálogo SUNAT 07: 20 exonerado, 30 inafecto, 40 exportación), descuento (default 0).
Notas de crédito/débito (tipo "07"/"08"): mismo endpoint, agregando motivo_codigo, motivo_descripcion y referencia (documento que afectan).
Respuesta (InvoiceOut):
Estados del comprobante (status)
| Estado | Significado |
|---|---|
| queued | en cola de envío a SUNAT (p. ej. SUNAT caído; el worker reintenta) |
| sent | enviado, esperando CDR |
| accepted | CDR aceptado — terminal |
| observed | CDR con observaciones — terminal |
| rejected | CDR rechazado — terminal |
| error | error de transporte / SUNAT — puede reintentar |
Los estados terminales (accepted, observed, rejected) son los que disparan el webhook de notificación (ver sección 5).
Consultar un comprobante
5. Webhooks
Configurar la URL de notificación
Respuesta:
El secret se muestra una sola vez, en esta respuesta — guárdalo. En el servidor se guarda cifrado, no en claro. Enviar {"url": ""} desactiva el webhook (y borra el secret). Cada llamada a PATCH /v1/webhook con una URL rota el secret (genera uno nuevo).
Payload
Cuando un comprobante llega a un estado terminal (accepted, observed o rejected), FacNow hace POST a tu URL con:
data tiene exactamente el shape de InvoiceOut (la misma respuesta que GET /v1/invoices/{id}).
Verificación de firma
Cada entrega incluye el header:
Donde v1 es HMAC-SHA256(secret, f"{t}.{body}") sobre el body crudo de la request (bytes exactos, antes de parsear JSON). Verifica así:
Puntos clave:
- •Usa
hmac.compare_digest(comparación en tiempo constante), nunca==. - •Rechaza si
|now - t| > 300segundos (ventana anti-replay de 5 minutos). - •Calcula el HMAC sobre el body tal cual llega (bytes), no sobre un JSON re-serializado por tu framework — puede diferir en espacios/orden.
Política de reintentos
Si tu endpoint no responde 2xx, FacNow reintenta con backoff exponencial hasta 5 intentos en total; al agotarlos, la entrega queda marcada como fallida definitivamente (no se reintenta más para ese evento). Por eso:
- •Responde
2xxrápido (idealmente <10s — es el timeout del lado de FacNow) y procesa el evento de forma asíncrona en tu sistema si toma más. - •Trata cada entrega como at-least-once: puedes recibir el mismo evento más de una vez (usa
data.id+data.statuspara deduplicar).
6. Buenas prácticas
- ✓Idempotency-Key por operación de negocio, no por request HTTP: usa un identificador estable de tu lado (p. ej.
"pedido-8842-emision") para que un reintento de red no genere un comprobante duplicado. - ✓Usuario secundario de Clave SOL: no uses el usuario SOL maestro del RUC para
sol_user/sol_pass; crea un usuario secundario en SUNAT con los permisos mínimos necesarios (emisión de comprobantes) para las credenciales que le das a FacNow. - ✓Empieza en modo
beta(sunat_mode) para probar tu integración contra el ambiente de homologación de SUNAT, y pasa aproductionrecién cuando el flujo esté validado extremo a extremo (certificado real + credenciales SOL de producción).
¿Listo para integrar FacNow?
Crea tu cuenta, activa tu certificado digital gratuito y empieza a emitir comprobantes SUNAT en minutos.
¡Probar 14 Días Gratis! 🦙