Ir al contenido

Documentación para desarrolladores · v1

Emite comprobantes del SRI desde tu propio sistema

Una API REST con llaves por empresa para crear facturas, notas de crédito, retenciones y más, seguir su autorización y descargar el RIDE y el XML. Integra tu ERP, tienda en línea o punto de venta en una tarde.

Base URL
facturon.ec/api/v1/ext
Autenticación
Bearer fec_…
Emisión
asíncrona, segundos
Planes
Negocio y superiores

Introducción

Qué puedes hacer con la API

Todo lo que hace el panel de Facturón al emitir, expuesto como recursos JSON: empresas y puntos de emisión, clientes, productos, documentos electrónicos con su ciclo completo ante el SRI y los catálogos oficiales. Las respuestas siguen siempre el mismo envoltorio.

Emitir

Facturas, liquidaciones, notas de crédito y débito, guías de remisión y retenciones.

Consultar

Estado de autorización, mensajes del SRI, RIDE en PDF y XML autorizado.

Sincronizar

Clientes y productos en ambos sentidos, con búsqueda por identificación.

envoltorio de respuesta
{ "success": true, "message": "…", "data": { … } }          // éxito
{ "success": true, "data": [ … ], "meta": { "current_page": 1, "last_page": 3, "per_page": 15, "total": 42 } }  // listados
{ "success": false, "error": "insufficient_scope", "message": "…", "required_scope": "documents:write" }       // error

Autenticación

Llaves de API por empresa

Cada llave pertenece a una cuenta de Facturón, se muestra una sola vez al crearla y se guarda cifrada (hash). Envíala en la cabecera Authorization: Bearer o, si tu herramienta lo prefiere, en X-API-Key. Nunca en la URL.
primera petición
curl https://facturon.ec/api/v1/ext/me \
  -H "Authorization: Bearer fec_TU_LLAVE"
  • Rotación. Genera una credencial nueva sin borrar la configuración; la anterior deja de funcionar al instante.
  • Caducidad. Opcional: 30, 90 o 365 días. Una llave caducada responde 401 expired_api_key.
  • Hasta 10 llaves activas por cuenta: una por sistema, con el mínimo de alcances que necesite.
  • Solo HTTPS. Las llaves equivalen a las credenciales del propietario de la cuenta: guárdalas como secretos.

Alcances y límites

Mínimo privilegio y cuotas por plan

Al crear la llave eliges qué puede hacer. Sin el alcance necesario la API responde 403 insufficient_scope indicando cuál falta.
AlcancePermite
documents:readListar y ver documentos, estado ante el SRI, RIDE y XML.
documents:writeCrear y emitir documentos, reenviar al SRI, anular, reenviar correo.
customers:readListar, ver y buscar clientes.
customers:writeCrear y actualizar clientes.
products:readListar y ver productos y servicios.
products:writeCrear y actualizar productos y servicios.
catalogs:readCatálogos del SRI. Incluido en toda llave.
*Acceso total (equivale a todos los anteriores).
PlanLímite
Negocio60 peticiones / minuto
Profesional120 peticiones / minuto
Enterprise300 peticiones / minuto

Cada respuesta incluye X-RateLimit-Limit y X-RateLimit-Remaining. Al superarlo: 429 rate_limit_exceeded con Retry-After. Puedes fijar un límite menor por llave. El límite mensual de documentos es el del plan.

Flujo para emitir

De tu pedido a un comprobante autorizado en cinco pasos

  1. 1

    Crea una llave

    En el panel: Configuración → API e integraciones. Elige alcances y caducidad. La llave se muestra una sola vez.

  2. 2

    Descubre tus ids

    GET /companies devuelve company_id y emission_point_id; GET /customers/lookup encuentra al cliente por cédula o RUC (o créalo con POST /customers).

  3. 3

    Emite

    POST /documents con los importes calculados y la cabecera Idempotency-Key. Responde 201 con el documento en processing.

  4. 4

    Confirma la autorización

    GET /documents/{id}/status cada 5–10 s hasta authorized (o rejected con los mensajes del SRI).

  5. 5

    Entrega el comprobante

    GET /documents/{id}/ride (PDF) y /xml, o POST /documents/{id}/email para reenviarlo al cliente.

Ambiente de pruebas. Configura la empresa emisora en ambiente de pruebas del SRI para integrar sin emitir comprobantes reales; la API y la llave son las mismas. Al pasar a producción solo cambias el ambiente de la empresa.

