Ritual ERP · Plataforma

Manual para desarrolladores

Cómo conectar apps y webs a Ritual ERP y cómo construir módulos dentro del sistema — manteniendo el aislamiento entre clientes y la seguridad a nivel senior que el ERP ya garantiza. Todo lo de aquí está vivo en producción.

Base URL  https://ritualerp.ch  ·  JSON  ·  TLS obligatorio

01 Fundamentos

El vocabulario y los formatos que se repiten en toda la API.

Multi-tenantCada centro (tenant) es un mundo aislado. Toda petición se resuelve a un centro y nunca ve datos de otro — lo garantiza la base de datos (RLS), no un where.
DineroEnteros en la unidad menor (Rappen). Los campos terminan en Minor: grossMinor: 12500 = CHF 125.00. Sin coma flotante.
FechasISO 8601 en UTC (2026-07-25T09:30:00.000Z). Zona de negocio: Europe/Zurich.
TextoUTF-8 completo (ñ, tildes, acentos).
MonedaCHF.
ErroresCódigo HTTP + cuerpo { "error": "…" }. 400 validación · 401 sin autenticar · 403 sin permiso/scope · 404 no existe/ajeno · 409 conflicto de idempotencia · 422 regla de negocio · 429 rate-limit.
Contrato vivo

La especificación OpenAPI 3.1 se genera desde el código (no se desincroniza) y es importable en Postman / Insomnia / Swagger:

GET /v1/developer/openapi.json      # requiere una sesión ADMIN

02 Los cimientos de seguridad

No son opcionales ni "buenas prácticas": es cómo está construido el sistema. Si escribes código o consumes la API, respétalos.

Aislamiento por centro — Row-Level Security (RLS)

Cada tabla con tenantId tiene RLS forzada en Postgres. El runtime corre con un rol sin privilegios (ritual_app) y toda operación pasa por withTenant(tenantId, tx => …), que fija app.current_tenant para esa transacción. La política de la tabla hace el resto:

ALTER TABLE "Mitabla" ENABLE ROW LEVEL SECURITY;
ALTER TABLE "Mitabla" FORCE  ROW LEVEL SECURITY;
CREATE POLICY "Mitabla_tenant_isolation" ON "Mitabla"
  USING      ("tenantId" = current_setting('app.current_tenant', true))
  WITH CHECK ("tenantId" = current_setting('app.current_tenant', true));
GRANT SELECT, INSERT, UPDATE, DELETE ON "Mitabla" TO ritual_app;
Regla · no negociable

Toda tabla nueva con tenantId DEBE registrarse en TENANT_TABLES y llevar RLS + GRANT en una migración. Una tabla de centro sin RLS es una fuga de datos entre clientes. El test rls-coverage.spec falla si te la saltas.

Secretos, dinero e integridad

03 Las cuatro credenciales

Elige la correcta según quién llama y a qué necesita acceder. No mezcles: cada una tiene un alcance distinto a propósito.

CredencialFormato / cabeceraParaAlcance
Sesión de usuario JWT
Authorization: Bearer <jwt>
El panel (personas). El rol del usuario (ADMIN, RECEPTION…).
Clave de API (M2M) rk_<tenant>_<keyId>_<secret>
Authorization: Bearer rk_…
Apps y módulos externos (servidor). Scopes por módulo. Nunca toca administración.
Clave de integración trk_live_<hex>
X-Api-Key: trk_live_…
Server-to-server: seguimiento + emitir sesiones de portal. Solo /public/tracking* y /public/portal/session.
Token de portal cst_<tenant>_<id>_<secret>
Authorization: Bearer cst_…
El navegador del cliente final. Solo los datos de ESE cliente. Corto (60 min), revocable.

04 Conectar una app o web (API M2M)

Para que tu backend hable con el ERP: crea una clave de API, dale scopes por módulo y llama a los recursos.

1 · Crea la clave y dale scopes

En el panel → Desarrolladores (o por API, rol ADMIN). Un scope es <modulo>:read o <modulo>:write (write incluye read), o * para todo. El método HTTP decide la acción: GET/HEAD = read; el resto = write.

GET/v1/developer/api-keyslistar
POST/v1/developer/api-keyscrear (devuelve el token 1 vez)
DEL/v1/developer/api-keys/:idrevocar

