Integración con la API de Plain
Este artículo explica cómo integrarse con la API de Plain para:
Autenticarse correctamente
Exportar fichajes y ausencias
Obtener vacaciones y otras cuentas de empleados
Importar empleados y workflows de forma masiva (bulk)
Trabajar en entornos multivenue
El objetivo es que puedas entender el flujo completo antes de empezar a programar.
1. Autenticación
Antes de utilizar cualquier endpoint de Plain, es necesario autenticarse y obtener un token JWT.
Este token identifica a tu aplicación y autoriza las peticiones que hagas a la API.
1.1 Cómo obtener el token JWT
El proceso tiene dos pasos:
Paso 1: Enviar credenciales al endpoint de autenticación
POST /saas/api/auth/token/
Este endpoint requiere un header Authorization con un token dummy estático:
Authorization: Bearer 9cb029da-b5a6-429e-8184-4ebfb89a732c
Body de la petición:
{
"grant_type": "password",
"username": "<usuario>",
"password": "<password>"
}
Notas importantes:
El
usernameen Plain siempre es un email.El usuario debe tener rol administrador.
Es recomendable crear un usuario específico para la integración.
Paso 2: Usar el token devuelto
La respuesta incluirá:
{
"access_token": "<token jwt>",
"token_type": "BEARER",
"expires_in": 43199,
"company_name": "<company name>"
}
El access_token es el token JWT que deberás incluir en todas las llamadas posteriores:
Authorization: Bearer <token jwt>
El token es válido durante 12 horas. No es necesario solicitar uno nuevo en cada llamada: puedes almacenarlo y reutilizarlo hasta que expire.
1.2 Entornos multivenue
Si trabajas en un entorno multivenue:
La respuesta incluirá
other_company_tokens.Cada venue tiene su propio token JWT.
Debes crear el usuario administrador en cada venue.
El
usernamedebe ser exactamente el mismo en todas las venues.Para operar en cada venue, debes usar el token correspondiente.
2. Exportación de datos a Excel
La exportación de datos (fichajes o ausencias) sigue siempre el mismo patrón:
Obtener un token de descarga.
Usar ese token para descargar el archivo.
Este sistema desacopla la generación del archivo de su descarga.
2.1 Exportación de fichajes
Paso 1: Obtener token de descarga
GET /saas/api/time-registrations/export?page_size=1000&start_from=2026-01-01&start_to=2026-02-28
Respuesta:
{
"access_token": "<token descarga documentos>",
"token_type": "EXPORT",
"expires_in": 31535999,
"company_name": "Nombre de empresa"
}
Este token no es el JWT de autenticación. Es un token específico para descargar el archivo generado.
Paso 2: Descargar el Excel
GET /saas/api/store/download/<token descarga documentos>
Este endpoint devuelve directamente el archivo Excel como un stream.
2.2 Exportación de ausencias
El proceso es exactamente el mismo, pero usando otro endpoint:
GET /saas/api/workflows/export?page_size=1000&workflow_type=Ausencias&sort=-requested&workflow_status=Pendiente&start_from=2026-01-01&start_to=2026-01-31
Después se descarga el archivo usando:
GET /saas/api/store/download/<token descarga documentos>
Multivenue en exportaciones
Si trabajas con multivenue:
Debes repetir el proceso completo para cada venue.
Cada venue requiere su propio JWT.
Cada exportación genera su propio token de descarga.
3. Obtener vacaciones y otras cuentas de empleados
Plain no expone directamente las vacaciones disponibles o disfrutadas mediante un endpoint aislado. Las cuentas se obtienen a través de una vista de planificación. Esto permite controlar qué columnas quieres exponer en la API.
3.1 Crear una vista de planificación
Desde la aplicación:
Planificación → Configurar vistas → Crear nueva vista
Añade las columnas:
Vacaciones pendientes periodo
Vacaciones disfrutadas periodo
Guarda la vista con un nombre identificable (por ejemplo: "Vista integración").
3.2 Obtener el ID de la vista
GET /saas/api/plan-views
Este endpoint devuelve un listado de vistas con su id y name. Busca la vista creada y guarda su id.
3.3 Obtener las cuentas mediante API
Con el id de la vista:
GET /api/accounts/assignments?page_size=1000&start=2026-01-01&end=2026-01-31&group_id=566&plan_view_id=<id obtenido>
La respuesta contiene un array data. Para cada empleado:
El email está en:
data[i].employee.emailLas cuentas están en:
data[i].counters
Cada elemento de counters contiene:
label→ nombre de la cuentavalue→ valor de la cuenta
Los valores devueltos corresponden exactamente a las columnas configuradas en la vista.
4. Importación masiva de datos (bulk imports)
Además de exportar y consultar datos, la API de Plain permite importar empleados y workflows de forma masiva. Estos endpoints aceptan un array de elementos en una sola petición y devuelven el resultado individual de cada uno.
4.1 Consideraciones generales
Aplican a todos los endpoints de importación:
Autenticación: requieren el token JWT obtenido en la sección 1, enviado en el header
Authorization: Bearer <token jwt>.Método y formato: son peticiones
POSTconContent-Type: application/json. El body es siempre un array JSON de elementos.Comportamiento upsert: cada endpoint tiene un identificador. Si ya existe un registro con ese identificador, su información se actualiza; si no existe, se crea uno nuevo.
Empleados → el identificador es el email.
Workflows → el identificador es la combinación de
employee_code+ fechastart+shift.
Respuesta: la API devuelve un array con un resultado por cada elemento enviado. Cada resultado es un objeto de éxito (
StatusDTO) o de error (ErrorDTO). En caso de error, el campoindexindica la posición del elemento dentro del array enviado.
4.2 Importar empleados
POST /api/employees/imports/bulk
El identificador del empleado es el email. Si ya existe un empleado con ese email, se actualizan sus datos; en caso contrario, se crea uno nuevo.
Campos de cada elemento (EmployeeBulkDTO):
Campo | Tipo | Descripción |
| string | Nombre del empleado |
| string | Apellidos del empleado |
| string | Email del empleado. Es el identificador usado para crear o actualizar |
| string (fecha) | Fecha de nacimiento. ISO-8601 sin hora ni offset (p. ej. |
| string (fecha) | Fecha de alta. Formato |
| string (fecha) | Fecha de baja. Formato |
| boolean | Indica si el empleado está activo |
| boolean | Indica si el empleado tiene acceso a la aplicación |
| boolean | Si es |
| boolean | Usar las horas diarias exactas definidas por el contrato |
| string | Teléfono. En formato internacional si es posible |
| string | Puesto del empleado |
| string | Código opcional que identifica al empleado en la empresa |
| string | NIF del empleado (opcional) |
| string | Código hexadecimal opcional de la tarjeta del empleado |
| integer | Orden de aparición del empleado en la planificación |
| string | Grupo del empleado (opcional) |
| string (fecha) | Fecha de inicio en el grupo. Formato |
| string | Contrato del empleado (opcional) |
| string (fecha) | Fecha de inicio del contrato. Formato |
| string | Función del empleado (opcional) |
| string | Patrón de turnos del empleado (opcional) |
| string (fecha) | Fecha de inicio del patrón de turnos. Formato |
Ejemplo de body:
[
{
"name": "María",
"surname": "García López",
"email": "[email protected]",
"code": "EMP-001",
"enabled": true,
"with_access": true,
"start_day": "2024-01-15",
"position": "Camarera",
"group": "Sala"
},
{
"name": "Juan",
"surname": "Pérez Ruiz",
"email": "[email protected]",
"code": "EMP-002",
"enabled": true,
"with_access": false,
"start_day": "2023-06-01"
}
]
4.3 Importar workflows
POST /api/workflows/imports/bulk
Un workflow representa una asignación planificada para un empleado durante un periodo, con un turno asociado. El identificador es la combinación de employee_code, la fecha start y el shift. Si ya existe un workflow con esa combinación, se actualiza; en caso contrario, se crea uno nuevo.
Campos de cada elemento (WorkflowBulkDTO):
Campo | Tipo | Descripción |
| string (date) | Inicio del workflow. fecha en formato ISO-8601 fe (p. ej. |
| string (date) | Fin del workflow. Mismo formato que |
| string | Comentarios de la solicitud |
| string | Comentarios de la respuesta |
| string | Turno a planificar durante la duración del workflow. Obligatorio. Parte del identificador |
| string | Código que identifica al empleado en la empresa. Parte del identificador |
| string | Permite cancelar workflows cuando recibe el valor "Anulado". Opcional. Si no es una cancelación, no enviar |
workflow_hourly_requested | booleano | true, cuando se trata de una ausencia por horas. Opcional. Si no se envía, por defecto es false. |
| string | Hora de inicio de la ausencia por horas. ISO-8601 con offset de zona horaria (p. ej. 2019-08-28T10:30:00+02:00, o 2019-08-28T10:30:00Z para UTC). Opcional. Únicamente para las ausencias por horas |
duration | float | Duración de la ausencia por horas. (p. ej. 3.5) Opcional. Únicamente para las ausencias por horas |
Ejemplo de body:
[
{
"employee_code": "EMP-001",
"start": "2026-03-01"
"shift": "V",
"comments": "Asignación de Vacaciones de un día creada por integración"
},
{
"employee_code": "EMP-001",
"start": "2026-05-01",
"end": "2026-05-05",
"shift": "V",
"comments": "Asignación de vacaciones de varios días para anular"
"status": "Anulado"
},
{
"employee_code": "EMP-001",
"start": "2026-03-01",
"shift": "V",
"comments": "Asignación de vacaciones de ausencia por horas",
"workflow_hourly_requested": true,
"start_hour": "2026-08-10T10:30:00Z",
"duration": 4.5
}
]
4.4 Importar fichajes
POST /api/time-registrations/imports/bulk
Un fichaje (time registration) representa un registro de tiempo de un empleado en un día concreto, con una hora de inicio, una hora de fin opcional y un tipo de tiempo asociado. El identificador de un fichaje es la combinación de empleado (employee_code) , día (day) y hora de inicio (start).
Si en el array items se envía un fichaje cuyo start todavía no existe para ese empleado y día, se crea.
Si ya existe un fichaje con ese start, se actualiza (tipo de tiempo y hora de fin).
Si un fichaje que existe en la base de datos para ese empleado y día no aparece en el array items, se considera eliminado y se borra.
Por este motivo, para un empleado y un día debes enviar siempre todos los fichajes de ese día, incluso los que no han cambiado. Si omites alguno, se interpretará como que se ha eliminado.
Esto no significa que tengas que enviar los fichajes de todos los días: solo debes incluir los días en los que haya fichajes añadidos, modificados o eliminados. Cada elemento del array representa el conjunto completo de fichajes de un empleado en un día.
Campos de cada elemento (TimeRegistrationBulkDTO):
Campo | Tipo | Descripción |
day | string (fecha) | Día del fichaje. ISO-8601 sin hora ni offset (p. ej. 2019-08-28). Parte del identificador |
employee_code | string | Código que identifica al empleado en la empresa. Obligatorio. Parte del identificador. Debe corresponder a un empleado existente |
items | array | Conjunto completo de fichajes del empleado en ese día (ver TimeRegistrationItemBulkDTO) |
Campos de cada fichaje dentro de items (TimeRegistrationItemBulkDTO):
Campo | Tipo | Descripción |
start | string (date-time) | Inicio del fichaje. ISO-8601 con offset de zona horaria (p. ej. 2019-08-28T10:30:00+02:00, o 2019-08-28T10:30:00Z para UTC). Parte del identificador. Cuando el servidor devuelve esta información, la zona es UTC y se expresa sin offset (p. ej. 2019-08-28T10:30:00) |
end | string (date-time) | Fin del fichaje. Mismo formato que start. Opcional. |
time_type | string | Nombre del tipo de tiempo del fichaje. Debe corresponder a un tipo de tiempo existente en la empresa |
Ejemplo de body:
[
{
"day": "2026-06-25",
"employee_code": "47",
"items": [
{
"start": "2026-06-25T07:30:12Z",
"end": "2026-06-25T11:45:15Z",
"time_type": "Trabajo"
},
{
"start": "2026-06-25T13:30:12Z",
"end": "",
"time_type": "Trabajo"
}
]
}
]
4.5 Formato de respuesta y manejo de errores
Los endpoints de importación devuelven un array con un resultado por cada elemento enviado, en el mismo orden.
Resultado correcto (
StatusDTO): contiene el campostatus(entero) con el código del resultado.Resultado con error (
ErrorDTO): contienestatus(entero) y un arrayerrorscon el detalle de cada problema (ErrorItemDTO).
Campos de ErrorItemDTO:
Campo | Tipo | Descripción |
| string | Código del error |
| string | Clave asociada al error |
| array de string | Campos afectados por el error |
| string | Valor que ha provocado el error |
| string | Valores válidos o disponibles, cuando aplica |
| string | Descripción legible del error |
| integer | Posición del elemento dentro del array enviado |
Ejemplo ilustrativo de respuesta (un elemento correcto y otro con error):
[
{ "status": 200 },
{
"status": 400,
"errors": [
{
"code": "...",
"key": "email",
"fields": ["email"],
"value": "juan.perez",
"availables": "...",
"description": "...",
"index": 1
}
]
}
]
Conviene recorrer siempre el array de respuesta y comprobar cada resultado: una petición puede importar correctamente unos elementos y fallar en otros. El campo index permite identificar exactamente qué elemento del array original ha fallado.
4.6 Multivenue en importaciones
Al igual que en las exportaciones, en un entorno multivenue debes repetir el proceso completo para cada venue, usando el token JWT correspondiente a cada una.
5. Traspaso de la bolsa de horas
Esta sección describe cómo sincronizar el balance de horas (bolsa de horas) de un sistema externo con Plain. El flujo consiste en consultar el balance actual de cada empleado en Plain, calcular la diferencia respecto al balance del sistema externo, y enviar una transacción de compensación que ajuste el balance al valor correcto.
Para evitar acumular transacciones de compensación en sincronizaciones sucesivas, se recomienda guardar el id de la transacción creada para cada empleado y, en las siguientes ejecuciones, actualizarla en vez de crear una nueva.
5.1 Visión general del flujo
Obtener el balance actual de cada empleado en Plain mediante el endpoint de cuentas (
/saas/api/accounts/assignments), paginando conpage_size=50.Identificar al empleado por
codeoemail(lo que utilice el sistema externo) y guardar también eliddel empleado de Plain.Calcular la diferencia:
compensación = balance_externo − balance_plain.Si no existe transacción previa para ese empleado, crear una nueva mediante
POST /saas/api/transactions/.Si ya existe transacción previa (porque se sincronizó antes), actualizarla mediante
PUT /saas/api/transactions/{transaction-id}.Guardar el
idde la transacción para futuras sincronizaciones.
5.2 Obtener el balance actual en Plain
GET /saas/api/accounts/assignments?start=2020-01-01&end=2026-05-29&transaction_rule_system_id=5&page_size=50&page=0
Parámetros relevantes:
Parámetro | Descripción |
| Fecha de inicio del período del balance. Debe ser una fecha anterior al inicio del uso del sistema externo. Si no hay un valor concreto, basta con una fecha arbitraria antigua, por ejemplo |
| Fecha final del balance. Normalmente el día actual |
|
|
| Tamaño de página recomendado: 50 |
| Número de página. Empieza en 0 y se incrementa hasta procesar todos los empleados |
Paginación recomendada: pedir páginas de 50 en 50 y procesar cada lote (calcular la compensación y enviar/actualizar la transacción de cada empleado) antes de solicitar la siguiente página.
Estructura de la respuesta (AccountAssignmentSearchBeanDTO):
{
"data": [
{
"employee": {
"id": 12345,
"name": "María",
"surname": "García López",
"email": "[email protected]",
"code": "EMP-001"
},
"counters": [
{
"label": "Balance",
"value": 12.5
}
]
}
],
"pagination": {
"total_items": 245,
"page": 0,
"page_size": 50
}
}
Notas sobre la respuesta:
dataes la lista de empleados de la página actual.Cada empleado puede identificarse en el sistema externo por
data[i].employee.codeodata[i].employee.email.Para enviar la transacción se necesita el
idinterno de Plain:data[i].employee.id.counterssiempre contiene un único elemento cuando se filtra portransaction_rule_system_id=5.El balance actual en Plain está en
counters[0].value.Para saber si hay más páginas, comprueba si
(pagination.page + 1) * pagination.page_size < pagination.total_items.
5.3 Calcular la diferencia
Para cada empleado de la página:
compensación = balance_externo − balance_plain
Donde:
balance_externoes el valor del balance que tiene el empleado en el sistema externo.balance_plainescounters[0].valuede la respuesta anterior.
Si la diferencia es 0, no es necesario crear ni actualizar ninguna transacción para ese empleado.
5.4 Crear la transacción de compensación (primera sincronización)
POST /saas/api/transactions/
Body de la petición (TransactionDTO):
{
"day": "2026-05-29",
"value": "12.5",
"account_id": 99474,
"employee_id": 12345,
"observations": "Compensación horas objetivo"
}
Campos relevantes:
Campo | Tipo | Descripción |
| string (fecha) | Día de la transacción. Se recomienda usar el día actual |
| string | Valor de compensación calculado. Puede ser positivo o negativo |
| integer |
|
| integer |
|
| string | Texto identificativo. Se recomienda |
La respuesta es un TransactionDTOResponse que incluye el id de la transacción creada:
{
"id": 7654321,
"day": "2026-05-29",
"value": "12.5",
"employee_id": 12345,
"observations": "Compensación horas objetivo",
"account": { "id": 99474, "name": "...", "type": "..." },
"employee": { "id": 12345, "name": "María", "surname": "García López" },
"created": "2026-05-29T08:00:00",
"updated": "2026-05-29T08:00:00",
"version": 1
}
Guarda este id en el sistema externo, asociado al empleado. Será necesario para actualizar la transacción en las siguientes sincronizaciones sin crear una nueva.
5.5 Actualizar la transacción (sincronizaciones posteriores)
Si en el sistema externo ya está guardado el id de la transacción de compensación previa para ese empleado, se actualiza esa misma transacción en lugar de crear una nueva. Esto evita acumular transacciones de compensación a lo largo del tiempo:
PUT /saas/api/transactions/{transaction-id}
Donde {transaction-id} es el id guardado de la transacción previa.
Body de la petición (mismo TransactionDTO, con el nuevo valor calculado):
{
"day": "2026-05-29",
"value": "10.0",
"account_id": 99474,
"employee_id": 12345,
"observations": "Compensación horas objetivo"
}
El day y el value se actualizan al nuevo cálculo de compensación; el id de la transacción se mantiene.
5.7 Multivenue
Al igual que en el resto de operaciones, en un entorno multivenue debes repetir el proceso completo para cada venue, usando el token JWT correspondiente a cada una. Los id de empleado, de transacción y de cuenta son específicos de cada venue, por lo que deben guardarse asociados al venue correspondiente.
Apéndice: Referencia de esquemas
Resumen de las estructuras de datos utilizadas por la API.
AuthenticationDTO — body de la autenticación
Campo | Tipo | Descripción |
| string | Tipo de operación de autenticación ( |
| string | Usuario con el que iniciar sesión (email) |
| string | Contraseña |
TokenDTO — respuesta de la autenticación
Campo | Tipo | Descripción |
| string | Token a usar en las peticiones posteriores |
| string | Tipo de token |
| integer | Tiempo de validez del token, en segundos |
StatusDTO — resultado correcto de una importación
Campo | Tipo | Descripción |
| integer | Código del resultado |
ErrorDTO — resultado con error
Campo | Tipo | Descripción |
| integer | Código de estado del error |
| array de | Detalle de cada error |
Los esquemas EmployeeBulkDTO, WorkflowBulkDTO y ErrorItemDTO están detallados en la sección 4.