Idempotencia

Reintenta sin duplicar facturas

Las redes fallan. Envía en POST /documents la cabecera Idempotency-Key con un identificador único de tu operación (por ejemplo, el número de pedido). Durante 24 horas, cualquier repetición con el mismo cuerpo devuelve exactamente la misma respuesta. En POST /documents/batch es obligatoria (sin ella responde 422 validation_error): un lote de 300 puede tardar minutos y, si la conexión se corta, reintentar con la misma llave devuelve el mismo lote (202 con Idempotent-Replayed: true) en vez de crear otro.
201 / 202 + Idempotent-Replayed: true

Misma llave, mismo cuerpo: se devuelve la respuesta original (201 en /documents, 202 en /documents/batch), no se crea otro documento ni otro lote.

409 idempotency_key_reused

Misma llave con otro cuerpo: usa una llave nueva para cada operación distinta.

409 idempotency_in_progress

Dos peticiones simultáneas con la misma llave: espera unos segundos y reintenta.

Referencia

Cuenta y empresas

Verifica la llave y descubre los identificadores que necesitas para emitir.
GET/mecualquier llave

Identidad de la integración

Cuenta, plan, límites, uso del período y datos de la llave. Úsalo para probar credenciales.

respuesta
{
  "success": true,
  "data": {
    "tenant": { "id": 12, "name": "ACME S.A.", "status": "active" },
    "plan": { "slug": "negocio", "name": "Negocio" },
    "limits": { "documents_per_month": 50, "effective_document_limit": 50, "unlimited": false, "rate_limit_per_minute": 60 },
    "usage": { "documents_this_period": 17, "period_start": "2026-09-01" },
    "api_key": { "name": "Tienda en línea", "key_prefix": "fec_a1B2c3D4", "scopes": ["documents:write", "customers:write"], "effective_rate_limit": 60, "expires_at": null }
  }
}
GET/companiesdocuments:read

Empresas emisoras con establecimientos y puntos de emisión

Devuelve company_id y emission_point_id (con su serie 001-001) y el ambiente del SRI configurado en cada empresa.

respuesta
{
  "success": true,
  "data": {
    "companies": [
      {
        "id": 1, "ruc": "1790012345001", "business_name": "ACME S.A.", "sri_environment": "2", "sri_environment_label": "Producción",
        "branches": [
          { "id": 1, "code": "001", "name": "Matriz", "is_main": true,
            "emission_points": [ { "id": 1, "code": "001", "name": "Caja 1", "series": "001-001" } ] }
        ]
      }
    ]
  }
}

Referencia

Documentos

Facturas (01), liquidaciones de compra (03), notas de crédito (04) y débito (05), guías de remisión (06) y retenciones (07). El mismo payload que usa el panel de Facturón.
POST/documentsdocuments:write

Crear y emitir un documento

Crea el comprobante y, salvo send: false, lo valida contra las reglas del SRI y lo envía a firmar y autorizar. Responde 201 con el documento en processing; la autorización llega en segundos y se consulta en /status. Facturón no recalcula importes: envía subtotal, tax_base, tax_value y total ya calculados.