Módulos con scope: agenda, clientes, ventas, servicios, vales, avisos, chat, terapeutas, inventario, marketing, contactos, facturas, contabilidad, nomina, b2b, comercial, recepcion (paquetería).

Barrera dura

Una clave de API solo alcanza rutas de un módulo de negocio. Toda la administración (usuarios, ajustes, plataforma, developer, copias de seguridad, suscripción) no está mapeada a ningún módulo y devuelve 403 siempre — una clave nunca llega ahí.

2 · Autentica y llama

# Listar clientes del centro dueño de la clave
curl https://ritualerp.ch/v1/customers?limit=50&offset=0 \
  -H "Authorization: Bearer rk_<tenant>_<keyId>_<secret>"

# Crear un cliente (write). En operaciones de dinero, añade Idempotency-Key.
curl -X POST https://ritualerp.ch/v1/customers \
  -H "Authorization: Bearer rk_…" \
  -H "Content-Type: application/json" \
  -d '{ "name": "Juan Pérez", "email": "juan@ejemplo.ch" }'

Recursos documentados en el OpenAPI: /v1/bookings, /v1/customers, /v1/services, /v1/products, /v1/invoices, /v1/sales, /v1/vouchers. Otros módulos son alcanzables con su scope (p. ej. paquetería: /v1/recepciones, /v1/bultos, /v1/tarifas… con recepcion:*).

Paginación: ?limit= (1–500, def. 100) y ?offset= (def. 0).  Dinero: añade Idempotency-Key: <8–200 chars>, estable por intento.

05 Webhooks — reaccionar a eventos

En vez de sondear, suscribe un endpoint y Ritual te avisa cuando algo pasa. Cada entrega va firmada.

Suscribir y eventos

POST/v1/developer/webhooks{ url, events[], … }
GET/v1/developer/webhookslistar
PATCH/v1/developer/webhooks/:ideditar / habilitar
POST/v1/developer/webhooks/:id/pingprueba
POST/v1/developer/webhooks/:id/rotate-secretrotar secreto

Eventos disponibles (o *): booking.created · booking.updated · booking.cancelled · customer.created · customer.updated · invoice.created · invoice.paid · sale.created · voucher.created. La URL debe ser https en producción (se bloquean IPs privadas y el endpoint de metadatos de la nube — anti-SSRF).

Tu endpoint recibe un POST con este sobre y estas cabeceras:

Headers
X-Ritual-Event:       invoice.paid
X-Ritual-Event-Id:    <uuid>          # idempotencia en tu lado
X-Ritual-Delivery-Id: <uuid>
X-Ritual-Signature:   t=<unix>,v1=<hmac>   # ver abajo

Body
{ "id": "<uuid>", "type": "invoice.paid", "tenantId": "…",
  "createdAt": "2026-07-25T…Z", "data": { /* el recurso */ } }

Verifica la firma (HMAC-SHA256) — obligatorio

La firma se calcula sobre <t>.<cuerpo-crudo>. Recalcúlala sobre el cuerpo sin parsear, compárala en tiempo constante y rechaza timestamps viejos (corta los ataques de repetición). Responde 2xx en menos de 10 s.

import { createHmac, timingSafeEqual } from 'node:crypto';

function verify(rawBody, header, secret) {
  const [t, v1] = header.split(',').map(p => p.split('=')[1]);
  if (Math.abs(Date.now()/1000 - Number(t)) > 300) return false;  // >5 min: fuera
  const mac = createHmac('sha256', secret).update(`${t}.${rawBody}`).digest('hex');
  return v1.length === mac.length && timingSafeEqual(Buffer.from(v1), Buffer.from(mac));
}
Entrega fiable

Reintentos con backoff (10 s · 30 s · 2 min · 10 min · 30 min · 2 h; 6 intentos), cola de fallidos y auto-desactivación tras 20 fallos consecutivos. Usa el id del evento para aplicar idempotencia en recepción.

06 Portal de clientes (para tu web)

Que tus clientes sigan sus datos desde tu web sin que tú tengas que reexponer nada — con la propiedad forzada en Ritual, no en tu código.

