> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://developer.crehana.com/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://developer.crehana.com/_mcp/server.

# Changelog

Acá publicamos los cambios que afectan el contrato de la API: endpoints
nuevos, deprecaciones, cambios de comportamiento y de formato de respuesta.

Los cambios internos que no alteran el contrato no se listan.

> **Note**
>
> Las deprecaciones se anuncian acá antes de retirarse, y los endpoints
> deprecados quedan marcados en la [API Reference](/api-reference) con su
> reemplazo.

Recibí los cambios por correo

Te escribimos cuando cambia el contrato: endpoints nuevos, deprecaciones y
cambios de comportamiento. Nada más que eso.

<form data-subscribe-form>
  <input type="email" name="email" placeholder="tu@empresa.com" autocomplete="email" required />

  <input type="text" name="website" autocomplete="off" data-honeypot />

  Suscribirme

   
</form>

## October 13, 2026

## Nuevo endpoint para crear turnos

`POST /org/{slug}/time/attendance/shifts/` crea un turno de Asistencia, fijo o
rotativo, con sus descansos y tolerancias, igual que un administrador desde
Crehana. Está pensado para sistemas de RR. HH. o de planificación que son
dueños de los horarios.

```json
{
  "shift_type": "NON_ROTATING",
  "name": "Mañana 09-18",
  "weekly_schedule": [
    { "day": "MONDAY", "entry_time": "09:00", "exit_time": "18:00" },
    { "day": "TUESDAY", "entry_time": "09:00", "exit_time": "18:00" },
    { "day": "WEDNESDAY", "entry_time": "09:00", "exit_time": "18:00" },
    { "day": "THURSDAY", "entry_time": "09:00", "exit_time": "18:00" },
    { "day": "FRIDAY", "entry_time": "09:00", "exit_time": "18:00" }
  ]
}
```

La respuesta trae el `id` del turno y, en un rotativo, el `id` de cada ciclo:
son el `shift_id` y el `start_cycle_id` que pide
[Asignar turnos](/documentacion/casos-de-uso/asignar-turnos). Los días que no
se mandan son de descanso, las horas van en `HH:MM` y una salida menor que la
entrada es un turno nocturno.

El nombre es único entre los turnos activos de la organización: reenviar la
misma creación responde `409` con el id del turno que ya existe, así que un
reintento nunca duplica. Requiere credenciales de un administrador de la
organización.

Guía completa en [Crear turnos](/documentacion/casos-de-uso/crear-turnos).

## Nuevo reporte: resultados de evaluaciones por clase

`GET /api/v5/rest/org/{organization_slug}/reports/learning/quiz-results/`
devuelve los resultados de los **quizzes de clase** (las evaluaciones de cada
video, no la evaluación final del curso, que ya está en
`reports/learning/quizzes`) con una fila por usuario, curso y quiz. Es la misma
información del Excel "Resultados de Evaluaciones por Clase" del panel de
administración, lista para integrar.

Cada fila consolida los intentos: `attempts_count`, las fechas del primero,
el último y el mejor intento (`first_attempt_at`, `last_attempt_at`,
`best_attempt_at`, en UTC), el puntaje del último intento (`score`) y el más
alto (`best_score`), el mínimo para aprobar (`passing_percentage`), respuestas
correctas e incorrectas, total de preguntas, duración y límite de tiempo en
segundos (`time_limit_seconds`, `null` cuando no hay límite) y el estado del
último intento (`status`: `approved`, `failed`, `incomplete` o `not_started`).
También viene el curso (`course_id`, `course_title`), el quiz (`quiz_id`,
`quiz_name`, `quiz_type` `summative` o `formative`) y la persona (`user_id`,
`user_full_name`, `user_email`, `user_document`).

Filtros: `course_id` (repetible, hasta 100), `user_email`, `document`, `status`
y el rango `from_date` / `to_date` sobre `last_attempt_at`. Las filas llegan
ordenadas por `last_attempt_at`, de la más reciente a la más antigua, y se
recorren con `cursor` / `next_cursor` (`limit` hasta 500). Este reporte no
acepta `offset`.

```json
{
  "total": 45,
  "next_cursor": "cXVpel9yZXN1bHRzOlsiMjAyNi0wMy0xMFQxOTozNDoxNSIsMjQ4Nzg4XQ==",
  "has_next_page": true,
  "results": [
    {
      "course_id": 47847,
      "course_title": "Curso con quiz",
      "user_id": 6816375,
      "user_full_name": "Ana Pérez",
      "user_email": "ana@empresa.com",
      "user_document": "0000000002",
      "quiz_id": 20461,
      "quiz_name": "Evaluación como clase 1",
      "quiz_type": "summative",
      "evaluation_type": "class",
      "attempts_count": 2,
      "first_attempt_at": "2026-03-10T19:30:00+00:00",
      "last_attempt_at": "2026-03-10T19:34:15+00:00",
      "best_attempt_at": "2026-03-10T19:34:15+00:00",
      "correct_answers": 8,
      "incorrect_answers": 1,
      "total_questions": 9,
      "passing_percentage": 80,
      "score": 88.89,
      "best_score": 88.89,
      "status": "approved",
      "duration_seconds": 60,
      "time_limit_seconds": null
    }
  ]
}
```

## October 7, 2026

## Nuevo endpoint para enviar marcajes de asistencia

`POST /org/{slug}/time/attendance/marks/` registra una marca de asistencia de
una persona (entrada, inicio o fin de descanso, salida) con la hora real en que
ocurrió. Está pensado para relojes biométricos y sistemas de control de acceso.