ParámetroEnTipoDescripción
Idempotency-Keyheaderstring ≤128Identificador único de la operación en tu sistema (ej. el id del pedido). Válido 24 h.
company_idobligatoriobodyintegerEmpresa emisora (ver /companies).
emission_point_idobligatoriobodyintegerPunto de emisión de la empresa (ver /companies).
customer_idobligatoriobodyintegerCliente (ver /customers y /customers/lookup).
document_typeobligatoriobody"01" | "03" | "04" | "05" | "06" | "07"Tipo de comprobante.
issue_datebodydateFecha de emisión (por defecto hoy).
subtotal_15 / subtotal_12 / subtotal_5 / subtotal_0 / subtotal_no_taxbodynumberSubtotales por tarifa de IVA.
total_taxbodynumberIVA total.
total_discountbodynumberDescuento total.
totalobligatoriobodynumberImporte total.
payment_methods[]body{ code, amount, term?, time_unit? }Formas de pago del SRI (ver catálogo).
items[]obligatoriobodyobjetomain_code, description, quantity, unit_price, discount, subtotal, tax_code, tax_percentage_code, tax_rate, tax_base, tax_value. Obligatorio salvo en retenciones.
additional_infobodyobjetoPares nombre → valor impresos en el RIDE (máx. 300 caracteres cada uno).
reference_document_id + modification_reasonbodyinteger + stringObligatorios en notas de crédito y débito.
withholding_details[]bodyobjetoObligatorio en retenciones (07): support_doc_*, tax_type (renta|iva), retention_code, tax_base, retention_rate, retained_value.
sendbodybooleantrue por defecto. Con false queda en borrador para enviarlo luego con /send.
external_referencebodystring ≤64Tu referencia interna (ej. el id del pedido). Se devuelve en GET /documents/{id} y en data.external_reference de cada webhook document.*.
cuerpo de la petición
{
  "company_id": 1,
  "emission_point_id": 1,
  "customer_id": 25,
  "document_type": "01",
  "issue_date": "2026-09-13",
  "subtotal_15": 100.00,
  "total_tax": 15.00,
  "total": 115.00,
  "payment_methods": [{ "code": "01", "amount": 115.00 }],
  "items": [
    {
      "main_code": "SERV-001",
      "description": "Servicio de consultoría",
      "quantity": 1,
      "unit_price": 100.00,
      "discount": 0,
      "subtotal": 100.00,
      "tax_code": "2",
      "tax_percentage_code": "4",
      "tax_rate": 15,
      "tax_base": 100.00,
      "tax_value": 15.00
    }
  ],
  "additional_info": { "Pedido": "WEB-1001" }
}
respuesta
{
  "success": true,
  "message": "Documento creado y enviado al SRI. Consulta su estado en GET /documents/{id}/status.",
  "data": {
    "document": {
      "id": 812,
      "document_type": "01",
      "document_number": "001-001-000000042",
      "access_key": "1309202601179001234500110010010000000421234567811",
      "status": "processing",
      "total": 115,
      "customer": { "id": 25, "identification_number": "1790012345001", "name": "ACME S.A." },
      "items": [ { "main_code": "SERV-001", "description": "Servicio de consultoría", "quantity": 1 } ]
    }
  }
}
POST/documents/batchdocuments:write

Emitir documentos en lote

Crea hasta 300 documentos en una sola llamada y responde 202 con el lote en processing una vez creados todos los borradores: con 300 ítems la respuesta puede tardar minutos, así que usa un timeout de cliente amplio (10 min). Cada ítem se valida y se crea por separado: uno inválido queda rejected (con error, message y errors) y no bloquea a los demás. errors es siempre un objeto campo → mensajes; {} si el ítem no tiene errores. Con send verdadero (por defecto) un proceso en segundo plano envía los borradores al SRI uno por uno; cada ítem pasa a sent (sigue el flujo normal y recibes los webhooks document.* de cada documento) o a send_failed (con errors.sri). El lote queda completed cuando ningún ítem sigue en created; con send: false, o si todos fueron rechazados, la respuesta ya trae el lote en completed y los borradores se envían luego con /documents/{id}/send. Cuenta como una petición para el límite por minuto; el cupo del plan se gasta por cada documento creado. La cabecera Idempotency-Key es obligatoria (sin ella, 422 validation_error): si la conexión se corta, reintentar con la misma llave devuelve el mismo lote en vez de crear otro.

