Skip to Content
UNIMAST

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:

  1. 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:
    Authorization: Bearer ue_cf8a2dbe48d745a7ecb9a2c18d45120f Content-Type: application/json
    Las claves se generan con el prefijo ue_ (Unimast ERP) al crearlas desde POST /api/v1/api-keys. El valor completo solo se devuelve en esa respuesta de creación; después queda oculto.
  2. 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 requeridosNota
/customersusers:read, users:update, users:deleteNo existe un scope customers:*; reutiliza los scopes de usuarios.
/invoicesinvoices:read, invoices:create, invoices:update, invoices:delete
/tax-documentstax-documents:readSolo lectura por REST; no existen POST/PUT/DELETE bajo este endpoint.
/received-tax-documentsreceived-tax-documents:readSolo lectura por REST; no existen POST/PUT/DELETE bajo este endpoint.
/cash-terminalscash-terminal:read, cash-terminal:create, cash-terminal:update, cash-terminal:deleteSingular (cash-terminal, no cash-terminals).
/cash-shiftscash-shift:read, cash-shift:create, cash-shift:updateSingular (cash-shift). Cierre de turno usa cash-shift:update.
/cash-movementspayments:read, payments:createNo existe cash-movements:*; el scope es payments:*.
/payment-methodspayment-methods:read, payment-methods:create, payment-methods:update, payment-methods:delete
/banksbanks:read, banks:create, banks:update, banks:delete
/bank-accountsbank-accounts:read, bank-accounts:create, bank-accounts:update, bank-accounts:delete
/webhookswebhooks:read, webhooks:create, webhooks:update, webhooks:delete
/api-keysapi-keys:read, api-keys:create, api-keys:update, api-keys:delete
/usersusers:read, users:create, users:update, users:delete
/roles (GET)users:readNo 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 429no hay rate limiting implementado en /api/v1.


👥 Clientes (/customers)

  • GET /api/v1/customers — Lista paginada. Filtros: status (ACTIVE|INACTIVE, default ACTIVE), search (nombre, email, teléfono o identificación, contains insensible a mayúsculas).
  • POST /api/v1/customers — Crea un cliente. Único campo obligatorio: name.
  • GET /api/v1/customers/{id} — Detalle. 404 si no existe.
  • PUT /api/v1/customers/{id} — Actualización parcial.
  • DELETE /api/v1/customers/{id} — Soft delete: cambia status a INACTIVE.

🧾 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 filtro status (ACTIVE/INACTIVE) para este listado, a diferencia de otros recursos.
  • POST /api/v1/invoices — Crea una factura. Obligatorios: customerId, dueDate, rows (mínimo 1). Calcula subTotal, taxesTotal y totalAmount a partir de las filas si no se envían. El paymentStatus inicial siempre es PENDING, sin importar lo que se envíe en el body. El tipo de comprobante fiscal (ncfType) se toma de metadata.ncfType o, si no se envía, del defaultTaxDocumentType del cliente.
  • GET /api/v1/invoices/{id} — Incluye customer, taxDocuments (historial e-CF/NCF) y cashMovements con su paymentMethod.
  • PUT /api/v1/invoices/{id} — Actualiza campos de la factura y dispara el webhook invoices:update.
  • DELETE /api/v1/invoices/{id} — Cancela la factura (status: INACTIVE, paymentStatus: CANCELLED). Si la factura tenía legacySystemId, intenta cancelar el pago/factura en Mikrowisp (best-effort, no bloquea si falla). Si tenía ncf asignado, emite una nota de crédito e-CF E34 ante la DGII. Dispara el webhook invoices:delete. Retorna 400 si la factura ya estaba INACTIVE.

📑 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 sobre createdAt). Incluye invoice (con su customer) y commercialApproval.
  • GET /api/v1/tax-documents/{id} — Detalle. Además de lo anterior, incluye sequence (secuencia/rango de NCF usado) y creditNote (nota de crédito E34 asociada, si el comprobante fue anulado). 404 si 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 sobre createdAt). Incluye commercialApproval.
  • GET /api/v1/received-tax-documents/{id} — Detalle. Incluye commercialApproval. 404 si no existe.

🏪 Terminales de caja (/cash-terminals)

  • GET /api/v1/cash-terminals — Filtro status (default ACTIVE).
  • 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 turno ACTIVE abierto (400 en ambos casos). userId es opcional; si se omite, se usa el usuario/clave que hace la llamada.
  • GET /api/v1/cash-shifts/{id} — Incluye terminal, user (id/name/email) y cashMovements con su paymentMethod.
  • PUT /api/v1/cash-shifts/{id}/close — Cierra el turno (closingBalance obligatorio). 400 si ya estaba cerrado.

💰 Movimientos de caja (/cash-movements)

  • GET /api/v1/cash-movements — Siempre filtra status: 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ía shiftId, que el turno esté ACTIVE. Si se envía reference, rechaza duplicados para el mismo método de pago. Si se incluye invoiceIds, 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 webhook payments:create.
  • GET /api/v1/cash-movements/{id} — Incluye paymentMethod y invoices (con su customer).

💳 Métodos de pago (/payment-methods)

  • GET /api/v1/payment-methods — Filtro status (default ACTIVE).
  • 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}POST requiere name.
  • DELETE /api/v1/banks/{id}Hard delete. Falla con 400 si el banco tiene cuentas vinculadas.
  • GET | POST /api/v1/bank-accounts, GET | PUT /api/v1/bank-accounts/{id}POST/PUT validan que bankId (y paymentMethodId, si se envía) existan.
  • DELETE /api/v1/bank-accounts/{id}Hard delete (sin verificación de dependencias).

🔗 Webhooks (/webhooks)

  • GET /api/v1/webhooks — Filtro status (default ACTIVE).
  • POST /api/v1/webhooks — Obligatorios: url, events (mínimo 1). Si no se envía secret, se genera uno con crypto.randomUUID(). Nota: el secret se 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 — Filtro status (default ACTIVE). Nunca incluye el valor key.
  • POST /api/v1/api-keys — Obligatorio: scopes (mínimo 1). Genera una clave ue_<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 valor key).
  • DELETE /api/v1/api-keys/{id} — Soft delete.

👤 Usuarios (/users)

  • GET /api/v1/users — Excluye usuarios con deletedAt distinto de null. Filtro search (nombre o email). Incluye role (id/name/description).
  • POST /api/v1/users — Obligatorios: name, email, roleId. Crea el usuario sin contraseña (password: null), genera un código en TempCode (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 (400 en ambos casos — actualmente no hay forma de reactivar por esta vía).
  • GET /api/v1/users/{id}, PUT /api/v1/users/{id}PUT valida roleId si se envía; si el body incluye password, 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 existen POST/PUT/DELETE bajo /api/v1/roles. Retorna todos los roles con sus scopes.

📦 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.