```json
{
  "user_id": 97099,
  "mark_type": "ENTRY",
  "marked_at": "2026-10-07T09:00:00-06:00",
  "timezone": "America/Mexico_City"
}
```

La marca pasa por las mismas reglas que una hecha desde Crehana: se asigna al
turno del día, se clasifica por puntualidad y entra en la gestión diaria.
Reenviar la misma marca no la duplica: responde `200` con
`already_registered: true`.

`marked_at` lleva desfase o `Z`, y `timezone` es la zona IANA del lugar donde
se marcó. Requiere credenciales de un administrador de la organización.

Guía completa en
[Enviar marcajes de asistencia](/documentacion/casos-de-uso/enviar-marcajes),
y qué zona mandar en
[Zonas horarias](/documentacion/conceptos/zonas-horarias).

## Nuevo endpoint para asignar turnos

`POST /org/{slug}/time/attendance/shift-assignments/` asigna un turno de
Asistencia a una o más personas por un período. Está pensado para sistemas de
RR. HH. o de planificación que son dueños del horario y lo empujan a Crehana.

```json
{
  "shift_id": "4ed0f6c7-a2a7-4de7-90cd-ae8cf26aabb1",
  "user_ids": [97097, 97099],
  "start_date": "2026-10-13",
  "end_date": "2026-12-31"
}
```

La asignación es igual a una hecha desde Crehana: a partir de esas fechas las
marcas se comparan contra ese turno y la gestión diaria se recalcula. Si la
persona ya tenía un turno en esas fechas, el nuevo lo reemplaza. La llamada es
todo o nada: si alguna persona no pertenece a la organización, no se asigna a
nadie. Reenviar la misma asignación no la duplica.

Los turnos rotativos llevan además `start_cycle_id`. Requiere credenciales de
un administrador de la organización.

Guía completa en
[Asignar turnos](/documentacion/casos-de-uso/asignar-turnos).

## September 30, 2026

## Filtro por policy_id en timeoff/policies

`GET /org/{slug}/timeoff/policies/` acepta el parámetro opcional `policy_id`
para traer una sola política a partir de su id:

```
GET /org/{slug}/timeoff/policies/?policy_id=1105
```

La respuesta mantiene el mismo formato paginado, con esa política como único
resultado (`{"total": 1, "results": [...]}`). Si el id no corresponde a una
política de la organización, la respuesta es una página vacía
(`{"total": 0, "results": []}`), no un error.

Se puede combinar con el resto de los filtros.

## El reporte de turnos exige filtrar por estado

`GET /org/{slug}/reports/attendance/shifts/` pide el parámetro obligatorio
`is_active` para elegir qué turnos devolver:

```
GET /org/{slug}/reports/attendance/shifts/?is_active=true
GET /org/{slug}/reports/attendance/shifts/?is_active=false
```

`true` trae solo los turnos activos y `false` solo los inactivos. El filtro se
aplica antes de paginar, así que `total`, `limit` y `offset` cuentan únicamente
los turnos de ese estado. Un turno sin estado cargado no aparece en ninguno de
los dos.

> **Warning**
>
> Sin `is_active` el endpoint responde `400` con
> `This field is required for this report` (`loc: ["query", "is_active"]`). Un
> valor que no sea booleano responde `422`. Si tu integración consultaba el
> catálogo completo, hacé dos pedidos, uno con `true` y otro con `false`.

## September 29, 2026

## El reporte de competencias deja de incluir configured_semaphore_color

`GET /org/{slug}/reports/talent/competences-results/` deja de incluir
`configured_semaphore_color` en cada fila. El nivel alcanzado sigue llegando en
`configured_label` (`Cumple`, `Sobrepasa`, …), junto con
`configured_scale_result` y `configured_percentage_result`.

> **Warning**
>
> Cambia el formato de respuesta: `configured_semaphore_color` ya no viene. Si tu
> integración lo usaba, tomá el nivel de `configured_label` y resolvé el color de
> tu lado a partir de ese nivel. El resto de las columnas, los filtros y la
> paginación no cambian.

## Filtro por correo en el listado de solicitudes de tiempo libre

`GET /org/{slug}/timeoff/requests/` acepta el parámetro opcional `email` para
traer solo las solicitudes de un colaborador a partir de su correo, sin tener
que conocer antes su `centralized_user_id`:

```
GET /org/{slug}/timeoff/requests/?email=lsanchez@empresa.com
```

La coincidencia es exacta y no distingue mayúsculas. Si ningún colaborador de la
organización tiene ese correo, la respuesta es una página vacía
(`{"total": 0, "results": []}`), no un error.

Se puede combinar con el resto de los filtros. Junto con `centralized_user_ids`,
se devuelven las solicitudes del correo solo si su usuario está entre esos ids.

## Las fechas de los reportes marks y daily-management figuran como obligatorias

En `GET /org/{slug}/reports/attendance/marks/` y
`GET /org/{slug}/reports/attendance/daily-management/`, `from_date` y `to_date`
ahora aparecen como obligatorios en la [API Reference](/api-reference) y en el
`openapi.json`. Ya lo eran: sin alguna de las dos el endpoint responde `400`
con `This field is required for this report`, pero la referencia los listaba
como opcionales.

El comportamiento no cambia. Si generás un cliente a partir del `openapi.json`,
al regenerarlo esos dos parámetros pasan a ser obligatorios.

`GET /org/{slug}/reports/attendance/shifts/` y
`GET /org/{slug}/reports/attendance/shifts-schedule/` siguen aceptando el rango
como opcional.

## September 24, 2026

## El listado de solicitudes de tiempo libre devuelve menos campos