Flujo: tu web autentica al cliente (tu login). Tu servidor, con la clave de integración, pide una sesión para ese cliente; Ritual devuelve un token corto atado a él; el navegador del cliente lo usa y solo ve lo suyo (sin enumeración ni fuga entre clientes).

# 1) Tu servidor (clave de integración) pide sesión para un cliente ya autenticado
POST /public/portal/session      X-Api-Key: trk_live_…
     { "customerId": "<id del cliente en Ritual>" }
  → { "token": "cst_…", "expiresAt": "…" }        # 60 min

# 2) El navegador del cliente, con ese token (Authorization: Bearer cst_…)
GET/portal/shipmentssus envíos + línea de tiempo
GET/portal/mesu ficha
GET/portal/facturas · /portal/facturas/:id/pdfsus facturas
GET/portal/destinatarios · /portal/remitentesver (GET) · crear (POST) · editar (PATCH)
GET/portal/envios/:ref/hbl.pdfel HBL de un envío suyo
Nunca en el navegador

La clave trk_live_… es de servidor: jamás en el navegador. El token cst_… sí puede vivir en el cliente — es corto, de un solo cliente y sin acceso a administración.

07 Construir un módulo dentro del ERP

Un módulo es una unidad vendible (agenda, facturas, paquetería…) con sus rutas, roles, plan y scopes. El catálogo vive en un solo sitio y todo lo demás se deriva de él.

Añadir un módulo con clave <clave>:

#DóndeQué
1entitlements.ts · MODULE_CATALOGLa clave y su grupo (operación / recursos / finanzas).
2MODULE_BY_PREFIXQué rutas /v1/<seg> cobra el módulo. Sin esto la ruta es gratis (no gateada).
3DEFAULT_ROLE_ACCESSQué roles lo ven por defecto.
4MODULE_REQUIRESSolo si necesita otro módulo para funcionar.
5PLAN_TIERSEn qué peldaños de plan se vende.
6Panel: permissions.ts + Dashboard.tsxMODULE_ACCESS / MODULE_MATRIX y la pestaña (Tab) + render.
7i18n nav.<clave>Etiqueta en es / de / fr / it.
8Tablas nuevas con tenantIdAñádelas a TENANT_TABLES y aplícales RLS+GRANT en una migración.

Los scopes de la API (<clave>:read / <clave>:write) se generan solos desde el catálogo — no hay listas paralelas. Hay un generador (tsx scripts/new-module.ts) que escribe los pasos 1–3 y 6–7; si te saltas alguno, plan-tiers.spec (1–3) o rls-coverage.spec (8) fallan a propósito.

Gating real

Una petición autenticada pasa tres puertas: rol (¿este usuario/clave puede?), plan/suscripción (¿el centro tiene el módulo contratado y al día?) y propiedad/RLS (¿es de su centro?). Un módulo bien dado de alta las respeta sin que escribas el chequeo en cada handler.

08 Checklist de seguridad senior

Antes de dar por bueno un endpoint, un módulo o una integración.

AislamientoToda tabla con tenantId → RLS + GRANT + TENANT_TABLES. Todo acceso vía withTenant. Nada de where tenantId a pelo como única barrera.
DineroIdempotency-Key en toda mutación de dinero. El servidor recalcula importes/impuestos; nunca confía en el cliente. Índices únicos que hagan imposible el doble cobro/devolución.
SecretosHash (SHA-256) + comparación en tiempo constante. Se enseñan una vez. Nunca en logs ni en la URL. Caducidad y revocación.
AutorizaciónRol × scope × propiedad en cada ruta. Valida el cuerpo con esquema. Las rutas de administración jamás son alcanzables por clave de API.
EnumeraciónNo expongas identificadores secuenciales sin atar la consulta a su dueño (el token del portal ata la propiedad; por eso no hay enumeración).
WebhooksFirma HMAC verificada sobre el cuerpo crudo + anti-repetición (timestamp) + idempotencia por id de evento. URL solo https, anti-SSRF.
TransporteTLS + HSTS. Rate-limit por IP. Responde rápido; el trabajo pesado, a una cola.
Ritual ERP · Manual para desarrolladores. El OpenAPI (/v1/developer/openapi.json) y el panel de Desarrolladores son la fuente de la verdad viva; este manual explica el porqué y las reglas que no salen en el contrato.