ParámetroEnTipoDescripción
Idempotency-Keyobligatorioheaderstring ≤128Obligatoria: identifica el lote en tu sistema (ej. lote-2026-12-01). Sin ella, 422 validation_error. Cubre el lote entero durante 24 h: mismo cuerpo, misma respuesta (202 con Idempotent-Replayed: true); otro cuerpo, 409 idempotency_key_reused.
items[]obligatoriobodyobjetoDe 1 a 300 documentos. Cada uno lleva el mismo cuerpo de POST /documents más reference. Más de 300: 422 batch_too_large.
items[].referencebodystring ≤64Tu id interno. Se guarda como external_reference del documento y vuelve en el ítem del lote y en los webhooks.
sendbodybooleantrue por defecto. Con false los documentos quedan en borrador y la respuesta ya trae el lote en completed.
cuerpo de la petición
{
  "send": true,
  "items": [
    {
      "reference": "PEDIDO-1001",
      "company_id": 1,
      "emission_point_id": 1,
      "customer_id": 25,
      "document_type": "01",
      "subtotal_15": 100,
      "total_tax": 15,
      "total": 115,
      "payment_methods": [{ "code": "01", "amount": 115 }],
      "items": [{ "main_code": "SERV-001", "description": "Servicio de consultoría", "quantity": 1, "unit_price": 100, "discount": 0, "subtotal": 100, "tax_code": "2", "tax_percentage_code": "4", "tax_rate": 15, "tax_base": 100, "tax_value": 15 }]
    },
    { "reference": "PEDIDO-1002", "...": "mismo cuerpo que POST /documents" }
  ]
}
respuesta
{
  "success": true,
  "data": {
    "batch": {
      "id": "01JBX8Z4K7Q2N5M9V3T6R1WYHE",
      "status": "processing",
      "total_items": 2,
      "created_count": 1,
      "rejected_count": 1,
      "sent_count": 0,
      "send_failed_count": 0,
      "items": [
        { "index": 0, "reference": "PEDIDO-1001", "status": "created", "document_id": 812, "document_number": "001-001-000000042", "error": null, "message": null, "errors": {} },
        { "index": 1, "reference": "PEDIDO-1002", "status": "rejected", "document_id": null, "document_number": null, "error": "validation_error", "message": "Los datos del comprobante no son válidos.", "errors": { "total": ["El total debe ser mayor que 0."] } }
      ],
      "created_at": "2026-10-27T09:30:00-05:00",
      "completed_at": null
    }
  }
}
GET/documents/batch/{id}documents:read

Ver un lote de emisión

Devuelve el mismo objeto batch que POST /documents/batch. Sondéalo cada pocos segundos hasta ver status: completed; mira el status de cada ítem (created, rejected, sent, send_failed). El resultado final de cada documento llega por webhook o con /documents/{id}/status. Un lote de otra cuenta responde 404.

ParámetroEnTipoDescripción
idobligatoriopathstring (ULID)Id del lote devuelto al crearlo.
respuesta
{
  "success": true,
  "data": {
    "batch": {
      "id": "01JBX8Z4K7Q2N5M9V3T6R1WYHE",
      "status": "processing",
      "total_items": 2,
      "created_count": 1,
      "rejected_count": 1,
      "sent_count": 0,
      "send_failed_count": 0,
      "items": [
        { "index": 0, "reference": "PEDIDO-1001", "status": "created", "document_id": 812, "document_number": "001-001-000000042", "error": null, "message": null, "errors": {} },
        { "index": 1, "reference": "PEDIDO-1002", "status": "rejected", "document_id": null, "document_number": null, "error": "validation_error", "message": "Los datos del comprobante no son válidos.", "errors": { "total": ["El total debe ser mayor que 0."] } }
      ],
      "created_at": "2026-10-27T09:30:00-05:00",
      "completed_at": null
    }
  }
}
GET/documentsdocuments:read

Listar documentos

Paginado (máximo 100 por página), ordenado por fecha de emisión descendente.

ParámetroEnTipoDescripción
statusquerystringdraft, processing, authorized, rejected, failed, voided.
document_typequerystring01, 03, 04, 05, 06, 07.
company_id / customer_idqueryintegerFiltra por empresa o cliente.
date_from / date_toquerydateRango de fecha de emisión (YYYY-MM-DD).
access_keyquerystring (49)Clave de acceso exacta.
searchquerystringClave de acceso, número o cliente.
page / per_pagequeryintegerPaginación; per_page máximo 100.
respuesta
{
  "success": true,
  "data": [ { "id": 812, "document_number": "001-001-000000042", "status": "authorized", "total": 115, "customer": { "name": "ACME S.A." } } ],
  "meta": { "current_page": 1, "last_page": 1, "per_page": 15, "total": 1 },
  "links": { "first": "…", "last": "…", "prev": null, "next": null }
}
GET/documents/{id}documents:read

Ver un documento

Detalle completo con ítems, cliente, empresa, mensajes del SRI y banderas has_ride / has_xml.

GET/documents/{id}/statusdocuments:read

Estado ante el SRI

Si el documento sigue en proceso, consulta al SRI en vivo antes de responder. Sondea cada 5–10 segundos tras emitir hasta ver authorized o rejected. Con rejected, sri_messages trae el motivo tal como lo devuelve el SRI: objetos con tipo, identificador, mensaje e informacionAdicional (por ejemplo «ERROR ESTABLECIMIENTO CERRADO»). Con contingency_active el SRI no respondió y Facturón reintenta solo.