`GET /org/{slug}/timeoff/requests/` deja de repetir en cada solicitud la
configuración completa de la política y los datos internos del usuario. Cada
elemento trae ahora la solicitud (`id`, `start_date`, `end_date`, `value`,
`status`, `comment`), quién la pidió en `user` y a qué política corresponde en
`policy`:

```json
{
  "total": 1,
  "results": [
    {
      "id": "701",
      "start_date": "2026-10-02",
      "end_date": "2026-10-06",
      "value": 3.0,
      "status": "APPROVED",
      "comment": "viaje familiar",
      "user": {
        "user_id": "20418",
        "firstname": "Laura",
        "lastname": "Sanchez",
        "email": "lsanchez@empresa.com",
        "username": "lsanchez",
        "position": { "position_id": 4974, "position_name": "Psicologo Social" }
      },
      "policy": {
        "policy_id": "1107",
        "name": "Vacaciones México",
        "is_active": true
      }
    }
  ]
}
```

> **Warning**
>
> Cambia el formato de respuesta. El id del usuario pasa a `user.user_id` y el de
> la política a `policy.policy_id`. Ya no vienen `user_id` ni `policy_id` en la
> raíz, ni `created_at`, `updated_at`, `is_active` e `is_current_user_approver` de
> la solicitud, ni `id`, `original_id`, `full_name` y fechas del `user`, ni el
> resto de los campos de la `policy`.

La configuración de cada política (categoría, si es pagada, unidad, si requiere
aprobación) se consulta en `GET /org/{slug}/timeoff/policies/`, cruzando por
`policy_id`. El detalle `GET /org/{slug}/timeoff/requests/{request_id}/` no
cambia y sigue devolviendo la solicitud completa con los datos del empleado.

Los filtros, el orden y la paginación del listado no cambian.

## September 22, 2026

## Nuevo reporte: procesos de competencias de Talent

`GET /api/v5/rest/org/{organization_slug}/reports/talent/competences-processes/`
lista los procesos de evaluación de competencias de la organización, con una fila
por proceso: `evaluation_id`, `process_id`, `process_name`, `status`,
`created_date`, `start_date`, `end_date`, `updated_date`, `max_scale` y
`competence_model_id`.

Es la puerta de entrada al reporte de resultados: el `evaluation_id` de cada
proceso es el que acepta `evaluation_id` en
`/reports/talent/competences-results/`, así que primero listás los procesos y
después pedís las notas finales del que te interesa.

> **Note**
>
> `evaluation_id` sale del identificador del proceso en Talent (`4M<id>P`). Si un
> proceso no sigue ese formato, llega con `evaluation_id` en `null` y no se puede
> pedir filtrando por él: traé las notas sin `evaluation_id` y cruzalas por
> `process_id`, que viene en las dos respuestas.

```json
{
  "total": 4,
  "next_cursor": null,
  "has_next_page": false,
  "results": [
    {
      "evaluation_id": 123,
      "process_id": "4M123P",
      "process_name": "Evaluación de competencias 2026",
      "status": "FINISHED",
      "start_date": "2026-02-01",
      "end_date": "2026-06-30",
      "max_scale": 5,
      "competence_model_id": 12
    }
  ]
}
```

Filtro opcional: `evaluation_id`. La paginación es por cursor, igual que en el
reporte de resultados: `limit` (de 1 a 500, por defecto 100) y, para la página
siguiente, reenviás `next_cursor` en `cursor` tal cual. Las filas vienen
ordenadas por proceso.

Sólo se listan procesos de la organización de la api-key: pedir el `slug` de
otra organización responde con error, no con datos. Un `limit` fuera de rango o
un `evaluation_id` que no sea un entero positivo responden `422`.

## September 18, 2026

## Consultar el detalle de una solicitud de tiempo libre

`GET /org/{slug}/timeoff/requests/{request_id}/` devuelve una solicitud de
ausencia puntual: quién la pidió, bajo qué política y en qué fechas. Es la pieza
que faltaba para los reportes que cruzan usuario, política y solicitud — antes
había que traerse el listado completo y buscar ahí adentro.

La solicitud es el mismo objeto que devuelve cada elemento de
`GET /org/{slug}/timeoff/requests/`, así que podés listar y después pedir el
detalle sin cambiar de forma: `id`, `start_date`, `end_date`, `value`, `status`,
`comment`, `created_at`, la `policy` completa (con su `policy_type` y su
`quantification`) y el `user` que la generó.

Sobre eso, el detalle agrega los datos del empleado que el listado no trae y que
un reporte necesita para leer la ausencia en contexto: su `area`, su `location`,
el `organization_status` de su membresía en la organización y su jefe directo en
`boss`, con nombre y correo.

```json
{
  "id": "701",
  "user_id": "20418",
  "policy_id": "1107",
  "created_at": "2026-09-11T10:20:31.113292+00:00",
  "start_date": "2026-09-14",
  "end_date": "2026-09-15",
  "value": 1,
  "status": "APPROVED",
  "policy": {
    "id": "1107",
    "name": "Vacaciones Mexico",
    "quantification": "DAYS",
    "policy_type": { "id": 13, "name": "Vacaciones" }
  },
  "user": {
    "id": "Q2VudHJhbGl6ZWRVc2VyTm9kZToyMDQxOA==",
    "original_id": 20418,
    "firstname": "Laura",
    "lastname": "Sanchez",
    "email": "lsanchez@empresa.com",
    "full_name": "Laura Sanchez",
    "organization_status": "ACTIVE",
    "position": { "position_id": 4974, "position_name": "Psicologo Social" },
    "area": { "id": 331, "name": "Gente", "label": "Area" },
    "location": { "id": 77, "name": "Bogota" },
    "boss": {
      "original_id": 4001,
      "email": "smejia@empresa.com",
      "full_name": "Samuel Mejia"
    }
  }
}
```

