# API de Regitap (v1)

Para que otro software (un CRM, un planificador de servicios, un ERP…), o el agente de IA que lo integra, mantenga el equipo, los
lugares y las tareas de cada día en Regitap, y fiche en nombre de los trabajadores desde su propia
app. Es genérica: no hay nada específico de ningún software. Rentalotop es un cliente más.

- **Base:** `https://www.regitap.com/api/v1` (dev: `https://regitap-dev.up.railway.app/api/v1`).
- **Autenticación:** `Authorization: Bearer rgt_…`. La clave la crea un administrador en
  Ajustes → API; se ve una sola vez y se guarda en una variable de entorno del software que la usa
  (`REGITAP_API_KEY`): nunca en el código, en un repositorio ni en el chat con un agente. Actúa como un administrador de **su** empresa y nada más.
  Todo lo que hace queda en la auditoría con el id de la clave. Revocarla es inmediato.
- **Formato:** JSON (UTF-8). Fechas `YYYY-MM-DD`; horas `HH:MM` (hora local de la empresa);
  instantes ISO 8601 en UTC (`2026-10-23T08:02:00.000Z`).
- **Ids:** cada recurso se identifica con **tu** id (`ref`, hasta 100 caracteres `A-Z a-z 0-9 . _ : -`).
  `PUT` crea o actualiza (idempotente): puedes reenviar lo mismo sin miedo a duplicados.
- **Errores:** `{ "error": "code", "details": [{ "path", "message" }]? }` con el código HTTP:
  `400 invalid`, `401 unauthorized`, `403 forbidden`, `404 not_found`, `409` (conflicto de estado),
  `422` (referencia desconocida: `worker`, `place`), `413 too_large`, `429` (límites, abajo).
- **Límites de cada cuenta** (para proteger el servicio; se amplían si hace falta, escribiendo a Regitap):
  600 peticiones por minuto por empresa, peticiones de hasta 64 KB, **una clave activa por empresa** (para
  cambiarla, se revoca y se crea otra), 100 tareas por trabajador y día, 1.000 lugares, 100 equipos y un máximo de trabajadores activos por cuenta
  (200 de entrada) por encima del tope que ponga la empresa. Al llegar a uno: `429` (o `409 worker_cap_reached`)
  con un `message`: **no reintentes**, avisa a una persona.
- **Tope de trabajadores:** la empresa puede fijar un máximo de trabajadores activos (Ajustes → API). Al
  pasarlo, `PUT /workers/{ref}` responde `409 worker_cap_reached` (con `cap`): **no reintentes**, avisa a una
  persona para que lo suba. Es solo un límite: se paga por los trabajadores que hay, no por el tope.

## Reglas que la API no puede saltarse

Son las del registro de jornada (art. 34.9 ET), iguales que en la app:

- El registro no se borra ni se edita: un error se corrige desde Regitap, con motivo y rastro.
- La hora de un fichaje es la del servidor al recibirlo. Solo si el móvil estaba sin conexión
  (`offline: true`) vale la hora del móvil (`deviceAt`), marcada como tal y nunca en el futuro.
- La ubicación solo del momento de pulsar (`location`), nunca seguimiento. Fuera de un lugar se
  guarda marcado, no se bloquea.
- Reintentos seguros: cada pulsación lleva un `clientId` único (8–64 caracteres); repetirla
  devuelve el primer resultado.

```json
"location": { "status": "ok", "lat": 40.4168, "lng": -3.7038, "accuracyM": 12 }
"location": { "status": "denied" }            // o "unavailable", "not_requested"
```

## Trabajadores

### `GET /workers`
Todos (también los de baja, con `endDate`).

### `PUT /workers/{ref}`
Crea o actualiza con **lo que tengas**: solo cambian los campos que mandes (lo que se haya rellenado
en Regitap no se pierde). Para crear basta el nombre; lo demás (documento, alta, contrato, horas)
se puede completar en Regitap, y la ficha sale como «incompleta» hasta entonces (puede fichar igual).
Si la persona ya está en Regitap sin `ref`, se **enlaza** por email o móvil (el de su ficha o el de la cuenta con la que entra en Regitap) en vez de duplicarla
(`linked: true`; `409 conflict` si coinciden varias). Todos los campos:

```json
{
  "firstName": "Ana", "lastName": "López García",
  "idType": "DNI", "idNumber": "12345678Z",
  "phone": "+34600000001", "email": "ana@example.com",
  "startDate": "2026-01-12", "endDate": null,
  "contractType": "Indefinido", "weeklyHours": 30,
  "custom": { "Nº Seguridad Social": "281234567890" },
  "schedule": {
    "validFrom": "2026-10-26",
    "week": [
      { "ranges": ["08:00-14:30"], "breakMinutes": 30, "breakPaid": false },
      { "ranges": ["08:00-14:30"], "breakMinutes": 30, "breakPaid": false },
      { "ranges": ["08:00-14:30"], "breakMinutes": 30, "breakPaid": false },
      { "ranges": ["08:00-14:30"], "breakMinutes": 30, "breakPaid": false },
      { "ranges": ["08:00-14:30"], "breakMinutes": 30, "breakPaid": false },
      { "ranges": [], "breakMinutes": 0, "breakPaid": false },
      { "ranges": [], "breakMinutes": 0, "breakPaid": false }
    ]
  }
}
```

`idType`: `DNI`, `NIE` o `PASSPORT`. `phone` en E.164 (o español, se normaliza). `custom`: los campos
de la ficha que la empresa haya creado, por su nombre. `schedule` (opcional): horario semanal
(lunes → domingo) desde una fecha; los días anteriores conservan el que tenían.
Respuesta: `{ ref, id, created, linked, missing }` (`missing`: lo que le falta a la ficha). `201` creado,
`200` actualizado o enlazado, `409` si el documento ya es de otro trabajador.

### `POST /workers/{ref}/end`
Baja: `{ "endDate": "2026-12-31", "reason": "Fin de contrato" }`. No se borra nada.

### `GET /workers/{ref}/status`
Estado ahora (`OFF`, `WORKING`, `PAUSED`), desde cuándo, segundos trabajados hoy, jornada sin
cerrar, ausencia de hoy y tareas de hoy.

### `POST /workers/{ref}/clock`
Fichar en nombre del trabajador (su pulsación en tu app):

```json
{ "action": "DAY_START", "clientId": "a1b2c3d4e5", "location": { "status": "ok", "lat": 40.41, "lng": -3.70, "accuracyM": 15 } }
```

`action`: `DAY_START`, `PAUSE_START`, `PAUSE_END`, `DAY_END`. Sin conexión: `"offline": true,
"deviceAt": "<instante del móvil>"`. `409` con `reason` (`invalid_state`, `not_employed`,
`company_inactive`, `paused`, `ended_today`) y el estado real. Terminar la jornada termina la tarea en curso.
Antes de su primer fichaje, la persona tiene que haber leído y aceptado en Regitap la información sobre su registro
(si no, `409 not_informed`; ábrele su registro con `POST /workers/{ref}/record-link` para que la lea).
Una jornada por día: tras `DAY_END`, otro `DAY_START` ese mismo día se rechaza con `ended_today` (la jornada
partida va con pausa; un fin antes de tiempo se corrige). El turno de noche cuenta para el día en que empezó.

## Equipos

Cada trabajador está en un equipo como mucho, y los **supervisores** del equipo (trabajadores también)
le aprueban las correcciones y las vacaciones. Lo de un supervisor lo aprueba un administrador, que además
puede aprobarlo todo. Crea primero los trabajadores y después los equipos.

### `PUT /teams/{ref}`
`{ "name": "Turno de mañana", "supervisors": ["emp-7"], "members": ["emp-1", "emp-2", "emp-7"] }` (refs de
trabajadores). Poner a alguien en `members` lo saca de su equipo anterior. Sin `members` o sin
`supervisors`, se quedan como estaban. `422 worker` con `missing` si algún ref no existe.

### `GET /teams` · `DELETE /teams/{ref}`
Los equipos con sus supervisores y miembros; `DELETE` lo archiva (sus personas se quedan sin equipo).

También se puede poner el equipo de una persona al darla de alta: `PUT /workers/{ref}` con `"team": "<ref del equipo>"`
(`null` = sin equipo).

## Lugares

### `PUT /places/{ref}`
Un cliente, una oficina, una obra…: `{ "name", "address", "lat", "lng", "radiusM"?, "isCenter"? }`.
Un fichaje o una tarea dentro del radio cuenta como «en el lugar». `isCenter`: centro de trabajo
(sale en los informes).

### `DELETE /places/{ref}`
Lo archiva (los fichajes antiguos siguen apuntando a él).

## Tareas

Una tarea es un trabajo de un día dentro de la jornada (una visita a un cliente, un servicio, una cita…). No es el
registro de jornada (eso son los fichajes), pero lo mueve:

- **Empezar la primera tarea empieza la jornada** si no estaba empezada (o termina la pausa), con
  esa hora y esa ubicación.