respuesta
{
  "success": true,
  "data": {
    "status": "authorized",
    "status_label": "Autorizado",
    "authorization_number": "1309202601179001234500110010010000000421234567811",
    "authorization_date": "2026-09-13T15:04:11.000000Z",
    "sri_messages": [],
    "contingency_active": false,
    "contingency_message": null
  }
}
POST/documents/{id}/senddocuments:write

Enviar al SRI

Para borradores (creados con send: false) y para reintentar documentos fallidos o rechazados tras corregir la causa.

POST/documents/{id}/voiddocuments:write

Marcar como anulado

Solo documentos autorizados. Registra el motivo en Facturón; la anulación fiscal se hace en SRI en línea o emitiendo una nota de crédito.

ParámetroEnTipoDescripción
reasonobligatoriobodystring ≤300Motivo de la anulación.
POST/documents/{id}/emaildocuments:write

Reenviar el comprobante por correo

ParámetroEnTipoDescripción
emailbodystringDestinatario. Si se omite, el correo del cliente.
GET/documents/{id}/ridedocuments:read

RIDE en PDF

Responde el PDF (application/pdf). Con ?url=1 responde JSON con una URL temporal de 30 minutos, útil para enlazar desde tu sistema.

GET/documents/{id}/xmldocuments:read

XML firmado / autorizado

Responde el XML (application/xml) o, con ?url=1, una URL temporal. Antes de la firma responde 404 xml_not_available.

Referencia

Clientes

Los comprobantes se emiten a un cliente registrado. Busca por identificación y crea solo si no existe.
GET/customers/lookupcustomers:read

Buscar por identificación exacta

ParámetroEnTipoDescripción
identificationobligatorioquerystringCédula, RUC o pasaporte.
respuesta
{ "success": true, "data": { "customer": { "id": 25, "identification_type": "04", "identification_number": "1790012345001", "name": "ACME S.A.", "email": "compras@acme.ec" } } }
POST/customerscustomers:write

Crear cliente

ParámetroEnTipoDescripción
identification_typeobligatoriobody"04" RUC | "05" cédula | "06" pasaporte | "07" consumidor final | "08" exteriorTipo de identificación del SRI.
identification_numberobligatoriobodystring ≤20Única por cuenta.
nameobligatoriobodystring ≤300Razón social o nombre.
email / additional_emails[] / phone / addressbodystringDatos de contacto opcionales.
cuerpo de la petición
{
  "identification_type": "04",
  "identification_number": "1790012345001",
  "name": "ACME S.A.",
  "email": "compras@acme.ec",
  "address": "Av. Amazonas N21-147, Quito"
}
GET/customerscustomers:read

Listar clientes

ParámetroEnTipoDescripción
searchquerystringNombre, identificación o correo.
GET/customers/{id}customers:read

Ver cliente

PATCH/customers/{id}customers:write

Actualizar cliente

Envía el registro completo (mismos campos que al crear).

Referencia

Productos y servicios

Opcional: los ítems de un documento pueden referenciar un product_id o describirse en línea.
GET/productsproducts:read

Listar productos y servicios

ParámetroEnTipoDescripción
searchquerystringCódigo o nombre.
POST/productsproducts:write

Crear producto o servicio

ParámetroEnTipoDescripción
codeobligatoriobodystring ≤50Código principal, único por cuenta.
skubodystring ≤50Código auxiliar.
nameobligatoriobodystring ≤300Nombre.
typeobligatoriobody"product" | "service"Bien o servicio.
unit_priceobligatoriobodynumberPrecio unitario sin IVA.
tax_percentage_code / tax_ratebodystring / numberTarifa de IVA (ej. "4" / 15).
track_inventory / stock / min_stockbodyboolean / integerControl de inventario (planes con inventario).
cuerpo de la petición
{ "code": "CAM-001", "sku": "SKU-CAM-001", "name": "Camiseta", "type": "product", "unit_price": 12.50, "tax_percentage_code": "4", "tax_rate": 15 }
GET/products/{id}products:read

Ver producto

PATCH/products/{id}products:write

Actualizar producto

Envía el registro completo (code, name, type y unit_price son obligatorios).

Referencia

Catálogos del SRI

Códigos oficiales que necesitas al construir documentos. Disponibles con cualquier llave.
GET/catalogs/identification-typescualquier llave

Tipos de identificación