`area`, `location`, `organization_status` y `boss` son opcionales: un empleado
sin área asignada, sin sede o sin jefe directo devuelve esos campos en `null`, y
la solicitud igual se responde completa.

Un `request_id` que no existe responde `404`. Sólo se responden solicitudes de
la organización de la api-key: pedir el `slug` de otra organización responde con
error, no con datos.

## September 16, 2026

## Sesiones en vivo: listar, crear, invitar y marcar asistencia

Nueva sección **Live Sessions** con los cuatro endpoints del ciclo completo de
una sesión en vivo, para que una integración haga por API lo que hoy solo se
puede hacer desde el panel de administración.

```
GET  /org/{slug}/learning/live-sessions/
POST /org/{slug}/learning/live-sessions/
POST /org/{slug}/learning/live-sessions/{live_session_id}/participants/
POST /org/{slug}/learning/live-sessions/{live_session_id}/attendance/
```

### Listar las sesiones

`GET .../live-sessions/` devuelve las sesiones de la organización, de la más
reciente a la más antigua. `status` elige el tramo: `NEXT` (por defecto) las que
todavía no terminaron y `PAST` las que ya terminaron — para recorrer todo el
calendario hay que pedir los dos. La paginación es por página: `first` (de 1 a
100, por defecto 10) y `page` (desde 1). `ids` acota el listado a las sesiones
que le pases.

Los contadores miran todo el calendario, no la página: `total` son todas las
sesiones de la organización, y `next_sessions_count` y `past_sessions_count`
dicen cuántas hay en cada tramo. `results` trae solo la página pedida del
`status` pedido.

```json
{
  "total": 3,
  "next_sessions_count": 2,
  "past_sessions_count": 1,
  "results": [
    {
      "id": "1258",
      "session_title": "Onboarding comercial",
      "instructor_name": "Katherine N.",
      "type_session": "ONLINE",
      "location": "https://meet.google.com/abc-defg-hij",
      "time_zone": "TIME_ZONE_ES_PE",
      "start_date": "2026-10-01T10:00:00",
      "end_date": "2026-10-01T11:00:00",
      "start_date_utc": "2026-10-01T15:00:00+00:00",
      "end_date_utc": "2026-10-01T16:00:00+00:00",
      "timeline": "FUTURE",
      "total_guests": 2,
      "has_qr_available": true,
      "issues_certificate": false
    }
  ]
}
```

Cada sesión trae las fechas dos veces: `start_date` y `end_date` son la hora de
reloj en la zona horaria de la sesión (`time_zone`) y van sin marca de zona, y
`start_date_utc` / `end_date_utc` son el mismo instante en UTC. Usá el par UTC
para ordenar o comparar sesiones de zonas distintas.

En la respuesta del `POST` de creación el par UTC viene en `null`: el servicio
devuelve ahí la misma hora de reloj que le mandaste, sin convertir. Leé la
sesión con el `GET` para obtener los instantes en UTC.

### Crear una sesión

`POST .../live-sessions/` crea la sesión y responde `201` con la sesión creada.
Obligatorios: `session_title`, `description`, `start_date`, `end_date`,
`time_zone` y `type_session` (`ONLINE` o `PRESENCE`). Opcionales:
`instructor_name`, `location` (el link de la reunión si es `ONLINE`, la
dirección si es `PRESENCE`), `cover_file_id`, `resource_file_ids`,
`participant_user_ids`, `has_qr_available` e `issues_certificate`.

Las fechas son hora de reloj en `time_zone` y van **sin** offset
(`2026-10-01T10:00:00`). Mandar un offset responde `422`: el offset se
descartaría y la sesión quedaría a otra hora que la pedida. `end_date` tiene que
ser posterior a `start_date`.

Si la organización ya tiene una sesión con ese título, el título se guarda con
un sufijo numérico — por eso conviene leer el `session_title` de la respuesta en
vez de asumir el que mandaste.

### Invitar participantes

`POST .../live-sessions/{id}/participants/` invita usuarios a la sesión. Los
`user_ids` son los ids de usuario de learning, los que devuelve
`GET /org/{slug}/learning/users/` en `user.id`. Con `send_notification: true` se
manda el correo de invitación, si la organización tiene esa notificación
activa.

La asignación se procesa en segundo plano: la respuesta es `202` y confirma qué
se aceptó, no qué ya está aplicado. Los usuarios que no pertenecen a la
organización vuelven en `invalid_user_ids` y no se asignan; si ninguno de los
pedidos pertenece, la respuesta es `422`.

```json
{
  "message": "Assignment of 2 users in live session 1258 queued",
  "code": "LIVE_SESSION_PARTICIPANTS_QUEUED",
  "datetime": "2026-09-16T10:30:00.000Z",
  "data": {
    "live_session_id": 1258,
    "engagement_id": "907",
    "state": "SUBMITTED",
    "total_users_requested": 3,
    "total_users_accepted": 2,
    "total_users_skipped": 1,
    "invalid_user_ids": [605999],
    "errors": []
  }
}
```

### Marcar asistencia

`POST .../live-sessions/{id}/attendance/` registra la asistencia. Mandá
`user_ids` con los usuarios a marcar, o `all_participants: true` para marcar a
todos los participantes de la sesión — uno u otro, no los dos: mandar ambos
responde `422`, igual que no mandar ninguno.

