API REST (v1)
Referencia de la API REST pública de UNIMAST ERP, expuesta bajo el prefijo /api/v1/. Esta es la única API pensada para integración programática con sistemas externos; todo lo demás bajo /api/ es infraestructura interna del dashboard o de la integración DGII (ver Otros endpoints al final).
La especificación OpenAPI completa está en docs/openapi.yaml.
🔐 Autenticación
El guardián central (apiGuard, en lib/auth/api-guard.ts) acepta dos formas de autenticación, evaluadas en este orden:
- Clave de API vía la cabecera
Authorization. El servidor separa el valor de la cabecera en un esquema y un token (authorizationHeader.split(' ')), así que la clave debe ir precedida de un esquema y un espacio — enviarla sola, sin prefijo, no funciona:Las claves se generan con el prefijoAuthorization: Bearer ue_cf8a2dbe48d745a7ecb9a2c18d45120f Content-Type: application/jsonue_(Unimast ERP) al crearlas desdePOST /api/v1/api-keys. El valor completo solo se devuelve en esa respuesta de creación; después queda oculto. - Sesión de cookie del dashboard: si no hay cabecera
Authorization, se usa la cookie de sesión (accessToken) del usuario autenticado en el navegador. Esto permite que los componentes del propio dashboard llamen a/api/v1/*reutilizando la sesión activa.
Si ninguna de las dos resuelve, la API responde 401. Si la clave está INACTIVE o expiresAt ya pasó, también 401.
Scopes (permisos)
Cada clave de API se vincula a una lista de scopes. Si falta el scope requerido por el endpoint, la respuesta es 403. Los nombres de scope no siempre coinciden con el nombre del recurso en la URL — varios endpoints reutilizan el scope de otro recurso:
Endpoint (/api/v1/...) | Scopes requeridos | Nota |
|---|---|---|
/customers | users:read, users:update, users:delete | No existe un scope customers:*; reutiliza los scopes de usuarios. |
/invoices | invoices:read, invoices:create, invoices:update, invoices:delete | |
/tax-documents | tax-documents:read | Solo lectura por REST; no existen POST/PUT/DELETE bajo este endpoint. |
/received-tax-documents | received-tax-documents:read | Solo lectura por REST; no existen POST/PUT/DELETE bajo este endpoint. |
/cash-terminals | cash-terminal:read, cash-terminal:create, cash-terminal:update, cash-terminal:delete | Singular (cash-terminal, no cash-terminals). |
/cash-shifts | cash-shift:read, cash-shift:create, cash-shift:update | Singular (cash-shift). Cierre de turno usa cash-shift:update. |
/cash-movements | payments:read, payments:create | No existe cash-movements:*; el scope es payments:*. |
/payment-methods | payment-methods:read, payment-methods:create, payment-methods:update, payment-methods:delete | |
/banks | banks:read, banks:create, banks:update, banks:delete | |
/bank-accounts | bank-accounts:read, bank-accounts:create, bank-accounts:update, bank-accounts:delete | |
/webhooks | webhooks:read, webhooks:create, webhooks:update, webhooks:delete | |
/api-keys | api-keys:read, api-keys:create, api-keys:update, api-keys:delete | |
/users | users:read, users:create, users:update, users:delete | |
/roles (GET) | users:read | No existe un scope roles:read propio para este endpoint. |
La lista completa de scopes definidos en el sistema vive en SCOPE (lib/constants.ts); la tabla anterior solo cubre los que protegen rutas de /api/v1.
📄 Formato de respuesta
Listas paginadas
{
"data": [ ... ],
"pagination": { "total": 42, "page": 1, "limit": 10, "totalPages": 5 }
}page y limit se envían como query params (?page=1&limit=10); limit se limita a un máximo de 100 en los endpoints que lo aplican (clientes, facturas, movimientos de caja, usuarios).
Un solo recurso
{ "data": { ... }, "message": "Opcional, presente en creaciones/actualizaciones" }Errores
No hay un código de error estructurado (code, requestId, etc.) — la forma real es:
{ "error": "Mensaje descriptivo en español." }Para errores de validación (Zod), se añade details con la lista de issues de Zod:
{ "error": "Error de validación de datos.", "details": [ { "path": ["email"], "message": "Email no válido" } ] }Códigos HTTP realmente usados: 200, 201, 400 (validación / regla de negocio), 401 (sin sesión / clave inválida o expirada), 403 (scope insuficiente), 404, 500. No hay 422 ni 429 — no hay rate limiting implementado en /api/v1.
👥 Clientes (/customers)
GET /api/v1/customers— Lista paginada. Filtros:status(ACTIVE|INACTIVE, defaultACTIVE),search(nombre, email, teléfono o identificación,containsinsensible a mayúsculas).POST /api/v1/customers— Crea un cliente. Único campo obligatorio:name.GET /api/v1/customers/{id}— Detalle.404si no existe.PUT /api/v1/customers/{id}— Actualización parcial.DELETE /api/v1/customers/{id}— Soft delete: cambiastatusaINACTIVE.
🧾 Facturas (/invoices)
GET /api/v1/invoices— Lista paginada. Filtros:customerId,paymentStatus(PENDING|PAID|CANCELLED),type(MANUAL|SUBSCRIPTION|LEGACY),search(NCF, nombre/RNC del cliente,legacySystemId). No existe un filtrostatus(ACTIVE/INACTIVE) para este listado, a diferencia de otros recursos.POST /api/v1/invoices— Crea una factura. Obligatorios:customerId,dueDate,rows(mínimo 1). CalculasubTotal,taxesTotalytotalAmounta partir de las filas si no se envían. ElpaymentStatusinicial siempre esPENDING, sin importar lo que se envíe en el body. El tipo de comprobante fiscal (ncfType) se toma demetadata.ncfTypeo, si no se envía, deldefaultTaxDocumentTypedel cliente.GET /api/v1/invoices/{id}— Incluyecustomer,taxDocuments(historial e-CF/NCF) ycashMovementscon supaymentMethod.PUT /api/v1/invoices/{id}— Actualiza campos de la factura y dispara el webhookinvoices:update.DELETE /api/v1/invoices/{id}— Cancela la factura (status: INACTIVE,paymentStatus: CANCELLED). Si la factura teníalegacySystemId, intenta cancelar el pago/factura en Mikrowisp (best-effort, no bloquea si falla). Si teníancfasignado, emite una nota de crédito e-CFE34ante la DGII. Dispara el webhookinvoices:delete. Retorna400si la factura ya estabaINACTIVE.
📑 Comprobantes fiscales emitidos (/tax-documents)
Solo lectura (GET). Representa los e-CF (TaxDocument) emitidos por la empresa; ver también Facturas y el módulo e-CF emitidos.
GET /api/v1/tax-documents— Lista paginada. Filtros:type(E31|E32|E33|E34|E41|E43|E44|E45|E46|E47),status(APPROVED|REJECTED|CONDITIONALLY_APPROVED|IN_PROCESS),invoiceId,startDate/endDate(rango sobrecreatedAt). Incluyeinvoice(con sucustomer) ycommercialApproval.GET /api/v1/tax-documents/{id}— Detalle. Además de lo anterior, incluyesequence(secuencia/rango de NCF usado) ycreditNote(nota de crédito E34 asociada, si el comprobante fue anulado).404si no existe.
📥 Comprobantes fiscales recibidos (/received-tax-documents)
Solo lectura (GET). Representa los e-CF (TaxDocumentReceived) recibidos de proveedores; ver también el módulo e-CF recibidos.
GET /api/v1/received-tax-documents— Lista paginada. Filtros:type(mismo enum que arriba),fromRNC(contains, insensible a mayúsculas),startDate/endDate(rango sobrecreatedAt). IncluyecommercialApproval.GET /api/v1/received-tax-documents/{id}— Detalle. IncluyecommercialApproval.404si no existe.
🏪 Terminales de caja (/cash-terminals)
GET /api/v1/cash-terminals— Filtrostatus(defaultACTIVE).POST /api/v1/cash-terminals— Obligatorio:name.GET | PUT /api/v1/cash-terminals/{id}DELETE /api/v1/cash-terminals/{id}— Soft delete.
🕓 Turnos de caja (/cash-shifts)
GET /api/v1/cash-shifts— Filtros:page,limit,status,terminalId,userId.POST /api/v1/cash-shifts— Abre un turno. Obligatorios:openingBalance,terminalId. Valida que el terminal exista y estéACTIVE, y que no tenga ya un turnoACTIVEabierto (400en ambos casos).userIdes opcional; si se omite, se usa el usuario/clave que hace la llamada.GET /api/v1/cash-shifts/{id}— Incluyeterminal,user(id/name/email) ycashMovementscon supaymentMethod.PUT /api/v1/cash-shifts/{id}/close— Cierra el turno (closingBalanceobligatorio).400si ya estaba cerrado.
💰 Movimientos de caja (/cash-movements)
GET /api/v1/cash-movements— Siempre filtrastatus: ACTIVE. Filtros adicionales:type(INCOME|EXPENSE),paymentMethodId,shiftId.POST /api/v1/cash-movements— Registra un cobro/gasto. Obligatorios:amount(> 0),paymentMethodId. Valida que el método de pago exista y estéACTIVE; si se envíashiftId, que el turno estéACTIVE. Si se envíareference, rechaza duplicados para el mismo método de pago. Si se incluyeinvoiceIds, aplica el monto a esas facturas en orden y, al saldarse por completo una factura, dispara la emisión automática del e-CF correspondiente. Dispara el webhookpayments:create.GET /api/v1/cash-movements/{id}— IncluyepaymentMethodyinvoices(con sucustomer).
💳 Métodos de pago (/payment-methods)
GET /api/v1/payment-methods— Filtrostatus(defaultACTIVE).POST /api/v1/payment-methods— Obligatorios:name,type(CASH|BANK_TRANSFER|DEBIT_CARD|CREDIT_CARD|CHECK). Rechaza nombres duplicados (400).GET | PUT /api/v1/payment-methods/{id}DELETE /api/v1/payment-methods/{id}— Soft delete.
🏦 Bancos y cuentas bancarias (/banks, /bank-accounts)
GET | POST /api/v1/banks,GET | PUT /api/v1/banks/{id}—POSTrequierename.DELETE /api/v1/banks/{id}— Hard delete. Falla con400si el banco tiene cuentas vinculadas.GET | POST /api/v1/bank-accounts,GET | PUT /api/v1/bank-accounts/{id}—POST/PUTvalidan quebankId(ypaymentMethodId, si se envía) existan.DELETE /api/v1/bank-accounts/{id}— Hard delete (sin verificación de dependencias).
🔗 Webhooks (/webhooks)
GET /api/v1/webhooks— Filtrostatus(defaultACTIVE).POST /api/v1/webhooks— Obligatorios:url,events(mínimo 1). Si no se envíasecret, se genera uno concrypto.randomUUID(). Nota: elsecretse guarda, pero la entrega actual (dispatchWebhookEvent) no firma el payload con él — no hay cabecera de firma HMAC.GET | PUT /api/v1/webhooks/{id},DELETE /api/v1/webhooks/{id}(soft delete).
Entrega de eventos
Un webhook se dispara si events incluye el nombre exacto del evento o el comodín "*". La entrega es un POST asíncrono (vía BullMQ) al url configurado:
POST <url del webhook>
Content-Type: application/json
X-Unimast-Webhook-Id: <id del webhook>
X-Unimast-Event-Type: invoices:create
X-Unimast-Event-Id: <id del recurso>
X-Unimast-Event-Timestamp: 2026-01-15T10:30:00.000Z
User-Agent: Unimast-Webhook/1.0
{ "type": "invoices:create", "payload": { ...recurso... } }Se reintenta hasta 15 veces con backoff exponencial si la URL no responde 2xx.
Eventos que efectivamente se disparan hoy desde el código: invoices:create, invoices:update, invoices:delete, payments:create, payments:delete. El catálogo WEBHOOK_EVENT (lib/constants.ts) define más nombres (usuarios, roles, métodos de pago, terminales, turnos, comprobantes fiscales) pero ningún flujo actual los emite todavía; no asuma que están activos.
🔑 Claves de API (/api-keys)
GET /api/v1/api-keys— Filtrostatus(defaultACTIVE). Nunca incluye el valorkey.POST /api/v1/api-keys— Obligatorio:scopes(mínimo 1). Genera una claveue_<hex de 24 bytes>. La clave completa solo aparece en esta respuesta — no se puede recuperar después.GET | PUT /api/v1/api-keys/{id}— Metadatos únicamente (sin el valorkey).DELETE /api/v1/api-keys/{id}— Soft delete.
👤 Usuarios (/users)
GET /api/v1/users— Excluye usuarios condeletedAtdistinto de null. Filtrosearch(nombre o email). Incluyerole(id/name/description).POST /api/v1/users— Obligatorios:name,email,roleId. Crea el usuario sin contraseña (password: null), genera un código enTempCode(purpose: "invite") y envía un correo de invitación con un enlace a/auth/invite?code=...&email=.... Si el envío de correo falla, el usuario igual se crea (el error solo se registra en consola). Rechaza si ya existe un usuario activo o eliminado con ese email (400en ambos casos — actualmente no hay forma de reactivar por esta vía).GET /api/v1/users/{id},PUT /api/v1/users/{id}—PUTvalidaroleIdsi se envía; si el body incluyepassword, se hashea con bcrypt y se guarda (este campo no pasa por el schema Zod, se lee directo del body).DELETE /api/v1/users/{id}— Soft delete (deletedAt).
🗂️ Roles (/roles)
GET /api/v1/roles— Único método disponible; no existenPOST/PUT/DELETEbajo/api/v1/roles. Retorna todos los roles con susscopes.
📦 Esquemas y enums
Los enums usados en los payloads (Status, InvoiceType, PaymentStatus, TaxDocumentType, CashMovementType, PaymentMethodType, BankAccountType) están definidos en prisma/schema.prisma y reflejados en docs/openapi.yaml. Consulte ese archivo para el detalle campo por campo de cada *Input/*Update schema (derivados de lib/utils/zod-schemas.ts).
🚫 Otros endpoints (fuera de esta API)
El resto de /api/* no es parte de la API REST pública v1 — son rutas internas del dashboard o de la integración DGII, con su propia autenticación (sesión de cookie, o sin autenticación en algunos casos por diseño):
GET /api/health— Health check público (sin auth) para monitoreo/load balancer.GET /api/search/customer,GET /api/search/contributor/[id]— Búsquedas usadas por el dashboard (proxy a Mikrowisp / consulta de RNC en DGII).GET /api/export,POST /api/export— Generación de reportes/exportaciones (PDF/Excel) del dashboard; requiere sesión de cookie.GET|DELETE /api/file/[...key]— Acceso a archivos en S3 (facturas, certificados, etc.); requiere sesión de cookie salvo prefijos públicos explícitos.POST /api/[env]/fe/send— Envío de e-CF a la DGII (usado por el flujo de facturación interno).POST /api/[env]/fe/aprobacioncomercial/api/ecf,POST /api/[env]/fe/recepcion/api/ecf— Endpoints de recepción de XML de la DGII (aprobación comercial y recepción de comprobantes), consumidos por la propia DGII, no por integradores.
Si necesita integrar alguno de estos flujos, revise el código fuente correspondiente directamente — no están cubiertos por esta referencia.