GET/catalogs/document-typescualquier llave

Tipos de comprobante

GET/catalogs/payment-methodscualquier llave

Formas de pago

GET/catalogs/tax-ratescualquier llave

Tarifas de IVA

GET/catalogs/retention-codescualquier llave

Códigos de retención (IVA y renta)

Webhooks

Avisos en tiempo real, firmados

En vez de sondear, registra la URL de tu sistema en Configuración → Webhooks (planes Negocio y superiores) y elige los eventos. Facturón envía un POST con JSON a esa URL cada vez que ocurre uno. Solo se aceptan URLs https:// públicas: nada de localhost ni redes privadas.
EventoCuándo
document.authorizedEl SRI autorizó el comprobante (ya tiene número y fecha de autorización).
document.rejectedEl SRI rechazó o devolvió el comprobante; sri_errors trae los mensajes.
document.voidedEl comprobante se anuló.
document.createdSe creó un comprobante (incluye borradores).
document.signedEl comprobante se firmó con la firma electrónica de la empresa.
document.failedError técnico al procesar el comprobante (Facturón lo reintenta).
webhook.testEvento de prueba enviado desde el panel; no se puede suscribir.
CabeceraContenido
X-Webhook-Signaturehex(HMAC-SHA256(secreto, "{X-Webhook-Timestamp}.{cuerpo crudo}")).
X-Webhook-TimestampSegundos Unix del envío. Rechaza firmas de más de 5 minutos.
X-Webhook-IdId del evento (UUID). Es el mismo en reintentos y reenvíos: úsalo para descartar duplicados.
X-Webhook-EventNombre del evento, igual al campo event del cuerpo.
X-Webhook-AttemptNúmero de intento de esta entrega (1 a 5).
ejemplo de envío
POST https://tu-sistema.com/webhooks/facturon
Content-Type: application/json
User-Agent: Facturon-Webhooks/1.0 (+https://facturon.ec/docs/api#webhooks)
X-Webhook-Event: document.authorized
X-Webhook-Id: 3f8a6c2e-9b1d-4f7a-8e2c-5d0b1a7c9e44
X-Webhook-Timestamp: 1791734651
X-Webhook-Signature: 5c0e…(64 caracteres hex)
X-Webhook-Attempt: 1

{
  "id": "3f8a6c2e-9b1d-4f7a-8e2c-5d0b1a7c9e44",
  "event": "document.authorized",
  "timestamp": 1791734651,
  "test": false,
  "data": {
    "document_id": 812,
    "external_reference": "PEDIDO-1001",
    "document_type": "01",
    "document_type_label": "Factura",
    "document_number": "001-001-000000042",
    "access_key": "1309202601179001234500110010010000000421234567811",
    "status": "authorized",
    "authorization_number": "1309202601179001234500110010010000000421234567811",
    "authorization_date": "2026-09-13T15:04:11-05:00",
    "issue_date": "2026-09-13",
    "subtotal": 100.0,
    "total_tax": 15.0,
    "total": 115.0,
    "currency": "DOLAR",
    "customer": { "identification": "1790012345001", "name": "ACME S.A.", "email": "pagos@acme.ec" },
    "company": { "ruc": "1790012345001", "business_name": "MI EMPRESA S.A." },
    "sri_errors": null
  }
}
  • Firma. Calcula HMAC-SHA256 con tu secreto (whsec_…, se muestra una sola vez) sobre {timestamp}.{cuerpo crudo} y compáralo en tiempo constante con X-Webhook-Signature.
  • Reintentos. Responde 2xx en menos de 10 s. Ante otro código, un timeout o un error de red reintentamos a los 1, 5, 15 y 60 minutos (5 intentos). No seguimos redirecciones.
  • Duplicados. Un reintento o un reenvío manual lleva el mismo id: guárdalo y descarta lo que ya procesaste.
  • Registro. En el panel ves cada entrega (código HTTP, duración, respuesta) durante 30 días, puedes reenviarla y mandar un evento de prueba. Tras 10 entregas fallidas seguidas el webhook se pausa hasta que lo reactives.
Verificar la firma · Node.js (Express)
import crypto from "node:crypto";

// express.raw: la firma se calcula sobre el cuerpo CRUDO, sin parsear.
app.post("/webhooks/facturon", express.raw({ type: "application/json" }), (req, res) => {
  const timestamp = req.get("X-Webhook-Timestamp") ?? "";
  const received = req.get("X-Webhook-Signature") ?? "";
  const expected = crypto
    .createHmac("sha256", process.env.FACTURON_WEBHOOK_SECRET)
    .update(`${timestamp}.${req.body}`)
    .digest("hex");

  const valid = received.length === expected.length &&
    crypto.timingSafeEqual(Buffer.from(received), Buffer.from(expected));
  const fresh = Math.abs(Date.now() / 1000 - Number(timestamp)) < 300;
  if (!valid || !fresh) return res.sendStatus(400);

  const event = JSON.parse(req.body);
  // Guarda event.id y responde 2xx de inmediato; procesa en segundo plano.
  res.sendStatus(200);
});
Verificar la firma · PHP
$body = file_get_contents('php://input');           // cuerpo crudo
$timestamp = $_SERVER['HTTP_X_WEBHOOK_TIMESTAMP'] ?? '';
$received = $_SERVER['HTTP_X_WEBHOOK_SIGNATURE'] ?? '';
$expected = hash_hmac('sha256', $timestamp . '.' . $body, getenv('FACTURON_WEBHOOK_SECRET'));

if (! hash_equals($expected, $received) || abs(time() - (int) $timestamp) > 300) {
    http_response_code(400);
    exit;
}

$event = json_decode($body, true); // ['id', 'event', 'timestamp', 'test', 'data']
// Descarta duplicados por $event['id'] y responde 200 rápido.
http_response_code(200);

Errores

Códigos estables para tu manejo de errores

Además del código HTTP, cada error trae un campo error en snake_case que no cambia entre versiones, y un message legible en español.
HTTPerrorCuándo
401missing_api_keyNo enviaste la cabecera Authorization ni X-API-Key.
401invalid_api_keyLa llave no existe o fue desactivada.
401expired_api_keyLa llave caducó. Genera una nueva en el panel.
403tenant_inactiveLa cuenta está suspendida o inactiva.
403subscription_requiredLa cuenta no tiene una suscripción vigente (incluye upgrade_url).
403api_access_not_allowedEl plan no incluye API (incluye upgrade_url).
403insufficient_scopeLa llave no tiene el alcance necesario (incluye required_scope).
403plan_limit_reachedSe alcanzó el límite mensual de documentos del plan.
404not_foundEl recurso no existe o pertenece a otra cuenta.
404xml_not_availableEl XML aún no se generó (documento sin firmar/autorizar).
409idempotency_key_reusedMismo Idempotency-Key con un cuerpo distinto.
409idempotency_in_progressOtra petición con el mismo Idempotency-Key está en curso.
409send_failedEl documento se creó pero la empresa no está lista para emitir (firma, establecimientos).
422batch_too_largeEl lote de POST /documents/batch trae más de 300 ítems. Divídelo en lotes más pequeños.
422validation_errorCampos inválidos (errors por campo), reglas del SRI (errors.sri) o POST /documents/batch sin la cabecera Idempotency-Key (errors.Idempotency-Key).
429rate_limit_exceededSe superó el límite por minuto (incluye retry_after y Retry-After).

Ejemplos de código

Del cero a la primera factura

Los tres ejemplos hacen lo mismo: verificar la llave, emitir con idempotencia, esperar la autorización y descargar el RIDE.
curl
# 1) Verifica la llave
curl https://facturon.ec/api/v1/ext/me \
  -H "Authorization: Bearer fec_TU_LLAVE"

# 2) Emite una factura (idempotente por pedido)
curl -X POST https://facturon.ec/api/v1/ext/documents \
  -H "Authorization: Bearer fec_TU_LLAVE" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: pedido-WEB-1001" \
  -d @factura.json

# 3) Consulta el estado hasta que sea authorized
curl https://facturon.ec/api/v1/ext/documents/812/status \
  -H "Authorization: Bearer fec_TU_LLAVE"

# 4) Descarga el RIDE
curl -o factura.pdf https://facturon.ec/api/v1/ext/documents/812/ride \
  -H "Authorization: Bearer fec_TU_LLAVE"
JavaScript (Node 18+)
const BASE = "https://facturon.ec/api/v1/ext";
const headers = {
  Authorization: `Bearer ${process.env.FACTURON_API_KEY}`,
  "Content-Type": "application/json",
};

async function emitirFactura(pedido) {
  const res = await fetch(`${BASE}/documents`, {
    method: "POST",
    headers: { ...headers, "Idempotency-Key": `pedido-${pedido.id}` },
    body: JSON.stringify(pedido.factura),
  });
  const json = await res.json();
  if (!res.ok) throw new Error(`${json.error}: ${json.message}`);
  return json.data.document; // status: "processing"
}

async function esperarAutorizacion(id, intentos = 12) {
  for (let i = 0; i < intentos; i++) {
    const res = await fetch(`${BASE}/documents/${id}/status`, { headers });
    const { data } = await res.json();
    if (data.status === "authorized" || data.status === "rejected") return data;
    await new Promise((r) => setTimeout(r, 5000));
  }
  throw new Error("El SRI aún no responde; reintenta más tarde");
}
PHP (Guzzle)
$client = new \GuzzleHttp\Client([
    'base_uri' => 'https://facturon.ec/api/v1/ext/',
    'headers' => ['Authorization' => 'Bearer ' . getenv('FACTURON_API_KEY')],
]);

// Cliente: buscar o crear
try {
    $cliente = json_decode($client->get('customers/lookup', ['query' => ['identification' => '1790012345001']])->getBody(), true)['data']['customer'];
} catch (\GuzzleHttp\Exception\ClientException $e) {
    if ($e->getResponse()->getStatusCode() !== 404) throw $e;
    $cliente = json_decode($client->post('customers', ['json' => [
        'identification_type' => '04', 'identification_number' => '1790012345001', 'name' => 'ACME S.A.',
    ]])->getBody(), true)['data']['customer'];
}

// Emitir
$factura = json_decode($client->post('documents', [
    'headers' => ['Idempotency-Key' => 'pedido-WEB-1001'],
    'json' => $payload + ['customer_id' => $cliente['id']],
])->getBody(), true)['data']['document'];

// Estado
$estado = json_decode($client->get("documents/{$factura['id']}/status")->getBody(), true)['data'];

Preguntas frecuentes

Lo que preguntan los integradores

¿Cómo pruebo sin emitir comprobantes reales?

Configura la empresa emisora en ambiente de pruebas del SRI (Configuración → Datos del emisor). Los documentos se firman y autorizan contra el ambiente de pruebas del SRI con la misma API; cuando pases a producción, cambia el ambiente de la empresa y usa la misma llave.

¿Cuánto tarda la autorización?

Normalmente entre 2 y 15 segundos. La API responde de inmediato con el documento en processing; sondea /status. Si el SRI está caído, el documento entra en contingencia y Facturón reintenta automáticamente; el estado lo refleja con contingency_active.

¿Qué pasa si mi sistema reintenta una petición?

Envía siempre Idempotency-Key en POST /documents. Una repetición con el mismo cuerpo devuelve la misma respuesta (cabecera Idempotent-Replayed: true) y no crea otra factura.

¿Cómo emito las facturas del día en una sola llamada?

Con POST /documents/batch: envía hasta 300 documentos en items (cada uno con su reference, tu id de pedido) y la cabecera Idempotency-Key, que es obligatoria. Responde 202 cuando creó todos los borradores (con 300 puede tardar minutos) y los envía al SRI en segundo plano; consulta GET /documents/batch/{id} hasta ver completed. Un ítem inválido se rechaza sin bloquear a los demás, y cada documento autorizado dispara su webhook con external_reference para que lo cruces con tu pedido. Si la conexión se corta, repite la misma petición con la misma llave: devuelve el lote original en vez de crear otro.

¿La API calcula el IVA?

No. Tu sistema envía subtotales, impuestos y total ya calculados (como lo hace el panel). Antes de enviar al SRI, Facturón valida reglas como el tope de $50 a Consumidor Final o la validez de la identificación.

¿Puedo recibir avisos cuando un documento se autoriza?

Sí, con webhooks (planes Negocio y superiores): registra tu URL en Configuración → Webhooks y elige los eventos, por ejemplo document.authorized. Cada aviso llega firmado con HMAC-SHA256; mira la sección Webhooks para verificarlo. Si prefieres no exponer una URL, sondea GET /documents/{id}/status.

¿Qué planes incluyen la API?

Negocio, Profesional y Enterprise. Cada plan define el límite de peticiones por minuto; el límite mensual de documentos es el mismo del plan.

¿Listo para integrar?

Crea tu cuenta, elige un plan con API y genera tu primera llave en minutos.