También se procesa en segundo plano (`202`). `errors` explica cada usuario que
quedó afuera: ya estaba registrado, no se encontró en la organización, o está
inactivo. Si no se pudo registrar a nadie, la respuesta es `422` con esos
mismos motivos.

### Una sola organización

Los cuatro endpoints trabajan sobre la organización de la api-key: pedir el
`slug` de otra organización responde `403`, no datos. Un `401` significa que
las credenciales no llegaron o no son válidas, y un `400` que el pedido tiene
un problema de validación — así el motivo del rechazo se distingue sin leer el
detalle.

## Listar las politicas de tiempo libre de la organizacion

`GET /org/{slug}/timeoff/policies/` devuelve el catálogo de políticas de
ausencia configuradas por la organización: vacaciones, licencias y permisos.
Es la configuración de cada política, no las solicitudes ni los datos de los
colaboradores — para eso sigue estando
`GET /org/{slug}/timeoff/requests/`.

Cada política trae `id`, `name`, `description`, `policy_type` (la categoría,
con su `id` y su `name`), `is_paid`, `is_unlimited`, `quantification` (`DAYS`
u `HOURS`), `needs_approvals` y `status` (`enabled` o `disabled`).

```json
{
  "total": 335,
  "results": [
    {
      "id": "1107",
      "name": "Vacaciones México",
      "description": "15 días por año",
      "policy_type": { "id": 15, "name": "Vacaciones" },
      "is_paid": true,
      "is_unlimited": true,
      "quantification": "DAYS",
      "needs_approvals": false,
      "status": "enabled"
    }
  ]
}
```

El `id` de cada política es el mismo que devuelve `policy_id` en el listado de
solicitudes, así que podés cruzar ambos recursos sin repetir la configuración
en cada fila.

La paginación es la misma del resto de la sección: `limit` (de 1 a 50) y
`offset`. Filtros opcionales: `search` (por nombre, sin distinguir
mayúsculas), `name` (nombre exacto), `policy_type_ids` (repetible) e `is_paid`.
Un `order_by` desconocido responde `422`.

Sólo se listan políticas de la organización de la api-key: pedir el `slug` de
otra organización responde `403`, no datos.

## September 10, 2026

## Nuevo reporte: resultados de competencias de Talent

`GET /api/v5/rest/org/{organization_slug}/reports/talent/competences-results/`
devuelve el resultado de la evaluación de competencias de cada colaborador, con
una fila por colaborador y proceso de competencias. Es el mismo dato que el
admin ve en Talent una vez aplicada la escala de resultados del proceso, sin
tener que descargar el Excel de "Respuestas cuestionarios finalizados".

Cada fila trae el proceso (`evaluation_id`, `process_id`, `process_name`,
`process_status`, `process_end_date`, `max_scale`), el colaborador
(`employee_id`, `centralized_user_id`, nombre, correo, documento, estado,
departamento, área, cargo, sede, nivel y correo del jefe) y el resultado:
`configured_scale_result` (en la escala del proceso, ver `max_scale`),
`configured_percentage_result` (normalmente 0–100), `configured_label` y
`configured_semaphore_color` (el nivel del semáforo tal como lo resolvió Talent)
y `updated_at`.

```json
{
  "total": 1325,
  "next_cursor": "Y29tcGV0ZW5jZXM6WyI0TTEyM1AiLDQyLDFd",
  "has_next_page": true,
  "results": [
    {
      "evaluation_id": 123,
      "process_id": "4M123P",
      "process_name": "Evaluación de competencias 2026",
      "process_status": "FINISHED",
      "max_scale": 5,
      "centralized_user_id": 9001,
      "user_email": "ana@empresa.com",
      "user_status": "active",
      "configured_scale_result": 4.2,
      "configured_percentage_result": 84.0,
      "configured_label": "Cumple",
      "configured_semaphore_color": "80E0B4"
    }
  ]
}
```

Filtros opcionales: `evaluation_id` (el número de la evaluación que ve el admin
en Talent) y `centralized_user_id`. Sin filtros recibís todos los procesos de
competencias de la organización. La paginación es por cursor, como en los
reportes de marcas: pedís `limit` (de 1 a 500, por defecto 100) y, para la
página siguiente, reenviás `next_cursor` en `cursor` tal cual, sin
interpretarlo; `has_next_page` te dice si queda algo por recorrer. Las filas
vienen ordenadas por proceso y colaborador, así que el recorrido es estable y
cada página cuesta lo mismo sin importar cuán adentro estés.

> **Note**
>
> `configured_percentage_result` viaja tal como lo calcula Talent. En casos
> puntuales, por configuraciones de escala atípicas, puede superar 100; si vas a
> promediarlo, acotalo a \[0, 100] antes. Un colaborador que todavía no tiene
> resultado llega con esos campos en `null`, no en cero.

Sólo se listan resultados de la organización de la api-key: pedir el `slug` de
otra organización responde con error, no con datos. Un `limit` fuera de rango o
un `evaluation_id` que no sea un entero positivo responden `422`.

## Asistencia: centralized_user_id tiene que ser un entero positivo

En los reportes de asistencia que filtran por colaborador
(`/reports/attendance/shifts-schedule/`, `/reports/attendance/marks/` y
`/reports/attendance/daily-management/`), `centralized_user_id=0` ahora responde
`422`. Antes se ignoraba en silencio y devolvía la organización completa con
`200`, como si no hubieras filtrado. Si tu integración mandaba `0` para decir
"sin filtro", omití el parámetro.

## September 4, 2026

## Listar las rutas de aprendizaje de la organizacion