- **Una a la vez:** empezar otra termina la que estaba en curso.
- **Entre tareas la jornada sigue:** los desplazamientos entre clientes son tiempo de trabajo
  (TJUE, C-266/14). Nada la pausa ni la termina sola.
- **Terminar la última del día** devuelve `askEndDay: true`: pregunta a la persona si ha terminado
  la jornada y, si dice que sí, `POST /workers/{ref}/clock` con `DAY_END`. Nunca se termina por ella.

### `PUT /tasks/{ref}`

```json
{
  "worker": "emp-1", "date": "2026-10-24", "title": "Revisión anual · Cliente Martínez",
  "address": "Calle Mayor, 10, Madrid", "place": "cliente-17",
  "plannedStart": "10:00", "plannedMinutes": 90, "notes": "Preguntar en recepción"
}
```

`worker`: el `ref` del trabajador. `place` (opcional): el `ref` de un lugar, para saber si empezó y
terminó allí. Una tarea ya empezada no cambia de trabajador ni de día (`409 started`).
`"informative": true`: **solo para verla**. Le sale en su app con su hora y dirección, sin Empezar ni Terminar y sin
duración; ficha solo su jornada. Empezarla o terminarla da `409 informative`.

### `GET /tasks?date=2026-10-24&worker=emp-1`
Las del día (`worker` opcional), con `status` (`PENDING`, `STARTED`, `DONE`, `CANCELLED`) y las horas
reales (`startedAt`, `endedAt`). `GET /tasks/{ref}`: una.

### `DELETE /tasks/{ref}`
La cancela (solo si no ha empezado; `409 started` si sí).

### `POST /tasks/{ref}/start` · `POST /tasks/{ref}/end`
La pulsación del trabajador en tu app: `{ "clientId", "location", "offline"?, "deviceAt"? }`.
`start` responde `{ ok, outsidePlace }`; `end`, `{ ok, askEndDay }`.

## Correcciones

Un olvido o una hora mal no se arreglan editando: se pide una **corrección**, alguien la aprueba o la
rechaza, y si se aprueba se añade un fichaje que sustituye o anula el original (que se queda como estaba).
Por la API **una petición nunca se aprueba sola**: espera a `POST /corrections/{id}/decide`. Las dos
llevan el nombre de quién pide y de quién decide (la trabajadora y su encargada en tu software).

### `GET /workers/{ref}/events?from=2026-10-20&to=2026-10-24`
Los fichajes que cuentan (con las correcciones ya aplicadas), cada uno con su `id`: lo que se corrige.

### `POST /corrections`

```json
{ "worker": "emp-1", "kind": "add", "action": "DAY_END", "proposedAt": "2026-10-22T14:05:00.000Z",
  "reason": "Se me olvidó fichar la salida", "requestedBy": "Ana López (desde el CRM)" }
```

`kind`: `add` (falta un fichaje: `action` y `proposedAt`), `change` (otra hora: `eventId` y
`proposedAt`) o `void` (anular: `eventId`). `reason` de 3 a 500 caracteres. Responde `{ id, status: "PENDING" }`.
Los encargados y administradores de Regitap la ven en Pendientes y también pueden decidirla allí.

### `GET /corrections?status=pending`
Las que esperan (`status=all`: también las decididas), con quién las pidió y quién las decidió.

### `POST /corrections/{id}/decide`
`{ "approve": true, "note": "Confirmado con el cliente", "decidedBy": "Marta Ruiz", "supervisor": "emp-7" }`.
Con `supervisor` (el ref de quien decide), Regitap comprueba que supervisa el equipo de esa persona y que esa
persona no es supervisora (eso va a un administrador): si no, `403 not_supervisor`. Sin `supervisor`, es una
decisión de administrador (la de la clave).
`409 decided` si ya estaba decidida; `409 target` si el fichaje ya se corrigió por otro lado.

## Días libres y ausencias

Vacaciones, días libres, bajas y permisos, entre dos fechas (las dos incluidas). Esos días la persona no
tiene horas previstas ni recibe avisos de «¿has empezado?», y las vacaciones y los días libres descuentan
de sus días del año. Si tu software ya los sabe, mándalos; en Regitap se pueden mantener, anular o añadir
más, y lo que cambie allí te llega por el webhook `absence.changed`.

### `PUT /absences/{ref}`

```json
{ "worker": "emp-1", "kind": "vacation", "from": "2026-08-03", "to": "2026-08-14", "note": "Agosto" }
```

`kind`: `vacation`, `personal_day`, `sick_leave`, `paid_leave`, `unpaid_leave` u `other`. Volver a
mandarla la actualiza (y la reactiva si estaba anulada). `422 worker` si no existe esa persona.

