28 eventos por suscripción y cero suposiciones
Configuras la URL desde la app, eliges los eventos y opcionalmente añades cabeceras personalizadas para autenticar tu receptor. La forma del payload es estándar y la lista de eventos vive sembrada en la migración inicial.
Resumen
Cuando un evento ocurre dentro del espacio de trabajo, factura.com.do envía un POST con cuerpo JSON a la URL que registraste. La entrega es fire-and-forget: tu receptor responde 2xx y la entrega queda persistida en el log de ejecución de webhooks. Si responde distinto, no hay reintento automático.
Modelo de suscripción
La suscripción se administra desde la sección Webhooks del espacio de trabajo, no por API pública. Configuras URL destino, método HTTP (GET/POST/PUT), cabeceras personalizadas (nombre-valor) y selección de eventos. La app expone un endpoint de prueba interno para verificar conectividad antes de activar.
Forma del payload
Todos los eventos comparten la misma forma de cuerpo: Event, el nombre del evento; Timestamp, en ISO 8601 y en la hora local del servidor (con su desfase, no normalizada a UTC), y data, específico del evento. Las claves llegan en PascalCase.
{ "Event": "dgii.aprobado", "Timestamp": "2026-05-08T10:33:21.4830000-04:00", "data": { "DocumentId": "7c9e6679-7425-40de-944b-e07fc1f90ae7", "StatusId": 5 }}Catálogo de 28 eventos
Los 28 eventos cubren cotizaciones, facturas, notas, gastos, estados DGII y catálogo. Todos están sembrados desde la migración inicial; suscríbete solo a los que tu integración necesita.
cotizacion.created Cotizaciones Una cotización fue creada en el espacio de trabajo.
cotizacion.updated Cotizaciones Una cotización fue editada.
cotizacion.anulated Cotizaciones Una cotización fue anulada.
factura.created Facturas Una factura (E31, E32, E44, E45 o E46) fue creada.
factura.updated Facturas Una factura existente fue modificada y guardada desde la app. edit-amount y edit-text no la disparan: emiten una nota nueva.
factura.anulated Facturas Una factura fue anulada (void).
nota_credito.created Notas de crédito Una nota de crédito (E34) fue emitida, también la que generan edit-amount y edit-text sobre una factura.
nota_credito.updated Notas de crédito Una nota de crédito existente fue modificada.
nota_credito.anulated Notas de crédito Una nota de crédito fue anulada.
nota_debito.created Notas de débito Una nota de débito (E33) fue emitida, también la que generan edit-amount y edit-text sobre un gasto, compra o pago al exterior.
nota_debito.updated Notas de débito Una nota de débito existente fue modificada.
nota_debito.anulated Notas de débito Una nota de débito fue anulada.
gasto.created Gastos Un gasto menor (E43), una compra (E41) o un pago al exterior (E47) fue registrado.
gasto.updated Gastos Un gasto existente fue modificado.
gasto.anulated Gastos Un gasto fue anulado.
dgii.aprobado Estado DGII La DGII aceptó el comprobante en firme (StatusId 5).
dgii.rechazado Estado DGII La DGII rechazó el comprobante (StatusId 6). Reconcilia con un GET al recurso para leer el motivo persistido.
dgii.en_proceso Estado DGII La DGII aún evalúa el comprobante (StatusId 7). El polling sigue.
dgii.aceptado_condicional Estado DGII La DGII aceptó con observaciones (StatusId 8). Registrar en bitácora.
customer.created Contactos Un cliente fue registrado en el espacio de trabajo.
customer.updated Contactos Un cliente fue actualizado.
customer.deactivated Contactos Un cliente fue desactivado.
product.created Catálogo Un producto fue agregado al catálogo.
product.updated Catálogo Un producto fue actualizado.
product.deactivated Catálogo Un producto fue desactivado.
supplier.created Contactos Un proveedor fue registrado.
supplier.updated Contactos Un proveedor fue actualizado.
supplier.deactivated Contactos Un proveedor fue desactivado.
Forma del data por evento
El envelope (<code>Event</code>, <code>Timestamp</code>, <code>data</code>) es invariante. Lo que cambia es el contenido de <code>data</code>. Estos son los campos que emite el dispatcher; ignora los que no uses, porque pueden aparecer campos nuevos.
| Eventos | Campos en data |
|---|---|
cotizacion.created, cotizacion.updated | DocumentId, SequenceTypeId, StatusId |
factura.created | DocumentId, SequenceTypeId, StatusId, Number, Lines, CustomerId, CurrencyId, AmountDue, PaymentTermId, LimitDate, CustomerName, CustomerTaxId, CustomerPhone, CustomerEmail, CustomerAddress, Accounting |
factura.updated | DocumentId, SequenceTypeId, StatusId, Lines, Number, Accounting |
nota_credito.created, nota_credito.updated, nota_debito.created, nota_debito.updated | DocumentId, SequenceTypeId, StatusId, Number, Lines, CustomerId, SupplierId, CurrencyId, AmountDue, ModifiedReferenceId, ReferenceSecuence, ModificationCode, CustomerName, CustomerTaxId, CustomerPhone, CustomerEmail, CustomerAddress, SupplierName, SupplierTaxId, SupplierPhone, SupplierEmail, SupplierAddress, Accounting |
gasto.created | DocumentId, SequenceSourceId, StatusId, Number, Lines, SupplierId, CurrencyId, AmountDue, PaymentTermId, LimitDate, SupplierName, SupplierTaxId, SupplierPhone, SupplierEmail, SupplierAddress, Accounting |
gasto.updated | DocumentId, SequenceSourceId, StatusId, Lines, Number, Accounting |
cotizacion.anulated, nota_credito.anulated, nota_debito.anulated | DocumentId |
factura.anulated, gasto.anulated | DocumentId, WasAcceptedByDGII, CreditNoteId |
dgii.aprobado, dgii.rechazado, dgii.en_proceso, dgii.aceptado_condicional | DocumentId, StatusId |
customer.created, customer.updated | CustomerId, LegalName, TaxIdentification |
supplier.created, supplier.updated | SupplierId, LegalName, TaxIdentification |
product.created, product.updated | ProductId, Code, Description |
customer.deactivated | CustomerId |
supplier.deactivated | SupplierId |
product.deactivated | ProductId |
Cada elemento de Lines trae LineId, ProductId, ProductVariantId, WarehouseId, Code, Order, Description, IsService, IsExempt, Quantity, UnitId, UnitPrice, Discount, DiscountPercentage, Recharge, NetAmount, TotalDiscount, TotalTax, TotalWithholdings, Subtotal, Taxes y Withholdings. Accounting es el desglose contable del encabezado: DocumentTypeId, BranchId, SequenceTypeId, IssuedAt, LimitDate, PaymentTermId, CurrencyCode, CurrencyRate, TaxableAmount, ExemptAmount, TotalDiscount, DiscountPercentage, TotalTaxes, TotalWithholdings, Total, AmountDue, Freight, Tip, TratamientoItbis, TipoBienServicioComprado y AwaitsFiscalValidation.
Receptor de ejemplo
Un receptor mínimo valida la cabecera secreta, lee el evento y enruta. Devuelve 2xx en cuanto persistas o encoles el trabajo; si tu lógica falla, persiste en background y reconcilia más tarde.
POST /webhooks/factura — receptor
// webhook-handler.ts (Express)import type { Request, Response } from "express";
const SECRETO = process.env.FACTURA_WEBHOOK_SECRET!;
export function manejarWebhook(req: Request, res: Response) { if (req.header("X-Webhook-Secret") !== SECRETO) { return res.status(401).end(); }
const { Event, Timestamp, data } = req.body;
switch (Event) { case "dgii.aprobado": return procesar(res, data, "aprobado"); case "dgii.rechazado": return procesar(res, data, "rechazado"); case "dgii.en_proceso": return procesar(res, data, "en_proceso"); case "dgii.aceptado_condicional": return procesar(res, data, "aceptado_condicional"); default: return res.status(204).end(); }}
function procesar(res: Response, data: unknown, estado: string) { return res.status(200).json({ estado });}# webhook_handler.py (FastAPI)import osfrom fastapi import FastAPI, Header, HTTPException, Request
app = FastAPI()SECRETO = os.environ["FACTURA_WEBHOOK_SECRET"]
@app.post("/webhooks/factura")async def recibir(req: Request, x_webhook_secret: str = Header(default="")): if x_webhook_secret != SECRETO: raise HTTPException(status_code=401)
body = await req.json() event = body["Event"]
if event == "dgii.aprobado": # persiste en tu ERP return {"estado": "aprobado"} if event == "dgii.rechazado": return {"estado": "rechazado"} return {}Reconciliación y eventos de plugins
Como la entrega no se reintenta, un evento perdido no vuelve a llegar. Estas rutas permiten recuperar lo que no llegó barriendo un rango de fechas: accounting-sync devuelve la misma proyección contable del webhook y treasury-sync, la cartera (qué se debe y a quién). Aceptan la X-Api-Key. plugins/events es distinto: solo lo usan los plugins instalados, con su propio token, para publicar eventos hacia otros plugins.
/api/v1/documents/accounting-sync X-Api-Key Estable Devuelve las ventas, notas y gastos emitidos en un rango de fechas con su desglose contable, la misma proyección del webhook. Responde { items, count, hasMore }.
| Nombre | Tipo | Descripción |
|---|---|---|
from query | string (ISO 8601) Requerido | Inicio del rango (fecha de emisión). |
to query | string (ISO 8601) Requerido | Fin del rango. Si es anterior a `from`, la API responde `400`. |
documentTypeId query | integer Opcional | Limita el barrido a un tipo de documento. |
pageSize query | integer Opcional | Por defecto 200, máximo 500. Si `hasMore` es `true`, vuelve a pedir desde la última fecha devuelta. |
/api/v1/documents/treasury-sync X-Api-Key Estable Devuelve ventas, compras y notas que representan una deuda, con cliente o proveedor y formas de pago. Responde { items, count, nextFrom, nextAfterDocumentId, hasMore }.
| Nombre | Tipo | Descripción |
|---|---|---|
from query | string (ISO 8601) Opcional | Cursor de fecha. En la página siguiente, envía el `nextFrom` recibido. |
afterDocumentId query | string (uuid) Opcional | Segunda parte del cursor. En la página siguiente, envía el `nextAfterDocumentId` recibido. |
to query | string (ISO 8601) Opcional | Fecha de emisión máxima. Si es anterior a `from`, la API responde `400`. |
documentTypeId query | integer Opcional | Limita el resultado a un tipo de documento. |
pageSize query | integer Opcional | Por defecto 200, máximo 500. |
/api/v1/plugins/events Estable Un plugin instalado publica un evento propio y el host lo reparte a los demás plugins suscritos. Solo acepta el token del plugin; con X-Api-Key responde 403.
| Nombre | Tipo | Descripción |
|---|---|---|
eventType body | string Requerido | Uno de `cobro.registrado`, `pago_proveedor.registrado` o `inventario.movimiento`. Cualquier otro responde `400`. |
data body | object Opcional | Contenido del evento. El host no lo interpreta: lo reenvía tal cual. |