`GET /org/{slug}/learning/content/knowledge-hub/catalog/v2/tracks/` devuelve
las rutas (tracks) de la organización, paginadas por cursor. Es la pieza que
faltaba para armar el flujo completo por API: listar las rutas, elegir una y
asignarla con `POST .../enrollments/v2/assign-track/` — el `id` de cada ruta
es el `track_id` que ese endpoint espera.

Cada ruta trae `id`, `name`, `track_type` (`TRACK` o `ACADEMY`), `is_active`,
`is_hidden`, `created_at`, `updated_at`, `start_date`, `end_date`, `version`,
`has_reward`, `courses_to_complete`, `courses_to_certificate` y
`total_courses_count`. `updated_at` sirve para detectar qué rutas cambiaron
desde tu última sincronización.

```json
{
  "total_count": 7,
  "page_info": {
    "has_next_page": true,
    "has_previous_page": false,
    "start_cursor": "YXJyYXljb25uZWN0aW9uOjA=",
    "end_cursor": "YXJyYXljb25uZWN0aW9uOjQ="
  },
  "edges": [
    {
      "node": {
        "id": "40889",
        "name": "Ruta X",
        "track_type": "TRACK",
        "is_active": true,
        "is_hidden": false,
        "created_at": "2026-01-10T12:00:00",
        "updated_at": "2026-02-01T09:30:00",
        "total_courses_count": 3
      }
    }
  ]
}
```

La paginación es por cursor: pedís `first` (de 1 a 100, por defecto 10) y,
para la página siguiente, reenviás `page_info.end_cursor` en `after` tal cual,
sin interpretarlo. Las rutas ocultas van al final y el resto de más reciente a
más antigua.

Filtros opcionales: `search` (por nombre, sin distinguir mayúsculas),
`track_type` (`TRACK` o `ACADEMY`), `is_draft` e `is_hidden`. Sin `is_draft`
recibís sólo las rutas activas y publicadas; con `is_draft=true`, los
borradores. Un `first` fuera de rango o un `track_type` desconocido responden
`422`.

Sólo se listan rutas de la organización de la api-key: pedir el `slug` de otra
organización responde con error, no con datos.

## September 2, 2026

## El reporte de performance acepta hasta 500 filas por llamada

`GET /api/v5/rest/org/{organization_slug}/reports/learning/performance/` ahora
admite `limit` hasta **500**. Antes cortaba en 100 y pedir más devolvía un
`422`.

Es el mismo techo que ya tenían el reporte general v2 y los cuatro de
Asistencia, así que una organización de 500 personas se baja en una sola llamada
en vez de cinco.

> **Note**
>
> **El valor por defecto no cambia:** si no mandás `limit`, seguís recibiendo 100
> filas. Nada de lo que hoy funciona cambia de comportamiento; sólo se corrió el
> techo de lo que podés pedir.

El `offset` funciona igual: para recorrer el listado completo sumá el `limit`
que hayas pedido. Tené en cuenta que una página de 500 filas tarda bastante más
en responder que una de 100.

## August 26, 2026

## Las vistas viejas de la documentacion redirigen a este portal

`GET /api/v5/rest/redocs` y `GET /api/v5/rest/docs` ya no dibujan la referencia
por su cuenta: ahora redirigen —con un `302`— al [API Reference](/api-reference) de este portal.

Las dos URLs siguen funcionando, así que un enlace guardado no se rompe: te deja
en el mismo contenido, con las guías al lado y con el explorador que ya trae el
panel de autenticación.

> **Note**
>
> **`GET /api/v5/rest/openapi.json` no cambia.** Sigue devolviendo el spec en
> crudo, con el mismo `200` y el mismo JSON. Es el que consumen los generadores de
> SDK, los importadores de Postman y cualquier script propio, y por eso se dejó
> igual: redirigirlo a una página HTML los rompería sin avisar.

Si tu integración le pegaba a `/docs` o `/redocs` de forma automatizada
—esperando HTML— vas a recibir un `302` con la nueva ubicación en el header
`Location`. La mayoría de los clientes HTTP la siguen solos; si el tuyo no,
apuntá a `openapi.json`, que es lo que corresponde para leer el contrato desde
código.

## August 20, 2026

## `course_enroll_date` deja de estar documentado en el reporte general v2

`GET /org/{slug}/reports/learning/general-v2/` ya no lista `course_enroll_date`
entre sus parámetros. El filtro por fecha de inscripción se pide con
`from_date` y `to_date`, igual que en los otros tres reportes de learning.

**El reemplazo es `from_date` solo, sin `to_date`.** Es la parte que se presta a
confusión: `course_enroll_date` no tenía techo —devolvía todo desde esa fecha en
adelante— así que poner la misma fecha en los dos extremos del rango no es
equivalente, deja un solo día.

| Antes                            | Ahora                                      |
| -------------------------------- | ------------------------------------------ |
| `?course_enroll_date=2026-01-01` | `?from_date=2026-01-01`                    |
| no se podía cerrar el rango      | `?from_date=2026-01-01&to_date=2026-03-31` |

Los resultados pueden diferir en el borde del rango: `from_date` y `to_date` se
interpretan en la zona horaria de la organización —la misma en la que el reporte
devuelve la fecha de inscripción— mientras que `course_enroll_date` comparaba en
UTC. Si una inscripción cae en las horas de diferencia entre las dos zonas,
antes podía quedar fuera del filtro y ahora entra, o al revés.

> **Note**
>
> El parámetro se sigue aceptando por compatibilidad, así que una integración que
> todavía lo mande no se rompe y no hace falta migrar de urgencia. Dejó de estar
> documentado porque va a desaparecer: al pasar a `from_date` te quedás con el
> mismo filtro, además del extremo superior del rango.

