Inicio rápido
- Un administrador crea la clave en FichMe → Ajustes → API, elige sus permisos (scopes) y la copia: solo se muestra una vez. Guárdala en una variable de entorno:
export FICHME_API_KEY=fm_live_… - Comprueba que funciona: a qué empresa pertenece y qué puede hacer.
curl "https://api.fichme.com/v1/me" \ -H "x-api-key: $FICHME_API_KEY"const res = await fetch("https://api.fichme.com/v1/me", { headers: { "x-api-key": process.env.FICHME_API_KEY, }, }); if (!res.ok) { const { error } = await res.json(); throw new Error(`${res.status} ${error.code}: ${error.message}`); } const data = await res.json();import os import requests res = requests.get( "https://api.fichme.com/v1/me", headers={ "x-api-key": os.environ["FICHME_API_KEY"], }, timeout=30, ) if not res.ok: error = res.json()["error"] raise RuntimeError(f"{res.status_code} {error['code']}: {error['message']}") data = res.json()<?php $ch = curl_init('https://api.fichme.com/v1/me'); curl_setopt_array($ch, [ CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => [ 'x-api-key: ' . getenv('FICHME_API_KEY'), ], ]); $body = curl_exec($ch); $status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE); $data = json_decode($body, true); if ($status >= 400) { throw new RuntimeException("$status {$data['error']['code']}: {$data['error']['message']}"); } - Lista la plantilla:
curl "https://api.fichme.com/v1/employees?limit=100" \ -H "x-api-key: $FICHME_API_KEY"const params = new URLSearchParams({ limit: "100", }); const res = await fetch(`https://api.fichme.com/v1/employees?${params}`, { headers: { "x-api-key": process.env.FICHME_API_KEY, }, }); if (!res.ok) { const { error } = await res.json(); throw new Error(`${res.status} ${error.code}: ${error.message}`); } const data = await res.json();import os import requests res = requests.get( "https://api.fichme.com/v1/employees", params={ "limit": "100", }, headers={ "x-api-key": os.environ["FICHME_API_KEY"], }, timeout=30, ) if not res.ok: error = res.json()["error"] raise RuntimeError(f"{res.status_code} {error['code']}: {error['message']}") data = res.json()<?php $ch = curl_init('https://api.fichme.com/v1/employees?' . http_build_query([ 'limit' => '100', ])); curl_setopt_array($ch, [ CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => [ 'x-api-key: ' . getenv('FICHME_API_KEY'), ], ]); $body = curl_exec($ch); $status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE); $data = json_decode($body, true); if ($status >= 400) { throw new RuntimeException("$status {$data['error']['code']}: {$data['error']['message']}"); } - Jornadas del mes, ya emparejadas y con las horas calculadas:
curl "https://api.fichme.com/v1/work-sessions?from=2026-09-01&to=2026-09-30" \ -H "x-api-key: $FICHME_API_KEY"const params = new URLSearchParams({ from: "2026-09-01", to: "2026-09-30", }); const res = await fetch(`https://api.fichme.com/v1/work-sessions?${params}`, { headers: { "x-api-key": process.env.FICHME_API_KEY, }, }); if (!res.ok) { const { error } = await res.json(); throw new Error(`${res.status} ${error.code}: ${error.message}`); } const data = await res.json();import os import requests res = requests.get( "https://api.fichme.com/v1/work-sessions", params={ "from": "2026-09-01", "to": "2026-09-30", }, headers={ "x-api-key": os.environ["FICHME_API_KEY"], }, timeout=30, ) if not res.ok: error = res.json()["error"] raise RuntimeError(f"{res.status_code} {error['code']}: {error['message']}") data = res.json()<?php $ch = curl_init('https://api.fichme.com/v1/work-sessions?' . http_build_query([ 'from' => '2026-09-01', 'to' => '2026-09-30', ])); curl_setopt_array($ch, [ CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => [ 'x-api-key: ' . getenv('FICHME_API_KEY'), ], ]); $body = curl_exec($ch); $status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE); $data = json_decode($body, true); if ($status >= 400) { throw new RuntimeException("$status {$data['error']['code']}: {$data['error']['message']}"); } - Para un caso completo (nómina, terminal, ERP, webhooks, Excel), sigue una de las guías.
Horas del mes para la nómina
Lo que necesita una gestoría para cerrar el mes. Usa una clave con el perfil Gestoría / nóminas, que ya lleva los permisos justos (incluido employees:read_pii para el DNI, el nº de la Seguridad Social y las bajas médicas, que son datos sensibles).
- La plantilla, incluidos los desactivados (
status=ALL), por si alguien causó baja en el mes:curl "https://api.fichme.com/v1/employees?status=ALL&limit=500" \ -H "x-api-key: $FICHME_API_KEY"const params = new URLSearchParams({ status: "ALL", limit: "500", }); const res = await fetch(`https://api.fichme.com/v1/employees?${params}`, { headers: { "x-api-key": process.env.FICHME_API_KEY, }, }); if (!res.ok) { const { error } = await res.json(); throw new Error(`${res.status} ${error.code}: ${error.message}`); } const data = await res.json();import os import requests res = requests.get( "https://api.fichme.com/v1/employees", params={ "status": "ALL", "limit": "500", }, headers={ "x-api-key": os.environ["FICHME_API_KEY"], }, timeout=30, ) if not res.ok: error = res.json()["error"] raise RuntimeError(f"{res.status_code} {error['code']}: {error['message']}") data = res.json()<?php $ch = curl_init('https://api.fichme.com/v1/employees?' . http_build_query([ 'status' => 'ALL', 'limit' => '500', ])); curl_setopt_array($ch, [ CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => [ 'x-api-key: ' . getenv('FICHME_API_KEY'), ], ]); $body = curl_exec($ch); $status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE); $data = json_decode($body, true); if ($status >= 400) { throw new RuntimeException("$status {$data['error']['code']}: {$data['error']['message']}"); } - Los totales del mes por empleado: trabajado, previsto y la diferencia.
curl "https://api.fichme.com/v1/hours-balance?from=2026-09-01&to=2026-09-30" \ -H "x-api-key: $FICHME_API_KEY"const params = new URLSearchParams({ from: "2026-09-01", to: "2026-09-30", }); const res = await fetch(`https://api.fichme.com/v1/hours-balance?${params}`, { headers: { "x-api-key": process.env.FICHME_API_KEY, }, }); if (!res.ok) { const { error } = await res.json(); throw new Error(`${res.status} ${error.code}: ${error.message}`); } const data = await res.json();import os import requests res = requests.get( "https://api.fichme.com/v1/hours-balance", params={ "from": "2026-09-01", "to": "2026-09-30", }, headers={ "x-api-key": os.environ["FICHME_API_KEY"], }, timeout=30, ) if not res.ok: error = res.json()["error"] raise RuntimeError(f"{res.status_code} {error['code']}: {error['message']}") data = res.json()<?php $ch = curl_init('https://api.fichme.com/v1/hours-balance?' . http_build_query([ 'from' => '2026-09-01', 'to' => '2026-09-30', ])); curl_setopt_array($ch, [ CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => [ 'x-api-key: ' . getenv('FICHME_API_KEY'), ], ]); $body = curl_exec($ch); $status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE); $data = json_decode($body, true); if ($status >= 400) { throw new RuntimeException("$status {$data['error']['code']}: {$data['error']['message']}"); } - Las ausencias aprobadas (vacaciones, permisos, bajas):
curl "https://api.fichme.com/v1/leave-requests?from=2026-09-01&to=2026-09-30&status=APPROVED" \ -H "x-api-key: $FICHME_API_KEY"const params = new URLSearchParams({ from: "2026-09-01", to: "2026-09-30", status: "APPROVED", }); const res = await fetch(`https://api.fichme.com/v1/leave-requests?${params}`, { headers: { "x-api-key": process.env.FICHME_API_KEY, }, }); if (!res.ok) { const { error } = await res.json(); throw new Error(`${res.status} ${error.code}: ${error.message}`); } const data = await res.json();import os import requests res = requests.get( "https://api.fichme.com/v1/leave-requests", params={ "from": "2026-09-01", "to": "2026-09-30", "status": "APPROVED", }, headers={ "x-api-key": os.environ["FICHME_API_KEY"], }, timeout=30, ) if not res.ok: error = res.json()["error"] raise RuntimeError(f"{res.status_code} {error['code']}: {error['message']}") data = res.json()<?php $ch = curl_init('https://api.fichme.com/v1/leave-requests?' . http_build_query([ 'from' => '2026-09-01', 'to' => '2026-09-30', 'status' => 'APPROVED', ])); curl_setopt_array($ch, [ CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => [ 'x-api-key: ' . getenv('FICHME_API_KEY'), ], ]); $body = curl_exec($ch); $status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE); $data = json_decode($body, true); if ($status >= 400) { throw new RuntimeException("$status {$data['error']['code']}: {$data['error']['message']}"); } - Si hace falta el detalle por día, las jornadas calculadas (tramos, pausas y ausencias de cada día): GET /v1/work-sessions.
- El registro de jornada firmado que exige la ley, en PDF: POST /v1/exports con
dataset: "clocking"yformat: "pdf".
| Para la nómina | Campo |
|---|---|
| Horas trabajadas | hoursBalance.workedMinutes (incluye pausas retribuidas y ausencias que computan) |
| Horas previstas por la jornada | hoursBalance.assignedMinutes |
| Diferencia | hoursBalance.balanceMinutes: positivo = de más; negativo = faltan |
| Días trabajados | hoursBalance.daysWorked |
| Vacaciones, permisos, bajas | leaveRequest.leaveType.code, startDate, endDate, businessDays |
| DNI y nº de la Seguridad Social | employee.identity.taxId y employee.identity.socialSecurityNumber |
daysWithOpenSegment no es 0, a alguien le falta una salida. Mira las correcciones pendientes (GET /v1/clock-corrections?status=PENDING) y espera a que se revisen. FichMe no decide qué es hora extra: balanceMinutes es la diferencia bruta, y cómo se paga o se compensa lo marca el convenio.Terminal de fichaje propio
Para un torno, un lector de tarjetas o un ERP que ficha. Usa el perfil Terminal o ERP que ficha, y antes pide a un administrador que active Fichaje por API en Ajustes → API (si no, 403 api_clocking_disabled).
- Identifica al empleado por
employeeId,emailodni(uno solo). Si tu terminal usa tarjetas, guarda la relación tarjeta →employeeIda partir de GET /v1/employees. Identificar pordniexige que la clave tenga tambiénemployees:read_pii(el perfil Terminal no lo lleva): sin él, la respuesta sería distinta según el DNI trabaje o no en la empresa. - Un solo botón:
POST /v1/clockhace lo que haría el botón de la app (entra, sale o cierra la pausa). Botones separados:/v1/clock/in,/out,/break/starty/break/end. - La hora la pone FichMe al recibir la petición. Por API no se fichan instantes pasados: si el terminal estuvo sin conexión, lo que falte se registra con una solicitud de corrección.
- Envía siempre
Idempotency-Key, una por pulsación. Si la red falla y reintentas con la MISMA, recibes el fichaje ya hecho (200,idempotentReplay: true) y nunca uno duplicado.
curl -X POST "https://api.fichme.com/v1/clock" \
-H "x-api-key: $FICHME_API_KEY" \
-H "Idempotency-Key: 5f1d6c2e-8a3b-4c7d-9e0f-1a2b3c4d5e6f" \
-H "Content-Type: application/json" \
-d '{
"employeeId": 4821
}'const res = await fetch("https://api.fichme.com/v1/clock", {
method: "POST",
headers: {
"x-api-key": process.env.FICHME_API_KEY,
"Idempotency-Key": crypto.randomUUID(),
"Content-Type": "application/json",
},
body: JSON.stringify({
employeeId: 4821,
}),
});
if (!res.ok) {
const { error } = await res.json();
throw new Error(`${res.status} ${error.code}: ${error.message}`);
}
const data = await res.json();import os
import uuid
import requests
res = requests.post(
"https://api.fichme.com/v1/clock",
headers={
"x-api-key": os.environ["FICHME_API_KEY"],
"Idempotency-Key": str(uuid.uuid4()),
},
json={
"employeeId": 4821,
},
timeout=30,
)
if not res.ok:
error = res.json()["error"]
raise RuntimeError(f"{res.status_code} {error['code']}: {error['message']}")
data = res.json()<?php
$ch = curl_init('https://api.fichme.com/v1/clock');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CUSTOMREQUEST => 'POST',
CURLOPT_HTTPHEADER => [
'x-api-key: ' . getenv('FICHME_API_KEY'),
'Idempotency-Key: ' . bin2hex(random_bytes(16)),
'Content-Type: application/json',
],
CURLOPT_POSTFIELDS => json_encode([
'employeeId' => 4821,
]),
]);
$body = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
$data = json_decode($body, true);
if ($status >= 400) {
throw new RuntimeException("$status {$data['error']['code']}: {$data['error']['message']}");
}JavaScript · reintentos seguros
// Una Idempotency-Key por pulsación, la MISMA en los reintentos: nunca se duplica un fichaje.
async function fichar(employeeId) {
const idempotencyKey = crypto.randomUUID();
for (let intento = 1; ; intento++) {
let res;
try {
res = await fetch("https://api.fichme.com/v1/clock", {
method: "POST",
headers: {
"x-api-key": process.env.FICHME_API_KEY,
"Idempotency-Key": idempotencyKey,
"Content-Type": "application/json",
},
body: JSON.stringify({ employeeId }),
});
} catch (errorDeRed) {
if (intento === 3) throw errorDeRed; // sin conexión: avisa al empleado
await new Promise((r) => setTimeout(r, 1000 * intento));
continue;
}
const cuerpo = await res.json();
if (res.ok) return cuerpo; // cuerpo.action y cuerpo.state: lo que enseñas en pantalla
if ((res.status === 429 || res.status >= 500) && intento < 3) {
await new Promise((r) => setTimeout(r, 1000 * intento));
continue;
}
throw new Error(`${cuerpo.error.code}: ${cuerpo.error.message}`);
}
}| Respuesta | Qué pasó | Qué enseñar en la pantalla |
|---|---|---|
201 action: IN / OUT | Entrada o salida registrada | «Entrada registrada a las 08:58» (la hora está en entry.timestamp) |
201 action: BREAK_START / BREAK_END | Pausa iniciada o terminada | «Pausa iniciada» / «De vuelta al trabajo» |
200 idempotentReplay: true | Era un reintento de algo ya registrado | Lo mismo que el 201 |
409 ALREADY_CLOCKED_IN | Ya tenía una entrada abierta | «Ya estás dentro» |
409 NO_ACTIVE_IN | No hay entrada que cerrar | «No tienes una entrada abierta» |
409 BREAK_ALREADY_OPEN / NO_ACTIVE_BREAK | Está en pausa / no lo está | «Termina la pausa antes» / «No estás en pausa» |
403 employee_inactive / clocking_blocked_for_employee | Ese empleado no puede fichar | «Consulta con tu responsable» |
El estado va en error.state del 409. Consulta de nuevo el estado real antes de ofrecer otra acción: puede haber fichado desde el móvil.
Sincronizar con tu ERP
Para mantener una copia al día sin descargarlo todo cada vez. Empleados, fichajes y ausencias admiten updatedSince: devuelven solo lo que cambió desde ese instante, bajas incluidas (deleted: true), en orden y paginado por cursor.
- La primera vez, pide desde una fecha antigua (
2000-01-01T00:00:00Z): te llega todo. - Recorre las páginas: mientras
hasMoresea true, repite concursor=<nextCursor>y el mismoupdatedSince. - Guarda el
updatedAtmás alto que hayas recibido: es elupdatedSincede la próxima sincronización.
# Primera página: todo lo que cambió desde tu última marca
curl "https://api.fichme.com/v1/employees?updatedSince=2026-09-01T00:00:00Z&limit=500" \
-H "x-api-key: $FICHME_API_KEY"
# Mientras hasMore sea true: la siguiente, con el nextCursor recibido y el MISMO updatedSince
curl "https://api.fichme.com/v1/employees?updatedSince=2026-09-01T00:00:00Z&limit=500&cursor=<nextCursor>" \
-H "x-api-key: $FICHME_API_KEY"const API = "https://api.fichme.com/v1";
const cabeceras = { "x-api-key": process.env.FICHME_API_KEY };
// desde: la marca guardada de la última vez (la primera, "2000-01-01T00:00:00Z").
// guardarEmpleado y borrarEmpleado son tuyas: escriben en tu sistema.
async function sincronizarEmpleados(desde) {
let marca = desde;
let cursor = null;
do {
const params = new URLSearchParams({ updatedSince: desde, limit: "500" });
if (cursor) params.set("cursor", cursor);
const res = await fetch(`${API}/employees?${params}`, { headers: cabeceras });
if (!res.ok) throw new Error(`FichMe respondió ${res.status}`);
const pagina = await res.json();
for (const empleado of pagina.data) {
if (empleado.deleted) await borrarEmpleado(empleado.id); // baja: quítalo
else await guardarEmpleado(empleado); // alta o cambio: upsert por id
if (new Date(empleado.updatedAt) > new Date(marca)) marca = empleado.updatedAt;
}
cursor = pagina.hasMore ? pagina.nextCursor : null;
} while (cursor);
return marca; // guárdala: es el updatedSince de la próxima vez
}import os
from datetime import datetime
import requests
API = "https://api.fichme.com/v1"
CABECERAS = {"x-api-key": os.environ["FICHME_API_KEY"]}
def a_fecha(iso: str) -> datetime:
return datetime.fromisoformat(iso.replace("Z", "+00:00"))
def sincronizar_empleados(desde: str) -> str:
"""desde: la marca de la última vez (la primera, "2000-01-01T00:00:00Z").
guardar_empleado y borrar_empleado son tuyas: escriben en tu sistema."""
marca, cursor = desde, None
while True:
params = {"updatedSince": desde, "limit": 500}
if cursor:
params["cursor"] = cursor
res = requests.get(f"{API}/employees", params=params, headers=CABECERAS, timeout=30)
res.raise_for_status()
pagina = res.json()
for empleado in pagina["data"]:
if empleado.get("deleted"):
borrar_empleado(empleado["id"]) # baja: quítalo
else:
guardar_empleado(empleado) # alta o cambio: upsert por id
if a_fecha(empleado["updatedAt"]) > a_fecha(marca):
marca = empleado["updatedAt"]
if not pagina["hasMore"]:
return marca # guárdala: es el updatedSince de la próxima vez
cursor = pagina["nextCursor"]<?php
const API = 'https://api.fichme.com/v1';
// $desde: la marca de la última vez (la primera, '2000-01-01T00:00:00Z').
// guardarEmpleado y borrarEmpleado son tuyas: escriben en tu sistema.
function sincronizarEmpleados(string $desde): string
{
$marca = $desde;
$cursor = null;
do {
$params = ['updatedSince' => $desde, 'limit' => 500];
if ($cursor !== null) {
$params['cursor'] = $cursor;
}
$ch = curl_init(API . '/employees?' . http_build_query($params));
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ['x-api-key: ' . getenv('FICHME_API_KEY')],
]);
$pagina = json_decode(curl_exec($ch), true);
if (curl_getinfo($ch, CURLINFO_RESPONSE_CODE) >= 400) {
throw new RuntimeException('FichMe: ' . $pagina['error']['code']);
}
foreach ($pagina['data'] as $empleado) {
if (!empty($empleado['deleted'])) {
borrarEmpleado($empleado['id']); // baja: quítalo
} else {
guardarEmpleado($empleado); // alta o cambio: upsert por id
}
if (new DateTimeImmutable($empleado['updatedAt']) > new DateTimeImmutable($marca)) {
$marca = $empleado['updatedAt'];
}
}
$cursor = $pagina['hasMore'] ? $pagina['nextCursor'] : null;
} while ($cursor !== null);
return $marca; // guárdala: es el updatedSince de la próxima vez
}- Upsert por id: un mismo objeto puede volver a llegar si cambia otra vez; no pasa nada.
- Los cambios de los últimos 2 segundos llegan en la siguiente sincronización (así ninguno se queda atrás).
- El cursor va ligado a la clave: no lo reutilices con otra, ni lo guardes entre sincronizaciones (guarda la marca).
Recibir webhooks
En vez de preguntar cada poco, FichMe te avisa cuando algo cambia (planes Business y Enterprise). Crea el webhook desde Ajustes → API → Webhooks o con POST /v1/webhooks, y guarda el secreto whsec_…: solo se muestra una vez.
Lo que recibes
Un POST con este cuerpo JSON (un evento por objeto: una pausa, que crea dos fichajes, genera dos eventos):
{
"id": "evt_01K5Q8Z3M2X7C9V4B6N1P8R0TQ",
"object": "event",
"type": "clockEntry.created",
"apiVersion": "v1",
"createdAt": "2026-09-15T06:58:32.114Z",
"companyId": 1042,
"data": {
"object": {
"object": "clockEntry",
"id": 918273,
"employeeId": 4821,
"type": "IN",
"timestamp": "2026-09-15T06:58:31.000Z",
"shiftDate": "2026-09-15",
"method": "APP",
"source": "MOBILE",
"status": "COMPLETE",
"location": {
"id": "cm1loczgz0001qx8f2k9d7h3a",
"name": "Oficina Zaragoza"
},
"isModified": false,
"requiresCorrection": false,
"offlineCreated": false,
"note": null,
"adminNote": null,
"expected": {
"startTime": "09:00",
"endTime": "17:00",
"hours": 7.5,
"arrivalStatus": "on-time",
"minutesLate": -2,
"departureStatus": null,
"minutesEarly": null
},
"createdAt": "2026-09-15T06:58:31.000Z",
"updatedAt": "2026-09-15T06:58:31.000Z",
"deletedAt": null
}
}
}
| Cabecera | Contenido |
|---|---|
FichMe-Signature | t=<unix>,v1=<hex>: la firma (ver abajo) |
FichMe-Event | El tipo, p. ej. clockEntry.created |
FichMe-Event-Id | El id del evento: deduplica por él |
FichMe-Delivery-Id | La entrega: una por evento y webhook, la misma en sus reintentos (búscala en el historial de entregas) |
Verifica la firma
v1 es HMAC-SHA256, en hexadecimal, del texto <t>.<cuerpo> con tu secreto completo (prefijo whsec_ incluido) como clave. Calcúlala sobre el cuerpo crudo, antes de parsear el JSON (si lo vuelves a serializar, la firma no coincide), compárala en tiempo constante y rechaza las de hace más de 5 minutos.
Sin versión en curl: se muestra en JavaScript.
import crypto from "node:crypto";
import express from "express";
// secreto: el whsec_… completo · cabecera: FichMe-Signature · cuerpo: el texto CRUDO recibido
function verificarFirma(secreto, cabecera, cuerpo, toleranciaSegundos = 300) {
const partes = Object.fromEntries(cabecera.split(",").map((p) => p.split("=", 2)));
const t = Number(partes.t);
if (!Number.isInteger(t) || Math.abs(Date.now() / 1000 - t) > toleranciaSegundos) return false;
const esperada = crypto.createHmac("sha256", secreto).update(`${t}.${cuerpo}`).digest("hex");
const recibida = String(partes.v1 ?? "");
return recibida.length === esperada.length && crypto.timingSafeEqual(Buffer.from(recibida), Buffer.from(esperada));
}
const app = express();
// El cuerpo tiene que llegar CRUDO (Buffer): la firma se calcula sobre los bytes exactos.
app.post("/webhooks/fichme", express.raw({ type: "application/json" }), (req, res) => {
const cuerpo = req.body.toString("utf8");
if (!verificarFirma(process.env.FICHME_WEBHOOK_SECRET, req.get("FichMe-Signature") ?? "", cuerpo)) {
return res.status(400).send("Firma no válida");
}
const evento = JSON.parse(cuerpo);
res.sendStatus(200); // responde enseguida (tienes 10 s)…
procesar(evento); // …y procesa después
});
async function procesar(evento) {
// Deduplica por evento.id: el mismo evento puede llegar más de una vez.
if (evento.type === "clockEntry.created") {
const fichaje = evento.data.object;
// …
}
}
app.listen(3000);import hashlib
import hmac
import os
import time
from flask import Flask, request
app = Flask(__name__)
SECRETO = os.environ["FICHME_WEBHOOK_SECRET"] # el whsec_… completo
def verificar_firma(secreto: str, cabecera: str, cuerpo: bytes, tolerancia: int = 300) -> bool:
partes = dict(p.split("=", 1) for p in cabecera.split(",") if "=" in p)
try:
t = int(partes.get("t", ""))
except ValueError:
return False
if abs(time.time() - t) > tolerancia:
return False
esperada = hmac.new(secreto.encode(), f"{t}.".encode() + cuerpo, hashlib.sha256).hexdigest()
return hmac.compare_digest(esperada, partes.get("v1", ""))
@app.post("/webhooks/fichme")
def webhook_fichme():
cuerpo = request.get_data() # bytes CRUDOS, antes de parsear el JSON
if not verificar_firma(SECRETO, request.headers.get("FichMe-Signature", ""), cuerpo):
return "Firma no válida", 400
evento = request.get_json()
# Responde enseguida (tienes 10 s) y procesa aparte. Deduplica por evento["id"].
return "", 200<?php
function verificarFirma(string $secreto, string $cabecera, string $cuerpo, int $tolerancia = 300): bool
{
$partes = [];
foreach (explode(',', $cabecera) as $par) {
[$clave, $valor] = array_pad(explode('=', $par, 2), 2, '');
$partes[$clave] = $valor;
}
$t = (int) ($partes['t'] ?? 0);
if ($t <= 0 || abs(time() - $t) > $tolerancia) {
return false;
}
$esperada = hash_hmac('sha256', $t . '.' . $cuerpo, $secreto);
return hash_equals($esperada, $partes['v1'] ?? '');
}
$cuerpo = file_get_contents('php://input'); // cuerpo CRUDO, antes de json_decode
$cabecera = $_SERVER['HTTP_FICHME_SIGNATURE'] ?? '';
if (!verificarFirma((string) getenv('FICHME_WEBHOOK_SECRET'), $cabecera, $cuerpo)) {
http_response_code(400);
exit('Firma no válida');
}
$evento = json_decode($cuerpo, true);
http_response_code(200);
// Procesa después (tienes 10 s para responder). Deduplica por $evento['id'].- Responde 2xx en menos de 10 segundos y procesa después. Si no, se reintenta a 1 min, 5 min, 30 min, 2 h y 12 h. Tras 72 h fallando, el webhook se desactiva y se avisa a los administradores.
- Al menos una vez y sin orden garantizado: deduplica por
idy, si el orden importa, comparaupdatedAto vuelve a pedir el objeto a la API. - Prueba tu servidor con POST /v1/webhooks/{id}/ping (evento
webhook.ping) y revisa el resultado en el historial de entregas.
Excel y Power BI
Sin programar: Power Query (dentro de Excel y de Power BI) llama a la API directamente. Usa una clave con el perfil BI / cuadros de mando, sin datos fiscales.
- Excel: Datos → Obtener datos → De otras fuentes → Consulta en blanco. Power BI: Obtener datos → Consulta en blanco.
- Abre el Editor avanzado, pega esto y cambia la clave y las fechas:
Power Query (M) · balance de horas del mes
let
// Mejor como parámetro de Power Query que escrita aquí: quien tenga el archivo tiene la clave.
Clave = "fm_live_…",
Pagina = (n as number) =>
Json.Document(
Web.Contents(
"https://api.fichme.com",
[
RelativePath = "v1/hours-balance",
Query = [from = "2026-09-01", to = "2026-09-30", page = Number.ToText(n), limit = "100"],
Headers = [#"x-api-key" = Clave]
]
)
),
Primera = Pagina(1),
Resto = List.Transform({2..Primera[totalPages]}, each Pagina(_)),
Filas = List.Combine(List.Transform({Primera} & Resto, each _[data])),
Tabla = Table.FromRecords(Filas)
in
TablaSi te pregunta por las credenciales, elige Anónimo: la clave ya va en la cabecera. Cambia RelativePath y Query para otros datos (por ejemplo v1/work-sessions para el detalle por día). Con RelativePath la actualización programada funciona también en el servicio de Power BI.
Probar sin programar
Importa la especificación en tu cliente de API y tendrás todas las operaciones con sus ejemplos:
- Postman: Import → Link →
https://api.fichme.com/v1/openapi.json. En la colección, pestaña Authorization: tipo API Key, clavex-api-key, valor tu clave, en Header. - Bruno o Insomnia: importa la misma URL.
- O desde la terminal:
export FICHME_API_KEY=fm_live_…y copia cualquiera de los ejemplos en curl de esta página.
Para probar sin tocar datos reales, usa una empresa en periodo de prueba: tiene la API completa, webhooks incluidos.
Autenticación y scopes
Envía la clave en la cabecera x-api-key o como Authorization: Bearer. La clave tiene el formato fm_live_<id>_<secreto>; el id (fm_live_<id>) es el que ves en el panel, y el secreto no se guarda en FichMe. Cada clave pertenece a UNA empresa, que es la única cuyos datos ve.
Las claves caducan (por defecto a los 365 días) y pueden limitarse a una lista de IPs. Cada clave lleva solo los permisos que se le conceden; escribir no incluye leer, y aprobar no incluye crear.
| Scope | Qué permite |
|---|---|
company:read | Datos de la empresa, ajustes básicos y centros de trabajo |
employees:read | Plantilla (datos laborales, sin DNI ni nº de la Seguridad Social) |
employees:read_pii | Datos sensibles: DNI, número de la Seguridad Social y datos de salud (bajas y consultas médicas, motivos de las ausencias); también identificar o dar de alta a un empleado por su DNI |
employees:write | Alta, edición y baja de empleados |
clock:read | Fichajes y jornadas calculadas |
clock:write | Registrar fichajes en tiempo real (requiere activarlo en la empresa) |
corrections:read | Solicitudes de corrección de fichajes |
corrections:write | Crear solicitudes de corrección de fichajes |
corrections:manage | Aprobar o rechazar correcciones de fichajes |
absences:read | Ausencias, tipos de ausencia y saldos |
absences:write | Crear y cancelar ausencias |
absences:manage | Aprobar o rechazar ausencias |
balance:read | Balance de horas (previstas frente a trabajadas) |
schedule:read | Turnos, asignaciones de turno y festivos |
exports:read | Consultar y descargar exportaciones |
exports:write | Lanzar exportaciones (informes en CSV, XLSX o PDF) |
webhooks:manage | Gestionar los webhooks de la empresa |
Perfiles habituales
| Uso | Scopes |
|---|---|
| Gestoría / nóminas | company:read employees:read employees:read_pii clock:read absences:read balance:read exports:read exports:write |
| BI / cuadros de mando | company:read employees:read clock:read balance:read schedule:read absences:read |
| Terminal o ERP que ficha | employees:read clock:read clock:write |
| RR. HH. / ERP de personas | company:read employees:read employees:write absences:read absences:write absences:manage |
| Zapier / Make | employees:read clock:read absences:read |
Convenciones
- JSON en camelCase. Cada objeto lleva
object("employee","clockEntry"…). Los campos documentados siempre aparecen (connullsi no hay valor). Ignora los campos que no conozcas: añadimos campos nuevos sin cambiar de versión. - Ids: numéricos en empleados y fichajes, texto en el resto. Trátalos como opacos.
- Instantes en ISO-8601 UTC con
Z. Fechas de jornada (shiftDate,from,to) enYYYY-MM-DD, en la zona horaria de la empresa (GET /v1/mete la dice). Un turno de noche que entra el lunes a las 22:00 y sale el martes a las 06:00 pertenece a la jornada del lunes. - Duraciones en minutos enteros (
workedMinutes) y, por comodidad, enHH:mm(workedTime). - Listas:
{ "object": "list", "data": [ … ], "page", "limit", "total", "totalPages" }. Pagina con?page=1&limit=50(máximo 500). - Sincronización incremental: en empleados, fichajes y ausencias,
?updatedSince=<instante>devuelve lo cambiado desde entonces en orden, bajas incluidas condeleted: true, paginado connextCursoryhasMore(entoncespage,totalytotalPagesson null). Ver la guía. - Idempotencia: al fichar, envía
Idempotency-Key(un valor único por intento lógico). Si reintentas por un corte de red con la misma clave, recibes el mismo fichaje, nunca uno duplicado. - Registro horario: por API solo se fichan instantes en tiempo real, con la hora del servidor. El pasado se toca con solicitudes de corrección, que aprueba una persona. No hay borrado ni edición de fichajes.
Errores
Todas las respuestas de error tienen la misma forma. Programa contra code (es estable); message está en castellano y puede cambiar. Cita el requestId (también en la cabecera X-Request-Id) si escribes a soporte.
{
"error": {
"code": "insufficient_scope",
"message": "Esta operación requiere el scope clock:write.",
"param": null,
"requiredScope": "clock:write",
"requestId": "req_01K5Q9A1B2C3D4E5F6G7H8J9KM"
}
}
| HTTP | code | Qué hacer |
|---|---|---|
| 400 | invalid_request | Un campo no es válido o no está admitido. |
| 400 | invalid_date_range · date_range_too_large · conflicting_filters · page_out_of_range | Filtros de fecha o de paginación fuera de lo admitido. Para periodos largos, POST /v1/exports. |
| 400 | tenant_override_forbidden | Llegó x-tenant o ?tenant. La empresa la determina la clave. |
| 401 | missing_api_key · invalid_api_key | Falta la clave o no es válida. No reintentes: revisa la clave. |
| 401 | api_key_revoked · api_key_expired | La clave ya no sirve. Pide una nueva al administrador de la empresa. |
| 403 | insufficient_scope | A la clave le falta el scope de |
| 403 | ip_not_allowed | La IP de origen no está en la lista permitida de la clave. |
| 403 | plan_upgrade_required · subscription_inactive | El plan no incluye la API (o esa función) o la suscripción no está al día. |
| 403 | api_clocking_disabled · clocking_blocked_for_employee · employee_inactive | Fichar por API no está activado en la empresa, o ese empleado no puede fichar. |
| 403 | key_owner_required · admin_protected | La clave no tiene un administrador vigente detrás, o la operación toca una cuenta de administrador. |
| 404 | not_found · unknown_endpoint | No existe o no es de tu empresa (la respuesta es idéntica en ambos casos). |
| 409 | clock_state_conflict | El fichaje no encaja con el estado del empleado ( |
| 409 | already_exists · overlap · invalid_state · idempotency_key_reused | Ya existe ( |
| 413 | payload_too_large | El cuerpo supera 256 KB. |
| 415 | unsupported_media_type | El cuerpo tiene que ser JSON. |
| 422 | validation_failed · timestamp_out_of_window · leave_balance_insufficient · seat_limit_reached | Una regla de negocio no se cumple. El mensaje explica cuál. |
| 429 | rate_limited · too_many_concurrent_requests · daily_quota_exceeded | Espera lo que indique Retry-After y reintenta con backoff. |
| 500 | internal_error | Fallo nuestro. Reintenta con backoff; si persiste, escribe a api@fichme.com con el requestId. |
| 503 | service_unavailable | Saturación o mantenimiento temporal. Reintenta tras Retry-After. |
Límites
- 120 peticiones por minuto por clave (cabeceras
RateLimit-Limit,RateLimit-Remaining,RateLimit-Reset) y una cuota diaria según el plan, que se renueva a las 00:00 UTC (01:00 o 02:00 en España). - Los endpoints pesados (jornadas, balance, saldos y exportaciones) admiten como mucho 1 petición simultánea por clave.
- Rangos de fecha: 93 días en fichajes, jornadas y asignaciones de turno; 366 en balance y ausencias. Para más,
POST /v1/exports. - Cuerpos JSON de hasta 256 KB.
- Ante un 429 o un 503, espera lo que diga
Retry-Aftery reintenta con backoff exponencial.
Clave
Introspección de la clave y de la empresa.
GET /v1/me
Comprobar la clave
El «hola mundo» de la API: a qué empresa pertenece la clave, qué scopes tiene, cuándo caduca y qué límites le aplican. No requiere ningún scope.
Ejemplo de llamada
curl "https://api.fichme.com/v1/me" \
-H "x-api-key: $FICHME_API_KEY"const res = await fetch("https://api.fichme.com/v1/me", {
headers: {
"x-api-key": process.env.FICHME_API_KEY,
},
});
if (!res.ok) {
const { error } = await res.json();
throw new Error(`${res.status} ${error.code}: ${error.message}`);
}
const data = await res.json();import os
import requests
res = requests.get(
"https://api.fichme.com/v1/me",
headers={
"x-api-key": os.environ["FICHME_API_KEY"],
},
timeout=30,
)
if not res.ok:
error = res.json()["error"]
raise RuntimeError(f"{res.status_code} {error['code']}: {error['message']}")
data = res.json()<?php
$ch = curl_init('https://api.fichme.com/v1/me');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'x-api-key: ' . getenv('FICHME_API_KEY'),
],
]);
$body = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
$data = json_decode($body, true);
if ($status >= 400) {
throw new RuntimeException("$status {$data['error']['code']}: {$data['error']['message']}");
}Respuesta 200
La clave y su empresa · Me
{
"object": "me",
"apiKey": {
"id": "cm1key9p0008qx5b3c7n2m4wl",
"name": "Gestoría Pérez",
"prefix": "fm_live_k7m2p5x4q3ab",
"scopes": [
"company:read",
"employees:read",
"clock:read"
],
"expiresAt": "2027-09-15T10:00:00.000Z"
},
"company": {
"id": 1042,
"name": "Construcciones Ebro",
"slug": "construcciones-ebro",
"timezone": "Europe/Madrid"
},
"plan": {
"name": "Business",
"webhooks": true
},
"rateLimit": {
"perMinute": 120,
"perDay": 50000,
"maxConcurrentHeavy": 1
},
"apiVersion": "v1",
"serverTime": "2026-09-15T10:04:12.000Z"
}
Errores
Además, los comunes a todas: 400 invalid_request, 401, 403 insufficient_scope y 429 (ver Errores).
Empresa
Datos de la empresa y centros de trabajo.
GET /v1/company
Datos de la empresa
La empresa de la clave y los ajustes que hacen falta para interpretar el resto de datos. Solo lectura: los ajustes se cambian en el panel.
Ejemplo de llamada
curl "https://api.fichme.com/v1/company" \
-H "x-api-key: $FICHME_API_KEY"const res = await fetch("https://api.fichme.com/v1/company", {
headers: {
"x-api-key": process.env.FICHME_API_KEY,
},
});
if (!res.ok) {
const { error } = await res.json();
throw new Error(`${res.status} ${error.code}: ${error.message}`);
}
const data = await res.json();import os
import requests
res = requests.get(
"https://api.fichme.com/v1/company",
headers={
"x-api-key": os.environ["FICHME_API_KEY"],
},
timeout=30,
)
if not res.ok:
error = res.json()["error"]
raise RuntimeError(f"{res.status_code} {error['code']}: {error['message']}")
data = res.json()<?php
$ch = curl_init('https://api.fichme.com/v1/company');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'x-api-key: ' . getenv('FICHME_API_KEY'),
],
]);
$body = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
$data = json_decode($body, true);
if ($status >= 400) {
throw new RuntimeException("$status {$data['error']['code']}: {$data['error']['message']}");
}Respuesta 200
La empresa · Company
{
"object": "company",
"id": 1042,
"name": "Construcciones Ebro",
"legalName": "Construcciones Ebro, S.L.",
"taxId": "B50123456",
"slug": "construcciones-ebro",
"address": "Calle del Coso 42",
"city": "Zaragoza",
"province": "Zaragoza",
"postalCode": "50004",
"country": "España",
"timezone": "Europe/Madrid",
"settings": {
"workingDays": [
1,
2,
3,
4,
5
],
"lateArrivalThreshold": 10,
"earlyDepartureThreshold": 10,
"mandatoryBreakMinutes": 30,
"maxShiftHours": 16,
"apiClockingEnabled": true,
"hourBankEnabled": true,
"geolocationEnabled": false
}
}
Errores
Además, los comunes a todas: 400 invalid_request, 401, 403 insufficient_scope y 429 (ver Errores).
GET /v1/locations
Centros de trabajo
Los centros de la empresa (sin paginar: es un catálogo pequeño), el principal primero: los mismos donde se puede fichar desde la web. Además de los propios, salen los de sistema que siguen en uso (isSystem: true): «Teletrabajo» si la empresa permite teletrabajo, y «Principal» solo mientras no tenga centros propios. Los de sistema no se asignan a empleados.
Parámetros
| Nombre | En | Tipo | Descripción | Ejemplo |
|---|---|---|---|---|
isActive | query | string | Filtrar por centros activos o inactivos. | true |
Ejemplo de llamada
curl "https://api.fichme.com/v1/locations?isActive=true" \
-H "x-api-key: $FICHME_API_KEY"const params = new URLSearchParams({
isActive: "true",
});
const res = await fetch(`https://api.fichme.com/v1/locations?${params}`, {
headers: {
"x-api-key": process.env.FICHME_API_KEY,
},
});
if (!res.ok) {
const { error } = await res.json();
throw new Error(`${res.status} ${error.code}: ${error.message}`);
}
const data = await res.json();import os
import requests
res = requests.get(
"https://api.fichme.com/v1/locations",
params={
"isActive": "true",
},
headers={
"x-api-key": os.environ["FICHME_API_KEY"],
},
timeout=30,
)
if not res.ok:
error = res.json()["error"]
raise RuntimeError(f"{res.status_code} {error['code']}: {error['message']}")
data = res.json()<?php
$ch = curl_init('https://api.fichme.com/v1/locations?' . http_build_query([
'isActive' => 'true',
]));
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'x-api-key: ' . getenv('FICHME_API_KEY'),
],
]);
$body = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
$data = json_decode($body, true);
if ($status >= 400) {
throw new RuntimeException("$status {$data['error']['code']}: {$data['error']['message']}");
}Respuesta 200
Lista de centros · Lista de Location
{
"object": "list",
"data": [
{
"object": "location",
"id": "cm1loczgz0001qx8f2k9d7h3a",
"name": "Oficina Zaragoza",
"code": "ZGZ",
"address": "Calle del Coso 42",
"city": "Zaragoza",
"province": "Zaragoza",
"timezone": null,
"isActive": true,
"isPrimary": true,
"isSystem": false,
"geofence": {
"enabled": true,
"radiusMeters": 150,
"latitude": 41.6523,
"longitude": -0.8773
},
"createdAt": "2025-02-03T09:12:44.000Z",
"updatedAt": "2026-06-11T15:30:02.000Z"
}
],
"page": 1,
"limit": 1,
"total": 1,
"totalPages": 1
}
Errores
Además, los comunes a todas: 400 invalid_request, 401, 403 insufficient_scope y 429 (ver Errores).
Empleados
Plantilla. Los datos fiscales (DNI, nº S. S.) solo con employees:read_pii.
GET /v1/employees
Lista de empleados
Plantilla de la empresa, ordenada por apellidos. Por defecto solo los activos. Con updatedSince devuelve los cambiados desde ese instante (bajas incluidas, con deleted: true) y pagina por cursor.
Parámetros
| Nombre | En | Tipo | Descripción | Ejemplo |
|---|---|---|---|---|
status | query | ACTIVE | INACTIVE | ALLpor defecto "ACTIVE" | Estado operativo. Por defecto ACTIVE. | ACTIVE |
locationId | query | string ≤ 64 caracteres | Centro de trabajo (principal o de pertenencia). | |
search | query | string ≤ 100 caracteres | Texto en el nombre o el email. | |
includeDeleted | query | string | Incluir empleados dados de baja lógica. | |
updatedSince | query | string (date-time) | Sincronización incremental: solo los modificados desde este instante (ver guía). | |
cursor | query | string ≤ 200 caracteres | Cursor devuelto en nextCursor (solo con updatedSince). | |
page | query | integer por defecto 1 · mín. 1 | Página, desde 1. | |
limit | query | integer por defecto 50 · mín. 1 · máx. 500 | Resultados por página (máx. 500). | 100 |
Ejemplo de llamada
curl "https://api.fichme.com/v1/employees?status=ACTIVE&limit=100" \
-H "x-api-key: $FICHME_API_KEY"const params = new URLSearchParams({
status: "ACTIVE",
limit: "100",
});
const res = await fetch(`https://api.fichme.com/v1/employees?${params}`, {
headers: {
"x-api-key": process.env.FICHME_API_KEY,
},
});
if (!res.ok) {
const { error } = await res.json();
throw new Error(`${res.status} ${error.code}: ${error.message}`);
}
const data = await res.json();import os
import requests
res = requests.get(
"https://api.fichme.com/v1/employees",
params={
"status": "ACTIVE",
"limit": "100",
},
headers={
"x-api-key": os.environ["FICHME_API_KEY"],
},
timeout=30,
)
if not res.ok:
error = res.json()["error"]
raise RuntimeError(f"{res.status_code} {error['code']}: {error['message']}")
data = res.json()<?php
$ch = curl_init('https://api.fichme.com/v1/employees?' . http_build_query([
'status' => 'ACTIVE',
'limit' => '100',
]));
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'x-api-key: ' . getenv('FICHME_API_KEY'),
],
]);
$body = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
$data = json_decode($body, true);
if ($status >= 400) {
throw new RuntimeException("$status {$data['error']['code']}: {$data['error']['message']}");
}Respuesta 200
Lista paginada · Lista de Employee
{
"object": "list",
"data": [
{
"object": "employee",
"id": 4821,
"firstName": "Ana",
"lastName": "García López",
"name": "Ana García López",
"email": "ana.garcia@example.com",
"role": "EMPLOYEE",
"status": "ACTIVE",
"activationStatus": "ACTIVATED",
"jobTitle": "Técnica de obra",
"department": "Producción",
"hireDate": "2024-03-01",
"seniorityDate": "2024-03-01",
"contract": {
"type": "FULL_TIME",
"weeklyHours": 40,
"annualHours": 1776,
"isNightWorker": false
},
"location": {
"id": "cm1loczgz0001qx8f2k9d7h3a",
"name": "Oficina Zaragoza"
},
"locations": [
{
"id": "cm1loczgz0001qx8f2k9d7h3a",
"name": "Oficina Zaragoza",
"isPrimary": true
}
],
"clockingPolicy": {
"exempt": false,
"web": null,
"app": true,
"terminal": null,
"requireGeolocation": null
},
"identity": {
"taxId": "12345678Z",
"socialSecurityNumber": "281234567840",
"phone": "+34 600 123 456"
},
"createdAt": "2024-02-20T10:05:13.000Z",
"updatedAt": "2026-09-01T08:14:55.000Z",
"deletedAt": null
}
],
"page": 1,
"limit": 50,
"total": 1,
"totalPages": 1
}
Errores
| HTTP | code | Cuándo pasa |
|---|---|---|
| 400 | page_out_of_range | page × limit supera 50.000. Para volcados completos usa updatedSince. |
| 400 | invalid_request | cursor sin updatedSince, o un cursor manipulado o de otra clave: vuelve a empezar desde updatedSince. |
Además, los comunes a todas: 400 invalid_request, 401, 403 insufficient_scope y 429 (ver Errores).
GET /v1/employees/{id}
Un empleado
Por id de FichMe o, si tu sistema no lo conoce, por email:ana@example.com o dni:12345678Z (este último requiere employees:read_pii). Devuelve también las bajas, con deletedAt.
Parámetros
| Nombre | En | Tipo | Descripción | Ejemplo |
|---|---|---|---|---|
id obligatorio | path | string ≤ 260 caracteres | Id numérico de FichMe, o | email:ana.garcia@example.com |
Ejemplo de llamada
curl "https://api.fichme.com/v1/employees/email:ana.garcia@example.com" \
-H "x-api-key: $FICHME_API_KEY"const res = await fetch("https://api.fichme.com/v1/employees/email:ana.garcia@example.com", {
headers: {
"x-api-key": process.env.FICHME_API_KEY,
},
});
if (!res.ok) {
const { error } = await res.json();
throw new Error(`${res.status} ${error.code}: ${error.message}`);
}
const data = await res.json();import os
import requests
res = requests.get(
"https://api.fichme.com/v1/employees/email:ana.garcia@example.com",
headers={
"x-api-key": os.environ["FICHME_API_KEY"],
},
timeout=30,
)
if not res.ok:
error = res.json()["error"]
raise RuntimeError(f"{res.status_code} {error['code']}: {error['message']}")
data = res.json()<?php
$ch = curl_init('https://api.fichme.com/v1/employees/email:ana.garcia@example.com');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'x-api-key: ' . getenv('FICHME_API_KEY'),
],
]);
$body = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
$data = json_decode($body, true);
if ($status >= 400) {
throw new RuntimeException("$status {$data['error']['code']}: {$data['error']['message']}");
}Respuesta 200
El empleado · Employee
{
"object": "employee",
"id": 4821,
"firstName": "Ana",
"lastName": "García López",
"name": "Ana García López",
"email": "ana.garcia@example.com",
"role": "EMPLOYEE",
"status": "ACTIVE",
"activationStatus": "ACTIVATED",
"jobTitle": "Técnica de obra",
"department": "Producción",
"hireDate": "2024-03-01",
"seniorityDate": "2024-03-01",
"contract": {
"type": "FULL_TIME",
"weeklyHours": 40,
"annualHours": 1776,
"isNightWorker": false
},
"location": {
"id": "cm1loczgz0001qx8f2k9d7h3a",
"name": "Oficina Zaragoza"
},
"locations": [
{
"id": "cm1loczgz0001qx8f2k9d7h3a",
"name": "Oficina Zaragoza",
"isPrimary": true
}
],
"clockingPolicy": {
"exempt": false,
"web": null,
"app": true,
"terminal": null,
"requireGeolocation": null
},
"identity": {
"taxId": "12345678Z",
"socialSecurityNumber": "281234567840",
"phone": "+34 600 123 456"
},
"createdAt": "2024-02-20T10:05:13.000Z",
"updatedAt": "2026-09-01T08:14:55.000Z",
"deletedAt": null
}
Errores
| HTTP | code | Cuándo pasa |
|---|---|---|
| 404 | not_found | No hay ningún empleado con ese id, email o DNI en tu empresa. |
| 403 | insufficient_scope | Buscar por |
Además, los comunes a todas: 400 invalid_request, 401, 403 insufficient_scope y 429 (ver Errores).
POST /v1/employees
Alta de empleado
Crea un empleado (rol EMPLOYEE) con las mismas validaciones que el panel: límite de empleados del plan (422 seat_limit_reached), email y DNI únicos (409 already_exists con existingId). Si tiene email y sendWelcomeEmail es true, recibe el correo de bienvenida con su acceso.
Cuerpo (JSON)
| Campo | Tipo | Descripción | |||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
firstName obligatorio | string ≤ 100 caracteres | Nombre. | |||||||||||||||
lastName | string ≤ 150 caracteres | Apellidos. | |||||||||||||||
email | string (email) ≤ 254 caracteres | Obligatorio si no se envía dni. | |||||||||||||||
dni | string ≤ 20 caracteres | DNI/NIE. Obligatorio si no se envía email. Enviarlo requiere el scope employees:read_pii. | |||||||||||||||
socialSecurityNumber | string ≤ 30 caracteres | Nº de afiliación a la Seguridad Social. | |||||||||||||||
phone | string ≤ 30 caracteres | Teléfono. | |||||||||||||||
jobTitle | string ≤ 120 caracteres | Puesto. | |||||||||||||||
department | string ≤ 120 caracteres | Departamento. | |||||||||||||||
hireDate | string | Fecha de alta (YYYY-MM-DD). | |||||||||||||||
seniorityDate | string | Fecha de antigüedad (YYYY-MM-DD), si no coincide con el alta. | |||||||||||||||
locationId | string ≤ 64 caracteres | Centro principal. | |||||||||||||||
locationIds | string[] ≤ 50 elementos | Centros adicionales de pertenencia. | |||||||||||||||
shiftId | string ≤ 64 caracteres | Turno fijo a asignar desde hoy. | |||||||||||||||
contract | object | Datos del contrato. Campos
| |||||||||||||||
sendWelcomeEmail | boolean por defecto true | Enviar el correo de bienvenida con el acceso (requiere email). |
Ejemplo de llamada
curl -X POST "https://api.fichme.com/v1/employees" \
-H "x-api-key: $FICHME_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"firstName": "Ana",
"lastName": "García López",
"email": "ana.garcia@example.com",
"jobTitle": "Técnica de obra",
"department": "Producción",
"hireDate": "2026-09-01",
"locationId": "cm1loczgz0001qx8f2k9d7h3a",
"contract": {
"type": "FULL_TIME",
"weeklyHours": 40
},
"sendWelcomeEmail": true
}'const res = await fetch("https://api.fichme.com/v1/employees", {
method: "POST",
headers: {
"x-api-key": process.env.FICHME_API_KEY,
"Content-Type": "application/json",
},
body: JSON.stringify({
firstName: "Ana",
lastName: "García López",
email: "ana.garcia@example.com",
jobTitle: "Técnica de obra",
department: "Producción",
hireDate: "2026-09-01",
locationId: "cm1loczgz0001qx8f2k9d7h3a",
contract: {
type: "FULL_TIME",
weeklyHours: 40,
},
sendWelcomeEmail: true,
}),
});
if (!res.ok) {
const { error } = await res.json();
throw new Error(`${res.status} ${error.code}: ${error.message}`);
}
const data = await res.json();import os
import requests
res = requests.post(
"https://api.fichme.com/v1/employees",
headers={
"x-api-key": os.environ["FICHME_API_KEY"],
},
json={
"firstName": "Ana",
"lastName": "García López",
"email": "ana.garcia@example.com",
"jobTitle": "Técnica de obra",
"department": "Producción",
"hireDate": "2026-09-01",
"locationId": "cm1loczgz0001qx8f2k9d7h3a",
"contract": {
"type": "FULL_TIME",
"weeklyHours": 40,
},
"sendWelcomeEmail": True,
},
timeout=30,
)
if not res.ok:
error = res.json()["error"]
raise RuntimeError(f"{res.status_code} {error['code']}: {error['message']}")
data = res.json()<?php
$ch = curl_init('https://api.fichme.com/v1/employees');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CUSTOMREQUEST => 'POST',
CURLOPT_HTTPHEADER => [
'x-api-key: ' . getenv('FICHME_API_KEY'),
'Content-Type: application/json',
],
CURLOPT_POSTFIELDS => json_encode([
'firstName' => 'Ana',
'lastName' => 'García López',
'email' => 'ana.garcia@example.com',
'jobTitle' => 'Técnica de obra',
'department' => 'Producción',
'hireDate' => '2026-09-01',
'locationId' => 'cm1loczgz0001qx8f2k9d7h3a',
'contract' => [
'type' => 'FULL_TIME',
'weeklyHours' => 40,
],
'sendWelcomeEmail' => true,
]),
]);
$body = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
$data = json_decode($body, true);
if ($status >= 400) {
throw new RuntimeException("$status {$data['error']['code']}: {$data['error']['message']}");
}Respuesta 201
Empleado creado · Employee
{
"object": "employee",
"id": 4821,
"firstName": "Ana",
"lastName": "García López",
"name": "Ana García López",
"email": "ana.garcia@example.com",
"role": "EMPLOYEE",
"status": "ACTIVE",
"activationStatus": "PENDING_ACTIVATION",
"jobTitle": "Técnica de obra",
"department": "Producción",
"hireDate": "2026-09-01",
"seniorityDate": "2026-09-01",
"contract": {
"type": "FULL_TIME",
"weeklyHours": 40,
"annualHours": 1776,
"isNightWorker": false
},
"location": {
"id": "cm1loczgz0001qx8f2k9d7h3a",
"name": "Oficina Zaragoza"
},
"locations": [
{
"id": "cm1loczgz0001qx8f2k9d7h3a",
"name": "Oficina Zaragoza",
"isPrimary": true
}
],
"clockingPolicy": {
"exempt": false,
"web": null,
"app": true,
"terminal": null,
"requireGeolocation": null
},
"identity": {
"taxId": "12345678Z",
"socialSecurityNumber": "281234567840",
"phone": "+34 600 123 456"
},
"createdAt": "2024-02-20T10:05:13.000Z",
"updatedAt": "2026-09-01T08:14:55.000Z",
"deletedAt": null
}
Errores
| HTTP | code | Cuándo pasa |
|---|---|---|
| 409 | already_exists | Ya hay un empleado con ese email o DNI: |
| 422 | seat_limit_reached | La empresa ha llegado al máximo de empleados de su plan. |
| 404 | not_found | El centro ( |
| 422 | validation_failed | Algún centro es de sistema («Principal» o «Teletrabajo», |
| 403 | insufficient_scope | Enviar |
Además, los comunes a todas: 400 invalid_request, 401, 403 insufficient_scope y 429 (ver Errores).
PATCH /v1/employees/{id}
Editar empleado
Actualiza solo los campos enviados. No cambia el rol ni credenciales, y no toca cuentas de administrador (403 admin_protected). Reactivar (status: ACTIVE) cuenta contra el límite de empleados del plan.
Parámetros
| Nombre | En | Tipo | Descripción | Ejemplo |
|---|---|---|---|---|
id obligatorio | path | string ≤ 260 caracteres | Id numérico de FichMe, o | 4821 |
Cuerpo (JSON)
| Campo | Tipo | Descripción | |||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
firstName | string ≤ 100 caracteres | Nombre. | |||||||||||||||
lastName | string | null | Apellidos (null los borra). | |||||||||||||||
email | string (email) ≤ 254 caracteres | Email (también es su usuario de acceso). | |||||||||||||||
dni | string ≤ 20 caracteres | DNI/NIE. Cambiarlo requiere el scope employees:read_pii. | |||||||||||||||
socialSecurityNumber | string | null | Nº de afiliación a la Seguridad Social. | |||||||||||||||
phone | string | null | Teléfono. | |||||||||||||||
jobTitle | string | null | Puesto. | |||||||||||||||
department | string | null | Departamento. | |||||||||||||||
hireDate | string | null | Fecha de alta (YYYY-MM-DD). | |||||||||||||||
seniorityDate | string | null | Fecha de antigüedad (YYYY-MM-DD). | |||||||||||||||
locationId | string ≤ 64 caracteres | Cambia el centro principal y conserva los demás. | |||||||||||||||
locationIds | string[] ≤ 50 elementos | Conjunto COMPLETO de centros de pertenencia ([] = ninguno). | |||||||||||||||
contract | object | Datos del contrato (solo los campos enviados). Campos
| |||||||||||||||
status | ACTIVE | INACTIVE | Reactivar cuenta contra el límite de empleados del plan. |
Ejemplo de llamada
curl -X PATCH "https://api.fichme.com/v1/employees/4821" \
-H "x-api-key: $FICHME_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"jobTitle": "Jefa de obra",
"department": "Producción"
}'const res = await fetch("https://api.fichme.com/v1/employees/4821", {
method: "PATCH",
headers: {
"x-api-key": process.env.FICHME_API_KEY,
"Content-Type": "application/json",
},
body: JSON.stringify({
jobTitle: "Jefa de obra",
department: "Producción",
}),
});
if (!res.ok) {
const { error } = await res.json();
throw new Error(`${res.status} ${error.code}: ${error.message}`);
}
const data = await res.json();import os
import requests
res = requests.patch(
"https://api.fichme.com/v1/employees/4821",
headers={
"x-api-key": os.environ["FICHME_API_KEY"],
},
json={
"jobTitle": "Jefa de obra",
"department": "Producción",
},
timeout=30,
)
if not res.ok:
error = res.json()["error"]
raise RuntimeError(f"{res.status_code} {error['code']}: {error['message']}")
data = res.json()<?php
$ch = curl_init('https://api.fichme.com/v1/employees/4821');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CUSTOMREQUEST => 'PATCH',
CURLOPT_HTTPHEADER => [
'x-api-key: ' . getenv('FICHME_API_KEY'),
'Content-Type: application/json',
],
CURLOPT_POSTFIELDS => json_encode([
'jobTitle' => 'Jefa de obra',
'department' => 'Producción',
]),
]);
$body = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
$data = json_decode($body, true);
if ($status >= 400) {
throw new RuntimeException("$status {$data['error']['code']}: {$data['error']['message']}");
}Respuesta 200
Empleado actualizado · Employee
{
"object": "employee",
"id": 4821,
"firstName": "Ana",
"lastName": "García López",
"name": "Ana García López",
"email": "ana.garcia@example.com",
"role": "EMPLOYEE",
"status": "ACTIVE",
"activationStatus": "ACTIVATED",
"jobTitle": "Jefa de obra",
"department": "Producción",
"hireDate": "2024-03-01",
"seniorityDate": "2024-03-01",
"contract": {
"type": "FULL_TIME",
"weeklyHours": 40,
"annualHours": 1776,
"isNightWorker": false
},
"location": {
"id": "cm1loczgz0001qx8f2k9d7h3a",
"name": "Oficina Zaragoza"
},
"locations": [
{
"id": "cm1loczgz0001qx8f2k9d7h3a",
"name": "Oficina Zaragoza",
"isPrimary": true
}
],
"clockingPolicy": {
"exempt": false,
"web": null,
"app": true,
"terminal": null,
"requireGeolocation": null
},
"identity": {
"taxId": "12345678Z",
"socialSecurityNumber": "281234567840",
"phone": "+34 600 123 456"
},
"createdAt": "2024-02-20T10:05:13.000Z",
"updatedAt": "2026-09-01T08:14:55.000Z",
"deletedAt": null
}
Errores
| HTTP | code | Cuándo pasa |
|---|---|---|
| 403 | admin_protected | Es una cuenta de administrador: solo se modifica desde el panel. |
| 409 | already_exists | Otro empleado ya tiene ese email o DNI ( |
| 422 | seat_limit_reached | Reactivar ( |
| 404 | not_found | El empleado o alguno de los centros no existen. |
| 422 | validation_failed | Algún centro es de sistema («Principal» o «Teletrabajo», |
| 403 | insufficient_scope | Usar |
Además, los comunes a todas: 400 invalid_request, 401, 403 insufficient_scope y 429 (ver Errores).
POST /v1/employees/{id}/deactivate
Desactivar empleado
Baja operativa: el empleado pasa a INACTIVE, deja de poder fichar y libera su plaza del plan. Sus fichajes y datos legales se conservan. Es idempotente y reversible con PATCH { "status": "ACTIVE" }. Nunca hay borrado físico por API.
Parámetros
| Nombre | En | Tipo | Descripción | Ejemplo |
|---|---|---|---|---|
id obligatorio | path | string ≤ 260 caracteres | Id numérico de FichMe, o | 4821 |
Ejemplo de llamada
curl -X POST "https://api.fichme.com/v1/employees/4821/deactivate" \
-H "x-api-key: $FICHME_API_KEY"const res = await fetch("https://api.fichme.com/v1/employees/4821/deactivate", {
method: "POST",
headers: {
"x-api-key": process.env.FICHME_API_KEY,
},
});
if (!res.ok) {
const { error } = await res.json();
throw new Error(`${res.status} ${error.code}: ${error.message}`);
}
const data = await res.json();import os
import requests
res = requests.post(
"https://api.fichme.com/v1/employees/4821/deactivate",
headers={
"x-api-key": os.environ["FICHME_API_KEY"],
},
timeout=30,
)
if not res.ok:
error = res.json()["error"]
raise RuntimeError(f"{res.status_code} {error['code']}: {error['message']}")
data = res.json()<?php
$ch = curl_init('https://api.fichme.com/v1/employees/4821/deactivate');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CUSTOMREQUEST => 'POST',
CURLOPT_HTTPHEADER => [
'x-api-key: ' . getenv('FICHME_API_KEY'),
],
]);
$body = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
$data = json_decode($body, true);
if ($status >= 400) {
throw new RuntimeException("$status {$data['error']['code']}: {$data['error']['message']}");
}Respuesta 200
Empleado desactivado · Employee
{
"object": "employee",
"id": 4821,
"firstName": "Ana",
"lastName": "García López",
"name": "Ana García López",
"email": "ana.garcia@example.com",
"role": "EMPLOYEE",
"status": "INACTIVE",
"activationStatus": "ACTIVATED",
"jobTitle": "Técnica de obra",
"department": "Producción",
"hireDate": "2024-03-01",
"seniorityDate": "2024-03-01",
"contract": {
"type": "FULL_TIME",
"weeklyHours": 40,
"annualHours": 1776,
"isNightWorker": false
},
"location": {
"id": "cm1loczgz0001qx8f2k9d7h3a",
"name": "Oficina Zaragoza"
},
"locations": [
{
"id": "cm1loczgz0001qx8f2k9d7h3a",
"name": "Oficina Zaragoza",
"isPrimary": true
}
],
"clockingPolicy": {
"exempt": false,
"web": null,
"app": true,
"terminal": null,
"requireGeolocation": null
},
"identity": {
"taxId": "12345678Z",
"socialSecurityNumber": "281234567840",
"phone": "+34 600 123 456"
},
"createdAt": "2024-02-20T10:05:13.000Z",
"updatedAt": "2026-09-01T08:14:55.000Z",
"deletedAt": null
}
Errores
| HTTP | code | Cuándo pasa |
|---|---|---|
| 403 | admin_protected | Es una cuenta de administrador: solo se desactiva desde el panel. |
| 404 | not_found | No existe en tu empresa. |
| 403 | insufficient_scope | Usar |
Además, los comunes a todas: 400 invalid_request, 401, 403 insufficient_scope y 429 (ver Errores).
Fichajes
Pulsaciones crudas, jornadas calculadas y registro de fichajes en tiempo real.
GET /v1/clock-entries
Fichajes crudos
Cada resultado es una pulsación (IN, OUT, BREAK_START, BREAK_END), ordenadas por instante. from/to filtran por la fecha de JORNADA (shiftDate), no por el instante UTC: así un turno de noche devuelve su entrada y su salida juntas. Máximo 93 días; para más, POST /v1/exports. Una pausa se registra como OUT + BREAK_START y su vuelta como BREAK_END + IN, con el mismo instante. Para jornadas ya emparejadas y con horas calculadas usa GET /v1/work-sessions.
Parámetros
| Nombre | En | Tipo | Descripción | Ejemplo |
|---|---|---|---|---|
from | query | string | Primer día de JORNADA (shiftDate), inclusive. | 2026-09-01 |
to | query | string | Último día de jornada, inclusive. Máximo 93 días. | 2026-09-30 |
timestampFrom | query | string (date-time) | Alternativa a from/to: instante UTC inicial. | |
timestampTo | query | string (date-time) | Alternativa a from/to: instante UTC final. | |
updatedSince | query | string (date-time) | Sincronización incremental (excluye los otros filtros de fecha). | |
cursor | query | string ≤ 200 caracteres | Cursor de nextCursor (solo con updatedSince). | |
employeeId | query | integer | Solo los fichajes de este empleado. | |
type | query | IN | OUT | BREAK_START | BREAK_END | Solo este tipo de pulsación. | |
method | query | WEB | APP | QR | KIOSK | SLACK | WHATSAPP | API | Solo los registrados por este canal. | |
locationId | query | string ≤ 64 caracteres | Solo los registrados en este centro. | |
includeDeleted | query | string | Incluir fichajes eliminados (con deletedAt). | |
page | query | integer por defecto 1 · mín. 1 | Página, desde 1. | |
limit | query | integer por defecto 50 · mín. 1 · máx. 500 | Resultados por página (máx. 500). | 500 |
Ejemplo de llamada
curl "https://api.fichme.com/v1/clock-entries?from=2026-09-01&to=2026-09-30&limit=500" \
-H "x-api-key: $FICHME_API_KEY"const params = new URLSearchParams({
from: "2026-09-01",
to: "2026-09-30",
limit: "500",
});
const res = await fetch(`https://api.fichme.com/v1/clock-entries?${params}`, {
headers: {
"x-api-key": process.env.FICHME_API_KEY,
},
});
if (!res.ok) {
const { error } = await res.json();
throw new Error(`${res.status} ${error.code}: ${error.message}`);
}
const data = await res.json();import os
import requests
res = requests.get(
"https://api.fichme.com/v1/clock-entries",
params={
"from": "2026-09-01",
"to": "2026-09-30",
"limit": "500",
},
headers={
"x-api-key": os.environ["FICHME_API_KEY"],
},
timeout=30,
)
if not res.ok:
error = res.json()["error"]
raise RuntimeError(f"{res.status_code} {error['code']}: {error['message']}")
data = res.json()<?php
$ch = curl_init('https://api.fichme.com/v1/clock-entries?' . http_build_query([
'from' => '2026-09-01',
'to' => '2026-09-30',
'limit' => '500',
]));
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'x-api-key: ' . getenv('FICHME_API_KEY'),
],
]);
$body = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
$data = json_decode($body, true);
if ($status >= 400) {
throw new RuntimeException("$status {$data['error']['code']}: {$data['error']['message']}");
}Respuesta 200
Lista paginada · Lista de ClockEntry
{
"object": "list",
"data": [
{
"object": "clockEntry",
"id": 918273,
"employeeId": 4821,
"type": "IN",
"timestamp": "2026-09-15T06:58:31.000Z",
"shiftDate": "2026-09-15",
"method": "APP",
"source": "MOBILE",
"status": "COMPLETE",
"location": {
"id": "cm1loczgz0001qx8f2k9d7h3a",
"name": "Oficina Zaragoza"
},
"isModified": false,
"requiresCorrection": false,
"offlineCreated": false,
"note": null,
"adminNote": null,
"expected": {
"startTime": "09:00",
"endTime": "17:00",
"hours": 7.5,
"arrivalStatus": "on-time",
"minutesLate": -2,
"departureStatus": null,
"minutesEarly": null
},
"createdAt": "2026-09-15T06:58:31.000Z",
"updatedAt": "2026-09-15T06:58:31.000Z",
"deletedAt": null
}
],
"page": 1,
"limit": 50,
"total": 1,
"totalPages": 1
}
Errores
| HTTP | code | Cuándo pasa |
|---|---|---|
| 400 | invalid_date_range | Falta from o to (van juntos), o from es posterior a to. |
| 400 | date_range_too_large | Más de 93 días: pide el periodo por partes o usa POST /v1/exports. |
| 400 | conflicting_filters | Mezclas from/to, timestampFrom/timestampTo y updatedSince: usa uno solo. |
Además, los comunes a todas: 400 invalid_request, 401, 403 insufficient_scope y 429 (ver Errores).
GET /v1/clock-entries/{id}
Un fichaje
Un fichaje por su id (el que devuelven los listados, las jornadas y los webhooks).
Parámetros
| Nombre | En | Tipo | Descripción | Ejemplo |
|---|---|---|---|---|
id obligatorio | path | string | Id numérico. | 918273 |
Ejemplo de llamada
curl "https://api.fichme.com/v1/clock-entries/918273" \
-H "x-api-key: $FICHME_API_KEY"const res = await fetch("https://api.fichme.com/v1/clock-entries/918273", {
headers: {
"x-api-key": process.env.FICHME_API_KEY,
},
});
if (!res.ok) {
const { error } = await res.json();
throw new Error(`${res.status} ${error.code}: ${error.message}`);
}
const data = await res.json();import os
import requests
res = requests.get(
"https://api.fichme.com/v1/clock-entries/918273",
headers={
"x-api-key": os.environ["FICHME_API_KEY"],
},
timeout=30,
)
if not res.ok:
error = res.json()["error"]
raise RuntimeError(f"{res.status_code} {error['code']}: {error['message']}")
data = res.json()<?php
$ch = curl_init('https://api.fichme.com/v1/clock-entries/918273');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'x-api-key: ' . getenv('FICHME_API_KEY'),
],
]);
$body = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
$data = json_decode($body, true);
if ($status >= 400) {
throw new RuntimeException("$status {$data['error']['code']}: {$data['error']['message']}");
}Respuesta 200
El fichaje · ClockEntry
{
"object": "clockEntry",
"id": 918273,
"employeeId": 4821,
"type": "IN",
"timestamp": "2026-09-15T06:58:31.000Z",
"shiftDate": "2026-09-15",
"method": "APP",
"source": "MOBILE",
"status": "COMPLETE",
"location": {
"id": "cm1loczgz0001qx8f2k9d7h3a",
"name": "Oficina Zaragoza"
},
"isModified": false,
"requiresCorrection": false,
"offlineCreated": false,
"note": null,
"adminNote": null,
"expected": {
"startTime": "09:00",
"endTime": "17:00",
"hours": 7.5,
"arrivalStatus": "on-time",
"minutesLate": -2,
"departureStatus": null,
"minutesEarly": null
},
"createdAt": "2026-09-15T06:58:31.000Z",
"updatedAt": "2026-09-15T06:58:31.000Z",
"deletedAt": null
}
Errores
| HTTP | code | Cuándo pasa |
|---|---|---|
| 404 | not_found | No existe o no es de tu empresa. |
Además, los comunes a todas: 400 invalid_request, 401, 403 insufficient_scope y 429 (ver Errores).
GET /v1/work-sessions
Jornadas calculadas
Una fila por empleado y día del periodo, con las pulsaciones ya emparejadas en tramos y las horas calculadas con el mismo motor que el Balance de horas del panel: trabajado, previsto, balance, pausas retribuidas y descontadas, y las ausencias aprobadas del día. Es lo que necesita una nómina.
La paginación va por EMPLEADOS: limit es el número de empleados por página y total el de empleados (sin bajas ni exentos de fichar). Cada página trae todos los días de sus empleados.
Parámetros
| Nombre | En | Tipo | Descripción | Ejemplo |
|---|---|---|---|---|
from obligatorio | query | string | Primer día de jornada (inclusive). | 2026-09-01 |
to obligatorio | query | string | Último día de jornada (inclusive). Máximo 93 días. | 2026-09-30 |
employeeId | query | integer | Solo este empleado. | |
locationId | query | string ≤ 64 caracteres | Empleados de este centro (principal o de pertenencia). | |
status | query | ACTIVE | INACTIVE | ALLpor defecto "ALL" | Estado del empleado. | |
page | query | integer por defecto 1 · mín. 1 | Página, desde 1. | |
limit | query | integer por defecto 25 · mín. 1 · máx. 100 | EMPLEADOS por página (máx. 100): cada uno trae todos sus días del periodo. | 50 |
Ejemplo de llamada
curl "https://api.fichme.com/v1/work-sessions?from=2026-09-01&to=2026-09-30&limit=50" \
-H "x-api-key: $FICHME_API_KEY"const params = new URLSearchParams({
from: "2026-09-01",
to: "2026-09-30",
limit: "50",
});
const res = await fetch(`https://api.fichme.com/v1/work-sessions?${params}`, {
headers: {
"x-api-key": process.env.FICHME_API_KEY,
},
});
if (!res.ok) {
const { error } = await res.json();
throw new Error(`${res.status} ${error.code}: ${error.message}`);
}
const data = await res.json();import os
import requests
res = requests.get(
"https://api.fichme.com/v1/work-sessions",
params={
"from": "2026-09-01",
"to": "2026-09-30",
"limit": "50",
},
headers={
"x-api-key": os.environ["FICHME_API_KEY"],
},
timeout=30,
)
if not res.ok:
error = res.json()["error"]
raise RuntimeError(f"{res.status_code} {error['code']}: {error['message']}")
data = res.json()<?php
$ch = curl_init('https://api.fichme.com/v1/work-sessions?' . http_build_query([
'from' => '2026-09-01',
'to' => '2026-09-30',
'limit' => '50',
]));
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'x-api-key: ' . getenv('FICHME_API_KEY'),
],
]);
$body = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
$data = json_decode($body, true);
if ($status >= 400) {
throw new RuntimeException("$status {$data['error']['code']}: {$data['error']['message']}");
}Respuesta 200
Lista paginada por empleados · Lista de WorkSession
{
"object": "list",
"data": [
{
"object": "workSession",
"employeeId": 4821,
"shiftDate": "2026-09-15",
"dayType": "WORKING",
"status": "COMPLETE",
"scheduleSource": "SHIFT",
"firstIn": "2026-09-15T06:58:31.000Z",
"lastOut": "2026-09-15T15:02:10.000Z",
"segments": [
{
"in": "2026-09-15T06:58:31.000Z",
"out": "2026-09-15T11:00:02.000Z",
"entryIds": [
918273,
918280
],
"location": {
"id": "cm1loczgz0001qx8f2k9d7h3a",
"name": "Oficina Zaragoza"
}
},
{
"in": "2026-09-15T11:30:15.000Z",
"out": "2026-09-15T15:02:10.000Z",
"entryIds": [
918283,
918290
],
"location": {
"id": "cm1loczgz0001qx8f2k9d7h3a",
"name": "Oficina Zaragoza"
}
}
],
"breaks": [
{
"start": "2026-09-15T11:00:02.000Z",
"end": "2026-09-15T11:30:15.000Z",
"entryIds": [
918281,
918282
]
}
],
"workedMinutes": 453,
"workedTime": "07:33",
"clockedMinutes": 453,
"breakMinutes": 30,
"paidBreakMinutes": 0,
"autoDeductedBreakMinutes": 0,
"assignedMinutes": 450,
"assignedTime": "07:30",
"balanceMinutes": 3,
"balanceTime": "00:03",
"leaves": [],
"isModified": false,
"requiresCorrection": false,
"hasOpenSegment": false,
"engine": "hours_balance_v1"
}
],
"page": 1,
"limit": 25,
"total": 1,
"totalPages": 1
}
Errores
| HTTP | code | Cuándo pasa |
|---|---|---|
| 400 | invalid_date_range | Falta from o to, o from es posterior a to. |
| 400 | date_range_too_large | Más de 93 días: pide el periodo por partes o usa POST /v1/exports. |
| 429 | too_many_concurrent_requests | Supera el máximo de 1 petición simultánea por clave en los endpoints pesados: espera a que terminen. |
Además, los comunes a todas: 400 invalid_request, 401, 403 insufficient_scope y 429 (ver Errores).
POST /v1/clock
Fichar (interruptor)
Como el botón de fichar: si el empleado no está trabajando registra una ENTRADA; si lo está, una SALIDA; si está en pausa, la cierra y reanuda la jornada. La hora es la del servidor. Requiere que la empresa haya activado el fichaje por API. Envía Idempotency-Key para poder reintentar sin duplicar.
Parámetros
| Nombre | En | Tipo | Descripción | Ejemplo |
|---|---|---|---|---|
Idempotency-Key | header | string ≤ 128 caracteres | Clave única por intento lógico (≤ 128 caracteres). Un reintento con la misma clave devuelve el mismo fichaje, nunca uno duplicado. |
Cuerpo (JSON)
| Campo | Tipo | Descripción |
|---|---|---|
employeeId | integer | Id del empleado en FichMe. |
email | string (email) ≤ 254 caracteres | Email del empleado (alternativa a employeeId). |
dni | string ≤ 20 caracteres | DNI/NIE del empleado (alternativa a employeeId). Requiere el scope employees:read_pii. |
locationId | string ≤ 64 caracteres | Centro de trabajo. Si se omite en una salida, hereda el de la entrada. |
deviceTimestamp | string (date-time) | Hora del dispositivo, como evidencia. La hora del fichaje es la del servidor; más de ±5 min de diferencia → 422. |
Ejemplo de llamada
curl -X POST "https://api.fichme.com/v1/clock" \
-H "x-api-key: $FICHME_API_KEY" \
-H "Idempotency-Key: 5f1d6c2e-8a3b-4c7d-9e0f-1a2b3c4d5e6f" \
-H "Content-Type: application/json" \
-d '{
"employeeId": 4821
}'const res = await fetch("https://api.fichme.com/v1/clock", {
method: "POST",
headers: {
"x-api-key": process.env.FICHME_API_KEY,
"Idempotency-Key": crypto.randomUUID(),
"Content-Type": "application/json",
},
body: JSON.stringify({
employeeId: 4821,
}),
});
if (!res.ok) {
const { error } = await res.json();
throw new Error(`${res.status} ${error.code}: ${error.message}`);
}
const data = await res.json();import os
import uuid
import requests
res = requests.post(
"https://api.fichme.com/v1/clock",
headers={
"x-api-key": os.environ["FICHME_API_KEY"],
"Idempotency-Key": str(uuid.uuid4()),
},
json={
"employeeId": 4821,
},
timeout=30,
)
if not res.ok:
error = res.json()["error"]
raise RuntimeError(f"{res.status_code} {error['code']}: {error['message']}")
data = res.json()<?php
$ch = curl_init('https://api.fichme.com/v1/clock');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CUSTOMREQUEST => 'POST',
CURLOPT_HTTPHEADER => [
'x-api-key: ' . getenv('FICHME_API_KEY'),
'Idempotency-Key: ' . bin2hex(random_bytes(16)),
'Content-Type: application/json',
],
CURLOPT_POSTFIELDS => json_encode([
'employeeId' => 4821,
]),
]);
$body = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
$data = json_decode($body, true);
if ($status >= 400) {
throw new RuntimeException("$status {$data['error']['code']}: {$data['error']['message']}");
}Respuesta 201
Fichaje registrado (200 si es un reintento) · ClockAction
{
"object": "clockAction",
"action": "IN",
"state": "working",
"entry": {
"object": "clockEntry",
"id": 918273,
"employeeId": 4821,
"type": "IN",
"timestamp": "2026-09-15T06:58:31.000Z",
"shiftDate": "2026-09-15",
"method": "API",
"source": "API",
"status": "COMPLETE",
"location": {
"id": "cm1loczgz0001qx8f2k9d7h3a",
"name": "Oficina Zaragoza"
},
"isModified": false,
"requiresCorrection": false,
"offlineCreated": false,
"note": null,
"adminNote": null,
"expected": {
"startTime": "09:00",
"endTime": "17:00",
"hours": 7.5,
"arrivalStatus": "on-time",
"minutesLate": -2,
"departureStatus": null,
"minutesEarly": null
},
"createdAt": "2026-09-15T06:58:31.000Z",
"updatedAt": "2026-09-15T06:58:31.000Z",
"deletedAt": null
},
"entries": [
{
"object": "clockEntry",
"id": 918273,
"employeeId": 4821,
"type": "IN",
"timestamp": "2026-09-15T06:58:31.000Z",
"shiftDate": "2026-09-15",
"method": "API",
"source": "API",
"status": "COMPLETE",
"location": {
"id": "cm1loczgz0001qx8f2k9d7h3a",
"name": "Oficina Zaragoza"
},
"isModified": false,
"requiresCorrection": false,
"offlineCreated": false,
"note": null,
"adminNote": null,
"expected": {
"startTime": "09:00",
"endTime": "17:00",
"hours": 7.5,
"arrivalStatus": "on-time",
"minutesLate": -2,
"departureStatus": null,
"minutesEarly": null
},
"createdAt": "2026-09-15T06:58:31.000Z",
"updatedAt": "2026-09-15T06:58:31.000Z",
"deletedAt": null
}
],
"idempotentReplay": false
}
Errores
| HTTP | code | Cuándo pasa |
|---|---|---|
| 403 | api_clocking_disabled | La empresa no ha activado el fichaje por API (Ajustes → API). |
| 403 | employee_inactive | El empleado está INACTIVE. |
| 403 | clocking_blocked_for_employee | El empleado está exento de fichar. |
| 404 | not_found | No hay ningún empleado con ese employeeId, email o DNI en tu empresa. |
| 403 | insufficient_scope | Identificar al empleado por |
| 409 | idempotency_key_reused | Ese Idempotency-Key ya se usó para otro fichaje: genera uno nuevo por intento. |
| 422 | timestamp_out_of_window | deviceTimestamp difiere más de 5 min de la hora del servidor. El pasado va por correcciones. |
| 409 | clock_state_conflict | El estado cambió a la vez desde otro dispositivo: vuelve a leer el estado y reintenta. |
Además, los comunes a todas: 400 invalid_request, 401, 403 insufficient_scope y 429 (ver Errores).
POST /v1/clock/in
Fichar entrada
Entrada explícita, para terminales que saben qué botón se pulsó. 409 clock_state_conflict con state: ALREADY_CLOCKED_IN si ya tiene una entrada activa (aunque sea de un día anterior que aún no se ha cerrado) o BREAK_ALREADY_OPEN si está en pausa. El estado se comprueba en el momento de escribir: dos peticiones simultáneas no crean dos entradas.
Parámetros
| Nombre | En | Tipo | Descripción | Ejemplo |
|---|---|---|---|---|
Idempotency-Key | header | string ≤ 128 caracteres | Clave única por intento lógico (≤ 128 caracteres). Un reintento con la misma clave devuelve el mismo fichaje, nunca uno duplicado. |
Cuerpo (JSON)
| Campo | Tipo | Descripción |
|---|---|---|
employeeId | integer | Id del empleado en FichMe. |
email | string (email) ≤ 254 caracteres | Email del empleado (alternativa a employeeId). |
dni | string ≤ 20 caracteres | DNI/NIE del empleado (alternativa a employeeId). Requiere el scope employees:read_pii. |
locationId | string ≤ 64 caracteres | Centro de trabajo. Si se omite en una salida, hereda el de la entrada. |
deviceTimestamp | string (date-time) | Hora del dispositivo, como evidencia. La hora del fichaje es la del servidor; más de ±5 min de diferencia → 422. |
Ejemplo de llamada
curl -X POST "https://api.fichme.com/v1/clock/in" \
-H "x-api-key: $FICHME_API_KEY" \
-H "Idempotency-Key: 5f1d6c2e-8a3b-4c7d-9e0f-1a2b3c4d5e6f" \
-H "Content-Type: application/json" \
-d '{
"employeeId": 4821,
"locationId": "cm1loczgz0001qx8f2k9d7h3a"
}'const res = await fetch("https://api.fichme.com/v1/clock/in", {
method: "POST",
headers: {
"x-api-key": process.env.FICHME_API_KEY,
"Idempotency-Key": crypto.randomUUID(),
"Content-Type": "application/json",
},
body: JSON.stringify({
employeeId: 4821,
locationId: "cm1loczgz0001qx8f2k9d7h3a",
}),
});
if (!res.ok) {
const { error } = await res.json();
throw new Error(`${res.status} ${error.code}: ${error.message}`);
}
const data = await res.json();import os
import uuid
import requests
res = requests.post(
"https://api.fichme.com/v1/clock/in",
headers={
"x-api-key": os.environ["FICHME_API_KEY"],
"Idempotency-Key": str(uuid.uuid4()),
},
json={
"employeeId": 4821,
"locationId": "cm1loczgz0001qx8f2k9d7h3a",
},
timeout=30,
)
if not res.ok:
error = res.json()["error"]
raise RuntimeError(f"{res.status_code} {error['code']}: {error['message']}")
data = res.json()<?php
$ch = curl_init('https://api.fichme.com/v1/clock/in');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CUSTOMREQUEST => 'POST',
CURLOPT_HTTPHEADER => [
'x-api-key: ' . getenv('FICHME_API_KEY'),
'Idempotency-Key: ' . bin2hex(random_bytes(16)),
'Content-Type: application/json',
],
CURLOPT_POSTFIELDS => json_encode([
'employeeId' => 4821,
'locationId' => 'cm1loczgz0001qx8f2k9d7h3a',
]),
]);
$body = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
$data = json_decode($body, true);
if ($status >= 400) {
throw new RuntimeException("$status {$data['error']['code']}: {$data['error']['message']}");
}Respuesta 201
Entrada registrada · ClockAction
{
"object": "clockAction",
"action": "IN",
"state": "working",
"entry": {
"object": "clockEntry",
"id": 918273,
"employeeId": 4821,
"type": "IN",
"timestamp": "2026-09-15T06:58:31.000Z",
"shiftDate": "2026-09-15",
"method": "API",
"source": "API",
"status": "COMPLETE",
"location": {
"id": "cm1loczgz0001qx8f2k9d7h3a",
"name": "Oficina Zaragoza"
},
"isModified": false,
"requiresCorrection": false,
"offlineCreated": false,
"note": null,
"adminNote": null,
"expected": {
"startTime": "09:00",
"endTime": "17:00",
"hours": 7.5,
"arrivalStatus": "on-time",
"minutesLate": -2,
"departureStatus": null,
"minutesEarly": null
},
"createdAt": "2026-09-15T06:58:31.000Z",
"updatedAt": "2026-09-15T06:58:31.000Z",
"deletedAt": null
},
"entries": [
{
"object": "clockEntry",
"id": 918273,
"employeeId": 4821,
"type": "IN",
"timestamp": "2026-09-15T06:58:31.000Z",
"shiftDate": "2026-09-15",
"method": "API",
"source": "API",
"status": "COMPLETE",
"location": {
"id": "cm1loczgz0001qx8f2k9d7h3a",
"name": "Oficina Zaragoza"
},
"isModified": false,
"requiresCorrection": false,
"offlineCreated": false,
"note": null,
"adminNote": null,
"expected": {
"startTime": "09:00",
"endTime": "17:00",
"hours": 7.5,
"arrivalStatus": "on-time",
"minutesLate": -2,
"departureStatus": null,
"minutesEarly": null
},
"createdAt": "2026-09-15T06:58:31.000Z",
"updatedAt": "2026-09-15T06:58:31.000Z",
"deletedAt": null
}
],
"idempotentReplay": false
}
Errores
| HTTP | code | Cuándo pasa |
|---|---|---|
| 409 | clock_state_conflict |
|
| 403 | api_clocking_disabled | La empresa no ha activado el fichaje por API (Ajustes → API). |
| 403 | employee_inactive | El empleado está INACTIVE. |
| 403 | clocking_blocked_for_employee | El empleado está exento de fichar. |
| 404 | not_found | No hay ningún empleado con ese employeeId, email o DNI en tu empresa. |
| 403 | insufficient_scope | Identificar al empleado por |
| 409 | idempotency_key_reused | Ese Idempotency-Key ya se usó para otro fichaje: genera uno nuevo por intento. |
| 422 | timestamp_out_of_window | deviceTimestamp difiere más de 5 min de la hora del servidor. El pasado va por correcciones. |
Además, los comunes a todas: 400 invalid_request, 401, 403 insufficient_scope y 429 (ver Errores).
POST /v1/clock/out
Fichar salida
Salida explícita: cierra la entrada activa y hereda su jornada (shiftDate) y su centro si no se indica otro. 409 clock_state_conflict con state: NO_ACTIVE_IN si no hay entrada activa o BREAK_ALREADY_OPEN si hay una pausa abierta (ciérrala antes).
Parámetros
| Nombre | En | Tipo | Descripción | Ejemplo |
|---|---|---|---|---|
Idempotency-Key | header | string ≤ 128 caracteres | Clave única por intento lógico (≤ 128 caracteres). Un reintento con la misma clave devuelve el mismo fichaje, nunca uno duplicado. |
Cuerpo (JSON)
| Campo | Tipo | Descripción |
|---|---|---|
employeeId | integer | Id del empleado en FichMe. |
email | string (email) ≤ 254 caracteres | Email del empleado (alternativa a employeeId). |
dni | string ≤ 20 caracteres | DNI/NIE del empleado (alternativa a employeeId). Requiere el scope employees:read_pii. |
locationId | string ≤ 64 caracteres | Centro de trabajo. Si se omite en una salida, hereda el de la entrada. |
deviceTimestamp | string (date-time) | Hora del dispositivo, como evidencia. La hora del fichaje es la del servidor; más de ±5 min de diferencia → 422. |
Ejemplo de llamada
curl -X POST "https://api.fichme.com/v1/clock/out" \
-H "x-api-key: $FICHME_API_KEY" \
-H "Idempotency-Key: 5f1d6c2e-8a3b-4c7d-9e0f-1a2b3c4d5e6f" \
-H "Content-Type: application/json" \
-d '{
"employeeId": 4821
}'const res = await fetch("https://api.fichme.com/v1/clock/out", {
method: "POST",
headers: {
"x-api-key": process.env.FICHME_API_KEY,
"Idempotency-Key": crypto.randomUUID(),
"Content-Type": "application/json",
},
body: JSON.stringify({
employeeId: 4821,
}),
});
if (!res.ok) {
const { error } = await res.json();
throw new Error(`${res.status} ${error.code}: ${error.message}`);
}
const data = await res.json();import os
import uuid
import requests
res = requests.post(
"https://api.fichme.com/v1/clock/out",
headers={
"x-api-key": os.environ["FICHME_API_KEY"],
"Idempotency-Key": str(uuid.uuid4()),
},
json={
"employeeId": 4821,
},
timeout=30,
)
if not res.ok:
error = res.json()["error"]
raise RuntimeError(f"{res.status_code} {error['code']}: {error['message']}")
data = res.json()<?php
$ch = curl_init('https://api.fichme.com/v1/clock/out');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CUSTOMREQUEST => 'POST',
CURLOPT_HTTPHEADER => [
'x-api-key: ' . getenv('FICHME_API_KEY'),
'Idempotency-Key: ' . bin2hex(random_bytes(16)),
'Content-Type: application/json',
],
CURLOPT_POSTFIELDS => json_encode([
'employeeId' => 4821,
]),
]);
$body = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
$data = json_decode($body, true);
if ($status >= 400) {
throw new RuntimeException("$status {$data['error']['code']}: {$data['error']['message']}");
}Respuesta 201
Salida registrada · ClockAction
{
"object": "clockAction",
"action": "OUT",
"state": "off",
"entry": {
"object": "clockEntry",
"id": 918290,
"employeeId": 4821,
"type": "OUT",
"timestamp": "2026-09-15T15:02:10.000Z",
"shiftDate": "2026-09-15",
"method": "API",
"source": "API",
"status": "COMPLETE",
"location": {
"id": "cm1loczgz0001qx8f2k9d7h3a",
"name": "Oficina Zaragoza"
},
"isModified": false,
"requiresCorrection": false,
"offlineCreated": false,
"note": null,
"adminNote": null,
"expected": {
"startTime": "09:00",
"endTime": "17:00",
"hours": 7.5,
"arrivalStatus": null,
"minutesLate": null,
"departureStatus": "on-time",
"minutesEarly": null
},
"createdAt": "2026-09-15T15:02:10.000Z",
"updatedAt": "2026-09-15T15:02:10.000Z",
"deletedAt": null
},
"entries": [
{
"object": "clockEntry",
"id": 918290,
"employeeId": 4821,
"type": "OUT",
"timestamp": "2026-09-15T15:02:10.000Z",
"shiftDate": "2026-09-15",
"method": "API",
"source": "API",
"status": "COMPLETE",
"location": {
"id": "cm1loczgz0001qx8f2k9d7h3a",
"name": "Oficina Zaragoza"
},
"isModified": false,
"requiresCorrection": false,
"offlineCreated": false,
"note": null,
"adminNote": null,
"expected": {
"startTime": "09:00",
"endTime": "17:00",
"hours": 7.5,
"arrivalStatus": null,
"minutesLate": null,
"departureStatus": "on-time",
"minutesEarly": null
},
"createdAt": "2026-09-15T15:02:10.000Z",
"updatedAt": "2026-09-15T15:02:10.000Z",
"deletedAt": null
}
],
"idempotentReplay": false
}
Errores
| HTTP | code | Cuándo pasa |
|---|---|---|
| 409 | clock_state_conflict |
|
| 403 | api_clocking_disabled | La empresa no ha activado el fichaje por API (Ajustes → API). |
| 403 | employee_inactive | El empleado está INACTIVE. |
| 403 | clocking_blocked_for_employee | El empleado está exento de fichar. |
| 404 | not_found | No hay ningún empleado con ese employeeId, email o DNI en tu empresa. |
| 403 | insufficient_scope | Identificar al empleado por |
| 409 | idempotency_key_reused | Ese Idempotency-Key ya se usó para otro fichaje: genera uno nuevo por intento. |
| 422 | timestamp_out_of_window | deviceTimestamp difiere más de 5 min de la hora del servidor. El pasado va por correcciones. |
Además, los comunes a todas: 400 invalid_request, 401, 403 insufficient_scope y 429 (ver Errores).
POST /v1/clock/break/start
Iniciar pausa
Registra la salida y el inicio de pausa en el mismo instante (dos fichajes, de forma atómica). 409 si no está trabajando. El empleado se puede indicar por email o DNI si tu sistema no conoce su id.
Parámetros
| Nombre | En | Tipo | Descripción | Ejemplo |
|---|---|---|---|---|
Idempotency-Key | header | string ≤ 128 caracteres | Clave única por intento lógico (≤ 128 caracteres). Un reintento con la misma clave devuelve el mismo fichaje, nunca uno duplicado. |
Cuerpo (JSON)
| Campo | Tipo | Descripción |
|---|---|---|
employeeId | integer | Id del empleado en FichMe. |
email | string (email) ≤ 254 caracteres | Email del empleado (alternativa a employeeId). |
dni | string ≤ 20 caracteres | DNI/NIE del empleado (alternativa a employeeId). Requiere el scope employees:read_pii. |
locationId | string ≤ 64 caracteres | Centro de trabajo. Si se omite en una salida, hereda el de la entrada. |
deviceTimestamp | string (date-time) | Hora del dispositivo, como evidencia. La hora del fichaje es la del servidor; más de ±5 min de diferencia → 422. |
Ejemplo de llamada
curl -X POST "https://api.fichme.com/v1/clock/break/start" \
-H "x-api-key: $FICHME_API_KEY" \
-H "Idempotency-Key: 5f1d6c2e-8a3b-4c7d-9e0f-1a2b3c4d5e6f" \
-H "Content-Type: application/json" \
-d '{
"email": "ana.garcia@example.com"
}'const res = await fetch("https://api.fichme.com/v1/clock/break/start", {
method: "POST",
headers: {
"x-api-key": process.env.FICHME_API_KEY,
"Idempotency-Key": crypto.randomUUID(),
"Content-Type": "application/json",
},
body: JSON.stringify({
email: "ana.garcia@example.com",
}),
});
if (!res.ok) {
const { error } = await res.json();
throw new Error(`${res.status} ${error.code}: ${error.message}`);
}
const data = await res.json();import os
import uuid
import requests
res = requests.post(
"https://api.fichme.com/v1/clock/break/start",
headers={
"x-api-key": os.environ["FICHME_API_KEY"],
"Idempotency-Key": str(uuid.uuid4()),
},
json={
"email": "ana.garcia@example.com",
},
timeout=30,
)
if not res.ok:
error = res.json()["error"]
raise RuntimeError(f"{res.status_code} {error['code']}: {error['message']}")
data = res.json()<?php
$ch = curl_init('https://api.fichme.com/v1/clock/break/start');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CUSTOMREQUEST => 'POST',
CURLOPT_HTTPHEADER => [
'x-api-key: ' . getenv('FICHME_API_KEY'),
'Idempotency-Key: ' . bin2hex(random_bytes(16)),
'Content-Type: application/json',
],
CURLOPT_POSTFIELDS => json_encode([
'email' => 'ana.garcia@example.com',
]),
]);
$body = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
$data = json_decode($body, true);
if ($status >= 400) {
throw new RuntimeException("$status {$data['error']['code']}: {$data['error']['message']}");
}Respuesta 201
Pausa iniciada · ClockAction
{
"object": "clockAction",
"action": "BREAK_START",
"state": "on_break",
"entry": {
"object": "clockEntry",
"id": 918281,
"employeeId": 4821,
"type": "BREAK_START",
"timestamp": "2026-09-15T11:00:02.000Z",
"shiftDate": "2026-09-15",
"method": "API",
"source": "API",
"status": "COMPLETE",
"location": {
"id": "cm1loczgz0001qx8f2k9d7h3a",
"name": "Oficina Zaragoza"
},
"isModified": false,
"requiresCorrection": false,
"offlineCreated": false,
"note": null,
"adminNote": null,
"expected": {
"startTime": "09:00",
"endTime": "17:00",
"hours": 7.5,
"arrivalStatus": null,
"minutesLate": null,
"departureStatus": null,
"minutesEarly": null
},
"createdAt": "2026-09-15T11:00:02.000Z",
"updatedAt": "2026-09-15T11:00:02.000Z",
"deletedAt": null
},
"entries": [
{
"object": "clockEntry",
"id": 918280,
"employeeId": 4821,
"type": "OUT",
"timestamp": "2026-09-15T11:00:02.000Z",
"shiftDate": "2026-09-15",
"method": "API",
"source": "API",
"status": "COMPLETE",
"location": {
"id": "cm1loczgz0001qx8f2k9d7h3a",
"name": "Oficina Zaragoza"
},
"isModified": false,
"requiresCorrection": false,
"offlineCreated": false,
"note": null,
"adminNote": null,
"expected": {
"startTime": "09:00",
"endTime": "17:00",
"hours": 7.5,
"arrivalStatus": null,
"minutesLate": null,
"departureStatus": null,
"minutesEarly": null
},
"createdAt": "2026-09-15T11:00:02.000Z",
"updatedAt": "2026-09-15T11:00:02.000Z",
"deletedAt": null
},
{
"object": "clockEntry",
"id": 918281,
"employeeId": 4821,
"type": "BREAK_START",
"timestamp": "2026-09-15T11:00:02.000Z",
"shiftDate": "2026-09-15",
"method": "API",
"source": "API",
"status": "COMPLETE",
"location": {
"id": "cm1loczgz0001qx8f2k9d7h3a",
"name": "Oficina Zaragoza"
},
"isModified": false,
"requiresCorrection": false,
"offlineCreated": false,
"note": null,
"adminNote": null,
"expected": {
"startTime": "09:00",
"endTime": "17:00",
"hours": 7.5,
"arrivalStatus": null,
"minutesLate": null,
"departureStatus": null,
"minutesEarly": null
},
"createdAt": "2026-09-15T11:00:02.000Z",
"updatedAt": "2026-09-15T11:00:02.000Z",
"deletedAt": null
}
],
"idempotentReplay": false
}
Errores
| HTTP | code | Cuándo pasa |
|---|---|---|
| 409 | clock_state_conflict |
|
| 403 | api_clocking_disabled | La empresa no ha activado el fichaje por API (Ajustes → API). |
| 403 | employee_inactive | El empleado está INACTIVE. |
| 403 | clocking_blocked_for_employee | El empleado está exento de fichar. |
| 404 | not_found | No hay ningún empleado con ese employeeId, email o DNI en tu empresa. |
| 403 | insufficient_scope | Identificar al empleado por |
| 409 | idempotency_key_reused | Ese Idempotency-Key ya se usó para otro fichaje: genera uno nuevo por intento. |
| 422 | timestamp_out_of_window | deviceTimestamp difiere más de 5 min de la hora del servidor. El pasado va por correcciones. |
Además, los comunes a todas: 400 invalid_request, 401, 403 insufficient_scope y 429 (ver Errores).
POST /v1/clock/break/end
Terminar pausa
Registra el fin de pausa y la vuelta al trabajo en el mismo instante (dos fichajes, de forma atómica). 409 si no hay pausa abierta.
Parámetros
| Nombre | En | Tipo | Descripción | Ejemplo |
|---|---|---|---|---|
Idempotency-Key | header | string ≤ 128 caracteres | Clave única por intento lógico (≤ 128 caracteres). Un reintento con la misma clave devuelve el mismo fichaje, nunca uno duplicado. |
Cuerpo (JSON)
| Campo | Tipo | Descripción |
|---|---|---|
employeeId | integer | Id del empleado en FichMe. |
email | string (email) ≤ 254 caracteres | Email del empleado (alternativa a employeeId). |
dni | string ≤ 20 caracteres | DNI/NIE del empleado (alternativa a employeeId). Requiere el scope employees:read_pii. |
locationId | string ≤ 64 caracteres | Centro de trabajo. Si se omite en una salida, hereda el de la entrada. |
deviceTimestamp | string (date-time) | Hora del dispositivo, como evidencia. La hora del fichaje es la del servidor; más de ±5 min de diferencia → 422. |
Ejemplo de llamada
curl -X POST "https://api.fichme.com/v1/clock/break/end" \
-H "x-api-key: $FICHME_API_KEY" \
-H "Idempotency-Key: 5f1d6c2e-8a3b-4c7d-9e0f-1a2b3c4d5e6f" \
-H "Content-Type: application/json" \
-d '{
"email": "ana.garcia@example.com"
}'const res = await fetch("https://api.fichme.com/v1/clock/break/end", {
method: "POST",
headers: {
"x-api-key": process.env.FICHME_API_KEY,
"Idempotency-Key": crypto.randomUUID(),
"Content-Type": "application/json",
},
body: JSON.stringify({
email: "ana.garcia@example.com",
}),
});
if (!res.ok) {
const { error } = await res.json();
throw new Error(`${res.status} ${error.code}: ${error.message}`);
}
const data = await res.json();import os
import uuid
import requests
res = requests.post(
"https://api.fichme.com/v1/clock/break/end",
headers={
"x-api-key": os.environ["FICHME_API_KEY"],
"Idempotency-Key": str(uuid.uuid4()),
},
json={
"email": "ana.garcia@example.com",
},
timeout=30,
)
if not res.ok:
error = res.json()["error"]
raise RuntimeError(f"{res.status_code} {error['code']}: {error['message']}")
data = res.json()<?php
$ch = curl_init('https://api.fichme.com/v1/clock/break/end');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CUSTOMREQUEST => 'POST',
CURLOPT_HTTPHEADER => [
'x-api-key: ' . getenv('FICHME_API_KEY'),
'Idempotency-Key: ' . bin2hex(random_bytes(16)),
'Content-Type: application/json',
],
CURLOPT_POSTFIELDS => json_encode([
'email' => 'ana.garcia@example.com',
]),
]);
$body = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
$data = json_decode($body, true);
if ($status >= 400) {
throw new RuntimeException("$status {$data['error']['code']}: {$data['error']['message']}");
}Respuesta 201
Pausa terminada · ClockAction
{
"object": "clockAction",
"action": "BREAK_END",
"state": "working",
"entry": {
"object": "clockEntry",
"id": 918282,
"employeeId": 4821,
"type": "BREAK_END",
"timestamp": "2026-09-15T11:30:15.000Z",
"shiftDate": "2026-09-15",
"method": "API",
"source": "API",
"status": "COMPLETE",
"location": {
"id": "cm1loczgz0001qx8f2k9d7h3a",
"name": "Oficina Zaragoza"
},
"isModified": false,
"requiresCorrection": false,
"offlineCreated": false,
"note": null,
"adminNote": null,
"expected": {
"startTime": "09:00",
"endTime": "17:00",
"hours": 7.5,
"arrivalStatus": null,
"minutesLate": null,
"departureStatus": null,
"minutesEarly": null
},
"createdAt": "2026-09-15T11:30:15.000Z",
"updatedAt": "2026-09-15T11:30:15.000Z",
"deletedAt": null
},
"entries": [
{
"object": "clockEntry",
"id": 918282,
"employeeId": 4821,
"type": "BREAK_END",
"timestamp": "2026-09-15T11:30:15.000Z",
"shiftDate": "2026-09-15",
"method": "API",
"source": "API",
"status": "COMPLETE",
"location": {
"id": "cm1loczgz0001qx8f2k9d7h3a",
"name": "Oficina Zaragoza"
},
"isModified": false,
"requiresCorrection": false,
"offlineCreated": false,
"note": null,
"adminNote": null,
"expected": {
"startTime": "09:00",
"endTime": "17:00",
"hours": 7.5,
"arrivalStatus": null,
"minutesLate": null,
"departureStatus": null,
"minutesEarly": null
},
"createdAt": "2026-09-15T11:30:15.000Z",
"updatedAt": "2026-09-15T11:30:15.000Z",
"deletedAt": null
},
{
"object": "clockEntry",
"id": 918283,
"employeeId": 4821,
"type": "IN",
"timestamp": "2026-09-15T11:30:15.000Z",
"shiftDate": "2026-09-15",
"method": "API",
"source": "API",
"status": "COMPLETE",
"location": {
"id": "cm1loczgz0001qx8f2k9d7h3a",
"name": "Oficina Zaragoza"
},
"isModified": false,
"requiresCorrection": false,
"offlineCreated": false,
"note": null,
"adminNote": null,
"expected": {
"startTime": "09:00",
"endTime": "17:00",
"hours": 7.5,
"arrivalStatus": null,
"minutesLate": null,
"departureStatus": null,
"minutesEarly": null
},
"createdAt": "2026-09-15T11:30:15.000Z",
"updatedAt": "2026-09-15T11:30:15.000Z",
"deletedAt": null
}
],
"idempotentReplay": false
}
Errores
| HTTP | code | Cuándo pasa |
|---|---|---|
| 409 | clock_state_conflict |
|
| 403 | api_clocking_disabled | La empresa no ha activado el fichaje por API (Ajustes → API). |
| 403 | employee_inactive | El empleado está INACTIVE. |
| 403 | clocking_blocked_for_employee | El empleado está exento de fichar. |
| 404 | not_found | No hay ningún empleado con ese employeeId, email o DNI en tu empresa. |
| 403 | insufficient_scope | Identificar al empleado por |
| 409 | idempotency_key_reused | Ese Idempotency-Key ya se usó para otro fichaje: genera uno nuevo por intento. |
| 422 | timestamp_out_of_window | deviceTimestamp difiere más de 5 min de la hora del servidor. El pasado va por correcciones. |
Además, los comunes a todas: 400 invalid_request, 401, 403 insufficient_scope y 429 (ver Errores).
Correcciones
Solicitudes de corrección de fichajes: la única vía para tocar el pasado.
GET /v1/clock-corrections
Solicitudes de corrección
Ordenadas de la más reciente a la más antigua. from/to filtran por fecha de creación.
Parámetros
| Nombre | En | Tipo | Descripción | Ejemplo |
|---|---|---|---|---|
status | query | PENDING | APPROVED | REJECTED | PENDING para la bandeja de aprobaciones. | PENDING |
employeeId | query | integer | Solo las de este empleado. | |
requestType | query | MISSING_CLOCK_IN | MISSING_CLOCK_OUT | INCORRECT_TIME | MANUAL_FULL_SHIFT | MANUAL_BREAK | Solo este tipo de solicitud. | |
from | query | string | Creadas desde este día (inclusive). | |
to | query | string | Creadas hasta este día (inclusive). Máximo 366 días. | |
page | query | integer por defecto 1 · mín. 1 | Página, desde 1. | |
limit | query | integer por defecto 50 · mín. 1 · máx. 500 | Resultados por página (máx. 500). |
Ejemplo de llamada
curl "https://api.fichme.com/v1/clock-corrections?status=PENDING" \
-H "x-api-key: $FICHME_API_KEY"const params = new URLSearchParams({
status: "PENDING",
});
const res = await fetch(`https://api.fichme.com/v1/clock-corrections?${params}`, {
headers: {
"x-api-key": process.env.FICHME_API_KEY,
},
});
if (!res.ok) {
const { error } = await res.json();
throw new Error(`${res.status} ${error.code}: ${error.message}`);
}
const data = await res.json();import os
import requests
res = requests.get(
"https://api.fichme.com/v1/clock-corrections",
params={
"status": "PENDING",
},
headers={
"x-api-key": os.environ["FICHME_API_KEY"],
},
timeout=30,
)
if not res.ok:
error = res.json()["error"]
raise RuntimeError(f"{res.status_code} {error['code']}: {error['message']}")
data = res.json()<?php
$ch = curl_init('https://api.fichme.com/v1/clock-corrections?' . http_build_query([
'status' => 'PENDING',
]));
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'x-api-key: ' . getenv('FICHME_API_KEY'),
],
]);
$body = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
$data = json_decode($body, true);
if ($status >= 400) {
throw new RuntimeException("$status {$data['error']['code']}: {$data['error']['message']}");
}Respuesta 200
Lista paginada · Lista de ClockCorrection
{
"object": "list",
"data": [
{
"object": "clockCorrection",
"id": "cm1corr8k2p0007qx3n5v1b9d",
"employeeId": 4821,
"clockEntryId": null,
"requestType": "MANUAL_FULL_SHIFT",
"missingType": null,
"proposedTime": null,
"originalTime": null,
"manual": {
"shiftDate": "2026-09-14",
"start": "2026-09-14T07:00:00.000Z",
"end": "2026-09-14T15:00:00.000Z"
},
"location": {
"id": "cm1loczgz0001qx8f2k9d7h3a",
"name": "Oficina Zaragoza"
},
"reason": "Olvidé fichar: estuve todo el día en la obra de Valdespartera.",
"status": "PENDING",
"reviewedAt": null,
"reviewedById": null,
"rejectionReason": null,
"isAutoApproved": false,
"createdAt": "2026-09-15T07:05:12.000Z",
"updatedAt": "2026-09-15T07:05:12.000Z"
}
],
"page": 1,
"limit": 50,
"total": 1,
"totalPages": 1
}
Errores
| HTTP | code | Cuándo pasa |
|---|---|---|
| 400 | date_range_too_large | Más de 366 días entre from y to. |
Además, los comunes a todas: 400 invalid_request, 401, 403 insufficient_scope y 429 (ver Errores).
GET /v1/clock-corrections/{id}
Una solicitud de corrección
Una solicitud por su id, con su estado actual.
Parámetros
| Nombre | En | Tipo | Descripción | Ejemplo |
|---|---|---|---|---|
id obligatorio | path | string ≤ 64 caracteres | Id de la solicitud de corrección. | cm1corr8k2p0007qx3n5v1b9d |
Ejemplo de llamada
curl "https://api.fichme.com/v1/clock-corrections/cm1corr8k2p0007qx3n5v1b9d" \
-H "x-api-key: $FICHME_API_KEY"const res = await fetch("https://api.fichme.com/v1/clock-corrections/cm1corr8k2p0007qx3n5v1b9d", {
headers: {
"x-api-key": process.env.FICHME_API_KEY,
},
});
if (!res.ok) {
const { error } = await res.json();
throw new Error(`${res.status} ${error.code}: ${error.message}`);
}
const data = await res.json();import os
import requests
res = requests.get(
"https://api.fichme.com/v1/clock-corrections/cm1corr8k2p0007qx3n5v1b9d",
headers={
"x-api-key": os.environ["FICHME_API_KEY"],
},
timeout=30,
)
if not res.ok:
error = res.json()["error"]
raise RuntimeError(f"{res.status_code} {error['code']}: {error['message']}")
data = res.json()<?php
$ch = curl_init('https://api.fichme.com/v1/clock-corrections/cm1corr8k2p0007qx3n5v1b9d');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'x-api-key: ' . getenv('FICHME_API_KEY'),
],
]);
$body = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
$data = json_decode($body, true);
if ($status >= 400) {
throw new RuntimeException("$status {$data['error']['code']}: {$data['error']['message']}");
}Respuesta 200
La solicitud · ClockCorrection
{
"object": "clockCorrection",
"id": "cm1corr8k2p0007qx3n5v1b9d",
"employeeId": 4821,
"clockEntryId": null,
"requestType": "MANUAL_FULL_SHIFT",
"missingType": null,
"proposedTime": null,
"originalTime": null,
"manual": {
"shiftDate": "2026-09-14",
"start": "2026-09-14T07:00:00.000Z",
"end": "2026-09-14T15:00:00.000Z"
},
"location": {
"id": "cm1loczgz0001qx8f2k9d7h3a",
"name": "Oficina Zaragoza"
},
"reason": "Olvidé fichar: estuve todo el día en la obra de Valdespartera.",
"status": "PENDING",
"reviewedAt": null,
"reviewedById": null,
"rejectionReason": null,
"isAutoApproved": false,
"createdAt": "2026-09-15T07:05:12.000Z",
"updatedAt": "2026-09-15T07:05:12.000Z"
}
Errores
| HTTP | code | Cuándo pasa |
|---|---|---|
| 404 | not_found | No existe o no es de tu empresa. |
Además, los comunes a todas: 400 invalid_request, 401, 403 insufficient_scope y 429 (ver Errores).
POST /v1/clock-corrections
Solicitar una corrección
La única vía para cambiar el pasado del registro horario. Nace PENDIENTE y la aprueba una persona desde el panel (o por API con corrections:manage). Dos formas:
type: "SINGLE" corrige o añade UN extremo (entrada o salida). Corregir entrada y salida de una misma sesión son dos solicitudes independientes: si la sesión se desplaza a más tarde, envía primero la salida.
type: "FULL_SHIFT" registra una jornada completa olvidada en una sola solicitud (recomendado). Con autoApprove: true y corrections:manage, los fichajes se crean en el acto y figura como autor el administrador que creó la clave. Las jornadas de un empleado ADMINISTRADOR solo se admiten así.
Cuerpo (JSON)
Una de estas formas, según type:
type: "SINGLE" Corregir o añadir UN extremo (entrada o salida).
| Campo | Tipo | Descripción |
|---|---|---|
type obligatorio | "SINGLE" | Corregir o añadir UN extremo (entrada o salida). |
employeeId | integer | Id del empleado en FichMe. |
email | string (email) ≤ 254 caracteres | Email del empleado (alternativa a employeeId). |
dni | string ≤ 20 caracteres | DNI/NIE del empleado (alternativa a employeeId). Requiere el scope employees:read_pii. |
missingType obligatorio | IN | OUT | Qué extremo se corrige o falta. |
proposedTime obligatorio | string (date-time) | Instante propuesto (ISO-8601 con zona). |
clockEntryId | integer | Fichaje a corregir. Sin él, se propone uno que falta. |
pairEntryId | integer | El otro extremo de la misma sesión, si lo conoces. |
locationId | string ≤ 64 caracteres | Centro del fichaje. |
reason obligatorio | string ≤ 1000 caracteres | Motivo (mínimo 10 caracteres): lo lee quien la aprueba. |
autoApprove | boolean por defecto false | Aprobar en la misma llamada (requiere corrections:manage). |
type: "FULL_SHIFT" Jornada completa olvidada: entrada y salida en una sola solicitud.
| Campo | Tipo | Descripción |
|---|---|---|
type obligatorio | "FULL_SHIFT" | Jornada completa olvidada: entrada y salida en una sola solicitud. |
employeeId | integer | Id del empleado en FichMe. |
email | string (email) ≤ 254 caracteres | Email del empleado (alternativa a employeeId). |
dni | string ≤ 20 caracteres | DNI/NIE del empleado (alternativa a employeeId). Requiere el scope employees:read_pii. |
shiftDate obligatorio | string | Día de la jornada (YYYY-MM-DD). |
clockInTime obligatorio | string | Hora de entrada (HH:mm, hora local de la empresa). |
clockOutTime obligatorio | string | Hora de salida (HH:mm). Si es anterior a la entrada, sale al día siguiente (turno de noche). |
locationId | string ≤ 64 caracteres | Centro de la jornada. |
reason obligatorio | string ≤ 1000 caracteres | Motivo (mínimo 10 caracteres): lo lee quien la aprueba. |
autoApprove | boolean por defecto false | Crear los fichajes en el acto (requiere corrections:manage). |
Ejemplo de llamada
curl -X POST "https://api.fichme.com/v1/clock-corrections" \
-H "x-api-key: $FICHME_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"type": "FULL_SHIFT",
"employeeId": 4821,
"shiftDate": "2026-09-14",
"clockInTime": "09:00",
"clockOutTime": "17:00",
"reason": "Olvidé fichar: estuve todo el día en la obra de Valdespartera."
}'const res = await fetch("https://api.fichme.com/v1/clock-corrections", {
method: "POST",
headers: {
"x-api-key": process.env.FICHME_API_KEY,
"Content-Type": "application/json",
},
body: JSON.stringify({
type: "FULL_SHIFT",
employeeId: 4821,
shiftDate: "2026-09-14",
clockInTime: "09:00",
clockOutTime: "17:00",
reason: "Olvidé fichar: estuve todo el día en la obra de Valdespartera.",
}),
});
if (!res.ok) {
const { error } = await res.json();
throw new Error(`${res.status} ${error.code}: ${error.message}`);
}
const data = await res.json();import os
import requests
res = requests.post(
"https://api.fichme.com/v1/clock-corrections",
headers={
"x-api-key": os.environ["FICHME_API_KEY"],
},
json={
"type": "FULL_SHIFT",
"employeeId": 4821,
"shiftDate": "2026-09-14",
"clockInTime": "09:00",
"clockOutTime": "17:00",
"reason": "Olvidé fichar: estuve todo el día en la obra de Valdespartera.",
},
timeout=30,
)
if not res.ok:
error = res.json()["error"]
raise RuntimeError(f"{res.status_code} {error['code']}: {error['message']}")
data = res.json()<?php
$ch = curl_init('https://api.fichme.com/v1/clock-corrections');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CUSTOMREQUEST => 'POST',
CURLOPT_HTTPHEADER => [
'x-api-key: ' . getenv('FICHME_API_KEY'),
'Content-Type: application/json',
],
CURLOPT_POSTFIELDS => json_encode([
'type' => 'FULL_SHIFT',
'employeeId' => 4821,
'shiftDate' => '2026-09-14',
'clockInTime' => '09:00',
'clockOutTime' => '17:00',
'reason' => 'Olvidé fichar: estuve todo el día en la obra de Valdespartera.',
]),
]);
$body = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
$data = json_decode($body, true);
if ($status >= 400) {
throw new RuntimeException("$status {$data['error']['code']}: {$data['error']['message']}");
}Respuesta 201
Solicitud creada · ClockCorrection
{
"object": "clockCorrection",
"id": "cm1corr8k2p0007qx3n5v1b9d",
"employeeId": 4821,
"clockEntryId": null,
"requestType": "MANUAL_FULL_SHIFT",
"missingType": null,
"proposedTime": null,
"originalTime": null,
"manual": {
"shiftDate": "2026-09-14",
"start": "2026-09-14T07:00:00.000Z",
"end": "2026-09-14T15:00:00.000Z"
},
"location": {
"id": "cm1loczgz0001qx8f2k9d7h3a",
"name": "Oficina Zaragoza"
},
"reason": "Olvidé fichar: estuve todo el día en la obra de Valdespartera.",
"status": "PENDING",
"reviewedAt": null,
"reviewedById": null,
"rejectionReason": null,
"isAutoApproved": false,
"createdAt": "2026-09-15T07:05:12.000Z",
"updatedAt": "2026-09-15T07:05:12.000Z"
}
Errores
| HTTP | code | Cuándo pasa |
|---|---|---|
| 422 | validation_failed | Fecha u hora futuras (por API solo se corrige el pasado), o jornada de un ADMIN sin autoApprove. |
| 403 | insufficient_scope | autoApprove sin el scope corrections:manage, o el empleado por |
| 403 | key_owner_required | autoApprove, pero el administrador que creó la clave ya no lo es. |
| 404 | not_found | El empleado, el fichaje ( |
Además, los comunes a todas: 400 invalid_request, 401, 403 insufficient_scope y 429 (ver Errores).
POST /v1/clock-corrections/{id}/approve
Aprobar una corrección
Aplica la corrección sobre el registro horario (la cadena de integridad se vuelve a firmar) y avisa al empleado. Figura como revisor el administrador que creó la clave (403 key_owner_required si ya no lo es).
Parámetros
| Nombre | En | Tipo | Descripción | Ejemplo |
|---|---|---|---|---|
id obligatorio | path | string ≤ 64 caracteres | Id de la solicitud de corrección. | cm1corr8k2p0007qx3n5v1b9d |
Ejemplo de llamada
curl -X POST "https://api.fichme.com/v1/clock-corrections/cm1corr8k2p0007qx3n5v1b9d/approve" \
-H "x-api-key: $FICHME_API_KEY"const res = await fetch("https://api.fichme.com/v1/clock-corrections/cm1corr8k2p0007qx3n5v1b9d/approve", {
method: "POST",
headers: {
"x-api-key": process.env.FICHME_API_KEY,
},
});
if (!res.ok) {
const { error } = await res.json();
throw new Error(`${res.status} ${error.code}: ${error.message}`);
}
const data = await res.json();import os
import requests
res = requests.post(
"https://api.fichme.com/v1/clock-corrections/cm1corr8k2p0007qx3n5v1b9d/approve",
headers={
"x-api-key": os.environ["FICHME_API_KEY"],
},
timeout=30,
)
if not res.ok:
error = res.json()["error"]
raise RuntimeError(f"{res.status_code} {error['code']}: {error['message']}")
data = res.json()<?php
$ch = curl_init('https://api.fichme.com/v1/clock-corrections/cm1corr8k2p0007qx3n5v1b9d/approve');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CUSTOMREQUEST => 'POST',
CURLOPT_HTTPHEADER => [
'x-api-key: ' . getenv('FICHME_API_KEY'),
],
]);
$body = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
$data = json_decode($body, true);
if ($status >= 400) {
throw new RuntimeException("$status {$data['error']['code']}: {$data['error']['message']}");
}Respuesta 200
Solicitud revisada · ClockCorrection
{
"object": "clockCorrection",
"id": "cm1corr8k2p0007qx3n5v1b9d",
"employeeId": 4821,
"clockEntryId": null,
"requestType": "MANUAL_FULL_SHIFT",
"missingType": null,
"proposedTime": null,
"originalTime": null,
"manual": {
"shiftDate": "2026-09-14",
"start": "2026-09-14T07:00:00.000Z",
"end": "2026-09-14T15:00:00.000Z"
},
"location": {
"id": "cm1loczgz0001qx8f2k9d7h3a",
"name": "Oficina Zaragoza"
},
"reason": "Olvidé fichar: estuve todo el día en la obra de Valdespartera.",
"status": "APPROVED",
"reviewedAt": "2026-09-15T09:40:31.000Z",
"reviewedById": 17,
"rejectionReason": null,
"isAutoApproved": false,
"createdAt": "2026-09-15T07:05:12.000Z",
"updatedAt": "2026-09-15T09:40:31.000Z"
}
Errores
| HTTP | code | Cuándo pasa |
|---|---|---|
| 409 | invalid_state | La solicitud ya no está PENDING ( |
| 403 | key_owner_required | El administrador que creó la clave ya no lo es: crea una clave nueva. |
| 404 | not_found | No existe o no es de tu empresa. |
Además, los comunes a todas: 400 invalid_request, 401, 403 insufficient_scope y 429 (ver Errores).
POST /v1/clock-corrections/{id}/reject
Rechazar una corrección
Rechaza la solicitud con un motivo (se envía al empleado). El registro horario no cambia.
Parámetros
| Nombre | En | Tipo | Descripción | Ejemplo |
|---|---|---|---|---|
id obligatorio | path | string ≤ 64 caracteres | Id de la solicitud de corrección. | cm1corr8k2p0007qx3n5v1b9d |
Cuerpo (JSON)
| Campo | Tipo | Descripción |
|---|---|---|
reason obligatorio | string ≤ 1000 caracteres | Motivo del rechazo (mínimo 10 caracteres): se envía al empleado. |
Ejemplo de llamada
curl -X POST "https://api.fichme.com/v1/clock-corrections/cm1corr8k2p0007qx3n5v1b9d/reject" \
-H "x-api-key: $FICHME_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"reason": "Ese día estabas de vacaciones: revisa la fecha."
}'const res = await fetch("https://api.fichme.com/v1/clock-corrections/cm1corr8k2p0007qx3n5v1b9d/reject", {
method: "POST",
headers: {
"x-api-key": process.env.FICHME_API_KEY,
"Content-Type": "application/json",
},
body: JSON.stringify({
reason: "Ese día estabas de vacaciones: revisa la fecha.",
}),
});
if (!res.ok) {
const { error } = await res.json();
throw new Error(`${res.status} ${error.code}: ${error.message}`);
}
const data = await res.json();import os
import requests
res = requests.post(
"https://api.fichme.com/v1/clock-corrections/cm1corr8k2p0007qx3n5v1b9d/reject",
headers={
"x-api-key": os.environ["FICHME_API_KEY"],
},
json={
"reason": "Ese día estabas de vacaciones: revisa la fecha.",
},
timeout=30,
)
if not res.ok:
error = res.json()["error"]
raise RuntimeError(f"{res.status_code} {error['code']}: {error['message']}")
data = res.json()<?php
$ch = curl_init('https://api.fichme.com/v1/clock-corrections/cm1corr8k2p0007qx3n5v1b9d/reject');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CUSTOMREQUEST => 'POST',
CURLOPT_HTTPHEADER => [
'x-api-key: ' . getenv('FICHME_API_KEY'),
'Content-Type: application/json',
],
CURLOPT_POSTFIELDS => json_encode([
'reason' => 'Ese día estabas de vacaciones: revisa la fecha.',
]),
]);
$body = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
$data = json_decode($body, true);
if ($status >= 400) {
throw new RuntimeException("$status {$data['error']['code']}: {$data['error']['message']}");
}Respuesta 200
Solicitud rechazada · ClockCorrection
{
"object": "clockCorrection",
"id": "cm1corr8k2p0007qx3n5v1b9d",
"employeeId": 4821,
"clockEntryId": null,
"requestType": "MANUAL_FULL_SHIFT",
"missingType": null,
"proposedTime": null,
"originalTime": null,
"manual": {
"shiftDate": "2026-09-14",
"start": "2026-09-14T07:00:00.000Z",
"end": "2026-09-14T15:00:00.000Z"
},
"location": {
"id": "cm1loczgz0001qx8f2k9d7h3a",
"name": "Oficina Zaragoza"
},
"reason": "Olvidé fichar: estuve todo el día en la obra de Valdespartera.",
"status": "REJECTED",
"reviewedAt": "2026-09-15T09:40:31.000Z",
"reviewedById": 17,
"rejectionReason": "Ese día estabas de vacaciones: revisa la fecha.",
"isAutoApproved": false,
"createdAt": "2026-09-15T07:05:12.000Z",
"updatedAt": "2026-09-15T09:40:31.000Z"
}
Errores
| HTTP | code | Cuándo pasa |
|---|---|---|
| 409 | invalid_state | La solicitud ya no está PENDING. |
| 403 | key_owner_required | El administrador que creó la clave ya no lo es. |
| 404 | not_found | No existe o no es de tu empresa. |
Además, los comunes a todas: 400 invalid_request, 401, 403 insufficient_scope y 429 (ver Errores).
Ausencias
Tipos, solicitudes y saldos de ausencias (vacaciones, permisos, bajas...). Las ausencias de salud (bajas, consultas médicas) y los motivos escritos son datos sensibles: solo con employees:read_pii.
GET /v1/leave-types
Tipos de ausencia
Catálogo de la empresa (configurable por cada una). Integra por code, no por el nombre. Los tipos con isSystem: true los gestiona FichMe (p. ej. el descanso compensatorio de la bolsa de horas): aparecen en las ausencias pero no se pueden usar al crearlas.
Parámetros
| Nombre | En | Tipo | Descripción | Ejemplo |
|---|---|---|---|---|
includeInactive | query | string | Incluir los tipos desactivados. |
Ejemplo de llamada
curl "https://api.fichme.com/v1/leave-types" \
-H "x-api-key: $FICHME_API_KEY"const res = await fetch("https://api.fichme.com/v1/leave-types", {
headers: {
"x-api-key": process.env.FICHME_API_KEY,
},
});
if (!res.ok) {
const { error } = await res.json();
throw new Error(`${res.status} ${error.code}: ${error.message}`);
}
const data = await res.json();import os
import requests
res = requests.get(
"https://api.fichme.com/v1/leave-types",
headers={
"x-api-key": os.environ["FICHME_API_KEY"],
},
timeout=30,
)
if not res.ok:
error = res.json()["error"]
raise RuntimeError(f"{res.status_code} {error['code']}: {error['message']}")
data = res.json()<?php
$ch = curl_init('https://api.fichme.com/v1/leave-types');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'x-api-key: ' . getenv('FICHME_API_KEY'),
],
]);
$body = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
$data = json_decode($body, true);
if ($status >= 400) {
throw new RuntimeException("$status {$data['error']['code']}: {$data['error']['message']}");
}Respuesta 200
Lista de tipos · Lista de LeaveType
{
"object": "list",
"data": [
{
"object": "leaveType",
"id": "cm1ltvac0001qx7d2k8f4h6jk",
"code": "VACATION",
"name": "Vacaciones",
"description": "Vacaciones anuales retribuidas.",
"color": "#3B82F6",
"unit": "DAYS",
"dayCountType": "BUSINESS_DAYS",
"requiresBalance": true,
"requiresApproval": true,
"requiresDocument": false,
"requiresReason": false,
"allowHalfDays": true,
"isPaid": true,
"blockClocking": true,
"affectsWorkingDays": true,
"isActive": true,
"isSystem": false,
"createdAt": "2025-01-08T11:20:00.000Z",
"updatedAt": "2026-01-02T09:00:00.000Z"
}
],
"page": 1,
"limit": 1,
"total": 1,
"totalPages": 1
}
Errores
| HTTP | code | Cuándo pasa |
|---|---|---|
| 403 | plan_upgrade_required | El plan de la empresa no incluye la gestión de ausencias. |
Además, los comunes a todas: 400 invalid_request, 401, 403 insufficient_scope y 429 (ver Errores).
GET /v1/leave-requests
Ausencias
Solicitudes de ausencia (vacaciones, permisos, bajas…), con su estado. from/to devuelven las que se solapan con el periodo. Para el detalle día a día (qué días cuenta y cuántas horas descuenta) usa GET /v1/work-sessions, que aplica el mismo calendario que el informe de fichajes. Sin employees:read_pii, las ausencias de salud salen con el tipo RESTRICTED y ninguna trae los motivos (texto libre).
Parámetros
| Nombre | En | Tipo | Descripción | Ejemplo |
|---|---|---|---|---|
from | query | string | Ausencias que se solapan con [from, to]. Máximo 366 días. | 2026-10-01 |
to | query | string | Último día del periodo (inclusive). | 2026-10-31 |
updatedSince | query | string (date-time) | Sincronización incremental (ver guía). | |
cursor | query | string ≤ 200 caracteres | Cursor de nextCursor (solo con updatedSince). | |
employeeId | query | integer | Solo las de este empleado. | |
leaveTypeId | query | string ≤ 64 caracteres | Solo este tipo (por id). | |
leaveTypeCode | query | string ≤ 50 caracteres | Solo este tipo (por código, p. ej. VACATION). | |
status | query | PENDING | APPROVED | REJECTED | CANCELLED | PENDING para la bandeja de aprobaciones. | APPROVED |
source | query | EMPLOYEE | ADMIN | API | SYSTEM | Solo las registradas por este origen. | |
page | query | integer por defecto 1 · mín. 1 | Página, desde 1. | |
limit | query | integer por defecto 50 · mín. 1 · máx. 500 | Resultados por página (máx. 500). |
Ejemplo de llamada
curl "https://api.fichme.com/v1/leave-requests?from=2026-10-01&to=2026-10-31&status=APPROVED" \
-H "x-api-key: $FICHME_API_KEY"const params = new URLSearchParams({
from: "2026-10-01",
to: "2026-10-31",
status: "APPROVED",
});
const res = await fetch(`https://api.fichme.com/v1/leave-requests?${params}`, {
headers: {
"x-api-key": process.env.FICHME_API_KEY,
},
});
if (!res.ok) {
const { error } = await res.json();
throw new Error(`${res.status} ${error.code}: ${error.message}`);
}
const data = await res.json();import os
import requests
res = requests.get(
"https://api.fichme.com/v1/leave-requests",
params={
"from": "2026-10-01",
"to": "2026-10-31",
"status": "APPROVED",
},
headers={
"x-api-key": os.environ["FICHME_API_KEY"],
},
timeout=30,
)
if not res.ok:
error = res.json()["error"]
raise RuntimeError(f"{res.status_code} {error['code']}: {error['message']}")
data = res.json()<?php
$ch = curl_init('https://api.fichme.com/v1/leave-requests?' . http_build_query([
'from' => '2026-10-01',
'to' => '2026-10-31',
'status' => 'APPROVED',
]));
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'x-api-key: ' . getenv('FICHME_API_KEY'),
],
]);
$body = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
$data = json_decode($body, true);
if ($status >= 400) {
throw new RuntimeException("$status {$data['error']['code']}: {$data['error']['message']}");
}Respuesta 200
Lista paginada · Lista de LeaveRequest
{
"object": "list",
"data": [
{
"object": "leaveRequest",
"id": "cm1lr5q2w0009qx4m7c3z8k1m",
"employeeId": 4821,
"leaveType": {
"id": "cm1ltvac0001qx7d2k8f4h6jk",
"code": "VACATION",
"name": "Vacaciones"
},
"unit": "DAYS",
"startDate": "2026-10-13",
"endDate": "2026-10-16",
"startHalf": null,
"endHalf": null,
"startTime": null,
"endTime": null,
"hours": null,
"businessDays": 4,
"status": "APPROVED",
"source": "API",
"reason": "Puente del Pilar",
"reviewedAt": "2026-09-16T08:02:40.000Z",
"reviewedById": 17,
"rejectionReason": null,
"cancelledAt": null,
"createdAt": "2026-09-15T16:20:03.000Z",
"updatedAt": "2026-09-16T08:02:40.000Z"
}
],
"page": 1,
"limit": 50,
"total": 1,
"totalPages": 1
}
Errores
| HTTP | code | Cuándo pasa |
|---|---|---|
| 403 | plan_upgrade_required | El plan de la empresa no incluye la gestión de ausencias. |
| 400 | date_range_too_large | Más de 366 días entre from y to. |
| 400 | conflicting_filters | from/to y updatedSince a la vez: usa uno solo. |
| 403 | insufficient_scope | Filtrar por un tipo de ausencia de salud (baja, consulta médica…) sin employees:read_pii. |
Además, los comunes a todas: 400 invalid_request, 401, 403 insufficient_scope y 429 (ver Errores).
GET /v1/leave-requests/{id}
Una ausencia
Una ausencia por su id, con su estado actual.
Parámetros
| Nombre | En | Tipo | Descripción | Ejemplo |
|---|---|---|---|---|
id obligatorio | path | string ≤ 64 caracteres | Id de la ausencia. | cm1lr5q2w0009qx4m7c3z8k1m |
Ejemplo de llamada
curl "https://api.fichme.com/v1/leave-requests/cm1lr5q2w0009qx4m7c3z8k1m" \
-H "x-api-key: $FICHME_API_KEY"const res = await fetch("https://api.fichme.com/v1/leave-requests/cm1lr5q2w0009qx4m7c3z8k1m", {
headers: {
"x-api-key": process.env.FICHME_API_KEY,
},
});
if (!res.ok) {
const { error } = await res.json();
throw new Error(`${res.status} ${error.code}: ${error.message}`);
}
const data = await res.json();import os
import requests
res = requests.get(
"https://api.fichme.com/v1/leave-requests/cm1lr5q2w0009qx4m7c3z8k1m",
headers={
"x-api-key": os.environ["FICHME_API_KEY"],
},
timeout=30,
)
if not res.ok:
error = res.json()["error"]
raise RuntimeError(f"{res.status_code} {error['code']}: {error['message']}")
data = res.json()<?php
$ch = curl_init('https://api.fichme.com/v1/leave-requests/cm1lr5q2w0009qx4m7c3z8k1m');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'x-api-key: ' . getenv('FICHME_API_KEY'),
],
]);
$body = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
$data = json_decode($body, true);
if ($status >= 400) {
throw new RuntimeException("$status {$data['error']['code']}: {$data['error']['message']}");
}Respuesta 200
La ausencia · LeaveRequest
{
"object": "leaveRequest",
"id": "cm1lr5q2w0009qx4m7c3z8k1m",
"employeeId": 4821,
"leaveType": {
"id": "cm1ltvac0001qx7d2k8f4h6jk",
"code": "VACATION",
"name": "Vacaciones"
},
"unit": "DAYS",
"startDate": "2026-10-13",
"endDate": "2026-10-16",
"startHalf": null,
"endHalf": null,
"startTime": null,
"endTime": null,
"hours": null,
"businessDays": 4,
"status": "APPROVED",
"source": "API",
"reason": "Puente del Pilar",
"reviewedAt": "2026-09-16T08:02:40.000Z",
"reviewedById": 17,
"rejectionReason": null,
"cancelledAt": null,
"createdAt": "2026-09-15T16:20:03.000Z",
"updatedAt": "2026-09-16T08:02:40.000Z"
}
Errores
| HTTP | code | Cuándo pasa |
|---|---|---|
| 403 | plan_upgrade_required | El plan de la empresa no incluye la gestión de ausencias. |
| 404 | not_found | No existe o no es de tu empresa. |
Además, los comunes a todas: 400 invalid_request, 401, 403 insufficient_scope y 429 (ver Errores).
POST /v1/leave-requests
Registrar una ausencia
Crea la ausencia PENDIENTE de aprobación, con las mismas validaciones que el panel (solapes → 409 overlap, saldo → 422 leave_balance_insufficient) y aviso a los administradores. Con autoApprove: true y absences:manage se aprueba en la misma llamada; figura como revisor el administrador que creó la clave.
Cuerpo (JSON)
| Campo | Tipo | Descripción |
|---|---|---|
employeeId | integer | Id del empleado en FichMe. |
email | string (email) ≤ 254 caracteres | Email del empleado (alternativa a employeeId). |
dni | string ≤ 20 caracteres | DNI/NIE del empleado (alternativa a employeeId). Requiere el scope employees:read_pii. |
leaveTypeId | string ≤ 64 caracteres | Tipo de ausencia por id. |
leaveTypeCode | string ≤ 50 caracteres | Tipo de ausencia por código (alternativa a leaveTypeId). |
startDate obligatorio | string | Primer día (YYYY-MM-DD). |
endDate obligatorio | string | Último día (YYYY-MM-DD), inclusive. |
startHalf | MORNING | AFTERNOON | Medio día al inicio (si el tipo lo admite). |
endHalf | MORNING | AFTERNOON | Medio día al final (si el tipo lo admite). |
startTime | string | Ausencias por horas: hora de inicio (HH:mm). |
endTime | string | Ausencias por horas: hora de fin (HH:mm). |
hours | number máx. 24 | Ausencias por horas: alternativa a startTime/endTime. |
reason | string ≤ 1000 caracteres | Motivo (lo ve quien la aprueba). |
autoApprove | boolean por defecto false | Aprobar en la misma llamada (requiere absences:manage). |
Ejemplo de llamada
curl -X POST "https://api.fichme.com/v1/leave-requests" \
-H "x-api-key: $FICHME_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"employeeId": 4821,
"leaveTypeCode": "VACATION",
"startDate": "2026-10-13",
"endDate": "2026-10-16",
"reason": "Puente del Pilar"
}'const res = await fetch("https://api.fichme.com/v1/leave-requests", {
method: "POST",
headers: {
"x-api-key": process.env.FICHME_API_KEY,
"Content-Type": "application/json",
},
body: JSON.stringify({
employeeId: 4821,
leaveTypeCode: "VACATION",
startDate: "2026-10-13",
endDate: "2026-10-16",
reason: "Puente del Pilar",
}),
});
if (!res.ok) {
const { error } = await res.json();
throw new Error(`${res.status} ${error.code}: ${error.message}`);
}
const data = await res.json();import os
import requests
res = requests.post(
"https://api.fichme.com/v1/leave-requests",
headers={
"x-api-key": os.environ["FICHME_API_KEY"],
},
json={
"employeeId": 4821,
"leaveTypeCode": "VACATION",
"startDate": "2026-10-13",
"endDate": "2026-10-16",
"reason": "Puente del Pilar",
},
timeout=30,
)
if not res.ok:
error = res.json()["error"]
raise RuntimeError(f"{res.status_code} {error['code']}: {error['message']}")
data = res.json()<?php
$ch = curl_init('https://api.fichme.com/v1/leave-requests');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CUSTOMREQUEST => 'POST',
CURLOPT_HTTPHEADER => [
'x-api-key: ' . getenv('FICHME_API_KEY'),
'Content-Type: application/json',
],
CURLOPT_POSTFIELDS => json_encode([
'employeeId' => 4821,
'leaveTypeCode' => 'VACATION',
'startDate' => '2026-10-13',
'endDate' => '2026-10-16',
'reason' => 'Puente del Pilar',
]),
]);
$body = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
$data = json_decode($body, true);
if ($status >= 400) {
throw new RuntimeException("$status {$data['error']['code']}: {$data['error']['message']}");
}Respuesta 201
Ausencia creada · LeaveRequest
{
"object": "leaveRequest",
"id": "cm1lr5q2w0009qx4m7c3z8k1m",
"employeeId": 4821,
"leaveType": {
"id": "cm1ltvac0001qx7d2k8f4h6jk",
"code": "VACATION",
"name": "Vacaciones"
},
"unit": "DAYS",
"startDate": "2026-10-13",
"endDate": "2026-10-16",
"startHalf": null,
"endHalf": null,
"startTime": null,
"endTime": null,
"hours": null,
"businessDays": 4,
"status": "PENDING",
"source": "API",
"reason": "Puente del Pilar",
"reviewedAt": null,
"reviewedById": null,
"rejectionReason": null,
"cancelledAt": null,
"createdAt": "2026-09-15T16:20:03.000Z",
"updatedAt": "2026-09-15T16:20:03.000Z"
}
Errores
| HTTP | code | Cuándo pasa |
|---|---|---|
| 409 | overlap | Se solapa con otra ausencia del empleado. |
| 422 | leave_balance_insufficient | No le queda cupo suficiente ( |
| 422 | validation_failed | El tipo está desactivado o es de sistema, o las fechas no encajan con el tipo. |
| 404 | not_found | El empleado o el tipo de ausencia no existen. |
| 403 | insufficient_scope | autoApprove sin el scope absences:manage, o el empleado por |
| 403 | plan_upgrade_required | El plan de la empresa no incluye la gestión de ausencias. |
| 503 | service_unavailable | Se están actualizando los cupos de ausencias (unos minutos): reintenta tras Retry-After. |
Además, los comunes a todas: 400 invalid_request, 401, 403 insufficient_scope y 429 (ver Errores).
POST /v1/leave-requests/{id}/approve
Aprobar una ausencia
Solo ausencias PENDIENTES. Avisa al empleado. Figura como revisor el administrador que creó la clave.
Parámetros
| Nombre | En | Tipo | Descripción | Ejemplo |
|---|---|---|---|---|
id obligatorio | path | string ≤ 64 caracteres | Id de la ausencia. | cm1lr5q2w0009qx4m7c3z8k1m |
Cuerpo (JSON)
| Campo | Tipo | Descripción |
|---|---|---|
notes | string ≤ 1000 caracteres | Nota para el empleado (opcional). |
Ejemplo de llamada
curl -X POST "https://api.fichme.com/v1/leave-requests/cm1lr5q2w0009qx4m7c3z8k1m/approve" \
-H "x-api-key: $FICHME_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"notes": "¡Buen puente!"
}'const res = await fetch("https://api.fichme.com/v1/leave-requests/cm1lr5q2w0009qx4m7c3z8k1m/approve", {
method: "POST",
headers: {
"x-api-key": process.env.FICHME_API_KEY,
"Content-Type": "application/json",
},
body: JSON.stringify({
notes: "¡Buen puente!",
}),
});
if (!res.ok) {
const { error } = await res.json();
throw new Error(`${res.status} ${error.code}: ${error.message}`);
}
const data = await res.json();import os
import requests
res = requests.post(
"https://api.fichme.com/v1/leave-requests/cm1lr5q2w0009qx4m7c3z8k1m/approve",
headers={
"x-api-key": os.environ["FICHME_API_KEY"],
},
json={
"notes": "¡Buen puente!",
},
timeout=30,
)
if not res.ok:
error = res.json()["error"]
raise RuntimeError(f"{res.status_code} {error['code']}: {error['message']}")
data = res.json()<?php
$ch = curl_init('https://api.fichme.com/v1/leave-requests/cm1lr5q2w0009qx4m7c3z8k1m/approve');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CUSTOMREQUEST => 'POST',
CURLOPT_HTTPHEADER => [
'x-api-key: ' . getenv('FICHME_API_KEY'),
'Content-Type: application/json',
],
CURLOPT_POSTFIELDS => json_encode([
'notes' => '¡Buen puente!',
]),
]);
$body = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
$data = json_decode($body, true);
if ($status >= 400) {
throw new RuntimeException("$status {$data['error']['code']}: {$data['error']['message']}");
}Respuesta 200
Ausencia aprobada · LeaveRequest
{
"object": "leaveRequest",
"id": "cm1lr5q2w0009qx4m7c3z8k1m",
"employeeId": 4821,
"leaveType": {
"id": "cm1ltvac0001qx7d2k8f4h6jk",
"code": "VACATION",
"name": "Vacaciones"
},
"unit": "DAYS",
"startDate": "2026-10-13",
"endDate": "2026-10-16",
"startHalf": null,
"endHalf": null,
"startTime": null,
"endTime": null,
"hours": null,
"businessDays": 4,
"status": "APPROVED",
"source": "API",
"reason": "Puente del Pilar",
"reviewedAt": "2026-09-16T08:02:40.000Z",
"reviewedById": 17,
"rejectionReason": null,
"cancelledAt": null,
"createdAt": "2026-09-15T16:20:03.000Z",
"updatedAt": "2026-09-16T08:02:40.000Z"
}
Errores
| HTTP | code | Cuándo pasa |
|---|---|---|
| 409 | invalid_state | La ausencia ya no está PENDING ( |
| 422 | leave_balance_insufficient | Ya no queda cupo para aprobarla. |
| 403 | key_owner_required | El administrador que creó la clave ya no lo es. |
| 403 | plan_upgrade_required | El plan de la empresa no incluye la gestión de ausencias. |
| 503 | service_unavailable | Se están actualizando los cupos de ausencias (unos minutos): reintenta tras Retry-After. |
Además, los comunes a todas: 400 invalid_request, 401, 403 insufficient_scope y 429 (ver Errores).
POST /v1/leave-requests/{id}/reject
Rechazar una ausencia
Solo ausencias PENDIENTES. El motivo (mínimo 10 caracteres) se envía al empleado; el cupo se libera.
Parámetros
| Nombre | En | Tipo | Descripción | Ejemplo |
|---|---|---|---|---|
id obligatorio | path | string ≤ 64 caracteres | Id de la ausencia. | cm1lr5q2w0009qx4m7c3z8k1m |
Cuerpo (JSON)
| Campo | Tipo | Descripción |
|---|---|---|
reason obligatorio | string ≤ 1000 caracteres | Motivo del rechazo (mínimo 10 caracteres): se envía al empleado. |
Ejemplo de llamada
curl -X POST "https://api.fichme.com/v1/leave-requests/cm1lr5q2w0009qx4m7c3z8k1m/reject" \
-H "x-api-key: $FICHME_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"reason": "Coincide con el cierre trimestral: elige otras fechas."
}'const res = await fetch("https://api.fichme.com/v1/leave-requests/cm1lr5q2w0009qx4m7c3z8k1m/reject", {
method: "POST",
headers: {
"x-api-key": process.env.FICHME_API_KEY,
"Content-Type": "application/json",
},
body: JSON.stringify({
reason: "Coincide con el cierre trimestral: elige otras fechas.",
}),
});
if (!res.ok) {
const { error } = await res.json();
throw new Error(`${res.status} ${error.code}: ${error.message}`);
}
const data = await res.json();import os
import requests
res = requests.post(
"https://api.fichme.com/v1/leave-requests/cm1lr5q2w0009qx4m7c3z8k1m/reject",
headers={
"x-api-key": os.environ["FICHME_API_KEY"],
},
json={
"reason": "Coincide con el cierre trimestral: elige otras fechas.",
},
timeout=30,
)
if not res.ok:
error = res.json()["error"]
raise RuntimeError(f"{res.status_code} {error['code']}: {error['message']}")
data = res.json()<?php
$ch = curl_init('https://api.fichme.com/v1/leave-requests/cm1lr5q2w0009qx4m7c3z8k1m/reject');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CUSTOMREQUEST => 'POST',
CURLOPT_HTTPHEADER => [
'x-api-key: ' . getenv('FICHME_API_KEY'),
'Content-Type: application/json',
],
CURLOPT_POSTFIELDS => json_encode([
'reason' => 'Coincide con el cierre trimestral: elige otras fechas.',
]),
]);
$body = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
$data = json_decode($body, true);
if ($status >= 400) {
throw new RuntimeException("$status {$data['error']['code']}: {$data['error']['message']}");
}Respuesta 200
Ausencia rechazada · LeaveRequest
{
"object": "leaveRequest",
"id": "cm1lr5q2w0009qx4m7c3z8k1m",
"employeeId": 4821,
"leaveType": {
"id": "cm1ltvac0001qx7d2k8f4h6jk",
"code": "VACATION",
"name": "Vacaciones"
},
"unit": "DAYS",
"startDate": "2026-10-13",
"endDate": "2026-10-16",
"startHalf": null,
"endHalf": null,
"startTime": null,
"endTime": null,
"hours": null,
"businessDays": 4,
"status": "REJECTED",
"source": "API",
"reason": "Puente del Pilar",
"reviewedAt": "2026-09-16T08:02:40.000Z",
"reviewedById": 17,
"rejectionReason": "Coincide con el cierre trimestral: elige otras fechas.",
"cancelledAt": null,
"createdAt": "2026-09-15T16:20:03.000Z",
"updatedAt": "2026-09-16T08:02:40.000Z"
}
Errores
| HTTP | code | Cuándo pasa |
|---|---|---|
| 409 | invalid_state | La ausencia ya no está PENDING. |
| 403 | key_owner_required | El administrador que creó la clave ya no lo es. |
| 403 | plan_upgrade_required | El plan de la empresa no incluye la gestión de ausencias. |
| 503 | service_unavailable | Se están actualizando los cupos de ausencias (unos minutos): reintenta tras Retry-After. |
Además, los comunes a todas: 400 invalid_request, 401, 403 insufficient_scope y 429 (ver Errores).
POST /v1/leave-requests/{id}/cancel
Cancelar una ausencia
Ausencias pendientes o aprobadas. Los días vuelven a su cupo. Los descansos que vienen de la bolsa de horas se anulan desde la bolsa, no desde aquí.
Parámetros
| Nombre | En | Tipo | Descripción | Ejemplo |
|---|---|---|---|---|
id obligatorio | path | string ≤ 64 caracteres | Id de la ausencia. | cm1lr5q2w0009qx4m7c3z8k1m |
Cuerpo (JSON)
| Campo | Tipo | Descripción |
|---|---|---|
reason | string ≤ 1000 caracteres | Motivo de la cancelación (opcional). |
Ejemplo de llamada
curl -X POST "https://api.fichme.com/v1/leave-requests/cm1lr5q2w0009qx4m7c3z8k1m/cancel" \
-H "x-api-key: $FICHME_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"reason": "Cambia las fechas del viaje."
}'const res = await fetch("https://api.fichme.com/v1/leave-requests/cm1lr5q2w0009qx4m7c3z8k1m/cancel", {
method: "POST",
headers: {
"x-api-key": process.env.FICHME_API_KEY,
"Content-Type": "application/json",
},
body: JSON.stringify({
reason: "Cambia las fechas del viaje.",
}),
});
if (!res.ok) {
const { error } = await res.json();
throw new Error(`${res.status} ${error.code}: ${error.message}`);
}
const data = await res.json();import os
import requests
res = requests.post(
"https://api.fichme.com/v1/leave-requests/cm1lr5q2w0009qx4m7c3z8k1m/cancel",
headers={
"x-api-key": os.environ["FICHME_API_KEY"],
},
json={
"reason": "Cambia las fechas del viaje.",
},
timeout=30,
)
if not res.ok:
error = res.json()["error"]
raise RuntimeError(f"{res.status_code} {error['code']}: {error['message']}")
data = res.json()<?php
$ch = curl_init('https://api.fichme.com/v1/leave-requests/cm1lr5q2w0009qx4m7c3z8k1m/cancel');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CUSTOMREQUEST => 'POST',
CURLOPT_HTTPHEADER => [
'x-api-key: ' . getenv('FICHME_API_KEY'),
'Content-Type: application/json',
],
CURLOPT_POSTFIELDS => json_encode([
'reason' => 'Cambia las fechas del viaje.',
]),
]);
$body = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
$data = json_decode($body, true);
if ($status >= 400) {
throw new RuntimeException("$status {$data['error']['code']}: {$data['error']['message']}");
}Respuesta 200
Ausencia cancelada · LeaveRequest
{
"object": "leaveRequest",
"id": "cm1lr5q2w0009qx4m7c3z8k1m",
"employeeId": 4821,
"leaveType": {
"id": "cm1ltvac0001qx7d2k8f4h6jk",
"code": "VACATION",
"name": "Vacaciones"
},
"unit": "DAYS",
"startDate": "2026-10-13",
"endDate": "2026-10-16",
"startHalf": null,
"endHalf": null,
"startTime": null,
"endTime": null,
"hours": null,
"businessDays": 4,
"status": "CANCELLED",
"source": "API",
"reason": "Puente del Pilar",
"reviewedAt": "2026-09-16T08:02:40.000Z",
"reviewedById": 17,
"rejectionReason": null,
"cancelledAt": "2026-09-20T10:11:05.000Z",
"createdAt": "2026-09-15T16:20:03.000Z",
"updatedAt": "2026-09-20T10:11:05.000Z"
}
Errores
| HTTP | code | Cuándo pasa |
|---|---|---|
| 409 | invalid_state | Solo se cancelan ausencias PENDING o APPROVED. |
| 403 | key_owner_required | El administrador que creó la clave ya no lo es. |
| 403 | plan_upgrade_required | El plan de la empresa no incluye la gestión de ausencias. |
| 503 | service_unavailable | Se están actualizando los cupos de ausencias (unos minutos): reintenta tras Retry-After. |
Además, los comunes a todas: 400 invalid_request, 401, 403 insufficient_scope y 429 (ver Errores).
GET /v1/leave-balances
Saldos de ausencias
Cupo por empleado y tipo para un año: asignado, arrastrado, ajustes, consumido y disponible. Equivale al "saldo de vacaciones" de una gestoría, pero para cualquier tipo con cupo. Los cupos de tipos de salud (p. ej. horas de consulta médica) solo salen con employees:read_pii.
Parámetros
| Nombre | En | Tipo | Descripción | Ejemplo |
|---|---|---|---|---|
year | query | integer mín. 2000 · máx. 2100 | Año del cupo. Por defecto, el actual. | 2026 |
employeeId | query | integer | Solo este empleado. | |
leaveTypeCode | query | string ≤ 50 caracteres | Solo este tipo (p. ej. VACATION). | VACATION |
page | query | integer por defecto 1 · mín. 1 | Página, desde 1. | |
limit | query | integer por defecto 50 · mín. 1 · máx. 500 | Resultados por página (máx. 500). |
Ejemplo de llamada
curl "https://api.fichme.com/v1/leave-balances?year=2026&leaveTypeCode=VACATION" \
-H "x-api-key: $FICHME_API_KEY"const params = new URLSearchParams({
year: "2026",
leaveTypeCode: "VACATION",
});
const res = await fetch(`https://api.fichme.com/v1/leave-balances?${params}`, {
headers: {
"x-api-key": process.env.FICHME_API_KEY,
},
});
if (!res.ok) {
const { error } = await res.json();
throw new Error(`${res.status} ${error.code}: ${error.message}`);
}
const data = await res.json();import os
import requests
res = requests.get(
"https://api.fichme.com/v1/leave-balances",
params={
"year": "2026",
"leaveTypeCode": "VACATION",
},
headers={
"x-api-key": os.environ["FICHME_API_KEY"],
},
timeout=30,
)
if not res.ok:
error = res.json()["error"]
raise RuntimeError(f"{res.status_code} {error['code']}: {error['message']}")
data = res.json()<?php
$ch = curl_init('https://api.fichme.com/v1/leave-balances?' . http_build_query([
'year' => '2026',
'leaveTypeCode' => 'VACATION',
]));
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'x-api-key: ' . getenv('FICHME_API_KEY'),
],
]);
$body = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
$data = json_decode($body, true);
if ($status >= 400) {
throw new RuntimeException("$status {$data['error']['code']}: {$data['error']['message']}");
}Respuesta 200
Lista paginada · Lista de LeaveBalance
{
"object": "list",
"data": [
{
"object": "leaveBalance",
"employeeId": 4821,
"leaveType": {
"id": "cm1ltvac0001qx7d2k8f4h6jk",
"code": "VACATION",
"name": "Vacaciones"
},
"year": 2026,
"unit": "DAYS",
"allocated": 22,
"carryOver": 2,
"adjustment": 0,
"used": 12,
"available": 12,
"carriedFromPreviousYears": 0
}
],
"page": 1,
"limit": 50,
"total": 1,
"totalPages": 1
}
Errores
| HTTP | code | Cuándo pasa |
|---|---|---|
| 403 | plan_upgrade_required | El plan de la empresa no incluye la gestión de ausencias. |
| 404 | not_found | El empleado de |
| 429 | too_many_concurrent_requests | Supera el máximo de 1 petición simultánea por clave en los endpoints pesados. |
| 403 | insufficient_scope | Filtrar por un tipo de ausencia de salud (baja, consulta médica…) sin employees:read_pii. |
Además, los comunes a todas: 400 invalid_request, 401, 403 insufficient_scope y 429 (ver Errores).
Balance
Horas previstas frente a trabajadas.
GET /v1/hours-balance
Balance de horas
Horas previstas frente a trabajadas, con el motor del Balance de horas del panel (el mismo que usan GET /v1/work-sessions y la bolsa de horas). Por defecto, el total del periodo por empleado; con groupBy=day, una fila por empleado y día con scheduleSource (de dónde sale la jornada prevista), que es lo que se pregunta cuando no cuadran las horas (los elementos son entonces HoursBalanceDay).
La paginación va por EMPLEADOS.
Parámetros
| Nombre | En | Tipo | Descripción | Ejemplo |
|---|---|---|---|---|
from obligatorio | query | string | Primer día del periodo (inclusive). | 2026-09-01 |
to obligatorio | query | string | Último día del periodo (inclusive). Máximo 366 días. | 2026-09-30 |
employeeId | query | integer | Solo este empleado. | |
locationId | query | string ≤ 64 caracteres | Empleados de este centro (principal o de pertenencia). | |
status | query | ACTIVE | INACTIVE | ALLpor defecto "ALL" | Estado del empleado. ALL incluye a quien se dio de baja en el periodo. | |
groupBy | query | employee | daypor defecto "employee" | employee = total del periodo por empleado; day = una fila por día. | |
page | query | integer por defecto 1 · mín. 1 | Página, desde 1. | |
limit | query | integer por defecto 50 · mín. 1 · máx. 100 | EMPLEADOS por página (máx. 100). |
Ejemplo de llamada
curl "https://api.fichme.com/v1/hours-balance?from=2026-09-01&to=2026-09-30" \
-H "x-api-key: $FICHME_API_KEY"const params = new URLSearchParams({
from: "2026-09-01",
to: "2026-09-30",
});
const res = await fetch(`https://api.fichme.com/v1/hours-balance?${params}`, {
headers: {
"x-api-key": process.env.FICHME_API_KEY,
},
});
if (!res.ok) {
const { error } = await res.json();
throw new Error(`${res.status} ${error.code}: ${error.message}`);
}
const data = await res.json();import os
import requests
res = requests.get(
"https://api.fichme.com/v1/hours-balance",
params={
"from": "2026-09-01",
"to": "2026-09-30",
},
headers={
"x-api-key": os.environ["FICHME_API_KEY"],
},
timeout=30,
)
if not res.ok:
error = res.json()["error"]
raise RuntimeError(f"{res.status_code} {error['code']}: {error['message']}")
data = res.json()<?php
$ch = curl_init('https://api.fichme.com/v1/hours-balance?' . http_build_query([
'from' => '2026-09-01',
'to' => '2026-09-30',
]));
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'x-api-key: ' . getenv('FICHME_API_KEY'),
],
]);
$body = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
$data = json_decode($body, true);
if ($status >= 400) {
throw new RuntimeException("$status {$data['error']['code']}: {$data['error']['message']}");
}Respuesta 200
Lista paginada por empleados · Lista de HoursBalance
{
"object": "list",
"data": [
{
"object": "hoursBalance",
"employeeId": 4821,
"from": "2026-09-01",
"to": "2026-09-30",
"workedMinutes": 9540,
"workedTime": "159:00",
"assignedMinutes": 9450,
"assignedTime": "157:30",
"balanceMinutes": 90,
"balanceTime": "01:30",
"breakMinutes": 630,
"paidBreakMinutes": 0,
"autoDeductedMinutes": 0,
"daysWorked": 21,
"daysWithOpenSegment": 0
}
],
"page": 1,
"limit": 50,
"total": 1,
"totalPages": 1
}
Errores
| HTTP | code | Cuándo pasa |
|---|---|---|
| 400 | invalid_date_range | Falta from o to, o from es posterior a to. |
| 400 | date_range_too_large | Más de 366 días. |
| 429 | too_many_concurrent_requests | Supera el máximo de 1 petición simultánea por clave en los endpoints pesados. |
Además, los comunes a todas: 400 invalid_request, 401, 403 insufficient_scope y 429 (ver Errores).
Turnos
Turnos, asignaciones y festivos.
GET /v1/shifts
Catálogo de turnos
Turnos definidos en la empresa, con sus tramos, días de aplicación y pausas.
Ejemplo de llamada
curl "https://api.fichme.com/v1/shifts" \
-H "x-api-key: $FICHME_API_KEY"const res = await fetch("https://api.fichme.com/v1/shifts", {
headers: {
"x-api-key": process.env.FICHME_API_KEY,
},
});
if (!res.ok) {
const { error } = await res.json();
throw new Error(`${res.status} ${error.code}: ${error.message}`);
}
const data = await res.json();import os
import requests
res = requests.get(
"https://api.fichme.com/v1/shifts",
headers={
"x-api-key": os.environ["FICHME_API_KEY"],
},
timeout=30,
)
if not res.ok:
error = res.json()["error"]
raise RuntimeError(f"{res.status_code} {error['code']}: {error['message']}")
data = res.json()<?php
$ch = curl_init('https://api.fichme.com/v1/shifts');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'x-api-key: ' . getenv('FICHME_API_KEY'),
],
]);
$body = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
$data = json_decode($body, true);
if ($status >= 400) {
throw new RuntimeException("$status {$data['error']['code']}: {$data['error']['message']}");
}Respuesta 200
Lista de turnos · Lista de Shift
{
"object": "list",
"data": [
{
"object": "shift",
"id": "cm1shmnn0002qx9p4c6v8b2de",
"name": "Mañana",
"type": "WORK",
"color": "#10B981",
"isNight": false,
"isSplit": false,
"segments": [
{
"start": "09:00",
"end": "17:00"
}
],
"totalHours": 7.5,
"applicableDays": [
1,
2,
3,
4,
5
],
"tolerance": {
"entryMinutes": 10,
"exitMinutes": 10
},
"breaks": [
{
"id": "cm1brkcm0003qx9p4c6v8b2df",
"name": "Comida",
"startTime": "13:00",
"durationMinutes": 30,
"isPaid": false,
"autoDeduct": false
}
],
"createdAt": "2025-02-03T09:30:00.000Z",
"updatedAt": "2026-03-10T12:00:00.000Z"
}
],
"page": 1,
"limit": 1,
"total": 1,
"totalPages": 1
}
Errores
Además, los comunes a todas: 400 invalid_request, 401, 403 insufficient_scope y 429 (ver Errores).
GET /v1/shift-assignments
Turno de cada empleado por día
Una fila por empleado y día con turno. Resuelve la misma jerarquía que el calendario: el turno pintado en el calendario manda; si no hay, el turno por rango de fechas y, si tampoco, el fijo, siempre que aplique ese día de la semana. Los días sin fila no tienen turno (rige el horario del centro o de la empresa).
Parámetros
| Nombre | En | Tipo | Descripción | Ejemplo |
|---|---|---|---|---|
from obligatorio | query | string | Primer día (inclusive). | 2026-09-14 |
to obligatorio | query | string | Último día (inclusive). Máximo 93 días. | 2026-09-20 |
employeeId | query | integer | Solo este empleado. | 4821 |
page | query | integer por defecto 1 · mín. 1 | Página, desde 1. | |
limit | query | integer por defecto 50 · mín. 1 · máx. 500 | Resultados por página (máx. 500). |
Ejemplo de llamada
curl "https://api.fichme.com/v1/shift-assignments?from=2026-09-14&to=2026-09-20&employeeId=4821" \
-H "x-api-key: $FICHME_API_KEY"const params = new URLSearchParams({
from: "2026-09-14",
to: "2026-09-20",
employeeId: "4821",
});
const res = await fetch(`https://api.fichme.com/v1/shift-assignments?${params}`, {
headers: {
"x-api-key": process.env.FICHME_API_KEY,
},
});
if (!res.ok) {
const { error } = await res.json();
throw new Error(`${res.status} ${error.code}: ${error.message}`);
}
const data = await res.json();import os
import requests
res = requests.get(
"https://api.fichme.com/v1/shift-assignments",
params={
"from": "2026-09-14",
"to": "2026-09-20",
"employeeId": "4821",
},
headers={
"x-api-key": os.environ["FICHME_API_KEY"],
},
timeout=30,
)
if not res.ok:
error = res.json()["error"]
raise RuntimeError(f"{res.status_code} {error['code']}: {error['message']}")
data = res.json()<?php
$ch = curl_init('https://api.fichme.com/v1/shift-assignments?' . http_build_query([
'from' => '2026-09-14',
'to' => '2026-09-20',
'employeeId' => '4821',
]));
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'x-api-key: ' . getenv('FICHME_API_KEY'),
],
]);
$body = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
$data = json_decode($body, true);
if ($status >= 400) {
throw new RuntimeException("$status {$data['error']['code']}: {$data['error']['message']}");
}Respuesta 200
Lista paginada · Lista de ShiftAssignment
{
"object": "list",
"data": [
{
"object": "shiftAssignment",
"employeeId": 4821,
"date": "2026-09-15",
"origin": "DEFAULT",
"shift": {
"id": "cm1shmnn0002qx9p4c6v8b2de",
"name": "Mañana"
},
"isDayOff": false,
"startTime": "09:00",
"endTime": "17:00",
"hours": 7.5,
"notes": null
}
],
"page": 1,
"limit": 50,
"total": 1,
"totalPages": 1
}
Errores
| HTTP | code | Cuándo pasa |
|---|---|---|
| 400 | invalid_date_range | Falta from o to, o from es posterior a to. |
| 400 | date_range_too_large | Más de 93 días. |
Además, los comunes a todas: 400 invalid_request, 401, 403 insufficient_scope y 429 (ver Errores).
GET /v1/holidays
Festivos
Festivos del año: los de la empresa y los de cada centro (o los de la empresa más los de un centro con locationId). workingHours > 0 indica una jornada reducida en vez de un festivo completo.
Parámetros
| Nombre | En | Tipo | Descripción | Ejemplo |
|---|---|---|---|---|
year | query | integer mín. 2000 · máx. 2100 | Año. Por defecto, el actual. | 2026 |
locationId | query | string ≤ 64 caracteres | Festivos de la empresa más los de este centro. |
Ejemplo de llamada
curl "https://api.fichme.com/v1/holidays?year=2026" \
-H "x-api-key: $FICHME_API_KEY"const params = new URLSearchParams({
year: "2026",
});
const res = await fetch(`https://api.fichme.com/v1/holidays?${params}`, {
headers: {
"x-api-key": process.env.FICHME_API_KEY,
},
});
if (!res.ok) {
const { error } = await res.json();
throw new Error(`${res.status} ${error.code}: ${error.message}`);
}
const data = await res.json();import os
import requests
res = requests.get(
"https://api.fichme.com/v1/holidays",
params={
"year": "2026",
},
headers={
"x-api-key": os.environ["FICHME_API_KEY"],
},
timeout=30,
)
if not res.ok:
error = res.json()["error"]
raise RuntimeError(f"{res.status_code} {error['code']}: {error['message']}")
data = res.json()<?php
$ch = curl_init('https://api.fichme.com/v1/holidays?' . http_build_query([
'year' => '2026',
]));
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'x-api-key: ' . getenv('FICHME_API_KEY'),
],
]);
$body = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
$data = json_decode($body, true);
if ($status >= 400) {
throw new RuntimeException("$status {$data['error']['code']}: {$data['error']['message']}");
}Respuesta 200
Lista de festivos · Lista de Holiday
{
"object": "list",
"data": [
{
"object": "holiday",
"id": "cm1hol12o0004qx2b7n5c9d3f",
"date": "2026-10-12",
"name": "Fiesta Nacional de España",
"type": "NATIONAL",
"scope": "COMPANY",
"locationId": null,
"province": null,
"workingHours": 0,
"recurrent": true
}
],
"page": 1,
"limit": 1,
"total": 1,
"totalPages": 1
}
Errores
Además, los comunes a todas: 400 invalid_request, 401, 403 insufficient_scope y 429 (ver Errores).
Exportaciones
Informes asíncronos (CSV, XLSX, PDF) para periodos largos.
POST /v1/exports
Lanzar una exportación
Genera en segundo plano uno de los informes del panel (hasta 2 años). Responde 202 con el id; consulta GET /v1/exports/{id} hasta que status sea completed y descarga el archivo de downloadUrl. Si ya hay una exportación idéntica en curso, devuelve esa. Requiere exports:write y employees:read_pii (los informes llevan el DNI).
Cuerpo (JSON)
| Campo | Tipo | Descripción |
|---|---|---|
dataset obligatorio | clocking | balance | absence | hour-bank | location-hours | clocking = registro de jornada (fichajes); balance = balance de horas; absence = ausencias; hour-bank = bolsa de horas; location-hours = horas por centro. |
format | csv | xlsx | pdfpor defecto "xlsx" | pdf = el informe firmado del panel. |
from obligatorio | string | Primer día del periodo (YYYY-MM-DD). |
to obligatorio | string | Último día del periodo (YYYY-MM-DD). Máximo 2 años. |
employeeIds | integer[] ≤ 500 elementos | Solo estos empleados (por defecto, todos). |
locationId | string ≤ 64 caracteres | Solo este centro. |
locationScope | assigned | clocked | Solo clocking: assigned = empleados del centro; clocked = solo los tramos fichados en él. |
employeeStatus | all | active | inactivepor defecto "all" | Empleados activos, dados de baja o todos. |
includeModifications | boolean por defecto true | Solo clocking: anexo con el detalle de modificaciones. |
Ejemplo de llamada
curl -X POST "https://api.fichme.com/v1/exports" \
-H "x-api-key: $FICHME_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"dataset": "clocking",
"format": "pdf",
"from": "2026-09-01",
"to": "2026-09-30"
}'const res = await fetch("https://api.fichme.com/v1/exports", {
method: "POST",
headers: {
"x-api-key": process.env.FICHME_API_KEY,
"Content-Type": "application/json",
},
body: JSON.stringify({
dataset: "clocking",
format: "pdf",
from: "2026-09-01",
to: "2026-09-30",
}),
});
if (!res.ok) {
const { error } = await res.json();
throw new Error(`${res.status} ${error.code}: ${error.message}`);
}
const data = await res.json();import os
import requests
res = requests.post(
"https://api.fichme.com/v1/exports",
headers={
"x-api-key": os.environ["FICHME_API_KEY"],
},
json={
"dataset": "clocking",
"format": "pdf",
"from": "2026-09-01",
"to": "2026-09-30",
},
timeout=30,
)
if not res.ok:
error = res.json()["error"]
raise RuntimeError(f"{res.status_code} {error['code']}: {error['message']}")
data = res.json()<?php
$ch = curl_init('https://api.fichme.com/v1/exports');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CUSTOMREQUEST => 'POST',
CURLOPT_HTTPHEADER => [
'x-api-key: ' . getenv('FICHME_API_KEY'),
'Content-Type: application/json',
],
CURLOPT_POSTFIELDS => json_encode([
'dataset' => 'clocking',
'format' => 'pdf',
'from' => '2026-09-01',
'to' => '2026-09-30',
]),
]);
$body = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
$data = json_decode($body, true);
if ($status >= 400) {
throw new RuntimeException("$status {$data['error']['code']}: {$data['error']['message']}");
}Respuesta 202
Exportación en cola · Export
{
"object": "export",
"id": "cm1exp7t0005qx6c3m9b2v8zq",
"status": "pending",
"dataset": "clocking",
"format": "pdf",
"from": "2026-09-01",
"to": "2026-09-30",
"progress": 0,
"rows": null,
"sizeBytes": null,
"downloadUrl": null,
"error": null,
"createdAt": "2026-10-01T07:00:03.000Z",
"completedAt": null,
"expiresAt": "2026-10-02T07:00:03.000Z"
}
Errores
| HTTP | code | Cuándo pasa |
|---|---|---|
| 403 | insufficient_scope | Falta employees:read_pii: los informes llevan el DNI de cada empleado. |
| 400 | date_range_too_large | Más de 2 años. |
| 403 | plan_upgrade_required | dataset hour-bank sin la bolsa de horas en el plan. |
| 422 | validation_failed | dataset hour-bank con la bolsa de horas desactivada en la empresa. |
| 404 | not_found | Algún empleado de employeeIds o el centro no existen. |
Además, los comunes a todas: 400 invalid_request, 401, 403 insufficient_scope y 429 (ver Errores).
GET /v1/exports/{id}
Estado y descarga de una exportación
Consulta cada pocos segundos (con backoff) hasta completed o failed. downloadUrl caduca en una hora: vuelve a pedir este endpoint para obtener otra. El archivo se borra 24 h después de crearse.
Parámetros
| Nombre | En | Tipo | Descripción | Ejemplo |
|---|---|---|---|---|
id obligatorio | path | string ≤ 64 caracteres | Id de la exportación. | cm1exp7t0005qx6c3m9b2v8zq |
Ejemplo de llamada
curl "https://api.fichme.com/v1/exports/cm1exp7t0005qx6c3m9b2v8zq" \
-H "x-api-key: $FICHME_API_KEY"const res = await fetch("https://api.fichme.com/v1/exports/cm1exp7t0005qx6c3m9b2v8zq", {
headers: {
"x-api-key": process.env.FICHME_API_KEY,
},
});
if (!res.ok) {
const { error } = await res.json();
throw new Error(`${res.status} ${error.code}: ${error.message}`);
}
const data = await res.json();import os
import requests
res = requests.get(
"https://api.fichme.com/v1/exports/cm1exp7t0005qx6c3m9b2v8zq",
headers={
"x-api-key": os.environ["FICHME_API_KEY"],
},
timeout=30,
)
if not res.ok:
error = res.json()["error"]
raise RuntimeError(f"{res.status_code} {error['code']}: {error['message']}")
data = res.json()<?php
$ch = curl_init('https://api.fichme.com/v1/exports/cm1exp7t0005qx6c3m9b2v8zq');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'x-api-key: ' . getenv('FICHME_API_KEY'),
],
]);
$body = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
$data = json_decode($body, true);
if ($status >= 400) {
throw new RuntimeException("$status {$data['error']['code']}: {$data['error']['message']}");
}Respuesta 200
La exportación · Export
{
"object": "export",
"id": "cm1exp7t0005qx6c3m9b2v8zq",
"status": "completed",
"dataset": "clocking",
"format": "pdf",
"from": "2026-09-01",
"to": "2026-09-30",
"progress": 100,
"rows": 412,
"sizeBytes": 183422,
"downloadUrl": "https://api.fichme.com/api/files/eyJqb2IiOiJjbTFleHA3dDAwMDUifQ/registro-jornada-2026-09.pdf",
"error": null,
"createdAt": "2026-10-01T07:00:03.000Z",
"completedAt": "2026-10-01T07:00:41.000Z",
"expiresAt": "2026-10-02T07:00:03.000Z"
}
Errores
| HTTP | code | Cuándo pasa |
|---|---|---|
| 403 | insufficient_scope | Falta employees:read_pii: los informes llevan el DNI de cada empleado. |
| 404 | not_found | No existe o no es de tu empresa. |
Además, los comunes a todas: 400 invalid_request, 401, 403 insufficient_scope y 429 (ver Errores).
Webhooks
Avisos firmados cuando algo cambia en la empresa (planes Business y Enterprise). Cómo recibirlos y verificar la firma: guía «Recibir webhooks».
GET /v1/webhooks
Webhooks de la empresa
Endpoints que reciben los eventos. Cada evento llega como POST JSON { id, object: "event", type, apiVersion, createdAt, companyId, data: { object } } con las cabeceras FichMe-Event, FichMe-Event-Id, FichMe-Delivery-Id y FichMe-Signature: t=<unix>,v1=<hex>, donde v1 = HMAC-SHA256(secreto, "<t>.<cuerpo>"). Responde 2xx en menos de 10 s; si no, se reintenta a 1 min, 5 min, 30 min, 2 h y 12 h. Entrega «al menos una vez» y sin orden garantizado: deduplica por id del evento. Tipos: clockEntry.created, clockEntry.updated, clockCorrection.created, clockCorrection.approved, clockCorrection.rejected, leaveRequest.created, leaveRequest.approved, leaveRequest.rejected, leaveRequest.cancelled, employee.created, employee.updated, employee.deactivated, employee.deleted.
Ejemplo de llamada
curl "https://api.fichme.com/v1/webhooks" \
-H "x-api-key: $FICHME_API_KEY"const res = await fetch("https://api.fichme.com/v1/webhooks", {
headers: {
"x-api-key": process.env.FICHME_API_KEY,
},
});
if (!res.ok) {
const { error } = await res.json();
throw new Error(`${res.status} ${error.code}: ${error.message}`);
}
const data = await res.json();import os
import requests
res = requests.get(
"https://api.fichme.com/v1/webhooks",
headers={
"x-api-key": os.environ["FICHME_API_KEY"],
},
timeout=30,
)
if not res.ok:
error = res.json()["error"]
raise RuntimeError(f"{res.status_code} {error['code']}: {error['message']}")
data = res.json()<?php
$ch = curl_init('https://api.fichme.com/v1/webhooks');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'x-api-key: ' . getenv('FICHME_API_KEY'),
],
]);
$body = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
$data = json_decode($body, true);
if ($status >= 400) {
throw new RuntimeException("$status {$data['error']['code']}: {$data['error']['message']}");
}Respuesta 200
Lista de webhooks · Lista de WebhookEndpoint
{
"object": "list",
"data": [
{
"object": "webhookEndpoint",
"id": "cm1whk4r0006qx8v2n6c3b7xy",
"url": "https://erp.example.com/webhooks/fichme",
"description": "ERP de nóminas",
"events": [
"clockEntry.created",
"leaveRequest.approved"
],
"status": "ACTIVE",
"failingSince": null,
"disabledReason": null,
"secretLast4": "hJ2l",
"createdAt": "2026-09-10T09:00:00.000Z",
"updatedAt": "2026-09-10T09:00:00.000Z",
"lastDelivery": {
"status": "DELIVERED",
"httpStatus": 200,
"at": "2026-09-15T06:58:33.000Z"
}
}
],
"page": 1,
"limit": 1,
"total": 1,
"totalPages": 1
}
Errores
| HTTP | code | Cuándo pasa |
|---|---|---|
| 403 | plan_upgrade_required | Los webhooks están en los planes Business y Enterprise. |
Además, los comunes a todas: 400 invalid_request, 401, 403 insufficient_scope y 429 (ver Errores).
POST /v1/webhooks
Crear un webhook
Devuelve el secreto de firma UNA sola vez. Como mucho 10 webhooks por empresa.
Cuerpo (JSON)
| Campo | Tipo | Descripción |
|---|---|---|
url obligatorio | string ≤ 2048 caracteres | https://… pública (no se admiten IPs ni redes internas). |
description | string ≤ 200 caracteres | Descripción libre (≤ 200 caracteres). |
events obligatorio | * | clockEntry.created | clockEntry.updated | clockCorrection.created | clockCorrection.approved | clockCorrection.rejected | leaveRequest.created | leaveRequest.approved | leaveRequest.rejected | leaveRequest.cancelled | employee.created | employee.updated | employee.deactivated | employee.deleted[]≤ 20 elementos | Eventos a los que se suscribe, o ["*"] para todos. |
Ejemplo de llamada
curl -X POST "https://api.fichme.com/v1/webhooks" \
-H "x-api-key: $FICHME_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://erp.example.com/webhooks/fichme",
"description": "ERP de nóminas",
"events": [
"clockEntry.created",
"leaveRequest.approved"
]
}'const res = await fetch("https://api.fichme.com/v1/webhooks", {
method: "POST",
headers: {
"x-api-key": process.env.FICHME_API_KEY,
"Content-Type": "application/json",
},
body: JSON.stringify({
url: "https://erp.example.com/webhooks/fichme",
description: "ERP de nóminas",
events: ["clockEntry.created", "leaveRequest.approved"],
}),
});
if (!res.ok) {
const { error } = await res.json();
throw new Error(`${res.status} ${error.code}: ${error.message}`);
}
const data = await res.json();import os
import requests
res = requests.post(
"https://api.fichme.com/v1/webhooks",
headers={
"x-api-key": os.environ["FICHME_API_KEY"],
},
json={
"url": "https://erp.example.com/webhooks/fichme",
"description": "ERP de nóminas",
"events": ["clockEntry.created", "leaveRequest.approved"],
},
timeout=30,
)
if not res.ok:
error = res.json()["error"]
raise RuntimeError(f"{res.status_code} {error['code']}: {error['message']}")
data = res.json()<?php
$ch = curl_init('https://api.fichme.com/v1/webhooks');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CUSTOMREQUEST => 'POST',
CURLOPT_HTTPHEADER => [
'x-api-key: ' . getenv('FICHME_API_KEY'),
'Content-Type: application/json',
],
CURLOPT_POSTFIELDS => json_encode([
'url' => 'https://erp.example.com/webhooks/fichme',
'description' => 'ERP de nóminas',
'events' => ['clockEntry.created', 'leaveRequest.approved'],
]),
]);
$body = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
$data = json_decode($body, true);
if ($status >= 400) {
throw new RuntimeException("$status {$data['error']['code']}: {$data['error']['message']}");
}Respuesta 201
Webhook creado · WebhookEndpointWithSecret
{
"endpoint": {
"object": "webhookEndpoint",
"id": "cm1whk4r0006qx8v2n6c3b7xy",
"url": "https://erp.example.com/webhooks/fichme",
"description": "ERP de nóminas",
"events": [
"clockEntry.created",
"leaveRequest.approved"
],
"status": "ACTIVE",
"failingSince": null,
"disabledReason": null,
"secretLast4": "hJ2l",
"createdAt": "2026-09-10T09:00:00.000Z",
"updatedAt": "2026-09-10T09:00:00.000Z",
"lastDelivery": null
},
"secret": "whsec_q9Xv2LmT7pR4sW8zN1bK6cY3fH0dJ5gA2eU7iO4hJ2l"
}
Errores
| HTTP | code | Cuándo pasa |
|---|---|---|
| 403 | plan_upgrade_required | Los webhooks están en los planes Business y Enterprise. |
| 422 | validation_failed | URL no https, con IP o de una red interna; evento desconocido; o ya hay 10 webhooks. |
| 503 | service_unavailable | Los webhooks no están disponibles en este momento. |
Además, los comunes a todas: 400 invalid_request, 401, 403 insufficient_scope y 429 (ver Errores).
GET /v1/webhooks/{id}
Un webhook
Un webhook con el resultado de su última entrega.
Parámetros
| Nombre | En | Tipo | Descripción | Ejemplo |
|---|---|---|---|---|
id obligatorio | path | string ≤ 64 caracteres | Id del webhook. | cm1whk4r0006qx8v2n6c3b7xy |
Ejemplo de llamada
curl "https://api.fichme.com/v1/webhooks/cm1whk4r0006qx8v2n6c3b7xy" \
-H "x-api-key: $FICHME_API_KEY"const res = await fetch("https://api.fichme.com/v1/webhooks/cm1whk4r0006qx8v2n6c3b7xy", {
headers: {
"x-api-key": process.env.FICHME_API_KEY,
},
});
if (!res.ok) {
const { error } = await res.json();
throw new Error(`${res.status} ${error.code}: ${error.message}`);
}
const data = await res.json();import os
import requests
res = requests.get(
"https://api.fichme.com/v1/webhooks/cm1whk4r0006qx8v2n6c3b7xy",
headers={
"x-api-key": os.environ["FICHME_API_KEY"],
},
timeout=30,
)
if not res.ok:
error = res.json()["error"]
raise RuntimeError(f"{res.status_code} {error['code']}: {error['message']}")
data = res.json()<?php
$ch = curl_init('https://api.fichme.com/v1/webhooks/cm1whk4r0006qx8v2n6c3b7xy');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'x-api-key: ' . getenv('FICHME_API_KEY'),
],
]);
$body = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
$data = json_decode($body, true);
if ($status >= 400) {
throw new RuntimeException("$status {$data['error']['code']}: {$data['error']['message']}");
}Respuesta 200
El webhook · WebhookEndpoint
{
"object": "webhookEndpoint",
"id": "cm1whk4r0006qx8v2n6c3b7xy",
"url": "https://erp.example.com/webhooks/fichme",
"description": "ERP de nóminas",
"events": [
"clockEntry.created",
"leaveRequest.approved"
],
"status": "ACTIVE",
"failingSince": null,
"disabledReason": null,
"secretLast4": "hJ2l",
"createdAt": "2026-09-10T09:00:00.000Z",
"updatedAt": "2026-09-10T09:00:00.000Z",
"lastDelivery": {
"status": "DELIVERED",
"httpStatus": 200,
"at": "2026-09-15T06:58:33.000Z"
}
}
Errores
| HTTP | code | Cuándo pasa |
|---|---|---|
| 404 | not_found | No existe o no es de tu empresa. |
Además, los comunes a todas: 400 invalid_request, 401, 403 insufficient_scope y 429 (ver Errores).
PATCH /v1/webhooks/{id}
Editar, pausar o reactivar un webhook
Cambia solo los campos enviados. Reactivar uno DISABLED le da otra oportunidad completa.
Parámetros
| Nombre | En | Tipo | Descripción | Ejemplo |
|---|---|---|---|---|
id obligatorio | path | string ≤ 64 caracteres | Id del webhook. | cm1whk4r0006qx8v2n6c3b7xy |
Cuerpo (JSON)
| Campo | Tipo | Descripción |
|---|---|---|
url | string ≤ 2048 caracteres | Nueva URL https. |
description | string | null | Descripción (null la borra). |
events | * | clockEntry.created | clockEntry.updated | clockCorrection.created | clockCorrection.approved | clockCorrection.rejected | leaveRequest.created | leaveRequest.approved | leaveRequest.rejected | leaveRequest.cancelled | employee.created | employee.updated | employee.deactivated | employee.deleted[]≤ 20 elementos | Conjunto COMPLETO de eventos suscritos. |
status | ACTIVE | PAUSED | PAUSED deja de enviar; ACTIVE lo reanuda (y reactiva uno DISABLED). |
Ejemplo de llamada
curl -X PATCH "https://api.fichme.com/v1/webhooks/cm1whk4r0006qx8v2n6c3b7xy" \
-H "x-api-key: $FICHME_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"status": "PAUSED"
}'const res = await fetch("https://api.fichme.com/v1/webhooks/cm1whk4r0006qx8v2n6c3b7xy", {
method: "PATCH",
headers: {
"x-api-key": process.env.FICHME_API_KEY,
"Content-Type": "application/json",
},
body: JSON.stringify({
status: "PAUSED",
}),
});
if (!res.ok) {
const { error } = await res.json();
throw new Error(`${res.status} ${error.code}: ${error.message}`);
}
const data = await res.json();import os
import requests
res = requests.patch(
"https://api.fichme.com/v1/webhooks/cm1whk4r0006qx8v2n6c3b7xy",
headers={
"x-api-key": os.environ["FICHME_API_KEY"],
},
json={
"status": "PAUSED",
},
timeout=30,
)
if not res.ok:
error = res.json()["error"]
raise RuntimeError(f"{res.status_code} {error['code']}: {error['message']}")
data = res.json()<?php
$ch = curl_init('https://api.fichme.com/v1/webhooks/cm1whk4r0006qx8v2n6c3b7xy');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CUSTOMREQUEST => 'PATCH',
CURLOPT_HTTPHEADER => [
'x-api-key: ' . getenv('FICHME_API_KEY'),
'Content-Type: application/json',
],
CURLOPT_POSTFIELDS => json_encode([
'status' => 'PAUSED',
]),
]);
$body = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
$data = json_decode($body, true);
if ($status >= 400) {
throw new RuntimeException("$status {$data['error']['code']}: {$data['error']['message']}");
}Respuesta 200
Webhook actualizado · WebhookEndpoint
{
"object": "webhookEndpoint",
"id": "cm1whk4r0006qx8v2n6c3b7xy",
"url": "https://erp.example.com/webhooks/fichme",
"description": "ERP de nóminas",
"events": [
"clockEntry.created",
"leaveRequest.approved"
],
"status": "PAUSED",
"failingSince": null,
"disabledReason": null,
"secretLast4": "hJ2l",
"createdAt": "2026-09-10T09:00:00.000Z",
"updatedAt": "2026-09-16T12:30:00.000Z",
"lastDelivery": {
"status": "DELIVERED",
"httpStatus": 200,
"at": "2026-09-15T06:58:33.000Z"
}
}
Errores
| HTTP | code | Cuándo pasa |
|---|---|---|
| 422 | validation_failed | URL no válida, evento desconocido, o no hay nada que cambiar. |
| 403 | plan_upgrade_required | Reactivar (status ACTIVE) sin webhooks en el plan. |
| 404 | not_found | No existe o no es de tu empresa. |
Además, los comunes a todas: 400 invalid_request, 401, 403 insufficient_scope y 429 (ver Errores).
DELETE /v1/webhooks/{id}
Borrar un webhook
Borra también su historial de entregas.
Parámetros
| Nombre | En | Tipo | Descripción | Ejemplo |
|---|---|---|---|---|
id obligatorio | path | string ≤ 64 caracteres | Id del webhook. | cm1whk4r0006qx8v2n6c3b7xy |
Ejemplo de llamada
curl -X DELETE "https://api.fichme.com/v1/webhooks/cm1whk4r0006qx8v2n6c3b7xy" \
-H "x-api-key: $FICHME_API_KEY"const res = await fetch("https://api.fichme.com/v1/webhooks/cm1whk4r0006qx8v2n6c3b7xy", {
method: "DELETE",
headers: {
"x-api-key": process.env.FICHME_API_KEY,
},
});
if (!res.ok) {
const { error } = await res.json();
throw new Error(`${res.status} ${error.code}: ${error.message}`);
}import os
import requests
res = requests.delete(
"https://api.fichme.com/v1/webhooks/cm1whk4r0006qx8v2n6c3b7xy",
headers={
"x-api-key": os.environ["FICHME_API_KEY"],
},
timeout=30,
)
if not res.ok:
error = res.json()["error"]
raise RuntimeError(f"{res.status_code} {error['code']}: {error['message']}")<?php
$ch = curl_init('https://api.fichme.com/v1/webhooks/cm1whk4r0006qx8v2n6c3b7xy');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CUSTOMREQUEST => 'DELETE',
CURLOPT_HTTPHEADER => [
'x-api-key: ' . getenv('FICHME_API_KEY'),
],
]);
$body = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
$data = json_decode($body, true);
if ($status >= 400) {
throw new RuntimeException("$status {$data['error']['code']}: {$data['error']['message']}");
}Respuesta 204
Borrado (sin cuerpo) · sin cuerpo
Errores
| HTTP | code | Cuándo pasa |
|---|---|---|
| 404 | not_found | No existe o no es de tu empresa. |
Además, los comunes a todas: 400 invalid_request, 401, 403 insufficient_scope y 429 (ver Errores).
POST /v1/webhooks/{id}/ping
Enviar un evento de prueba
Encola un evento webhook.ping (un solo intento). Consulta el resultado en las entregas.
Parámetros
| Nombre | En | Tipo | Descripción | Ejemplo |
|---|---|---|---|---|
id obligatorio | path | string ≤ 64 caracteres | Id del webhook. | cm1whk4r0006qx8v2n6c3b7xy |
Ejemplo de llamada
curl -X POST "https://api.fichme.com/v1/webhooks/cm1whk4r0006qx8v2n6c3b7xy/ping" \
-H "x-api-key: $FICHME_API_KEY"const res = await fetch("https://api.fichme.com/v1/webhooks/cm1whk4r0006qx8v2n6c3b7xy/ping", {
method: "POST",
headers: {
"x-api-key": process.env.FICHME_API_KEY,
},
});
if (!res.ok) {
const { error } = await res.json();
throw new Error(`${res.status} ${error.code}: ${error.message}`);
}
const data = await res.json();import os
import requests
res = requests.post(
"https://api.fichme.com/v1/webhooks/cm1whk4r0006qx8v2n6c3b7xy/ping",
headers={
"x-api-key": os.environ["FICHME_API_KEY"],
},
timeout=30,
)
if not res.ok:
error = res.json()["error"]
raise RuntimeError(f"{res.status_code} {error['code']}: {error['message']}")
data = res.json()<?php
$ch = curl_init('https://api.fichme.com/v1/webhooks/cm1whk4r0006qx8v2n6c3b7xy/ping');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CUSTOMREQUEST => 'POST',
CURLOPT_HTTPHEADER => [
'x-api-key: ' . getenv('FICHME_API_KEY'),
],
]);
$body = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
$data = json_decode($body, true);
if ($status >= 400) {
throw new RuntimeException("$status {$data['error']['code']}: {$data['error']['message']}");
}Respuesta 202
Prueba en cola · WebhookPing
{
"deliveryId": "48214"
}
Errores
| HTTP | code | Cuándo pasa |
|---|---|---|
| 409 | invalid_state | El webhook no está ACTIVE: reactívalo antes. |
| 403 | plan_upgrade_required | Los webhooks están en los planes Business y Enterprise. |
| 404 | not_found | No existe o no es de tu empresa. |
Además, los comunes a todas: 400 invalid_request, 401, 403 insufficient_scope y 429 (ver Errores).
POST /v1/webhooks/{id}/rotate-secret
Rotar el secreto de firma
El secreto anterior deja de valer al instante; el nuevo se devuelve una sola vez.
Parámetros
| Nombre | En | Tipo | Descripción | Ejemplo |
|---|---|---|---|---|
id obligatorio | path | string ≤ 64 caracteres | Id del webhook. | cm1whk4r0006qx8v2n6c3b7xy |
Ejemplo de llamada
curl -X POST "https://api.fichme.com/v1/webhooks/cm1whk4r0006qx8v2n6c3b7xy/rotate-secret" \
-H "x-api-key: $FICHME_API_KEY"const res = await fetch("https://api.fichme.com/v1/webhooks/cm1whk4r0006qx8v2n6c3b7xy/rotate-secret", {
method: "POST",
headers: {
"x-api-key": process.env.FICHME_API_KEY,
},
});
if (!res.ok) {
const { error } = await res.json();
throw new Error(`${res.status} ${error.code}: ${error.message}`);
}
const data = await res.json();import os
import requests
res = requests.post(
"https://api.fichme.com/v1/webhooks/cm1whk4r0006qx8v2n6c3b7xy/rotate-secret",
headers={
"x-api-key": os.environ["FICHME_API_KEY"],
},
timeout=30,
)
if not res.ok:
error = res.json()["error"]
raise RuntimeError(f"{res.status_code} {error['code']}: {error['message']}")
data = res.json()<?php
$ch = curl_init('https://api.fichme.com/v1/webhooks/cm1whk4r0006qx8v2n6c3b7xy/rotate-secret');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CUSTOMREQUEST => 'POST',
CURLOPT_HTTPHEADER => [
'x-api-key: ' . getenv('FICHME_API_KEY'),
],
]);
$body = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
$data = json_decode($body, true);
if ($status >= 400) {
throw new RuntimeException("$status {$data['error']['code']}: {$data['error']['message']}");
}Respuesta 200
Secreto nuevo · WebhookEndpointWithSecret
{
"endpoint": {
"object": "webhookEndpoint",
"id": "cm1whk4r0006qx8v2n6c3b7xy",
"url": "https://erp.example.com/webhooks/fichme",
"description": "ERP de nóminas",
"events": [
"clockEntry.created",
"leaveRequest.approved"
],
"status": "ACTIVE",
"failingSince": null,
"disabledReason": null,
"secretLast4": "hJ2l",
"createdAt": "2026-09-10T09:00:00.000Z",
"updatedAt": "2026-09-10T09:00:00.000Z",
"lastDelivery": {
"status": "DELIVERED",
"httpStatus": 200,
"at": "2026-09-15T06:58:33.000Z"
}
},
"secret": "whsec_q9Xv2LmT7pR4sW8zN1bK6cY3fH0dJ5gA2eU7iO4hJ2l"
}
Errores
| HTTP | code | Cuándo pasa |
|---|---|---|
| 403 | plan_upgrade_required | Los webhooks están en los planes Business y Enterprise. |
| 404 | not_found | No existe o no es de tu empresa. |
Además, los comunes a todas: 400 invalid_request, 401, 403 insufficient_scope y 429 (ver Errores).
GET /v1/webhooks/{id}/deliveries
Historial de entregas
Las más recientes primero. Se conservan 30 días.
Parámetros
| Nombre | En | Tipo | Descripción | Ejemplo |
|---|---|---|---|---|
id obligatorio | path | string ≤ 64 caracteres | Id del webhook. | cm1whk4r0006qx8v2n6c3b7xy |
status | query | all | failed | pending | deliveredpor defecto "all" | Filtrar por resultado. | failed |
limit | query | integer por defecto 50 · mín. 1 · máx. 200 | Cuántas entregas (máx. 200). |
Ejemplo de llamada
curl "https://api.fichme.com/v1/webhooks/cm1whk4r0006qx8v2n6c3b7xy/deliveries?status=failed" \
-H "x-api-key: $FICHME_API_KEY"const params = new URLSearchParams({
status: "failed",
});
const res = await fetch(`https://api.fichme.com/v1/webhooks/cm1whk4r0006qx8v2n6c3b7xy/deliveries?${params}`, {
headers: {
"x-api-key": process.env.FICHME_API_KEY,
},
});
if (!res.ok) {
const { error } = await res.json();
throw new Error(`${res.status} ${error.code}: ${error.message}`);
}
const data = await res.json();import os
import requests
res = requests.get(
"https://api.fichme.com/v1/webhooks/cm1whk4r0006qx8v2n6c3b7xy/deliveries",
params={
"status": "failed",
},
headers={
"x-api-key": os.environ["FICHME_API_KEY"],
},
timeout=30,
)
if not res.ok:
error = res.json()["error"]
raise RuntimeError(f"{res.status_code} {error['code']}: {error['message']}")
data = res.json()<?php
$ch = curl_init('https://api.fichme.com/v1/webhooks/cm1whk4r0006qx8v2n6c3b7xy/deliveries?' . http_build_query([
'status' => 'failed',
]));
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'x-api-key: ' . getenv('FICHME_API_KEY'),
],
]);
$body = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
$data = json_decode($body, true);
if ($status >= 400) {
throw new RuntimeException("$status {$data['error']['code']}: {$data['error']['message']}");
}Respuesta 200
Entregas · Lista de WebhookDelivery
{
"object": "list",
"data": [
{
"object": "webhookDelivery",
"id": "48213",
"eventId": "evt_01K5Q8Z3M2X7C9V4B6N1P8R0TQ",
"eventType": "clockEntry.created",
"status": "DELIVERED",
"attempts": 1,
"lastStatus": 200,
"lastError": null,
"createdAt": "2026-09-15T06:58:32.000Z",
"deliveredAt": "2026-09-15T06:58:33.000Z",
"nextAttemptAt": null
}
],
"page": 1,
"limit": 1,
"total": 1,
"totalPages": 1
}
Errores
| HTTP | code | Cuándo pasa |
|---|---|---|
| 404 | not_found | No existe o no es de tu empresa. |
Además, los comunes a todas: 400 invalid_request, 401, 403 insufficient_scope y 429 (ver Errores).
POST /v1/webhooks/{id}/deliveries/{deliveryId}/retry
Reintentar una entrega
Vuelve a encolar una entrega (con un intento más aunque se hubieran agotado), p. ej. tras arreglar tu servidor.
Parámetros
| Nombre | En | Tipo | Descripción | Ejemplo |
|---|---|---|---|---|
id obligatorio | path | string ≤ 64 caracteres | Id del webhook. | cm1whk4r0006qx8v2n6c3b7xy |
deliveryId obligatorio | path | string | Id de la entrega. | 48213 |
Ejemplo de llamada
curl -X POST "https://api.fichme.com/v1/webhooks/cm1whk4r0006qx8v2n6c3b7xy/deliveries/48213/retry" \
-H "x-api-key: $FICHME_API_KEY"const res = await fetch("https://api.fichme.com/v1/webhooks/cm1whk4r0006qx8v2n6c3b7xy/deliveries/48213/retry", {
method: "POST",
headers: {
"x-api-key": process.env.FICHME_API_KEY,
},
});
if (!res.ok) {
const { error } = await res.json();
throw new Error(`${res.status} ${error.code}: ${error.message}`);
}
const data = await res.json();import os
import requests
res = requests.post(
"https://api.fichme.com/v1/webhooks/cm1whk4r0006qx8v2n6c3b7xy/deliveries/48213/retry",
headers={
"x-api-key": os.environ["FICHME_API_KEY"],
},
timeout=30,
)
if not res.ok:
error = res.json()["error"]
raise RuntimeError(f"{res.status_code} {error['code']}: {error['message']}")
data = res.json()<?php
$ch = curl_init('https://api.fichme.com/v1/webhooks/cm1whk4r0006qx8v2n6c3b7xy/deliveries/48213/retry');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CUSTOMREQUEST => 'POST',
CURLOPT_HTTPHEADER => [
'x-api-key: ' . getenv('FICHME_API_KEY'),
],
]);
$body = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
$data = json_decode($body, true);
if ($status >= 400) {
throw new RuntimeException("$status {$data['error']['code']}: {$data['error']['message']}");
}Respuesta 200
Entrega reencolada · WebhookDelivery
{
"object": "webhookDelivery",
"id": "48213",
"eventId": "evt_01K5Q8Z3M2X7C9V4B6N1P8R0TQ",
"eventType": "clockEntry.created",
"status": "PENDING",
"attempts": 6,
"lastStatus": 503,
"lastError": "HTTP 503",
"createdAt": "2026-09-15T06:58:32.000Z",
"deliveredAt": null,
"nextAttemptAt": "2026-09-16T12:31:00.000Z"
}
Errores
| HTTP | code | Cuándo pasa |
|---|---|---|
| 404 | not_found | No existe o no es de tu empresa. |
Además, los comunes a todas: 400 invalid_request, 401, 403 insufficient_scope y 429 (ver Errores).
Objetos
Todos los campos de cada objeto, con un valor de ejemplo. Despliega «Ejemplo completo» para ver el JSON entero.
Me
| Campo | Tipo | Descripción | Ejemplo | ||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
object | "me" | Tipo de objeto: siempre | "me" | ||||||||||||||||||||||||
apiKey | object | La clave con la que se hace la llamada. Campos
| |||||||||||||||||||||||||
company | object | La empresa de la clave: la única cuyos datos ve. Campos
| |||||||||||||||||||||||||
plan | object | Plan de la empresa. Campos
| |||||||||||||||||||||||||
rateLimit | object | Límites que se aplican a esta clave. Campos
| |||||||||||||||||||||||||
apiVersion | "v1" | Versión de la API. | "v1" | ||||||||||||||||||||||||
serverTime | string | Hora del servidor en UTC: sirve para comprobar el reloj de tu sistema. | "2026-09-15T10:04:12.000Z" |
Ejemplo completo
{
"object": "me",
"apiKey": {
"id": "cm1key9p0008qx5b3c7n2m4wl",
"name": "Gestoría Pérez",
"prefix": "fm_live_k7m2p5x4q3ab",
"scopes": [
"company:read",
"employees:read",
"clock:read"
],
"expiresAt": "2027-09-15T10:00:00.000Z"
},
"company": {
"id": 1042,
"name": "Construcciones Ebro",
"slug": "construcciones-ebro",
"timezone": "Europe/Madrid"
},
"plan": {
"name": "Business",
"webhooks": true
},
"rateLimit": {
"perMinute": 120,
"perDay": 50000,
"maxConcurrentHeavy": 1
},
"apiVersion": "v1",
"serverTime": "2026-09-15T10:04:12.000Z"
}Company
| Campo | Tipo | Descripción | Ejemplo | ||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
object | "company" | Tipo de objeto: siempre | "company" | ||||||||||||||||||||||||||||||||||||
id | integer | Id de la empresa en FichMe. | 1042 | ||||||||||||||||||||||||||||||||||||
name | string | Nombre comercial. | "Construcciones Ebro" | ||||||||||||||||||||||||||||||||||||
legalName | string | null | Razón social. | "Construcciones Ebro, S.L." | ||||||||||||||||||||||||||||||||||||
taxId | string | null | CIF de la empresa. | "B50123456" | ||||||||||||||||||||||||||||||||||||
slug | string | Identificador corto (el subdominio del panel). | "construcciones-ebro" | ||||||||||||||||||||||||||||||||||||
address | string | null | Dirección fiscal. | "Calle del Coso 42" | ||||||||||||||||||||||||||||||||||||
city | string | null | Localidad. | "Zaragoza" | ||||||||||||||||||||||||||||||||||||
province | string | null | Provincia. | "Zaragoza" | ||||||||||||||||||||||||||||||||||||
postalCode | string | null | Código postal. | "50004" | ||||||||||||||||||||||||||||||||||||
country | string | null | País. | "España" | ||||||||||||||||||||||||||||||||||||
timezone | string | Zona horaria IANA contra la que se resuelve | "Europe/Madrid" | ||||||||||||||||||||||||||||||||||||
settings | object | Ajustes que hacen falta para interpretar el resto de datos. Campos
|
Ejemplo completo
{
"object": "company",
"id": 1042,
"name": "Construcciones Ebro",
"legalName": "Construcciones Ebro, S.L.",
"taxId": "B50123456",
"slug": "construcciones-ebro",
"address": "Calle del Coso 42",
"city": "Zaragoza",
"province": "Zaragoza",
"postalCode": "50004",
"country": "España",
"timezone": "Europe/Madrid",
"settings": {
"workingDays": [
1,
2,
3,
4,
5
],
"lateArrivalThreshold": 10,
"earlyDepartureThreshold": 10,
"mandatoryBreakMinutes": 30,
"maxShiftHours": 16,
"apiClockingEnabled": true,
"hourBankEnabled": true,
"geolocationEnabled": false
}
}Location
| Campo | Tipo | Descripción | Ejemplo | ||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
object | "location" | Tipo de objeto: siempre | "location" | ||||||||||||||||||||
id | string | Id del centro de trabajo. | "cm1loczgz0001qx8f2k9d7h3a" | ||||||||||||||||||||
name | string | Nombre del centro. | "Oficina Zaragoza" | ||||||||||||||||||||
code | string | null | Código interno del centro, si la empresa lo usa. | "ZGZ" | ||||||||||||||||||||
address | string | null | Dirección. | "Calle del Coso 42" | ||||||||||||||||||||
city | string | null | Localidad. | "Zaragoza" | ||||||||||||||||||||
province | string | null | Provincia (decide los festivos autonómicos). | "Zaragoza" | ||||||||||||||||||||
timezone | string | null | Zona horaria propia del centro (null = la de la empresa). | null | ||||||||||||||||||||
isActive | boolean | El centro está en uso. | true | ||||||||||||||||||||
isPrimary | boolean | Es el centro principal de la empresa. | true | ||||||||||||||||||||
isSystem | boolean | Centro que crea FichMe al dar de alta la empresa («Principal» o «Teletrabajo»). Se puede fichar en él, pero no se asigna a empleados. | false | ||||||||||||||||||||
geofence | object | Geovalla: coordenadas del CENTRO, nunca de personas. Campos
| |||||||||||||||||||||
createdAt | string | Alta del centro (UTC). | "2025-02-03T09:12:44.000Z" | ||||||||||||||||||||
updatedAt | string | Último cambio (UTC). | "2026-06-11T15:30:02.000Z" |
Ejemplo completo
{
"object": "location",
"id": "cm1loczgz0001qx8f2k9d7h3a",
"name": "Oficina Zaragoza",
"code": "ZGZ",
"address": "Calle del Coso 42",
"city": "Zaragoza",
"province": "Zaragoza",
"timezone": null,
"isActive": true,
"isPrimary": true,
"isSystem": false,
"geofence": {
"enabled": true,
"radiusMeters": 150,
"latitude": 41.6523,
"longitude": -0.8773
},
"createdAt": "2025-02-03T09:12:44.000Z",
"updatedAt": "2026-06-11T15:30:02.000Z"
}Employee
| Campo | Tipo | Descripción | Ejemplo | ||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
object | "employee" | Tipo de objeto: siempre | "employee" | ||||||||||||||||||||||||
id | integer | Id del empleado en FichMe. | 4821 | ||||||||||||||||||||||||
firstName | string | null | Nombre. | "Ana" | ||||||||||||||||||||||||
lastName | string | null | Apellidos. | "García López" | ||||||||||||||||||||||||
name | string | Nombre completo, como aparece en los informes. | "Ana García López" | ||||||||||||||||||||||||
email | string | null | Email (también es su usuario de acceso). | "ana.garcia@example.com" | ||||||||||||||||||||||||
role | ADMIN | EMPLOYEE | Rol en FichMe. Las cuentas ADMIN solo se modifican desde el panel. | "EMPLOYEE" | ||||||||||||||||||||||||
status | ACTIVE | INACTIVE | INACTIVE = dado de baja operativa: no ficha ni ocupa plaza del plan. | "ACTIVE" | ||||||||||||||||||||||||
activationStatus | string | Estado del acceso a FichMe: PENDING_SETUP | PENDING_ACTIVATION | EMAIL_FAILED | ACTIVATED | NOT_APPLICABLE. | "ACTIVATED" | ||||||||||||||||||||||||
jobTitle | string | null | Puesto. | "Técnica de obra" | ||||||||||||||||||||||||
department | string | null | Departamento. | "Producción" | ||||||||||||||||||||||||
hireDate | string | null | Fecha de alta (YYYY-MM-DD). | "2024-03-01" | ||||||||||||||||||||||||
seniorityDate | string | null | Fecha de antigüedad (YYYY-MM-DD). | "2024-03-01" | ||||||||||||||||||||||||
contract | object | Datos del contrato. Campos
| |||||||||||||||||||||||||
location | object | null | Centro principal. Campos
| |||||||||||||||||||||||||
locations | object[] | Todos los centros a los que pertenece. Campos
| |||||||||||||||||||||||||
clockingPolicy | object | Excepciones del empleado a la política de fichaje de la empresa. Campos
| |||||||||||||||||||||||||
identity | object | null | Datos fiscales. Solo con el scope employees:read_pii; sin él, null. Campos
| |||||||||||||||||||||||||
createdAt | string | Alta en FichMe (UTC). | "2024-02-20T10:05:13.000Z" | ||||||||||||||||||||||||
updatedAt | string | Último cambio (UTC). Es la marca de la sincronización incremental. | "2026-09-01T08:14:55.000Z" | ||||||||||||||||||||||||
deletedAt | string | null | Baja lógica (UTC), o null si sigue en la empresa. | null |
Ejemplo completo
{
"object": "employee",
"id": 4821,
"firstName": "Ana",
"lastName": "García López",
"name": "Ana García López",
"email": "ana.garcia@example.com",
"role": "EMPLOYEE",
"status": "ACTIVE",
"activationStatus": "ACTIVATED",
"jobTitle": "Técnica de obra",
"department": "Producción",
"hireDate": "2024-03-01",
"seniorityDate": "2024-03-01",
"contract": {
"type": "FULL_TIME",
"weeklyHours": 40,
"annualHours": 1776,
"isNightWorker": false
},
"location": {
"id": "cm1loczgz0001qx8f2k9d7h3a",
"name": "Oficina Zaragoza"
},
"locations": [
{
"id": "cm1loczgz0001qx8f2k9d7h3a",
"name": "Oficina Zaragoza",
"isPrimary": true
}
],
"clockingPolicy": {
"exempt": false,
"web": null,
"app": true,
"terminal": null,
"requireGeolocation": null
},
"identity": {
"taxId": "12345678Z",
"socialSecurityNumber": "281234567840",
"phone": "+34 600 123 456"
},
"createdAt": "2024-02-20T10:05:13.000Z",
"updatedAt": "2026-09-01T08:14:55.000Z",
"deletedAt": null
}ClockEntry
| Campo | Tipo | Descripción | Ejemplo | ||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
object | "clockEntry" | Tipo de objeto: siempre | "clockEntry" | ||||||||||||||||||||||||||||||||
id | integer | Id del fichaje. | 918273 | ||||||||||||||||||||||||||||||||
employeeId | integer | Empleado que fichó. | 4821 | ||||||||||||||||||||||||||||||||
type | IN | OUT | BREAK_START | BREAK_END | IN = entrada · OUT = salida · BREAK_START / BREAK_END = inicio y fin de pausa. | "IN" | ||||||||||||||||||||||||||||||||
timestamp | string | Instante del fichaje en UTC (la hora oficial, la del servidor). | "2026-09-15T06:58:31.000Z" | ||||||||||||||||||||||||||||||||
shiftDate | string | Día de jornada en la zona horaria de la empresa. Un turno de noche que sale de madrugada conserva el día de la entrada. | "2026-09-15" | ||||||||||||||||||||||||||||||||
method | WEB | APP | QR | KIOSK | SLACK | WHATSAPP | API | Canal por el que se fichó. | "APP" | ||||||||||||||||||||||||||||||||
source | string | Origen técnico: WEB | MOBILE | KIOSK | SLACK | WHATSAPP | API. | "MOBILE" | ||||||||||||||||||||||||||||||||
status | COMPLETE | INCOMPLETE | PENDING | INCOMPLETE = la jornada se quedó sin cerrar y la marcó el sistema. | "COMPLETE" | ||||||||||||||||||||||||||||||||
location | object | null | Centro de trabajo del fichaje. Campos
| |||||||||||||||||||||||||||||||||
isModified | boolean | Se corrigió después de registrarse (queda trazado en la cadena de integridad). | false | ||||||||||||||||||||||||||||||||
requiresCorrection | boolean | Tiene una corrección pendiente o se marcó para revisión. | false | ||||||||||||||||||||||||||||||||
offlineCreated | boolean | Se registró sin conexión en un terminal y se sincronizó después. | false | ||||||||||||||||||||||||||||||||
note | string | null | Nota del empleado. | null | ||||||||||||||||||||||||||||||||
adminNote | string | null | Nota del administrador. | null | ||||||||||||||||||||||||||||||||
expected | object | Lo que esperaba el turno asignado cuando se registró el fichaje. Campos
| |||||||||||||||||||||||||||||||||
createdAt | string | Cuándo se guardó (UTC). Puede ser posterior a timestamp si llegó sin conexión. | "2026-09-15T06:58:31.000Z" | ||||||||||||||||||||||||||||||||
updatedAt | string | Último cambio (UTC). Es la marca de la sincronización incremental. | "2026-09-15T06:58:31.000Z" | ||||||||||||||||||||||||||||||||
deletedAt | string | null | Eliminado (UTC), o null. | null |
Ejemplo completo
{
"object": "clockEntry",
"id": 918273,
"employeeId": 4821,
"type": "IN",
"timestamp": "2026-09-15T06:58:31.000Z",
"shiftDate": "2026-09-15",
"method": "APP",
"source": "MOBILE",
"status": "COMPLETE",
"location": {
"id": "cm1loczgz0001qx8f2k9d7h3a",
"name": "Oficina Zaragoza"
},
"isModified": false,
"requiresCorrection": false,
"offlineCreated": false,
"note": null,
"adminNote": null,
"expected": {
"startTime": "09:00",
"endTime": "17:00",
"hours": 7.5,
"arrivalStatus": "on-time",
"minutesLate": -2,
"departureStatus": null,
"minutesEarly": null
},
"createdAt": "2026-09-15T06:58:31.000Z",
"updatedAt": "2026-09-15T06:58:31.000Z",
"deletedAt": null
}WorkSession
| Campo | Tipo | Descripción | Ejemplo | ||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
object | "workSession" | Tipo de objeto: siempre | "workSession" | ||||||||||||||||||||||||||||||||
employeeId | integer | Empleado. | 4821 | ||||||||||||||||||||||||||||||||
shiftDate | string | Día de jornada (YYYY-MM-DD, zona de la empresa). | "2026-09-15" | ||||||||||||||||||||||||||||||||
dayType | WORKING | HOLIDAY | REST | Tipo de día según el calendario. | "WORKING" | ||||||||||||||||||||||||||||||||
status | COMPLETE | INCOMPLETE | ABSENT | LEAVE | HOLIDAY | REST | ABSENT = día laborable sin fichajes ni ausencia; INCOMPLETE = falta una entrada o una salida (revísalo antes de cerrar la nómina). | "COMPLETE" | ||||||||||||||||||||||||||||||||
scheduleSource | SHIFT | EMPLOYEE_SCHEDULE | LOCATION | COMPANY | CALCULATED | HOLIDAY | LEAVE | NONE | De dónde sale la jornada prevista: turno, horario del empleado, del centro, de la empresa… | "SHIFT" | ||||||||||||||||||||||||||||||||
firstIn | string | null | Primera entrada del día (UTC). | "2026-09-15T06:58:31.000Z" | ||||||||||||||||||||||||||||||||
lastOut | string | null | Última salida del día (UTC). | "2026-09-15T15:02:10.000Z" | ||||||||||||||||||||||||||||||||
segments | object[] | Tramos de trabajo emparejados (IN → OUT), en orden. Campos
| [{"in":"2026-09-15T06:58:31.000Z","out":"2026… | ||||||||||||||||||||||||||||||||
breaks | object[] | Pausas fichadas. Campos
| |||||||||||||||||||||||||||||||||
workedMinutes | integer | Minutos computados como trabajados: lo fichado, más las pausas retribuidas y las ausencias que computan. | 453 | ||||||||||||||||||||||||||||||||
workedTime | string | workedMinutes en HH:mm. | "07:33" | ||||||||||||||||||||||||||||||||
clockedMinutes | integer | null | Parte de workedMinutes que sale de los fichajes. | 453 | ||||||||||||||||||||||||||||||||
breakMinutes | integer | Minutos de pausa. | 30 | ||||||||||||||||||||||||||||||||
paidBreakMinutes | integer | De ellos, retribuidos (cuentan como trabajo). | 0 | ||||||||||||||||||||||||||||||||
autoDeductedBreakMinutes | integer | Pausa del turno descontada aunque no se fichara. | 0 | ||||||||||||||||||||||||||||||||
assignedMinutes | integer | Jornada prevista en minutos. | 450 | ||||||||||||||||||||||||||||||||
assignedTime | string | assignedMinutes en HH:mm. | "07:30" | ||||||||||||||||||||||||||||||||
balanceMinutes | integer | workedMinutes − assignedMinutes. Negativo = faltan minutos. | 3 | ||||||||||||||||||||||||||||||||
balanceTime | string | balanceMinutes en HH:mm (con signo). | "00:03" | ||||||||||||||||||||||||||||||||
leaves | object[] | Ausencias aprobadas que cubren el día (vacío si no hay). Campos
| [] | ||||||||||||||||||||||||||||||||
isModified | boolean | Algún fichaje del día se corrigió. | false | ||||||||||||||||||||||||||||||||
requiresCorrection | boolean | Algún fichaje del día está pendiente de revisión. | false | ||||||||||||||||||||||||||||||||
hasOpenSegment | boolean | El día acaba con una entrada sin salida. | false | ||||||||||||||||||||||||||||||||
engine | string | Motor que produjo las cifras. Si cambia, el changelog de la API lo explica. | "hours_balance_v1" |
Ejemplo completo
{
"object": "workSession",
"employeeId": 4821,
"shiftDate": "2026-09-15",
"dayType": "WORKING",
"status": "COMPLETE",
"scheduleSource": "SHIFT",
"firstIn": "2026-09-15T06:58:31.000Z",
"lastOut": "2026-09-15T15:02:10.000Z",
"segments": [
{
"in": "2026-09-15T06:58:31.000Z",
"out": "2026-09-15T11:00:02.000Z",
"entryIds": [
918273,
918280
],
"location": {
"id": "cm1loczgz0001qx8f2k9d7h3a",
"name": "Oficina Zaragoza"
}
},
{
"in": "2026-09-15T11:30:15.000Z",
"out": "2026-09-15T15:02:10.000Z",
"entryIds": [
918283,
918290
],
"location": {
"id": "cm1loczgz0001qx8f2k9d7h3a",
"name": "Oficina Zaragoza"
}
}
],
"breaks": [
{
"start": "2026-09-15T11:00:02.000Z",
"end": "2026-09-15T11:30:15.000Z",
"entryIds": [
918281,
918282
]
}
],
"workedMinutes": 453,
"workedTime": "07:33",
"clockedMinutes": 453,
"breakMinutes": 30,
"paidBreakMinutes": 0,
"autoDeductedBreakMinutes": 0,
"assignedMinutes": 450,
"assignedTime": "07:30",
"balanceMinutes": 3,
"balanceTime": "00:03",
"leaves": [],
"isModified": false,
"requiresCorrection": false,
"hasOpenSegment": false,
"engine": "hours_balance_v1"
}ClockAction
| Campo | Tipo | Descripción | Ejemplo | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
object | "clockAction" | Tipo de objeto: siempre | "clockAction" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
action | IN | OUT | BREAK_START | BREAK_END | Lo que se ha registrado. | "IN" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
state | working | off | on_break | Estado del empleado después del fichaje: úsalo para pintar la pantalla. | "working" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
entry | object | El fichaje principal (un ClockEntry). Campos
| |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
entries | object[] | Todos los fichajes creados: las pausas crean dos a la vez (OUT + BREAK_START, o BREAK_END + IN). Campos
| [{"object":"clockEntry","id":918273,"employee… | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
idempotentReplay | boolean | true si es la respuesta repetida de un Idempotency-Key ya usado (no se ha fichado otra vez). | false |
Ejemplo completo
{
"object": "clockAction",
"action": "IN",
"state": "working",
"entry": {
"object": "clockEntry",
"id": 918273,
"employeeId": 4821,
"type": "IN",
"timestamp": "2026-09-15T06:58:31.000Z",
"shiftDate": "2026-09-15",
"method": "API",
"source": "API",
"status": "COMPLETE",
"location": {
"id": "cm1loczgz0001qx8f2k9d7h3a",
"name": "Oficina Zaragoza"
},
"isModified": false,
"requiresCorrection": false,
"offlineCreated": false,
"note": null,
"adminNote": null,
"expected": {
"startTime": "09:00",
"endTime": "17:00",
"hours": 7.5,
"arrivalStatus": "on-time",
"minutesLate": -2,
"departureStatus": null,
"minutesEarly": null
},
"createdAt": "2026-09-15T06:58:31.000Z",
"updatedAt": "2026-09-15T06:58:31.000Z",
"deletedAt": null
},
"entries": [
{
"object": "clockEntry",
"id": 918273,
"employeeId": 4821,
"type": "IN",
"timestamp": "2026-09-15T06:58:31.000Z",
"shiftDate": "2026-09-15",
"method": "API",
"source": "API",
"status": "COMPLETE",
"location": {
"id": "cm1loczgz0001qx8f2k9d7h3a",
"name": "Oficina Zaragoza"
},
"isModified": false,
"requiresCorrection": false,
"offlineCreated": false,
"note": null,
"adminNote": null,
"expected": {
"startTime": "09:00",
"endTime": "17:00",
"hours": 7.5,
"arrivalStatus": "on-time",
"minutesLate": -2,
"departureStatus": null,
"minutesEarly": null
},
"createdAt": "2026-09-15T06:58:31.000Z",
"updatedAt": "2026-09-15T06:58:31.000Z",
"deletedAt": null
}
],
"idempotentReplay": false
}ClockCorrection
| Campo | Tipo | Descripción | Ejemplo | ||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
object | "clockCorrection" | Tipo de objeto: siempre | "clockCorrection" | ||||||||||||||||
id | string | Id de la solicitud. | "cm1corr8k2p0007qx3n5v1b9d" | ||||||||||||||||
employeeId | integer | Empleado del fichaje. | 4821 | ||||||||||||||||
clockEntryId | integer | null | Fichaje que corrige (null si propone uno que falta o una jornada completa). | null | ||||||||||||||||
requestType | MISSING_CLOCK_IN | MISSING_CLOCK_OUT | INCORRECT_TIME | MANUAL_FULL_SHIFT | MANUAL_BREAK | MISSING_CLOCK_IN / MISSING_CLOCK_OUT = falta un extremo · INCORRECT_TIME = hora equivocada · MANUAL_FULL_SHIFT = jornada completa olvidada · MANUAL_BREAK = pausa. | "MANUAL_FULL_SHIFT" | ||||||||||||||||
missingType | IN | OUT | BREAK_START | BREAK_END | null | Extremo que se corrige (null en jornadas y pausas completas). | null | ||||||||||||||||
proposedTime | string | null | Hora propuesta (UTC). Null en jornadas y pausas completas: van en | null | ||||||||||||||||
originalTime | string | null | Hora que tenía el fichaje antes de corregirlo (UTC). | null | ||||||||||||||||
manual | object | null | Solo en jornadas y pausas completas: el día y las horas propuestas. Campos
| |||||||||||||||||
location | object | null | Centro propuesto para los fichajes. Campos
| |||||||||||||||||
reason | string | Motivo que dio quien la pidió. | "Olvidé fichar: estuve todo el día en la obra… | ||||||||||||||||
status | PENDING | APPROVED | REJECTED | PENDING hasta que una persona la aprueba o la rechaza. | "PENDING" | ||||||||||||||||
reviewedAt | string | null | Cuándo se revisó (UTC). | null | ||||||||||||||||
reviewedById | integer | null | Administrador que la revisó. | null | ||||||||||||||||
rejectionReason | string | null | Motivo del rechazo (se envía al empleado). | null | ||||||||||||||||
isAutoApproved | boolean | Se aprobó en la misma llamada con autoApprove. | false | ||||||||||||||||
createdAt | string | Cuándo se pidió (UTC). | "2026-09-15T07:05:12.000Z" | ||||||||||||||||
updatedAt | string | Último cambio (UTC). | "2026-09-15T07:05:12.000Z" |
Ejemplo completo
{
"object": "clockCorrection",
"id": "cm1corr8k2p0007qx3n5v1b9d",
"employeeId": 4821,
"clockEntryId": null,
"requestType": "MANUAL_FULL_SHIFT",
"missingType": null,
"proposedTime": null,
"originalTime": null,
"manual": {
"shiftDate": "2026-09-14",
"start": "2026-09-14T07:00:00.000Z",
"end": "2026-09-14T15:00:00.000Z"
},
"location": {
"id": "cm1loczgz0001qx8f2k9d7h3a",
"name": "Oficina Zaragoza"
},
"reason": "Olvidé fichar: estuve todo el día en la obra de Valdespartera.",
"status": "PENDING",
"reviewedAt": null,
"reviewedById": null,
"rejectionReason": null,
"isAutoApproved": false,
"createdAt": "2026-09-15T07:05:12.000Z",
"updatedAt": "2026-09-15T07:05:12.000Z"
}LeaveType
| Campo | Tipo | Descripción | Ejemplo |
|---|---|---|---|
object | "leaveType" | Tipo de objeto: siempre | "leaveType" |
id | string | Id del tipo de ausencia. | "cm1ltvac0001qx7d2k8f4h6jk" |
code | string | Identificador estable para integrar (configurable por empresa): usa este, no el nombre. | "VACATION" |
name | string | Nombre visible. | "Vacaciones" |
description | string | null | Descripción que ve el empleado. | "Vacaciones anuales retribuidas." |
color | string | Color en el calendario (hex). | "#3B82F6" |
unit | DAYS | HOURS | Se pide por días o por horas. | "DAYS" |
dayCountType | BUSINESS_DAYS | NATURAL_DAYS | Qué días cuenta: laborables o naturales. | "BUSINESS_DAYS" |
requiresBalance | boolean | Consume un cupo anual (ver GET /v1/leave-balances). | true |
requiresApproval | boolean | Necesita aprobación; si no, nace aprobada. | true |
requiresDocument | boolean | Pide justificante. | false |
requiresReason | boolean | Pide motivo. | false |
allowHalfDays | boolean | Admite medios días. | true |
isPaid | boolean | Computa como tiempo trabajado. | true |
blockClocking | boolean | Impide fichar esos días. | true |
affectsWorkingDays | boolean | Descuenta la jornada prevista de esos días. | true |
isActive | boolean | Se puede usar al crear ausencias. | true |
isSystem | boolean | Tipo de sistema (p. ej. descanso compensatorio de la bolsa de horas): se lee, no se usa al crear. | false |
createdAt | string | Alta (UTC). | "2025-01-08T11:20:00.000Z" |
updatedAt | string | Último cambio (UTC). | "2026-01-02T09:00:00.000Z" |
Ejemplo completo
{
"object": "leaveType",
"id": "cm1ltvac0001qx7d2k8f4h6jk",
"code": "VACATION",
"name": "Vacaciones",
"description": "Vacaciones anuales retribuidas.",
"color": "#3B82F6",
"unit": "DAYS",
"dayCountType": "BUSINESS_DAYS",
"requiresBalance": true,
"requiresApproval": true,
"requiresDocument": false,
"requiresReason": false,
"allowHalfDays": true,
"isPaid": true,
"blockClocking": true,
"affectsWorkingDays": true,
"isActive": true,
"isSystem": false,
"createdAt": "2025-01-08T11:20:00.000Z",
"updatedAt": "2026-01-02T09:00:00.000Z"
}LeaveRequest
| Campo | Tipo | Descripción | Ejemplo | ||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
object | "leaveRequest" | Tipo de objeto: siempre | "leaveRequest" | ||||||||||||||||
id | string | Id de la ausencia. | "cm1lr5q2w0009qx4m7c3z8k1m" | ||||||||||||||||
employeeId | integer | Empleado. | 4821 | ||||||||||||||||
leaveType | object | Tipo de ausencia. Los de salud (bajas, IT, consultas médicas…) son datos sensibles: solo con employees:read_pii. Campos
| |||||||||||||||||
unit | DAYS | HOURS | Por días o por horas. | "DAYS" | ||||||||||||||||
startDate | string | Primer día (YYYY-MM-DD). | "2026-10-13" | ||||||||||||||||
endDate | string | Último día (YYYY-MM-DD), inclusive. | "2026-10-16" | ||||||||||||||||
startHalf | MORNING | AFTERNOON | null | Medio día al inicio, si lo hay. | null | ||||||||||||||||
endHalf | MORNING | AFTERNOON | null | Medio día al final, si lo hay. | null | ||||||||||||||||
startTime | string | null | Hora de inicio (HH:mm, hora local), solo en ausencias por horas. | null | ||||||||||||||||
endTime | string | null | Hora de fin (HH:mm, hora local), solo en ausencias por horas. | null | ||||||||||||||||
hours | number | null | Horas, solo en ausencias por horas. | null | ||||||||||||||||
businessDays | number | Días que consume (0,5 en medios días; 0 en ausencias por horas). | 4 | ||||||||||||||||
status | PENDING | APPROVED | REJECTED | CANCELLED | Estado de la solicitud. | "APPROVED" | ||||||||||||||||
source | EMPLOYEE | ADMIN | API | SYSTEM | Quién la registró: el empleado, un administrador, la API o el sistema. | "API" | ||||||||||||||||
reason | string | null | Motivo que dio el empleado. Es texto libre y puede contener datos de salud: solo con employees:read_pii (sin él, null). | "Puente del Pilar" | ||||||||||||||||
reviewedAt | string | null | Cuándo se revisó (UTC). | "2026-09-16T08:02:40.000Z" | ||||||||||||||||
reviewedById | integer | null | Administrador que la revisó. | 17 | ||||||||||||||||
rejectionReason | string | null | Motivo del rechazo. Texto libre: solo con employees:read_pii (sin él, null). | null | ||||||||||||||||
cancelledAt | string | null | Cuándo se canceló (UTC). | null | ||||||||||||||||
createdAt | string | Cuándo se pidió (UTC). | "2026-09-15T16:20:03.000Z" | ||||||||||||||||
updatedAt | string | Último cambio (UTC). Es la marca de la sincronización incremental. | "2026-09-16T08:02:40.000Z" |
Ejemplo completo
{
"object": "leaveRequest",
"id": "cm1lr5q2w0009qx4m7c3z8k1m",
"employeeId": 4821,
"leaveType": {
"id": "cm1ltvac0001qx7d2k8f4h6jk",
"code": "VACATION",
"name": "Vacaciones"
},
"unit": "DAYS",
"startDate": "2026-10-13",
"endDate": "2026-10-16",
"startHalf": null,
"endHalf": null,
"startTime": null,
"endTime": null,
"hours": null,
"businessDays": 4,
"status": "APPROVED",
"source": "API",
"reason": "Puente del Pilar",
"reviewedAt": "2026-09-16T08:02:40.000Z",
"reviewedById": 17,
"rejectionReason": null,
"cancelledAt": null,
"createdAt": "2026-09-15T16:20:03.000Z",
"updatedAt": "2026-09-16T08:02:40.000Z"
}LeaveBalance
| Campo | Tipo | Descripción | Ejemplo | ||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
object | "leaveBalance" | Tipo de objeto: siempre | "leaveBalance" | ||||||||||||||||
employeeId | integer | Empleado. | 4821 | ||||||||||||||||
leaveType | object | Tipo de ausencia del cupo. Campos
| |||||||||||||||||
year | integer | Año del cupo. | 2026 | ||||||||||||||||
unit | DAYS | HOURS | Unidad de todas las cifras: días u horas. | "DAYS" | ||||||||||||||||
allocated | number | Asignado para el año. | 22 | ||||||||||||||||
carryOver | number | Arrastrado del año anterior. | 2 | ||||||||||||||||
adjustment | number | Ajustes manuales del administrador (positivos o negativos). | 0 | ||||||||||||||||
used | number | Consumido o reservado por ausencias pendientes y aprobadas. | 12 | ||||||||||||||||
available | number | Disponible: asignado + arrastrado + ajustes − consumido. | 12 | ||||||||||||||||
carriedFromPreviousYears | number | Deuda arrastrada de años anteriores (negativa o 0). | 0 |
Ejemplo completo
{
"object": "leaveBalance",
"employeeId": 4821,
"leaveType": {
"id": "cm1ltvac0001qx7d2k8f4h6jk",
"code": "VACATION",
"name": "Vacaciones"
},
"year": 2026,
"unit": "DAYS",
"allocated": 22,
"carryOver": 2,
"adjustment": 0,
"used": 12,
"available": 12,
"carriedFromPreviousYears": 0
}HoursBalance
| Campo | Tipo | Descripción | Ejemplo |
|---|---|---|---|
object | "hoursBalance" | Tipo de objeto: siempre | "hoursBalance" |
employeeId | integer | Empleado. | 4821 |
from | string | Primer día del periodo. | "2026-09-01" |
to | string | Último día del periodo. | "2026-09-30" |
workedMinutes | integer | Minutos trabajados en el periodo (con pausas retribuidas y ausencias que computan). | 9540 |
workedTime | string | workedMinutes en HH:mm (las horas pueden pasar de 24). | "159:00" |
assignedMinutes | integer | Minutos previstos por la jornada en el periodo. | 9450 |
assignedTime | string | assignedMinutes en HH:mm. | "157:30" |
balanceMinutes | integer | Trabajado − previsto. Positivo = minutos de más; negativo = faltan. | 90 |
balanceTime | string | balanceMinutes en HH:mm (con signo). | "01:30" |
breakMinutes | integer | Minutos de pausa. | 630 |
paidBreakMinutes | integer | De ellos, retribuidos. | 0 |
autoDeductedMinutes | integer | Pausas del turno descontadas aunque no se ficharan. | 0 |
daysWorked | integer | Días con tiempo trabajado. | 21 |
daysWithOpenSegment | integer | Días que acaban con una entrada sin salida. Si no es 0, revísalos antes de cerrar la nómina. | 0 |
Ejemplo completo
{
"object": "hoursBalance",
"employeeId": 4821,
"from": "2026-09-01",
"to": "2026-09-30",
"workedMinutes": 9540,
"workedTime": "159:00",
"assignedMinutes": 9450,
"assignedTime": "157:30",
"balanceMinutes": 90,
"balanceTime": "01:30",
"breakMinutes": 630,
"paidBreakMinutes": 0,
"autoDeductedMinutes": 0,
"daysWorked": 21,
"daysWithOpenSegment": 0
}HoursBalanceDay
| Campo | Tipo | Descripción | Ejemplo |
|---|---|---|---|
object | "hoursBalanceDay" | Tipo de objeto: siempre | "hoursBalanceDay" |
employeeId | integer | Empleado. | 4821 |
date | string | Día de jornada. | "2026-09-15" |
clockIn | string | null | Primera entrada del día, HH:mm en hora local. | "08:58" |
clockOut | string | null | Última salida del día, HH:mm en hora local. | "17:02" |
workedMinutes | integer | Minutos trabajados. | 453 |
assignedMinutes | integer | Minutos previstos. | 450 |
balanceMinutes | integer | Trabajado − previsto. | 3 |
breakMinutes | integer | Minutos de pausa. | 30 |
paidBreakMinutes | integer | De ellos, retribuidos. | 0 |
autoDeductedMinutes | integer | Pausa del turno descontada aunque no se fichara. | 0 |
scheduleSource | SHIFT | EMPLOYEE_SCHEDULE | LOCATION | COMPANY | CALCULATED | HOLIDAY | LEAVE | NONE | De dónde sale la jornada prevista: es lo primero que mirar cuando no cuadran las horas. | "SHIFT" |
hasOpenSegment | boolean | El día acaba con una entrada sin salida. | false |
notes | string | null | Cómo se calculó el día (festivo, permiso, turno…). Texto informativo, no contrato. | null |
Ejemplo completo
{
"object": "hoursBalanceDay",
"employeeId": 4821,
"date": "2026-09-15",
"clockIn": "08:58",
"clockOut": "17:02",
"workedMinutes": 453,
"assignedMinutes": 450,
"balanceMinutes": 3,
"breakMinutes": 30,
"paidBreakMinutes": 0,
"autoDeductedMinutes": 0,
"scheduleSource": "SHIFT",
"hasOpenSegment": false,
"notes": null
}Shift
| Campo | Tipo | Descripción | Ejemplo | ||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
object | "shift" | Tipo de objeto: siempre | "shift" | ||||||||||||||||||||||||||||
id | string | Id del turno. | "cm1shmnn0002qx9p4c6v8b2de" | ||||||||||||||||||||||||||||
name | string | Nombre del turno. | "Mañana" | ||||||||||||||||||||||||||||
type | WORK | FREE | WORK = turno de trabajo; FREE = día libre marcado en el calendario. | "WORK" | ||||||||||||||||||||||||||||
color | string | Color en el calendario (hex). | "#10B981" | ||||||||||||||||||||||||||||
isNight | boolean | Turno de noche (sale al día siguiente). | false | ||||||||||||||||||||||||||||
isSplit | boolean | Jornada partida (más de un tramo). | false | ||||||||||||||||||||||||||||
segments | object[] | Tramos del turno. Campos
| |||||||||||||||||||||||||||||
totalHours | number | Horas de trabajo del turno (sin las pausas no retribuidas). | 7.5 | ||||||||||||||||||||||||||||
applicableDays | integer[] | Días en los que aplica: 0 = domingo … 6 = sábado. | [1,2,3,4,5] | ||||||||||||||||||||||||||||
tolerance | object | Tolerancias de puntualidad. Campos
| |||||||||||||||||||||||||||||
breaks | object[] | Pausas previstas del turno. Campos
| |||||||||||||||||||||||||||||
createdAt | string | Alta (UTC). | "2025-02-03T09:30:00.000Z" | ||||||||||||||||||||||||||||
updatedAt | string | Último cambio (UTC). | "2026-03-10T12:00:00.000Z" |
Ejemplo completo
{
"object": "shift",
"id": "cm1shmnn0002qx9p4c6v8b2de",
"name": "Mañana",
"type": "WORK",
"color": "#10B981",
"isNight": false,
"isSplit": false,
"segments": [
{
"start": "09:00",
"end": "17:00"
}
],
"totalHours": 7.5,
"applicableDays": [
1,
2,
3,
4,
5
],
"tolerance": {
"entryMinutes": 10,
"exitMinutes": 10
},
"breaks": [
{
"id": "cm1brkcm0003qx9p4c6v8b2df",
"name": "Comida",
"startTime": "13:00",
"durationMinutes": 30,
"isPaid": false,
"autoDeduct": false
}
],
"createdAt": "2025-02-03T09:30:00.000Z",
"updatedAt": "2026-03-10T12:00:00.000Z"
}ShiftAssignment
| Campo | Tipo | Descripción | Ejemplo | ||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
object | "shiftAssignment" | Tipo de objeto: siempre | "shiftAssignment" | ||||||||||||
employeeId | integer | Empleado. | 4821 | ||||||||||||
date | string | Día. | "2026-09-15" | ||||||||||||
origin | ASSIGNMENT | PERIOD | DEFAULT | ASSIGNMENT = calendario día a día (manda); PERIOD = turno por rango de fechas; DEFAULT = turno fijo. | "DEFAULT" | ||||||||||||
shift | object | null | Turno de ese día (null en un día libre pintado sin turno). Campos
| |||||||||||||
isDayOff | boolean | Día libre marcado en el calendario. | false | ||||||||||||
startTime | string | null | Entrada prevista (HH:mm, hora local). | "09:00" | ||||||||||||
endTime | string | null | Salida prevista (HH:mm, hora local). | "17:00" | ||||||||||||
hours | number | null | Horas previstas. | 7.5 | ||||||||||||
notes | string | null | Nota del calendario. | null |
Ejemplo completo
{
"object": "shiftAssignment",
"employeeId": 4821,
"date": "2026-09-15",
"origin": "DEFAULT",
"shift": {
"id": "cm1shmnn0002qx9p4c6v8b2de",
"name": "Mañana"
},
"isDayOff": false,
"startTime": "09:00",
"endTime": "17:00",
"hours": 7.5,
"notes": null
}Holiday
| Campo | Tipo | Descripción | Ejemplo |
|---|---|---|---|
object | "holiday" | Tipo de objeto: siempre | "holiday" |
id | string | Id del festivo. | "cm1hol12o0004qx2b7n5c9d3f" |
date | string | Día (YYYY-MM-DD, zona de la empresa). | "2026-10-12" |
name | string | Nombre. | "Fiesta Nacional de España" |
type | NATIONAL | REGIONAL | LOCAL | COMPANY | Nacional, autonómico, local o propio de la empresa. | "NATIONAL" |
scope | COMPANY | LOCATION | COMPANY = de toda la empresa; LOCATION = solo de un centro. | "COMPANY" |
locationId | string | null | Centro al que aplica (solo con scope LOCATION). | null |
province | string | null | Provincia (festivos autonómicos). | null |
workingHours | number | Horas de trabajo previstas ese día (0 = festivo completo; más = jornada reducida). | 0 |
recurrent | boolean | Se repite cada año en la misma fecha. | true |
Ejemplo completo
{
"object": "holiday",
"id": "cm1hol12o0004qx2b7n5c9d3f",
"date": "2026-10-12",
"name": "Fiesta Nacional de España",
"type": "NATIONAL",
"scope": "COMPANY",
"locationId": null,
"province": null,
"workingHours": 0,
"recurrent": true
}Export
| Campo | Tipo | Descripción | Ejemplo |
|---|---|---|---|
object | "export" | Tipo de objeto: siempre | "export" |
id | string | Id de la exportación. | "cm1exp7t0005qx6c3m9b2v8zq" |
status | pending | processing | completed | failed | expired | pending → processing → completed (o failed). expired = el archivo ya se borró. | "completed" |
dataset | clocking | balance | absence | hour-bank | location-hours | Informe pedido. | "clocking" |
format | csv | xlsx | pdf | Formato del archivo. | "pdf" |
from | string | null | Primer día del periodo. | "2026-09-01" |
to | string | null | Último día del periodo. | "2026-09-30" |
progress | integer | Progreso, de 0 a 100. | 100 |
rows | integer | null | Filas de datos del informe (cuando termina). | 412 |
sizeBytes | integer | null | Tamaño del archivo en bytes. | 183422 |
downloadUrl | string | null | Solo con status completed. Válida 1 hora: pídela de nuevo a este endpoint cuando la necesites. | "https://api.fichme.com/api/files/eyJqb2IiOiJ… |
error | string | null | Qué falló, si status es failed. | null |
createdAt | string | Cuándo se pidió (UTC). | "2026-10-01T07:00:03.000Z" |
completedAt | string | null | Cuándo terminó (UTC). | "2026-10-01T07:00:41.000Z" |
expiresAt | string | A partir de aquí el archivo se borra (24 h tras crearse). | "2026-10-02T07:00:03.000Z" |
Ejemplo completo
{
"object": "export",
"id": "cm1exp7t0005qx6c3m9b2v8zq",
"status": "completed",
"dataset": "clocking",
"format": "pdf",
"from": "2026-09-01",
"to": "2026-09-30",
"progress": 100,
"rows": 412,
"sizeBytes": 183422,
"downloadUrl": "https://api.fichme.com/api/files/eyJqb2IiOiJjbTFleHA3dDAwMDUifQ/registro-jornada-2026-09.pdf",
"error": null,
"createdAt": "2026-10-01T07:00:03.000Z",
"completedAt": "2026-10-01T07:00:41.000Z",
"expiresAt": "2026-10-02T07:00:03.000Z"
}WebhookEndpoint
| Campo | Tipo | Descripción | Ejemplo | ||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
object | "webhookEndpoint" | Tipo de objeto: siempre | "webhookEndpoint" | ||||||||||||||||
id | string | Id del webhook. | "cm1whk4r0006qx8v2n6c3b7xy" | ||||||||||||||||
url | string | URL https que recibe los eventos. | "https://erp.example.com/webhooks/fichme" | ||||||||||||||||
description | string | null | Descripción libre. | "ERP de nóminas" | ||||||||||||||||
events | string[] | Tipos suscritos, o ["*"] para todos. | ["clockEntry.created","leaveRequest.approved"] | ||||||||||||||||
status | ACTIVE | PAUSED | DISABLED | DISABLED = desactivado automáticamente tras 72 h fallando (reactívalo con PATCH status ACTIVE). | "ACTIVE" | ||||||||||||||||
failingSince | string | null | Desde cuándo falla sin interrupción (UTC), o null. | null | ||||||||||||||||
disabledReason | string | null | Por qué se desactivó. | null | ||||||||||||||||
secretLast4 | string | Últimos 4 caracteres del secreto, para reconocerlo. | "hJ2l" | ||||||||||||||||
createdAt | string | Alta (UTC). | "2026-09-10T09:00:00.000Z" | ||||||||||||||||
updatedAt | string | Último cambio (UTC). | "2026-09-10T09:00:00.000Z" | ||||||||||||||||
lastDelivery | object | null | La entrega más reciente, o null si aún no hubo ninguna. Campos
|
Ejemplo completo
{
"object": "webhookEndpoint",
"id": "cm1whk4r0006qx8v2n6c3b7xy",
"url": "https://erp.example.com/webhooks/fichme",
"description": "ERP de nóminas",
"events": [
"clockEntry.created",
"leaveRequest.approved"
],
"status": "ACTIVE",
"failingSince": null,
"disabledReason": null,
"secretLast4": "hJ2l",
"createdAt": "2026-09-10T09:00:00.000Z",
"updatedAt": "2026-09-10T09:00:00.000Z",
"lastDelivery": {
"status": "DELIVERED",
"httpStatus": 200,
"at": "2026-09-15T06:58:33.000Z"
}
}WebhookEndpointWithSecret
| Campo | Tipo | Descripción | Ejemplo | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
endpoint | object | El webhook (WebhookEndpoint). Campos
| |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
secret | string | whsec_…: se muestra UNA sola vez. Guárdalo y úsalo completo, prefijo incluido, para verificar FichMe-Signature. | "whsec_q9Xv2LmT7pR4sW8zN1bK6cY3fH0dJ5gA2eU7iO… |
Ejemplo completo
{
"endpoint": {
"object": "webhookEndpoint",
"id": "cm1whk4r0006qx8v2n6c3b7xy",
"url": "https://erp.example.com/webhooks/fichme",
"description": "ERP de nóminas",
"events": [
"clockEntry.created",
"leaveRequest.approved"
],
"status": "ACTIVE",
"failingSince": null,
"disabledReason": null,
"secretLast4": "hJ2l",
"createdAt": "2026-09-10T09:00:00.000Z",
"updatedAt": "2026-09-10T09:00:00.000Z",
"lastDelivery": {
"status": "DELIVERED",
"httpStatus": 200,
"at": "2026-09-15T06:58:33.000Z"
}
},
"secret": "whsec_q9Xv2LmT7pR4sW8zN1bK6cY3fH0dJ5gA2eU7iO4hJ2l"
}WebhookDelivery
| Campo | Tipo | Descripción | Ejemplo |
|---|---|---|---|
object | "webhookDelivery" | Tipo de objeto: siempre | "webhookDelivery" |
id | string | Id de la entrega. | "48213" |
eventId | string | Id del evento (el | "evt_01K5Q8Z3M2X7C9V4B6N1P8R0TQ" |
eventType | string | Tipo de evento. | "clockEntry.created" |
status | DELIVERED | FAILED | PENDING | DELIVERED = tu servidor respondió 2xx; FAILED = se agotaron los intentos. | "DELIVERED" |
attempts | integer | Intentos hechos. | 1 |
lastStatus | integer | null | Código HTTP del último intento. | 200 |
lastError | string | null | Error del último intento (timeout, TLS…). | null |
createdAt | string | Cuándo se generó el evento (UTC). | "2026-09-15T06:58:32.000Z" |
deliveredAt | string | null | Cuándo se entregó (UTC). | "2026-09-15T06:58:33.000Z" |
nextAttemptAt | string | null | Próximo reintento (UTC), si queda alguno. | null |
Ejemplo completo
{
"object": "webhookDelivery",
"id": "48213",
"eventId": "evt_01K5Q8Z3M2X7C9V4B6N1P8R0TQ",
"eventType": "clockEntry.created",
"status": "DELIVERED",
"attempts": 1,
"lastStatus": 200,
"lastError": null,
"createdAt": "2026-09-15T06:58:32.000Z",
"deliveredAt": "2026-09-15T06:58:33.000Z",
"nextAttemptAt": null
}WebhookPing
| Campo | Tipo | Descripción | Ejemplo |
|---|---|---|---|
deliveryId | string | Entrega de la prueba: consulta su resultado en el historial de entregas. | "48214" |
Ejemplo completo
{
"deliveryId": "48214"
}Documento generado a partir de /v1/openapi.json (OpenAPI 3.1).