### `DELETE /absences/{ref}`
La anula (no se borra). Si venía de una petición aprobada, la petición queda cancelada y los días vuelven.

### `GET /absences?from=2026-08-01&to=2026-08-31&worker=emp-1`
Todas las que tocan esas fechas (un año como mucho), vengan de donde vengan: tuyas (con `ref`), añadidas
en Regitap o peticiones aprobadas (`ref: null`). Las anuladas salen con `cancelled: true`. Úsalo antes de
asignar trabajo a alguien.

## Ver su registro sin cuenta en Regitap

Solo si tu equipo ficha dentro de tu software. La ley le da a cada persona derecho a ver su registro.
Si no tiene cuenta en Regitap, tu software le abre un enlace.

### `POST /workers/{ref}/record-link`
Responde `201 { "ref", "url", "expiresAt" }`. La `url` vale **una vez** y durante **10 minutos**: ábrela al
momento para esa persona (botón «Ver mi registro» o «Pedir vacaciones»). Le da una hora en la que ve su
registro, sus resúmenes mensuales y sus vacaciones, y pide correcciones y vacaciones. No crea cuenta. Lo que
pide queda a su nombre y lo decide la empresa en Regitap. No guardes la url ni la mandes por email.

## Webhooks

Regitap avisa a tu software de lo que pasa, para que no tenga que preguntar. Una dirección por empresa:
la pone una persona en **Ajustes → API → Avisos** (o tu software con `PUT /webhook`). Tiene que ser
`https://` y pública.

### `PUT /webhook` · `GET /webhook` · `DELETE /webhook` · `POST /webhook/test`
`PUT { "url": "https://tu-software.com/api/regitap/webhook" }` responde el **secreto de firma**
(`whsec_…`): guárdalo en `REGITAP_WEBHOOK_SECRET`. Cambiar la url mantiene el secreto;
`"rotateSecret": true` da uno nuevo. `POST /webhook/test` manda un `ping` y responde lo que contestó tu software.

### Qué llega

`POST` a tu dirección con JSON: `{ "id": "whd_…", "type": "…", "created_at": "…", "data": { … } }`.
`data.worker` es `{ id, ref, name }` (`ref`: tu id). Los avisos para la persona traen `data.message`
(`title` y `body`, en español, listos para enseñárselos).

| `type` | Cuándo | Para enseñar a la persona |
|---|---|---|
| `worker.reminder` | A la hora de empezar o terminar según su horario y aún no ha fichado (`data.kind`: `missing_start` o `missing_end`) | Sí |
| `correction.decided` | Se decide una corrección suya, o alguien corrige su registro (`requestedByWorker: false`) | Sí |
| `leave.decided` | Se aprueba o rechaza una petición de vacaciones o día libre | Sí |
| `summary.ready` | Su resumen mensual está listo (lo ve con «Ver mi registro») | Sí |
| `absence.changed` | Se añade, aprueba o anula una ausencia en Regitap (no las que mandas tú) | Para tu planificación |
| `clock.recorded` | Cada fichaje, venga de donde venga | Para tu software |
| `task.started` · `task.ended` | Empieza o termina una tarea (`reason`: `ended`, `next_task` al empezar otra, `day_end` al terminar la jornada) | Para tu software |
| `ping` | `POST /webhook/test` | No |

### Comprobar la firma

Cabeceras: `Regitap-Event`, `Regitap-Delivery` (el id: si llega dos veces, ignora la segunda) y
`Regitap-Signature: t=<segundos>,v1=<hex>`, donde `v1` es el HMAC-SHA256 de `"<t>.<cuerpo tal cual>"` con
tu secreto. Rechaza si no coincide o si `t` tiene más de 5 minutos.

```js
import { createHmac, timingSafeEqual } from "node:crypto";

export function verifyRegitap(rawBody, header, secret = process.env.REGITAP_WEBHOOK_SECRET) {
  const { t, v1 } = Object.fromEntries(header.split(",").map((p) => p.split("=")));
  if (!t || !v1 || Math.abs(Date.now() / 1000 - Number(t)) > 300) return false;
  const expected = createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex");
  return expected.length === v1.length && timingSafeEqual(Buffer.from(expected), Buffer.from(v1));
}
```

Responde `2xx` en menos de 10 segundos. Si no, Regitap reintenta (1 min, 5 min, 15 min, 1 h, 3 h, 6 h,
12 h, 24 h) y después lo da por perdido. Los últimos envíos y su resultado se ven en Ajustes → API.