El campo `course_enroll_date` de la respuesta no cambia: sigue siendo la fecha
de inscripción de cada matrícula. Lo que se retira es el parámetro de consulta
homónimo.

## Los endpoints deprecados salen del portal

Ocho endpoints que ya estaban marcados como deprecados dejan de aparecer en esta
documentación. **Siguen respondiendo con normalidad**: si tu integración los usa,
no se rompe nada y no hay que hacer nada con urgencia.

Lo que cambia es que dejamos de ofrecerlos a quien recién empieza, para que nadie
construya sobre un camino que ya está marcado para salir.

Cada uno tiene su reemplazo:

| En lugar de                                               | Usá                                                  |
| --------------------------------------------------------- | ---------------------------------------------------- |
| `GET /org/{slug}/areas`                                   | `GET /org/{slug}/organizations/areas/`               |
| `GET /org/{slug}/headquarters`                            | `GET /org/{slug}/organizations/headquarters/`        |
| `GET /org/{slug}/positions`                               | `GET /org/{slug}/organizations/positions/`           |
| `GET /org/{slug}/position_categories`                     | `GET /org/{slug}/organizations/position-categories/` |
| `POST /org/{slug}/organizations/custom-fields/`           | `POST /org/{slug}/custom-fields/`                    |
| `PUT /org/{slug}/organizations/users/{id}/custom-fields/` | `POST /org/{slug}/users/{id}/custom-fields/`         |
| `PUT /org/{slug}/users/{id}/`                             | `POST /org/{slug}/users/update_user/`                |
| `GET /org/{slug}/permission_groups`                       | —                                                    |

El listado de grupos de permisos no tiene sucesor: se retira sin reemplazo.

Los reemplazos no son sólo un cambio de ruta: devuelven la misma envoltura que el
resto de la API y aceptan paginación donde corresponde, así que conviene migrar
aunque el viejo siga funcionando.

`GET /org/{slug}/users/` se queda documentado aunque esté deprecado, porque es el
listado que más se usa y su reemplazo todavía no cubre todos los casos.

## August 19, 2026

## Eliminar una solicitud de tiempo libre

`DELETE /org/{slug}/timeoff/requests/{request_id}/` elimina una solicitud de
ausencia de la organización. La solicitud deja de aparecer en
`GET /org/{slug}/timeoff/requests/` y el tiempo que consumía vuelve al saldo
del colaborador.

Con una credencial de administrador de la organización —así se crean las
api-key— podés eliminar cualquier solicitud y en cualquier estado. Si la
credencial corresponde a un colaborador, sólo puede eliminar las solicitudes
propias y sólo mientras sigan pendientes; en el resto de los casos la respuesta
es `403 Forbidden`.

Una baja correcta responde:

```json
{
  "success": true,
  "message": "Time off request deleted correctly.",
  "code": "timeoff-001",
  "datetime": "2026-08-19T14:48:58.833958",
  "data": { "request_id": "531" }
}
```

La respuesta distingue el motivo del rechazo:

| Situación                                         | Status          |
| ------------------------------------------------- | --------------- |
| Solicitud eliminada                               | `200 OK`        |
| La solicitud no existe                            | `404 Not Found` |
| Sin permiso sobre esa solicitud                   | `403 Forbidden` |
| Ya estaba eliminada, o ya fue aprobada o denegada | `409 Conflict`  |

Reintentar es seguro: un segundo `DELETE` sobre la misma solicitud responde
`409` y no vuelve a tocar el saldo.

## Los reportes de asistencia se pueden consultar por API

Los cuatro seguimientos del producto **Asistencia** —los mismos que hasta ahora
solo se podían bajar en Excel desde el panel— ya están disponibles como
endpoints paginados:

| Endpoint                                               | Qué devuelve             | Una fila es                                      |
| ------------------------------------------------------ | ------------------------ | ------------------------------------------------ |
| `GET /org/{slug}/reports/attendance/shifts/`           | Creación de turnos       | un turno configurado                             |
| `GET /org/{slug}/reports/attendance/shifts-schedule/`  | Programación de turnos   | una asignación vigente de turno a un colaborador |
| `GET /org/{slug}/reports/attendance/marks/`            | Recepción de marcas      | una marca individual                             |
| `GET /org/{slug}/reports/attendance/daily-management/` | Gestión diaria de marcas | un colaborador en un día                         |

Los cuatro responden `{ "total": N, "results": [...] }` y aceptan `limit` (hasta
500\), pero paginan de dos maneras distintas según el volumen del reporte:

* **Turnos y programación** usan `offset`, como el resto de los reportes.
* **Marcas y gestión diaria** usan `cursor`. La respuesta trae `next_cursor` y
  `has_next_page`; para la página siguiente reenviás ese `next_cursor` tal cual, sin
  interpretarlo. Estos dos **no aceptan `offset`**: si lo mandás la respuesta es
  `400`, para que no recibas siempre la primera página creyendo que avanzás. La
  paginación es solo hacia adelante — no hay cursor previo. El detalle está en
  [Paginación](/pages/paginacion).

**El rango de fechas es obligatorio en marcas y en gestión diaria**, con una
ventana máxima de 31 días: son los dos reportes de mayor volumen y sin acotar
el período una sola consulta puede recorrer millones de registros. Si faltan
`from_date` o `to_date`, o el rango es más ancho que 31 días, la respuesta es
`400 Bad Request` indicando el campo. En turnos y programación el rango es
opcional.

En `shifts-schedule`, `marks` y `daily-management` podés filtrar por
colaborador con `centralized_user_id`.

> **Warning**
>
> `centralized_user_id` es el identificador **centralizado** del colaborador, que
> no es el mismo que el `user_id` de los reportes de learning: para la misma
> persona son números distintos. Si mandás el equivocado vas a recibir `200` con
> `results` vacío, no un error.

Tres diferencias a propósito respecto del archivo de Excel:

* **Los campos de tipo catálogo no vienen traducidos.** `mark_type`, `source`,
  `classification`, `day_status` y `shift_type` devuelven el valor tal como lo
  emite el motor de asistencia (`entry`, `APP`, `late`, `rotating`), no la
  etiqueta en español que ves en el Excel. Es lo que te permite mapearlos de
  forma estable: la etiqueta puede cambiar, el valor no. `days` también va en
  inglés y ordenado de lunes a domingo (`monday, friday`); en turnos rotativos
  es el ciclo `5 work / 2 rest`.
* El estado de un turno y el indicador de feriado son booleanos (`true` /
  `false`), no los textos "Activo"/"Inactivo" ni "Sí"/"No".
* Las duraciones de la jornada (`worked_minutes`, `tardiness_minutes`,
  `overtime_minutes`) vienen en minutos como número entero, listas para sumar,
  en vez del formato `hh:mm`.

> **Warning**
>
> `day_status` es el único que no es homogéneo en el origen: cuatro de sus valores
> llegan en español (`feriado`, `dia_descanso`, `feriado_trabajado`,
> `dia_descanso_trabajado`) y el resto en inglés (`ok`, `justified`,
> `unjustified_absence`, `incomplete`, `with_novelty`, `no_shift`). Se exponen tal
> como llegan, sin traducir ni retraducir: tratalos como identificadores opacos.

Los datos se consolidan con un corte periódico, así que reflejan la
información procesada hasta ese corte y no el marcaje del minuto anterior.

## July 30, 2026

## Los rechazos de autorización devuelven 401 y 403

En la sección de **usuarios**, cuando una operación se rechaza por
credenciales o por permisos, la respuesta ahora trae el status HTTP que le
corresponde en vez de uno genérico:

| Situación                                          | Status             |
| -------------------------------------------------- | ------------------ |
| Credenciales ausentes o inválidas                  | `401 Unauthorized` |
| Credenciales válidas, sin permiso sobre el recurso | `403 Forbidden`    |

> **Warning**
>
> Si tu integración distinguía estos casos leyendo el `message` de la
> respuesta, cambiá a leer el status HTTP o el campo `code`. Los dos son
> estables; el texto del mensaje no.

## July 29, 2026

## Los custom fields de `organizations` quedan deprecados

La funcionalidad de campos personalizados se movió a su propia sección. Los
dos endpoints que vivían bajo `organizations` quedan deprecados:

| Deprecado                                                 | Reemplazo                                    |
| --------------------------------------------------------- | -------------------------------------------- |
| `POST /org/{slug}/organizations/custom-fields/`           | `POST /org/{slug}/custom-fields/`            |
| `PUT /org/{slug}/organizations/users/{id}/custom-fields/` | `POST /org/{slug}/users/{id}/custom-fields/` |

La sección nueva además cubre lo que la vieja no tenía: listar
(`GET /org/{slug}/custom-fields/`), editar
(`PUT /org/{slug}/custom-fields/{id}`) y borrar
(`DELETE /org/{slug}/custom-fields/{id}`).

Los endpoints deprecados siguen respondiendo con normalidad. Todavía no hay
fecha de retiro; se va a anunciar acá con anticipación.

## July 28, 2026

## Se dejaron de emitir los headers internos de diagnóstico

Las respuestas ya no incluyen los headers que la capa proxy usaba para
depurar (`crehana-graphql-proxy-*`, `cache-graphql-*`,
`crehana-rest-app-name`, `crehana-rest-api-version` y variantes). Exponían
detalle interno —nombre de operación, módulo y versión— que no forma parte
del contrato.

El cuerpo de las respuestas no cambió. Si tu integración leía alguno de esos
headers, va a encontrarlos ausentes.

## July 22, 2026

## Nueva sección: Time off

`GET /org/{slug}/timeoff/requests/` lista las solicitudes de ausencia de la
organización —vacaciones, licencias y permisos— paginadas y filtrables.

## Asignar un track a varios usuarios

`POST /org/{slug}/learning/content/knowledge-hub/enrollments/v2/assign-track/`
asigna un track a un conjunto de usuarios en una sola llamada, igual que el
`assign-course/` que ya existía.

## Asignaciones sin usuarios devuelven 422

Los dos endpoints de asignación (`assign-course/` y `assign-track/`) ahora
responden `422 Unprocessable Entity` cuando la llamada no termina asignando
a ningún usuario, en vez de devolver un `200` con la lista vacía.

## July 13, 2026

## Forzar el cambio de contraseña en el próximo login

Los endpoints de cambio de contraseña aceptan un campo nuevo,
`force_change_password`. Cuando va en `true`, al usuario se le va a exigir
cambiar la contraseña la próxima vez que ingrese.

```json
{
  "password": "...",
  "force_change_password": true
}
```

El campo es opcional y por defecto es `false`, así que las integraciones
existentes no cambian de comportamiento.

## July 8, 2026

## Asignar un curso a varios usuarios

`POST /org/{slug}/learning/content/knowledge-hub/enrollments/v2/assign-course/`
asigna un curso a un conjunto de usuarios en una sola llamada, en lugar de
una petición por usuario.

_Showing the 20 most recent of 21 entries. Append `/llms.txt` to the changelog URL for the complete index._