API de FichMe v1

Inicio rápido

  1. 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_…
  2. 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']}");
    }
  3. 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']}");
    }
  4. 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']}");
    }
  5. Para un caso completo (nómina, terminal, ERP, webhooks, Excel), sigue una de las guías.
No uses la clave desde un navegador ni la subas a un repositorio: da acceso a los datos de la empresa. Si se filtra, el administrador puede rotarla o revocarla al instante desde el panel.

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).

  1. 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']}");
    }
  2. 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']}");
    }
  3. 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']}");
    }
  4. Si hace falta el detalle por día, las jornadas calculadas (tramos, pausas y ausencias de cada día): GET /v1/work-sessions.
  5. El registro de jornada firmado que exige la ley, en PDF: POST /v1/exports con dataset: "clocking" y format: "pdf".
Para la nóminaCampo
Horas trabajadashoursBalance.workedMinutes (incluye pausas retribuidas y ausencias que computan)
Horas previstas por la jornadahoursBalance.assignedMinutes
DiferenciahoursBalance.balanceMinutes: positivo = de más; negativo = faltan
Días trabajadoshoursBalance.daysWorked
Vacaciones, permisos, bajasleaveRequest.leaveType.code, startDate, endDate, businessDays
DNI y nº de la Seguridad Socialemployee.identity.taxId y employee.identity.socialSecurityNumber
Antes de cerrar: si 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, email o dni (uno solo). Si tu terminal usa tarjetas, guarda la relación tarjeta → employeeId a partir de GET /v1/employees. Identificar por dni exige que la clave tenga también employees: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/clock hace lo que haría el botón de la app (entra, sale o cierra la pausa). Botones separados: /v1/clock/in, /out, /break/start y /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}`);
  }
}
RespuestaQué pasóQué enseñar en la pantalla
201 action: IN / OUTEntrada o salida registrada«Entrada registrada a las 08:58» (la hora está en entry.timestamp)
201 action: BREAK_START / BREAK_ENDPausa iniciada o terminada«Pausa iniciada» / «De vuelta al trabajo»
200 idempotentReplay: trueEra un reintento de algo ya registradoLo mismo que el 201
409 ALREADY_CLOCKED_INYa tenía una entrada abierta«Ya estás dentro»
409 NO_ACTIVE_INNo hay entrada que cerrar«No tienes una entrada abierta»
409 BREAK_ALREADY_OPEN / NO_ACTIVE_BREAKEstá en pausa / no lo está«Termina la pausa antes» / «No estás en pausa»
403 employee_inactive / clocking_blocked_for_employeeEse 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.

  1. La primera vez, pide desde una fecha antigua (2000-01-01T00:00:00Z): te llega todo.
  2. Recorre las páginas: mientras hasMore sea true, repite con cursor=<nextCursor> y el mismo updatedSince.
  3. Guarda el updatedAt más alto que hayas recibido: es el updatedSince de 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
    }
  }
}
CabeceraContenido
FichMe-Signaturet=<unix>,v1=<hex>: la firma (ver abajo)
FichMe-EventEl tipo, p. ej. clockEntry.created
FichMe-Event-IdEl id del evento: deduplica por él
FichMe-Delivery-IdLa 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 id y, si el orden importa, compara updatedAt o 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.

  1. Excel: Datos → Obtener datos → De otras fuentes → Consulta en blanco. Power BI: Obtener datos → Consulta en blanco.
  2. 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
    Tabla

Si 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, clave x-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.

ScopeQué permite
company:readDatos de la empresa, ajustes básicos y centros de trabajo
employees:readPlantilla (datos laborales, sin DNI ni nº de la Seguridad Social)
employees:read_piiDatos 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:writeAlta, edición y baja de empleados
clock:readFichajes y jornadas calculadas
clock:writeRegistrar fichajes en tiempo real (requiere activarlo en la empresa)
corrections:readSolicitudes de corrección de fichajes
corrections:writeCrear solicitudes de corrección de fichajes
corrections:manageAprobar o rechazar correcciones de fichajes
absences:readAusencias, tipos de ausencia y saldos
absences:writeCrear y cancelar ausencias
absences:manageAprobar o rechazar ausencias
balance:readBalance de horas (previstas frente a trabajadas)
schedule:readTurnos, asignaciones de turno y festivos
exports:readConsultar y descargar exportaciones
exports:writeLanzar exportaciones (informes en CSV, XLSX o PDF)
webhooks:manageGestionar los webhooks de la empresa

Perfiles habituales

UsoScopes
Gestoría / nóminascompany:read employees:read employees:read_pii clock:read absences:read balance:read exports:read exports:write
BI / cuadros de mandocompany:read employees:read clock:read balance:read schedule:read absences:read
Terminal o ERP que fichaemployees:read clock:read clock:write
RR. HH. / ERP de personascompany:read employees:read employees:write absences:read absences:write absences:manage
Zapier / Makeemployees:read clock:read absences:read

Convenciones

  • JSON en camelCase. Cada objeto lleva object ("employee", "clockEntry"…). Los campos documentados siempre aparecen (con null si 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) en YYYY-MM-DD, en la zona horaria de la empresa (GET /v1/me te 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, en HH: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 con deleted: true, paginado con nextCursor y hasMore (entonces page, total y totalPages son 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"
  }
}
HTTPcodeQué hacer
400invalid_request

Un campo no es válido o no está admitido. param dice cuál.

400invalid_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.

400tenant_override_forbidden

Llegó x-tenant o ?tenant. La empresa la determina la clave.

401missing_api_key · invalid_api_key

Falta la clave o no es válida. No reintentes: revisa la clave.

401api_key_revoked · api_key_expired

La clave ya no sirve. Pide una nueva al administrador de la empresa.

403insufficient_scope

A la clave le falta el scope de requiredScope.

403ip_not_allowed

La IP de origen no está en la lista permitida de la clave.

403plan_upgrade_required · subscription_inactive

El plan no incluye la API (o esa función) o la suscripción no está al día.

403api_clocking_disabled · clocking_blocked_for_employee · employee_inactive

Fichar por API no está activado en la empresa, o ese empleado no puede fichar.

403key_owner_required · admin_protected

La clave no tiene un administrador vigente detrás, o la operación toca una cuenta de administrador.

404not_found · unknown_endpoint

No existe o no es de tu empresa (la respuesta es idéntica en ambos casos).

409clock_state_conflict

El fichaje no encaja con el estado del empleado (state: NO_ACTIVE_IN, ALREADY_CLOCKED_IN, BREAK_ALREADY_OPEN, NO_ACTIVE_BREAK, SHIFT_MARKED_INCOMPLETE).

409already_exists · overlap · invalid_state · idempotency_key_reused

Ya existe (existingId), se solapa con otra ausencia, ya no está pendiente, o el Idempotency-Key ya se usó para otra cosa.

413payload_too_large

El cuerpo supera 256 KB.

415unsupported_media_type

El cuerpo tiene que ser JSON.

422validation_failed · timestamp_out_of_window · leave_balance_insufficient · seat_limit_reached

Una regla de negocio no se cumple. El mensaje explica cuál.

429rate_limited · too_many_concurrent_requests · daily_quota_exceeded

Espera lo que indique Retry-After y reintenta con backoff.

500internal_error

Fallo nuestro. Reintenta con backoff; si persiste, escribe a api@fichme.com con el requestId.

503service_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-After y reintenta con backoff exponencial.

Clave

Introspección de la clave y de la empresa.

GET /v1/me

Comprobar la clave

sin scope

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

scope: company:read

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

scope: company:read

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

NombreEnTipoDescripciónEjemplo
isActivequerystring

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

scope: employees:read

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

NombreEnTipoDescripciónEjemplo
statusqueryACTIVE | INACTIVE | ALL
por defecto "ACTIVE"

Estado operativo. Por defecto ACTIVE.

ACTIVE
locationIdquerystring
≤ 64 caracteres

Centro de trabajo (principal o de pertenencia).

searchquerystring
≤ 100 caracteres

Texto en el nombre o el email.

includeDeletedquerystring

Incluir empleados dados de baja lógica.

updatedSincequerystring (date-time)

Sincronización incremental: solo los modificados desde este instante (ver guía).

cursorquerystring
≤ 200 caracteres

Cursor devuelto en nextCursor (solo con updatedSince).

pagequeryinteger
por defecto 1 · mín. 1

Página, desde 1.

limitqueryinteger
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

HTTPcodeCuándo pasa
400page_out_of_range

page × limit supera 50.000. Para volcados completos usa updatedSince.

400invalid_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

scope: employees:read

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

NombreEnTipoDescripciónEjemplo
id obligatoriopathstring
≤ 260 caracteres

Id numérico de FichMe, o email:ana@example.com, o dni:12345678Z (este último requiere employees:read_pii).

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

HTTPcodeCuándo pasa
404not_found

No hay ningún empleado con ese id, email o DNI en tu empresa.

403insufficient_scope

Buscar por dni: sin el scope employees:read_pii.

Además, los comunes a todas: 400 invalid_request, 401, 403 insufficient_scope y 429 (ver Errores).

POST /v1/employees

Alta de empleado

scope: employees:write

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)

CampoTipoDescripción
firstName obligatoriostring
≤ 100 caracteres

Nombre.

lastNamestring
≤ 150 caracteres

Apellidos.

emailstring (email)
≤ 254 caracteres

Obligatorio si no se envía dni.

dnistring
≤ 20 caracteres

DNI/NIE. Obligatorio si no se envía email. Enviarlo requiere el scope employees:read_pii.

socialSecurityNumberstring
≤ 30 caracteres

Nº de afiliación a la Seguridad Social.

phonestring
≤ 30 caracteres

Teléfono.

jobTitlestring
≤ 120 caracteres

Puesto.

departmentstring
≤ 120 caracteres

Departamento.

hireDatestring

Fecha de alta (YYYY-MM-DD).

seniorityDatestring

Fecha de antigüedad (YYYY-MM-DD), si no coincide con el alta.

locationIdstring
≤ 64 caracteres

Centro principal.

locationIdsstring[]
≤ 50 elementos

Centros adicionales de pertenencia.

shiftIdstring
≤ 64 caracteres

Turno fijo a asignar desde hoy.

contractobject

Datos del contrato.

Campos
CampoTipoDescripción
typeFULL_TIME | PART_TIME | TEMPORARY | null

Tipo de contrato.

weeklyHoursnumber | null

Horas semanales contratadas.

annualHoursnumber | null

Horas anuales contratadas.

isNightWorkerboolean

Trabajador nocturno (art. 36 ET).

sendWelcomeEmailboolean
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

HTTPcodeCuándo pasa
409already_exists

Ya hay un empleado con ese email o DNI: existingId dice cuál (actualízalo con PATCH).

422seat_limit_reached

La empresa ha llegado al máximo de empleados de su plan.

404not_found

El centro (locationId/locationIds) o el turno (shiftId) no existen o están inactivos.

422validation_failed

Algún centro es de sistema («Principal» o «Teletrabajo», isSystem: true): no se asignan a empleados.

403insufficient_scope

Enviar dni sin el scope employees:read_pii.

Además, los comunes a todas: 400 invalid_request, 401, 403 insufficient_scope y 429 (ver Errores).

PATCH /v1/employees/{id}

Editar empleado

scope: employees:write

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

NombreEnTipoDescripciónEjemplo
id obligatoriopathstring
≤ 260 caracteres

Id numérico de FichMe, o email:ana@example.com, o dni:12345678Z (este último requiere employees:read_pii).

4821

Cuerpo (JSON)

CampoTipoDescripción
firstNamestring
≤ 100 caracteres

Nombre.

lastNamestring | null

Apellidos (null los borra).

emailstring (email)
≤ 254 caracteres

Email (también es su usuario de acceso).

dnistring
≤ 20 caracteres

DNI/NIE. Cambiarlo requiere el scope employees:read_pii.

socialSecurityNumberstring | null

Nº de afiliación a la Seguridad Social.

phonestring | null

Teléfono.

jobTitlestring | null

Puesto.

departmentstring | null

Departamento.

hireDatestring | null

Fecha de alta (YYYY-MM-DD).

seniorityDatestring | null

Fecha de antigüedad (YYYY-MM-DD).

locationIdstring
≤ 64 caracteres

Cambia el centro principal y conserva los demás.

locationIdsstring[]
≤ 50 elementos

Conjunto COMPLETO de centros de pertenencia ([] = ninguno).

contractobject

Datos del contrato (solo los campos enviados).

Campos
CampoTipoDescripción
typeFULL_TIME | PART_TIME | TEMPORARY | null

Tipo de contrato.

weeklyHoursnumber | null

Horas semanales contratadas.

annualHoursnumber | null

Horas anuales contratadas.

isNightWorkerboolean

Trabajador nocturno (art. 36 ET).

statusACTIVE | 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

HTTPcodeCuándo pasa
403admin_protected

Es una cuenta de administrador: solo se modifica desde el panel.

409already_exists

Otro empleado ya tiene ese email o DNI (existingId).

422seat_limit_reached

Reactivar (status: ACTIVE) superaría el máximo de empleados del plan.

404not_found

El empleado o alguno de los centros no existen.

422validation_failed

Algún centro es de sistema («Principal» o «Teletrabajo», isSystem: true): no se asignan a empleados.

403insufficient_scope

Usar dni: en la ruta o cambiar el dni sin el scope employees:read_pii.

Además, los comunes a todas: 400 invalid_request, 401, 403 insufficient_scope y 429 (ver Errores).

POST /v1/employees/{id}/deactivate

Desactivar empleado

scope: employees:write

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

NombreEnTipoDescripciónEjemplo
id obligatoriopathstring
≤ 260 caracteres

Id numérico de FichMe, o email:ana@example.com, o dni:12345678Z (este último requiere employees:read_pii).

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

HTTPcodeCuándo pasa
403admin_protected

Es una cuenta de administrador: solo se desactiva desde el panel.

404not_found

No existe en tu empresa.

403insufficient_scope

Usar dni: en la ruta sin el scope employees:read_pii.

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

scope: clock:read

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

NombreEnTipoDescripciónEjemplo
fromquerystring

Primer día de JORNADA (shiftDate), inclusive.

2026-09-01
toquerystring

Último día de jornada, inclusive. Máximo 93 días.

2026-09-30
timestampFromquerystring (date-time)

Alternativa a from/to: instante UTC inicial.

timestampToquerystring (date-time)

Alternativa a from/to: instante UTC final.

updatedSincequerystring (date-time)

Sincronización incremental (excluye los otros filtros de fecha).

cursorquerystring
≤ 200 caracteres

Cursor de nextCursor (solo con updatedSince).

employeeIdqueryinteger

Solo los fichajes de este empleado.

typequeryIN | OUT | BREAK_START | BREAK_END

Solo este tipo de pulsación.

methodqueryWEB | APP | QR | KIOSK | SLACK | WHATSAPP | API

Solo los registrados por este canal.

locationIdquerystring
≤ 64 caracteres

Solo los registrados en este centro.

includeDeletedquerystring

Incluir fichajes eliminados (con deletedAt).

pagequeryinteger
por defecto 1 · mín. 1

Página, desde 1.

limitqueryinteger
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

HTTPcodeCuándo pasa
400invalid_date_range

Falta from o to (van juntos), o from es posterior a to.

400date_range_too_large

Más de 93 días: pide el periodo por partes o usa POST /v1/exports.

400conflicting_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

scope: clock:read

Un fichaje por su id (el que devuelven los listados, las jornadas y los webhooks).

Parámetros

NombreEnTipoDescripciónEjemplo
id obligatoriopathstring

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

HTTPcodeCuándo pasa
404not_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

scope: clock:readpesado · máx. 1 a la vez

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

NombreEnTipoDescripciónEjemplo
from obligatorioquerystring

Primer día de jornada (inclusive).

2026-09-01
to obligatorioquerystring

Último día de jornada (inclusive). Máximo 93 días.

2026-09-30
employeeIdqueryinteger

Solo este empleado.

locationIdquerystring
≤ 64 caracteres

Empleados de este centro (principal o de pertenencia).

statusqueryACTIVE | INACTIVE | ALL
por defecto "ALL"

Estado del empleado.

pagequeryinteger
por defecto 1 · mín. 1

Página, desde 1.

limitqueryinteger
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

HTTPcodeCuándo pasa
400invalid_date_range

Falta from o to, o from es posterior a to.

400date_range_too_large

Más de 93 días: pide el periodo por partes o usa POST /v1/exports.

429too_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)

scope: clock:writeIdempotency-Key

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

NombreEnTipoDescripciónEjemplo
Idempotency-Keyheaderstring
≤ 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)

CampoTipoDescripción
employeeIdinteger

Id del empleado en FichMe.

emailstring (email)
≤ 254 caracteres

Email del empleado (alternativa a employeeId).

dnistring
≤ 20 caracteres

DNI/NIE del empleado (alternativa a employeeId). Requiere el scope employees:read_pii.

locationIdstring
≤ 64 caracteres

Centro de trabajo. Si se omite en una salida, hereda el de la entrada.

deviceTimestampstring (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

HTTPcodeCuándo pasa
403api_clocking_disabled

La empresa no ha activado el fichaje por API (Ajustes → API).

403employee_inactive

El empleado está INACTIVE.

403clocking_blocked_for_employee

El empleado está exento de fichar.

404not_found

No hay ningún empleado con ese employeeId, email o DNI en tu empresa.

403insufficient_scope

Identificar al empleado por dni sin el scope employees:read_pii.

409idempotency_key_reused

Ese Idempotency-Key ya se usó para otro fichaje: genera uno nuevo por intento.

422timestamp_out_of_window

deviceTimestamp difiere más de 5 min de la hora del servidor. El pasado va por correcciones.

409clock_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

scope: clock:writeIdempotency-Key

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

NombreEnTipoDescripciónEjemplo
Idempotency-Keyheaderstring
≤ 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)

CampoTipoDescripción
employeeIdinteger

Id del empleado en FichMe.

emailstring (email)
≤ 254 caracteres

Email del empleado (alternativa a employeeId).

dnistring
≤ 20 caracteres

DNI/NIE del empleado (alternativa a employeeId). Requiere el scope employees:read_pii.

locationIdstring
≤ 64 caracteres

Centro de trabajo. Si se omite en una salida, hereda el de la entrada.

deviceTimestampstring (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

HTTPcodeCuándo pasa
409clock_state_conflict

state: ALREADY_CLOCKED_IN (ya está dentro) o BREAK_ALREADY_OPEN (está en pausa).

403api_clocking_disabled

La empresa no ha activado el fichaje por API (Ajustes → API).

403employee_inactive

El empleado está INACTIVE.

403clocking_blocked_for_employee

El empleado está exento de fichar.

404not_found

No hay ningún empleado con ese employeeId, email o DNI en tu empresa.

403insufficient_scope

Identificar al empleado por dni sin el scope employees:read_pii.

409idempotency_key_reused

Ese Idempotency-Key ya se usó para otro fichaje: genera uno nuevo por intento.

422timestamp_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

scope: clock:writeIdempotency-Key

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

NombreEnTipoDescripciónEjemplo
Idempotency-Keyheaderstring
≤ 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)

CampoTipoDescripción
employeeIdinteger

Id del empleado en FichMe.

emailstring (email)
≤ 254 caracteres

Email del empleado (alternativa a employeeId).

dnistring
≤ 20 caracteres

DNI/NIE del empleado (alternativa a employeeId). Requiere el scope employees:read_pii.

locationIdstring
≤ 64 caracteres

Centro de trabajo. Si se omite en una salida, hereda el de la entrada.

deviceTimestampstring (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

HTTPcodeCuándo pasa
409clock_state_conflict

state: NO_ACTIVE_IN (no tiene una entrada abierta) o BREAK_ALREADY_OPEN (está en pausa).

403api_clocking_disabled

La empresa no ha activado el fichaje por API (Ajustes → API).

403employee_inactive

El empleado está INACTIVE.

403clocking_blocked_for_employee

El empleado está exento de fichar.

404not_found

No hay ningún empleado con ese employeeId, email o DNI en tu empresa.

403insufficient_scope

Identificar al empleado por dni sin el scope employees:read_pii.

409idempotency_key_reused

Ese Idempotency-Key ya se usó para otro fichaje: genera uno nuevo por intento.

422timestamp_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

scope: clock:writeIdempotency-Key

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

NombreEnTipoDescripciónEjemplo
Idempotency-Keyheaderstring
≤ 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)

CampoTipoDescripción
employeeIdinteger

Id del empleado en FichMe.

emailstring (email)
≤ 254 caracteres

Email del empleado (alternativa a employeeId).

dnistring
≤ 20 caracteres

DNI/NIE del empleado (alternativa a employeeId). Requiere el scope employees:read_pii.

locationIdstring
≤ 64 caracteres

Centro de trabajo. Si se omite en una salida, hereda el de la entrada.

deviceTimestampstring (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

HTTPcodeCuándo pasa
409clock_state_conflict

state: NO_ACTIVE_IN (no está trabajando) o BREAK_ALREADY_OPEN (ya está en pausa).

403api_clocking_disabled

La empresa no ha activado el fichaje por API (Ajustes → API).

403employee_inactive

El empleado está INACTIVE.

403clocking_blocked_for_employee

El empleado está exento de fichar.

404not_found

No hay ningún empleado con ese employeeId, email o DNI en tu empresa.

403insufficient_scope

Identificar al empleado por dni sin el scope employees:read_pii.

409idempotency_key_reused

Ese Idempotency-Key ya se usó para otro fichaje: genera uno nuevo por intento.

422timestamp_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

scope: clock:writeIdempotency-Key

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

NombreEnTipoDescripciónEjemplo
Idempotency-Keyheaderstring
≤ 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)

CampoTipoDescripción
employeeIdinteger

Id del empleado en FichMe.

emailstring (email)
≤ 254 caracteres

Email del empleado (alternativa a employeeId).

dnistring
≤ 20 caracteres

DNI/NIE del empleado (alternativa a employeeId). Requiere el scope employees:read_pii.

locationIdstring
≤ 64 caracteres

Centro de trabajo. Si se omite en una salida, hereda el de la entrada.

deviceTimestampstring (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

HTTPcodeCuándo pasa
409clock_state_conflict

state: NO_ACTIVE_BREAK: no hay ninguna pausa abierta.

403api_clocking_disabled

La empresa no ha activado el fichaje por API (Ajustes → API).

403employee_inactive

El empleado está INACTIVE.

403clocking_blocked_for_employee

El empleado está exento de fichar.

404not_found

No hay ningún empleado con ese employeeId, email o DNI en tu empresa.

403insufficient_scope

Identificar al empleado por dni sin el scope employees:read_pii.

409idempotency_key_reused

Ese Idempotency-Key ya se usó para otro fichaje: genera uno nuevo por intento.

422timestamp_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

scope: corrections:read

Ordenadas de la más reciente a la más antigua. from/to filtran por fecha de creación.

Parámetros

NombreEnTipoDescripciónEjemplo
statusqueryPENDING | APPROVED | REJECTED

PENDING para la bandeja de aprobaciones.

PENDING
employeeIdqueryinteger

Solo las de este empleado.

requestTypequeryMISSING_CLOCK_IN | MISSING_CLOCK_OUT | INCORRECT_TIME | MANUAL_FULL_SHIFT | MANUAL_BREAK

Solo este tipo de solicitud.

fromquerystring

Creadas desde este día (inclusive).

toquerystring

Creadas hasta este día (inclusive). Máximo 366 días.

pagequeryinteger
por defecto 1 · mín. 1

Página, desde 1.

limitqueryinteger
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

HTTPcodeCuándo pasa
400date_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

scope: corrections:read

Una solicitud por su id, con su estado actual.

Parámetros

NombreEnTipoDescripciónEjemplo
id obligatoriopathstring
≤ 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

HTTPcodeCuándo pasa
404not_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

scope: corrections:write

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).

CampoTipoDescripción
type obligatorio"SINGLE"

Corregir o añadir UN extremo (entrada o salida).

employeeIdinteger

Id del empleado en FichMe.

emailstring (email)
≤ 254 caracteres

Email del empleado (alternativa a employeeId).

dnistring
≤ 20 caracteres

DNI/NIE del empleado (alternativa a employeeId). Requiere el scope employees:read_pii.

missingType obligatorioIN | OUT

Qué extremo se corrige o falta.

proposedTime obligatoriostring (date-time)

Instante propuesto (ISO-8601 con zona).

clockEntryIdinteger

Fichaje a corregir. Sin él, se propone uno que falta.

pairEntryIdinteger

El otro extremo de la misma sesión, si lo conoces.

locationIdstring
≤ 64 caracteres

Centro del fichaje.

reason obligatoriostring
≤ 1000 caracteres

Motivo (mínimo 10 caracteres): lo lee quien la aprueba.

autoApproveboolean
por defecto false

Aprobar en la misma llamada (requiere corrections:manage).

type: "FULL_SHIFT" Jornada completa olvidada: entrada y salida en una sola solicitud.

CampoTipoDescripción
type obligatorio"FULL_SHIFT"

Jornada completa olvidada: entrada y salida en una sola solicitud.

employeeIdinteger

Id del empleado en FichMe.

emailstring (email)
≤ 254 caracteres

Email del empleado (alternativa a employeeId).

dnistring
≤ 20 caracteres

DNI/NIE del empleado (alternativa a employeeId). Requiere el scope employees:read_pii.

shiftDate obligatoriostring

Día de la jornada (YYYY-MM-DD).

clockInTime obligatoriostring

Hora de entrada (HH:mm, hora local de la empresa).

clockOutTime obligatoriostring

Hora de salida (HH:mm). Si es anterior a la entrada, sale al día siguiente (turno de noche).

locationIdstring
≤ 64 caracteres

Centro de la jornada.

reason obligatoriostring
≤ 1000 caracteres

Motivo (mínimo 10 caracteres): lo lee quien la aprueba.

autoApproveboolean
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

HTTPcodeCuándo pasa
422validation_failed

Fecha u hora futuras (por API solo se corrige el pasado), o jornada de un ADMIN sin autoApprove.

403insufficient_scope

autoApprove sin el scope corrections:manage, o el empleado por dni sin employees:read_pii.

403key_owner_required

autoApprove, pero el administrador que creó la clave ya no lo es.

404not_found

El empleado, el fichaje (clockEntryId/pairEntryId) o el centro no existen en tu empresa.

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

scope: corrections:manage

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

NombreEnTipoDescripciónEjemplo
id obligatoriopathstring
≤ 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

HTTPcodeCuándo pasa
409invalid_state

La solicitud ya no está PENDING (status dice cómo quedó).

403key_owner_required

El administrador que creó la clave ya no lo es: crea una clave nueva.

404not_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

scope: corrections:manage

Rechaza la solicitud con un motivo (se envía al empleado). El registro horario no cambia.

Parámetros

NombreEnTipoDescripciónEjemplo
id obligatoriopathstring
≤ 64 caracteres

Id de la solicitud de corrección.

cm1corr8k2p0007qx3n5v1b9d

Cuerpo (JSON)

CampoTipoDescripción
reason obligatoriostring
≤ 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

HTTPcodeCuándo pasa
409invalid_state

La solicitud ya no está PENDING.

403key_owner_required

El administrador que creó la clave ya no lo es.

404not_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

scope: absences:read

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

NombreEnTipoDescripciónEjemplo
includeInactivequerystring

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

HTTPcodeCuándo pasa
403plan_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

scope: absences:read

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

NombreEnTipoDescripciónEjemplo
fromquerystring

Ausencias que se solapan con [from, to]. Máximo 366 días.

2026-10-01
toquerystring

Último día del periodo (inclusive).

2026-10-31
updatedSincequerystring (date-time)

Sincronización incremental (ver guía).

cursorquerystring
≤ 200 caracteres

Cursor de nextCursor (solo con updatedSince).

employeeIdqueryinteger

Solo las de este empleado.

leaveTypeIdquerystring
≤ 64 caracteres

Solo este tipo (por id).

leaveTypeCodequerystring
≤ 50 caracteres

Solo este tipo (por código, p. ej. VACATION).

statusqueryPENDING | APPROVED | REJECTED | CANCELLED

PENDING para la bandeja de aprobaciones.

APPROVED
sourcequeryEMPLOYEE | ADMIN | API | SYSTEM

Solo las registradas por este origen.

pagequeryinteger
por defecto 1 · mín. 1

Página, desde 1.

limitqueryinteger
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

HTTPcodeCuándo pasa
403plan_upgrade_required

El plan de la empresa no incluye la gestión de ausencias.

400date_range_too_large

Más de 366 días entre from y to.

400conflicting_filters

from/to y updatedSince a la vez: usa uno solo.

403insufficient_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

scope: absences:read

Una ausencia por su id, con su estado actual.

Parámetros

NombreEnTipoDescripciónEjemplo
id obligatoriopathstring
≤ 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

HTTPcodeCuándo pasa
403plan_upgrade_required

El plan de la empresa no incluye la gestión de ausencias.

404not_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

scope: absences:write

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)

CampoTipoDescripción
employeeIdinteger

Id del empleado en FichMe.

emailstring (email)
≤ 254 caracteres

Email del empleado (alternativa a employeeId).

dnistring
≤ 20 caracteres

DNI/NIE del empleado (alternativa a employeeId). Requiere el scope employees:read_pii.

leaveTypeIdstring
≤ 64 caracteres

Tipo de ausencia por id.

leaveTypeCodestring
≤ 50 caracteres

Tipo de ausencia por código (alternativa a leaveTypeId).

startDate obligatoriostring

Primer día (YYYY-MM-DD).

endDate obligatoriostring

Último día (YYYY-MM-DD), inclusive.

startHalfMORNING | AFTERNOON

Medio día al inicio (si el tipo lo admite).

endHalfMORNING | AFTERNOON

Medio día al final (si el tipo lo admite).

startTimestring

Ausencias por horas: hora de inicio (HH:mm).

endTimestring

Ausencias por horas: hora de fin (HH:mm).

hoursnumber
máx. 24

Ausencias por horas: alternativa a startTime/endTime.

reasonstring
≤ 1000 caracteres

Motivo (lo ve quien la aprueba).

autoApproveboolean
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

HTTPcodeCuándo pasa
409overlap

Se solapa con otra ausencia del empleado.

422leave_balance_insufficient

No le queda cupo suficiente (details dice cuánto).

422validation_failed

El tipo está desactivado o es de sistema, o las fechas no encajan con el tipo.

404not_found

El empleado o el tipo de ausencia no existen.

403insufficient_scope

autoApprove sin el scope absences:manage, o el empleado por dni sin employees:read_pii.

403plan_upgrade_required

El plan de la empresa no incluye la gestión de ausencias.

503service_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

scope: absences:manage

Solo ausencias PENDIENTES. Avisa al empleado. Figura como revisor el administrador que creó la clave.

Parámetros

NombreEnTipoDescripciónEjemplo
id obligatoriopathstring
≤ 64 caracteres

Id de la ausencia.

cm1lr5q2w0009qx4m7c3z8k1m

Cuerpo (JSON)

CampoTipoDescripción
notesstring
≤ 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

HTTPcodeCuándo pasa
409invalid_state

La ausencia ya no está PENDING (status dice cómo quedó).

422leave_balance_insufficient

Ya no queda cupo para aprobarla.

403key_owner_required

El administrador que creó la clave ya no lo es.

403plan_upgrade_required

El plan de la empresa no incluye la gestión de ausencias.

503service_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

scope: absences:manage

Solo ausencias PENDIENTES. El motivo (mínimo 10 caracteres) se envía al empleado; el cupo se libera.

Parámetros

NombreEnTipoDescripciónEjemplo
id obligatoriopathstring
≤ 64 caracteres

Id de la ausencia.

cm1lr5q2w0009qx4m7c3z8k1m

Cuerpo (JSON)

CampoTipoDescripción
reason obligatoriostring
≤ 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

HTTPcodeCuándo pasa
409invalid_state

La ausencia ya no está PENDING.

403key_owner_required

El administrador que creó la clave ya no lo es.

403plan_upgrade_required

El plan de la empresa no incluye la gestión de ausencias.

503service_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

scope: absences:write

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

NombreEnTipoDescripciónEjemplo
id obligatoriopathstring
≤ 64 caracteres

Id de la ausencia.

cm1lr5q2w0009qx4m7c3z8k1m

Cuerpo (JSON)

CampoTipoDescripción
reasonstring
≤ 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

HTTPcodeCuándo pasa
409invalid_state

Solo se cancelan ausencias PENDING o APPROVED.

403key_owner_required

El administrador que creó la clave ya no lo es.

403plan_upgrade_required

El plan de la empresa no incluye la gestión de ausencias.

503service_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

scope: absences:readpesado · máx. 1 a la vez

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

NombreEnTipoDescripciónEjemplo
yearqueryinteger
mín. 2000 · máx. 2100

Año del cupo. Por defecto, el actual.

2026
employeeIdqueryinteger

Solo este empleado.

leaveTypeCodequerystring
≤ 50 caracteres

Solo este tipo (p. ej. VACATION).

VACATION
pagequeryinteger
por defecto 1 · mín. 1

Página, desde 1.

limitqueryinteger
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

HTTPcodeCuándo pasa
403plan_upgrade_required

El plan de la empresa no incluye la gestión de ausencias.

404not_found

El empleado de employeeId no existe.

429too_many_concurrent_requests

Supera el máximo de 1 petición simultánea por clave en los endpoints pesados.

403insufficient_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

scope: balance:readpesado · máx. 1 a la vez

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

NombreEnTipoDescripciónEjemplo
from obligatorioquerystring

Primer día del periodo (inclusive).

2026-09-01
to obligatorioquerystring

Último día del periodo (inclusive). Máximo 366 días.

2026-09-30
employeeIdqueryinteger

Solo este empleado.

locationIdquerystring
≤ 64 caracteres

Empleados de este centro (principal o de pertenencia).

statusqueryACTIVE | INACTIVE | ALL
por defecto "ALL"

Estado del empleado. ALL incluye a quien se dio de baja en el periodo.

groupByqueryemployee | day
por defecto "employee"

employee = total del periodo por empleado; day = una fila por día.

pagequeryinteger
por defecto 1 · mín. 1

Página, desde 1.

limitqueryinteger
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

HTTPcodeCuándo pasa
400invalid_date_range

Falta from o to, o from es posterior a to.

400date_range_too_large

Más de 366 días.

429too_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

scope: schedule:read

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

scope: schedule:read

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

NombreEnTipoDescripciónEjemplo
from obligatorioquerystring

Primer día (inclusive).

2026-09-14
to obligatorioquerystring

Último día (inclusive). Máximo 93 días.

2026-09-20
employeeIdqueryinteger

Solo este empleado.

4821
pagequeryinteger
por defecto 1 · mín. 1

Página, desde 1.

limitqueryinteger
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

HTTPcodeCuándo pasa
400invalid_date_range

Falta from o to, o from es posterior a to.

400date_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

scope: schedule:read

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

NombreEnTipoDescripciónEjemplo
yearqueryinteger
mín. 2000 · máx. 2100

Año. Por defecto, el actual.

2026
locationIdquerystring
≤ 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

scope: exports:writepesado · máx. 1 a la vez

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)

CampoTipoDescripción
dataset obligatorioclocking | 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.

formatcsv | xlsx | pdf
por defecto "xlsx"

pdf = el informe firmado del panel.

from obligatoriostring

Primer día del periodo (YYYY-MM-DD).

to obligatoriostring

Último día del periodo (YYYY-MM-DD). Máximo 2 años.

employeeIdsinteger[]
≤ 500 elementos

Solo estos empleados (por defecto, todos).

locationIdstring
≤ 64 caracteres

Solo este centro.

locationScopeassigned | clocked

Solo clocking: assigned = empleados del centro; clocked = solo los tramos fichados en él.

employeeStatusall | active | inactive
por defecto "all"

Empleados activos, dados de baja o todos.

includeModificationsboolean
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

HTTPcodeCuándo pasa
403insufficient_scope

Falta employees:read_pii: los informes llevan el DNI de cada empleado.

400date_range_too_large

Más de 2 años.

403plan_upgrade_required

dataset hour-bank sin la bolsa de horas en el plan.

422validation_failed

dataset hour-bank con la bolsa de horas desactivada en la empresa.

404not_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

scope: exports:read

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

NombreEnTipoDescripciónEjemplo
id obligatoriopathstring
≤ 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

HTTPcodeCuándo pasa
403insufficient_scope

Falta employees:read_pii: los informes llevan el DNI de cada empleado.

404not_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

scope: webhooks:manage

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

HTTPcodeCuándo pasa
403plan_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

scope: webhooks:manage

Devuelve el secreto de firma UNA sola vez. Como mucho 10 webhooks por empresa.

Cuerpo (JSON)

CampoTipoDescripción
url obligatoriostring
≤ 2048 caracteres

https://… pública (no se admiten IPs ni redes internas).

descriptionstring
≤ 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

HTTPcodeCuándo pasa
403plan_upgrade_required

Los webhooks están en los planes Business y Enterprise.

422validation_failed

URL no https, con IP o de una red interna; evento desconocido; o ya hay 10 webhooks.

503service_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

scope: webhooks:manage

Un webhook con el resultado de su última entrega.

Parámetros

NombreEnTipoDescripciónEjemplo
id obligatoriopathstring
≤ 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

HTTPcodeCuándo pasa
404not_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

scope: webhooks:manage

Cambia solo los campos enviados. Reactivar uno DISABLED le da otra oportunidad completa.

Parámetros

NombreEnTipoDescripciónEjemplo
id obligatoriopathstring
≤ 64 caracteres

Id del webhook.

cm1whk4r0006qx8v2n6c3b7xy

Cuerpo (JSON)

CampoTipoDescripción
urlstring
≤ 2048 caracteres

Nueva URL https.

descriptionstring | 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.

statusACTIVE | 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

HTTPcodeCuándo pasa
422validation_failed

URL no válida, evento desconocido, o no hay nada que cambiar.

403plan_upgrade_required

Reactivar (status ACTIVE) sin webhooks en el plan.

404not_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

scope: webhooks:manage

Borra también su historial de entregas.

Parámetros

NombreEnTipoDescripciónEjemplo
id obligatoriopathstring
≤ 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

HTTPcodeCuándo pasa
404not_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

scope: webhooks:manage

Encola un evento webhook.ping (un solo intento). Consulta el resultado en las entregas.

Parámetros

NombreEnTipoDescripciónEjemplo
id obligatoriopathstring
≤ 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

HTTPcodeCuándo pasa
409invalid_state

El webhook no está ACTIVE: reactívalo antes.

403plan_upgrade_required

Los webhooks están en los planes Business y Enterprise.

404not_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

scope: webhooks:manage

El secreto anterior deja de valer al instante; el nuevo se devuelve una sola vez.

Parámetros

NombreEnTipoDescripciónEjemplo
id obligatoriopathstring
≤ 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

HTTPcodeCuándo pasa
403plan_upgrade_required

Los webhooks están en los planes Business y Enterprise.

404not_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

scope: webhooks:manage

Las más recientes primero. Se conservan 30 días.

Parámetros

NombreEnTipoDescripciónEjemplo
id obligatoriopathstring
≤ 64 caracteres

Id del webhook.

cm1whk4r0006qx8v2n6c3b7xy
statusqueryall | failed | pending | delivered
por defecto "all"

Filtrar por resultado.

failed
limitqueryinteger
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

HTTPcodeCuándo pasa
404not_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

scope: webhooks:manage

Vuelve a encolar una entrega (con un intento más aunque se hubieran agotado), p. ej. tras arreglar tu servidor.

Parámetros

NombreEnTipoDescripciónEjemplo
id obligatoriopathstring
≤ 64 caracteres

Id del webhook.

cm1whk4r0006qx8v2n6c3b7xy
deliveryId obligatoriopathstring

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

HTTPcodeCuándo pasa
404not_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

CampoTipoDescripciónEjemplo
object"me"

Tipo de objeto: siempre "me".

"me"
apiKeyobject

La clave con la que se hace la llamada.

Campos
CampoTipoDescripciónEjemplo
idstring

Id interno de la clave.

"cm1key9p0008qx5b3c7n2m4wl"
namestring

Nombre que le puso el administrador al crearla.

"Gestoría Pérez"
prefixstring

fm_live_<keyId>: identifica la clave sin revelar el secreto (es lo que se ve en el panel).

"fm_live_k7m2p5x4q3ab"
scopesstring[]

Permisos concedidos a la clave.

["company:read","employees:read","clock:read"]
expiresAtstring | null

Caducidad de la clave (null = sin caducidad, solo en Enterprise).

"2027-09-15T10:00:00.000Z"
companyobject

La empresa de la clave: la única cuyos datos ve.

Campos
CampoTipoDescripciónEjemplo
idinteger

Id de la empresa en FichMe.

1042
namestring

Nombre comercial.

"Construcciones Ebro"
slugstring

Identificador corto (el subdominio del panel).

"construcciones-ebro"
timezonestring

Zona horaria IANA: la de las fechas de jornada (shiftDate).

"Europe/Madrid"
planobject

Plan de la empresa.

Campos
CampoTipoDescripciónEjemplo
namestring | null

Plan contratado.

"Business"
webhooksboolean

El plan incluye webhooks (Business y Enterprise).

true
rateLimitobject

Límites que se aplican a esta clave.

Campos
CampoTipoDescripciónEjemplo
perMinuteinteger

Peticiones por minuto de esta clave.

120
perDayinteger

Cuota diaria de la clave según el plan (se renueva a las 00:00 UTC).

50000
maxConcurrentHeavyinteger

Peticiones pesadas simultáneas (jornadas, balance, saldos, exportaciones).

1
apiVersion"v1"

Versión de la API.

"v1"
serverTimestring

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

CampoTipoDescripciónEjemplo
object"company"

Tipo de objeto: siempre "company".

"company"
idinteger

Id de la empresa en FichMe.

1042
namestring

Nombre comercial.

"Construcciones Ebro"
legalNamestring | null

Razón social.

"Construcciones Ebro, S.L."
taxIdstring | null

CIF de la empresa.

"B50123456"
slugstring

Identificador corto (el subdominio del panel).

"construcciones-ebro"
addressstring | null

Dirección fiscal.

"Calle del Coso 42"
citystring | null

Localidad.

"Zaragoza"
provincestring | null

Provincia.

"Zaragoza"
postalCodestring | null

Código postal.

"50004"
countrystring | null

País.

"España"
timezonestring

Zona horaria IANA contra la que se resuelve shiftDate.

"Europe/Madrid"
settingsobject

Ajustes que hacen falta para interpretar el resto de datos.

Campos
CampoTipoDescripciónEjemplo
workingDaysinteger[]

Días laborables por defecto: 0 = domingo … 6 = sábado.

[1,2,3,4,5]
lateArrivalThresholdinteger

Minutos de tolerancia antes de marcar un retraso.

10
earlyDepartureThresholdinteger

Minutos de tolerancia antes de marcar una salida anticipada.

10
mandatoryBreakMinutesinteger

Pausa mínima obligatoria de la jornada, en minutos.

30
maxShiftHoursinteger

Horas tras las que una jornada abierta se marca incompleta.

16
apiClockingEnabledboolean

La empresa permite registrar fichajes por API (lo activa un administrador).

true
hourBankEnabledboolean

La empresa usa la bolsa de horas.

true
geolocationEnabledboolean

La empresa pide la ubicación al fichar desde la app.

false
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

CampoTipoDescripciónEjemplo
object"location"

Tipo de objeto: siempre "location".

"location"
idstring

Id del centro de trabajo.

"cm1loczgz0001qx8f2k9d7h3a"
namestring

Nombre del centro.

"Oficina Zaragoza"
codestring | null

Código interno del centro, si la empresa lo usa.

"ZGZ"
addressstring | null

Dirección.

"Calle del Coso 42"
citystring | null

Localidad.

"Zaragoza"
provincestring | null

Provincia (decide los festivos autonómicos).

"Zaragoza"
timezonestring | null

Zona horaria propia del centro (null = la de la empresa).

null
isActiveboolean

El centro está en uso.

true
isPrimaryboolean

Es el centro principal de la empresa.

true
isSystemboolean

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
geofenceobject

Geovalla: coordenadas del CENTRO, nunca de personas.

Campos
CampoTipoDescripciónEjemplo
enabledboolean

Solo se puede fichar dentro del radio.

true
radiusMetersinteger | null

Radio en metros.

150
latitudenumber | null

Latitud del centro.

41.6523
longitudenumber | null

Longitud del centro.

-0.8773
createdAtstring

Alta del centro (UTC).

"2025-02-03T09:12:44.000Z"
updatedAtstring

Ú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

CampoTipoDescripciónEjemplo
object"employee"

Tipo de objeto: siempre "employee".

"employee"
idinteger

Id del empleado en FichMe.

4821
firstNamestring | null

Nombre.

"Ana"
lastNamestring | null

Apellidos.

"García López"
namestring

Nombre completo, como aparece en los informes.

"Ana García López"
emailstring | null

Email (también es su usuario de acceso).

"ana.garcia@example.com"
roleADMIN | EMPLOYEE

Rol en FichMe. Las cuentas ADMIN solo se modifican desde el panel.

"EMPLOYEE"
statusACTIVE | INACTIVE

INACTIVE = dado de baja operativa: no ficha ni ocupa plaza del plan.

"ACTIVE"
activationStatusstring

Estado del acceso a FichMe: PENDING_SETUP | PENDING_ACTIVATION | EMAIL_FAILED | ACTIVATED | NOT_APPLICABLE.

"ACTIVATED"
jobTitlestring | null

Puesto.

"Técnica de obra"
departmentstring | null

Departamento.

"Producción"
hireDatestring | null

Fecha de alta (YYYY-MM-DD).

"2024-03-01"
seniorityDatestring | null

Fecha de antigüedad (YYYY-MM-DD).

"2024-03-01"
contractobject

Datos del contrato.

Campos
CampoTipoDescripciónEjemplo
typestring | null

FULL_TIME | PART_TIME | TEMPORARY.

"FULL_TIME"
weeklyHoursnumber | null

Horas semanales contratadas.

40
annualHoursnumber | null

Horas anuales contratadas.

1776
isNightWorkerboolean

Trabajador nocturno (art. 36 ET).

false
locationobject | null

Centro principal.

Campos
CampoTipoDescripciónEjemplo
idstring

Id del centro de trabajo.

"cm1loczgz0001qx8f2k9d7h3a"
namestring

Nombre del centro.

"Oficina Zaragoza"
locationsobject[]

Todos los centros a los que pertenece.

Campos
CampoTipoDescripciónEjemplo
idstring

Id del centro de trabajo.

"cm1loczgz0001qx8f2k9d7h3a"
namestring

Nombre del centro.

"Oficina Zaragoza"
isPrimaryboolean

Es su centro principal.

true
clockingPolicyobject

Excepciones del empleado a la política de fichaje de la empresa.

Campos
CampoTipoDescripciónEjemplo
exemptboolean

Exento de fichar (no aparece en los informes de jornada).

false
webboolean | null

Puede fichar desde la web (null = lo que diga la empresa).

null
appboolean | null

Puede fichar desde la app (null = lo que diga la empresa).

true
terminalboolean | null

Puede fichar en un terminal con PIN (null = lo que diga la empresa).

null
requireGeolocationboolean | null

Se le exige ubicación al fichar (null = lo que diga la empresa).

null
identityobject | null

Datos fiscales. Solo con el scope employees:read_pii; sin él, null.

Campos
CampoTipoDescripciónEjemplo
taxIdstring | null

DNI/NIE.

"12345678Z"
socialSecurityNumberstring | null

Nº de afiliación a la Seguridad Social.

"281234567840"
phonestring | null

Teléfono.

"+34 600 123 456"
createdAtstring

Alta en FichMe (UTC).

"2024-02-20T10:05:13.000Z"
updatedAtstring

Último cambio (UTC). Es la marca de la sincronización incremental.

"2026-09-01T08:14:55.000Z"
deletedAtstring | 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

CampoTipoDescripciónEjemplo
object"clockEntry"

Tipo de objeto: siempre "clockEntry".

"clockEntry"
idinteger

Id del fichaje.

918273
employeeIdinteger

Empleado que fichó.

4821
typeIN | OUT | BREAK_START | BREAK_END

IN = entrada · OUT = salida · BREAK_START / BREAK_END = inicio y fin de pausa.

"IN"
timestampstring

Instante del fichaje en UTC (la hora oficial, la del servidor).

"2026-09-15T06:58:31.000Z"
shiftDatestring

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"
methodWEB | APP | QR | KIOSK | SLACK | WHATSAPP | API

Canal por el que se fichó.

"APP"
sourcestring

Origen técnico: WEB | MOBILE | KIOSK | SLACK | WHATSAPP | API.

"MOBILE"
statusCOMPLETE | INCOMPLETE | PENDING

INCOMPLETE = la jornada se quedó sin cerrar y la marcó el sistema.

"COMPLETE"
locationobject | null

Centro de trabajo del fichaje.

Campos
CampoTipoDescripciónEjemplo
idstring

Id del centro de trabajo.

"cm1loczgz0001qx8f2k9d7h3a"
namestring

Nombre del centro.

"Oficina Zaragoza"
isModifiedboolean

Se corrigió después de registrarse (queda trazado en la cadena de integridad).

false
requiresCorrectionboolean

Tiene una corrección pendiente o se marcó para revisión.

false
offlineCreatedboolean

Se registró sin conexión en un terminal y se sincronizó después.

false
notestring | null

Nota del empleado.

null
adminNotestring | null

Nota del administrador.

null
expectedobject

Lo que esperaba el turno asignado cuando se registró el fichaje.

Campos
CampoTipoDescripciónEjemplo
startTimestring | null

Entrada prevista por el turno (HH:mm, hora local).

"09:00"
endTimestring | null

Salida prevista (HH:mm, hora local).

"17:00"
hoursnumber | null

Horas previstas ese día.

7.5
arrivalStatusstring | null

Puntualidad de la entrada: early | on-time | late | very-late.

"on-time"
minutesLateinteger | null

Minutos de retraso (negativo = llegó antes).

-2
departureStatusstring | null

Puntualidad de la salida: early | on-time.

null
minutesEarlyinteger | null

Minutos que salió antes de la hora.

null
createdAtstring

Cuándo se guardó (UTC). Puede ser posterior a timestamp si llegó sin conexión.

"2026-09-15T06:58:31.000Z"
updatedAtstring

Último cambio (UTC). Es la marca de la sincronización incremental.

"2026-09-15T06:58:31.000Z"
deletedAtstring | 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

CampoTipoDescripciónEjemplo
object"workSession"

Tipo de objeto: siempre "workSession".

"workSession"
employeeIdinteger

Empleado.

4821
shiftDatestring

Día de jornada (YYYY-MM-DD, zona de la empresa).

"2026-09-15"
dayTypeWORKING | HOLIDAY | REST

Tipo de día según el calendario.

"WORKING"
statusCOMPLETE | 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"
scheduleSourceSHIFT | 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"
firstInstring | null

Primera entrada del día (UTC).

"2026-09-15T06:58:31.000Z"
lastOutstring | null

Última salida del día (UTC).

"2026-09-15T15:02:10.000Z"
segmentsobject[]

Tramos de trabajo emparejados (IN → OUT), en orden.

Campos
CampoTipoDescripciónEjemplo
instring | null

Entrada del tramo (UTC); null si falta.

"2026-09-15T06:58:31.000Z"
outstring | null

Salida del tramo (UTC); null si sigue abierto o falta.

"2026-09-15T11:00:02.000Z"
entryIdsinteger[]

Fichajes que forman el tramo.

[918273,918280]
locationobject | null

Centro del tramo.

Campos
CampoTipoDescripciónEjemplo
idstring

Id del centro de trabajo.

"cm1loczgz0001qx8f2k9d7h3a"
namestring

Nombre del centro.

"Oficina Zaragoza"
[{"in":"2026-09-15T06:58:31.000Z","out":"2026…
breaksobject[]

Pausas fichadas.

Campos
CampoTipoDescripciónEjemplo
startstring | null

Inicio de la pausa (UTC).

"2026-09-15T11:00:02.000Z"
endstring | null

Fin de la pausa (UTC); null si sigue abierta.

"2026-09-15T11:30:15.000Z"
entryIdsinteger[]

Fichajes BREAK_START y BREAK_END de la pausa.

[918281,918282]
workedMinutesinteger

Minutos computados como trabajados: lo fichado, más las pausas retribuidas y las ausencias que computan.

453
workedTimestring

workedMinutes en HH:mm.

"07:33"
clockedMinutesinteger | null

Parte de workedMinutes que sale de los fichajes.

453
breakMinutesinteger

Minutos de pausa.

30
paidBreakMinutesinteger

De ellos, retribuidos (cuentan como trabajo).

0
autoDeductedBreakMinutesinteger

Pausa del turno descontada aunque no se fichara.

0
assignedMinutesinteger

Jornada prevista en minutos.

450
assignedTimestring

assignedMinutes en HH:mm.

"07:30"
balanceMinutesinteger

workedMinutes − assignedMinutes. Negativo = faltan minutos.

3
balanceTimestring

balanceMinutes en HH:mm (con signo).

"00:03"
leavesobject[]

Ausencias aprobadas que cubren el día (vacío si no hay).

Campos
CampoTipoDescripciónEjemplo
leaveRequestIdstring

Id de la ausencia.

"cm1lr5q2w0009qx4m7c3z8k1m"
leaveTypeCodestring

Código del tipo de ausencia. RESTRICTED si es de salud (baja, consulta médica…) y la clave no tiene employees:read_pii.

"MEDICAL"
leaveTypeNamestring

Nombre del tipo («Ausencia» si está reservado).

"Consulta médica"
isPaidboolean

Computa como tiempo trabajado.

true
unitDAYS | HOURS

Por días o por horas.

"HOURS"
hoursnumber | null

Horas de la ausencia ese día (por horas).

2
halfDaystring | null

MORNING | AFTERNOON en medios días.

null
[]
isModifiedboolean

Algún fichaje del día se corrigió.

false
requiresCorrectionboolean

Algún fichaje del día está pendiente de revisión.

false
hasOpenSegmentboolean

El día acaba con una entrada sin salida.

false
enginestring

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

CampoTipoDescripciónEjemplo
object"clockAction"

Tipo de objeto: siempre "clockAction".

"clockAction"
actionIN | OUT | BREAK_START | BREAK_END

Lo que se ha registrado.

"IN"
stateworking | off | on_break

Estado del empleado después del fichaje: úsalo para pintar la pantalla.

"working"
entryobject

El fichaje principal (un ClockEntry).

Campos
CampoTipoDescripciónEjemplo
object"clockEntry"

Tipo de objeto: siempre "clockEntry".

"clockEntry"
idinteger

Id del fichaje.

918273
employeeIdinteger

Empleado que fichó.

4821
typeIN | OUT | BREAK_START | BREAK_END

IN = entrada · OUT = salida · BREAK_START / BREAK_END = inicio y fin de pausa.

"IN"
timestampstring

Instante del fichaje en UTC (la hora oficial, la del servidor).

"2026-09-15T06:58:31.000Z"
shiftDatestring

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"
methodWEB | APP | QR | KIOSK | SLACK | WHATSAPP | API

Canal por el que se fichó.

"APP"
sourcestring

Origen técnico: WEB | MOBILE | KIOSK | SLACK | WHATSAPP | API.

"MOBILE"
statusCOMPLETE | INCOMPLETE | PENDING

INCOMPLETE = la jornada se quedó sin cerrar y la marcó el sistema.

"COMPLETE"
locationobject | null

Centro de trabajo del fichaje.

Campos
CampoTipoDescripciónEjemplo
idstring

Id del centro de trabajo.

"cm1loczgz0001qx8f2k9d7h3a"
namestring

Nombre del centro.

"Oficina Zaragoza"
isModifiedboolean

Se corrigió después de registrarse (queda trazado en la cadena de integridad).

false
requiresCorrectionboolean

Tiene una corrección pendiente o se marcó para revisión.

false
offlineCreatedboolean

Se registró sin conexión en un terminal y se sincronizó después.

false
notestring | null

Nota del empleado.

null
adminNotestring | null

Nota del administrador.

null
expectedobject

Lo que esperaba el turno asignado cuando se registró el fichaje.

Campos
CampoTipoDescripciónEjemplo
startTimestring | null

Entrada prevista por el turno (HH:mm, hora local).

"09:00"
endTimestring | null

Salida prevista (HH:mm, hora local).

"17:00"
hoursnumber | null

Horas previstas ese día.

7.5
arrivalStatusstring | null

Puntualidad de la entrada: early | on-time | late | very-late.

"on-time"
minutesLateinteger | null

Minutos de retraso (negativo = llegó antes).

-2
departureStatusstring | null

Puntualidad de la salida: early | on-time.

null
minutesEarlyinteger | null

Minutos que salió antes de la hora.

null
createdAtstring

Cuándo se guardó (UTC). Puede ser posterior a timestamp si llegó sin conexión.

"2026-09-15T06:58:31.000Z"
updatedAtstring

Último cambio (UTC). Es la marca de la sincronización incremental.

"2026-09-15T06:58:31.000Z"
deletedAtstring | null

Eliminado (UTC), o null.

null
entriesobject[]

Todos los fichajes creados: las pausas crean dos a la vez (OUT + BREAK_START, o BREAK_END + IN).

Campos
CampoTipoDescripciónEjemplo
object"clockEntry"

Tipo de objeto: siempre "clockEntry".

"clockEntry"
idinteger

Id del fichaje.

918273
employeeIdinteger

Empleado que fichó.

4821
typeIN | OUT | BREAK_START | BREAK_END

IN = entrada · OUT = salida · BREAK_START / BREAK_END = inicio y fin de pausa.

"IN"
timestampstring

Instante del fichaje en UTC (la hora oficial, la del servidor).

"2026-09-15T06:58:31.000Z"
shiftDatestring

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"
methodWEB | APP | QR | KIOSK | SLACK | WHATSAPP | API

Canal por el que se fichó.

"APP"
sourcestring

Origen técnico: WEB | MOBILE | KIOSK | SLACK | WHATSAPP | API.

"MOBILE"
statusCOMPLETE | INCOMPLETE | PENDING

INCOMPLETE = la jornada se quedó sin cerrar y la marcó el sistema.

"COMPLETE"
locationobject | null

Centro de trabajo del fichaje.

Campos
CampoTipoDescripciónEjemplo
idstring

Id del centro de trabajo.

"cm1loczgz0001qx8f2k9d7h3a"
namestring

Nombre del centro.

"Oficina Zaragoza"
isModifiedboolean

Se corrigió después de registrarse (queda trazado en la cadena de integridad).

false
requiresCorrectionboolean

Tiene una corrección pendiente o se marcó para revisión.

false
offlineCreatedboolean

Se registró sin conexión en un terminal y se sincronizó después.

false
notestring | null

Nota del empleado.

null
adminNotestring | null

Nota del administrador.

null
expectedobject

Lo que esperaba el turno asignado cuando se registró el fichaje.

Campos
CampoTipoDescripciónEjemplo
startTimestring | null

Entrada prevista por el turno (HH:mm, hora local).

"09:00"
endTimestring | null

Salida prevista (HH:mm, hora local).

"17:00"
hoursnumber | null

Horas previstas ese día.

7.5
arrivalStatusstring | null

Puntualidad de la entrada: early | on-time | late | very-late.

"on-time"
minutesLateinteger | null

Minutos de retraso (negativo = llegó antes).

-2
departureStatusstring | null

Puntualidad de la salida: early | on-time.

null
minutesEarlyinteger | null

Minutos que salió antes de la hora.

null
createdAtstring

Cuándo se guardó (UTC). Puede ser posterior a timestamp si llegó sin conexión.

"2026-09-15T06:58:31.000Z"
updatedAtstring

Último cambio (UTC). Es la marca de la sincronización incremental.

"2026-09-15T06:58:31.000Z"
deletedAtstring | null

Eliminado (UTC), o null.

null
[{"object":"clockEntry","id":918273,"employee…
idempotentReplayboolean

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

CampoTipoDescripciónEjemplo
object"clockCorrection"

Tipo de objeto: siempre "clockCorrection".

"clockCorrection"
idstring

Id de la solicitud.

"cm1corr8k2p0007qx3n5v1b9d"
employeeIdinteger

Empleado del fichaje.

4821
clockEntryIdinteger | null

Fichaje que corrige (null si propone uno que falta o una jornada completa).

null
requestTypeMISSING_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"
missingTypeIN | OUT | BREAK_START | BREAK_END | null

Extremo que se corrige (null en jornadas y pausas completas).

null
proposedTimestring | null

Hora propuesta (UTC). Null en jornadas y pausas completas: van en manual.

null
originalTimestring | null

Hora que tenía el fichaje antes de corregirlo (UTC).

null
manualobject | null

Solo en jornadas y pausas completas: el día y las horas propuestas.

Campos
CampoTipoDescripciónEjemplo
shiftDatestring | null

Día de la jornada.

"2026-09-14"
startstring | null

Entrada propuesta (UTC).

"2026-09-14T07:00:00.000Z"
endstring | null

Salida propuesta (UTC).

"2026-09-14T15:00:00.000Z"
locationobject | null

Centro propuesto para los fichajes.

Campos
CampoTipoDescripciónEjemplo
idstring

Id del centro de trabajo.

"cm1loczgz0001qx8f2k9d7h3a"
namestring

Nombre del centro.

"Oficina Zaragoza"
reasonstring

Motivo que dio quien la pidió.

"Olvidé fichar: estuve todo el día en la obra…
statusPENDING | APPROVED | REJECTED

PENDING hasta que una persona la aprueba o la rechaza.

"PENDING"
reviewedAtstring | null

Cuándo se revisó (UTC).

null
reviewedByIdinteger | null

Administrador que la revisó.

null
rejectionReasonstring | null

Motivo del rechazo (se envía al empleado).

null
isAutoApprovedboolean

Se aprobó en la misma llamada con autoApprove.

false
createdAtstring

Cuándo se pidió (UTC).

"2026-09-15T07:05:12.000Z"
updatedAtstring

Ú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

CampoTipoDescripciónEjemplo
object"leaveType"

Tipo de objeto: siempre "leaveType".

"leaveType"
idstring

Id del tipo de ausencia.

"cm1ltvac0001qx7d2k8f4h6jk"
codestring

Identificador estable para integrar (configurable por empresa): usa este, no el nombre.

"VACATION"
namestring

Nombre visible.

"Vacaciones"
descriptionstring | null

Descripción que ve el empleado.

"Vacaciones anuales retribuidas."
colorstring

Color en el calendario (hex).

"#3B82F6"
unitDAYS | HOURS

Se pide por días o por horas.

"DAYS"
dayCountTypeBUSINESS_DAYS | NATURAL_DAYS

Qué días cuenta: laborables o naturales.

"BUSINESS_DAYS"
requiresBalanceboolean

Consume un cupo anual (ver GET /v1/leave-balances).

true
requiresApprovalboolean

Necesita aprobación; si no, nace aprobada.

true
requiresDocumentboolean

Pide justificante.

false
requiresReasonboolean

Pide motivo.

false
allowHalfDaysboolean

Admite medios días.

true
isPaidboolean

Computa como tiempo trabajado.

true
blockClockingboolean

Impide fichar esos días.

true
affectsWorkingDaysboolean

Descuenta la jornada prevista de esos días.

true
isActiveboolean

Se puede usar al crear ausencias.

true
isSystemboolean

Tipo de sistema (p. ej. descanso compensatorio de la bolsa de horas): se lee, no se usa al crear.

false
createdAtstring

Alta (UTC).

"2025-01-08T11:20:00.000Z"
updatedAtstring

Ú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

CampoTipoDescripciónEjemplo
object"leaveRequest"

Tipo de objeto: siempre "leaveRequest".

"leaveRequest"
idstring

Id de la ausencia.

"cm1lr5q2w0009qx4m7c3z8k1m"
employeeIdinteger

Empleado.

4821
leaveTypeobject

Tipo de ausencia. Los de salud (bajas, IT, consultas médicas…) son datos sensibles: solo con employees:read_pii.

Campos
CampoTipoDescripciónEjemplo
idstring | null

Id del tipo de ausencia. null cuando el tipo está reservado (ver code).

"cm1ltvac0001qx7d2k8f4h6jk"
codestring

Código estable del tipo: integra por él. RESTRICTED si es un tipo de salud (baja, consulta médica…) y la clave no tiene employees:read_pii.

"VACATION"
namestring

Nombre del tipo («Ausencia» si está reservado).

"Vacaciones"
unitDAYS | HOURS

Por días o por horas.

"DAYS"
startDatestring

Primer día (YYYY-MM-DD).

"2026-10-13"
endDatestring

Último día (YYYY-MM-DD), inclusive.

"2026-10-16"
startHalfMORNING | AFTERNOON | null

Medio día al inicio, si lo hay.

null
endHalfMORNING | AFTERNOON | null

Medio día al final, si lo hay.

null
startTimestring | null

Hora de inicio (HH:mm, hora local), solo en ausencias por horas.

null
endTimestring | null

Hora de fin (HH:mm, hora local), solo en ausencias por horas.

null
hoursnumber | null

Horas, solo en ausencias por horas.

null
businessDaysnumber

Días que consume (0,5 en medios días; 0 en ausencias por horas).

4
statusPENDING | APPROVED | REJECTED | CANCELLED

Estado de la solicitud.

"APPROVED"
sourceEMPLOYEE | ADMIN | API | SYSTEM

Quién la registró: el empleado, un administrador, la API o el sistema.

"API"
reasonstring | 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"
reviewedAtstring | null

Cuándo se revisó (UTC).

"2026-09-16T08:02:40.000Z"
reviewedByIdinteger | null

Administrador que la revisó.

17
rejectionReasonstring | null

Motivo del rechazo. Texto libre: solo con employees:read_pii (sin él, null).

null
cancelledAtstring | null

Cuándo se canceló (UTC).

null
createdAtstring

Cuándo se pidió (UTC).

"2026-09-15T16:20:03.000Z"
updatedAtstring

Ú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

CampoTipoDescripciónEjemplo
object"leaveBalance"

Tipo de objeto: siempre "leaveBalance".

"leaveBalance"
employeeIdinteger

Empleado.

4821
leaveTypeobject

Tipo de ausencia del cupo.

Campos
CampoTipoDescripciónEjemplo
idstring

Id del tipo de ausencia.

"cm1ltvac0001qx7d2k8f4h6jk"
codestring

Código estable del tipo: integra por él.

"VACATION"
namestring

Nombre del tipo.

"Vacaciones"
yearinteger

Año del cupo.

2026
unitDAYS | HOURS

Unidad de todas las cifras: días u horas.

"DAYS"
allocatednumber

Asignado para el año.

22
carryOvernumber

Arrastrado del año anterior.

2
adjustmentnumber

Ajustes manuales del administrador (positivos o negativos).

0
usednumber

Consumido o reservado por ausencias pendientes y aprobadas.

12
availablenumber

Disponible: asignado + arrastrado + ajustes − consumido.

12
carriedFromPreviousYearsnumber

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

CampoTipoDescripciónEjemplo
object"hoursBalance"

Tipo de objeto: siempre "hoursBalance".

"hoursBalance"
employeeIdinteger

Empleado.

4821
fromstring

Primer día del periodo.

"2026-09-01"
tostring

Último día del periodo.

"2026-09-30"
workedMinutesinteger

Minutos trabajados en el periodo (con pausas retribuidas y ausencias que computan).

9540
workedTimestring

workedMinutes en HH:mm (las horas pueden pasar de 24).

"159:00"
assignedMinutesinteger

Minutos previstos por la jornada en el periodo.

9450
assignedTimestring

assignedMinutes en HH:mm.

"157:30"
balanceMinutesinteger

Trabajado − previsto. Positivo = minutos de más; negativo = faltan.

90
balanceTimestring

balanceMinutes en HH:mm (con signo).

"01:30"
breakMinutesinteger

Minutos de pausa.

630
paidBreakMinutesinteger

De ellos, retribuidos.

0
autoDeductedMinutesinteger

Pausas del turno descontadas aunque no se ficharan.

0
daysWorkedinteger

Días con tiempo trabajado.

21
daysWithOpenSegmentinteger

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

CampoTipoDescripciónEjemplo
object"hoursBalanceDay"

Tipo de objeto: siempre "hoursBalanceDay".

"hoursBalanceDay"
employeeIdinteger

Empleado.

4821
datestring

Día de jornada.

"2026-09-15"
clockInstring | null

Primera entrada del día, HH:mm en hora local.

"08:58"
clockOutstring | null

Última salida del día, HH:mm en hora local.

"17:02"
workedMinutesinteger

Minutos trabajados.

453
assignedMinutesinteger

Minutos previstos.

450
balanceMinutesinteger

Trabajado − previsto.

3
breakMinutesinteger

Minutos de pausa.

30
paidBreakMinutesinteger

De ellos, retribuidos.

0
autoDeductedMinutesinteger

Pausa del turno descontada aunque no se fichara.

0
scheduleSourceSHIFT | 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"
hasOpenSegmentboolean

El día acaba con una entrada sin salida.

false
notesstring | 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

CampoTipoDescripciónEjemplo
object"shift"

Tipo de objeto: siempre "shift".

"shift"
idstring

Id del turno.

"cm1shmnn0002qx9p4c6v8b2de"
namestring

Nombre del turno.

"Mañana"
typeWORK | FREE

WORK = turno de trabajo; FREE = día libre marcado en el calendario.

"WORK"
colorstring

Color en el calendario (hex).

"#10B981"
isNightboolean

Turno de noche (sale al día siguiente).

false
isSplitboolean

Jornada partida (más de un tramo).

false
segmentsobject[]

Tramos del turno.

Campos
CampoTipoDescripciónEjemplo
startstring

Inicio del tramo (HH:mm, hora local).

"09:00"
endstring

Fin del tramo (HH:mm, hora local).

"17:00"
totalHoursnumber

Horas de trabajo del turno (sin las pausas no retribuidas).

7.5
applicableDaysinteger[]

Días en los que aplica: 0 = domingo … 6 = sábado.

[1,2,3,4,5]
toleranceobject

Tolerancias de puntualidad.

Campos
CampoTipoDescripciónEjemplo
entryMinutesinteger

Minutos de margen en la entrada antes de marcar retraso.

10
exitMinutesinteger

Minutos de margen en la salida.

10
breaksobject[]

Pausas previstas del turno.

Campos
CampoTipoDescripciónEjemplo
idstring

Id de la pausa.

"cm1brkcm0003qx9p4c6v8b2df"
namestring

Nombre.

"Comida"
startTimestring

Hora de inicio (HH:mm, hora local).

"13:00"
durationMinutesinteger

Duración en minutos.

30
isPaidboolean

Retribuida: cuenta como trabajo.

false
autoDeductboolean

Se descuenta aunque no se fiche.

false
createdAtstring

Alta (UTC).

"2025-02-03T09:30:00.000Z"
updatedAtstring

Ú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

CampoTipoDescripciónEjemplo
object"shiftAssignment"

Tipo de objeto: siempre "shiftAssignment".

"shiftAssignment"
employeeIdinteger

Empleado.

4821
datestring

Día.

"2026-09-15"
originASSIGNMENT | PERIOD | DEFAULT

ASSIGNMENT = calendario día a día (manda); PERIOD = turno por rango de fechas; DEFAULT = turno fijo.

"DEFAULT"
shiftobject | null

Turno de ese día (null en un día libre pintado sin turno).

Campos
CampoTipoDescripciónEjemplo
idstring

Id del turno.

"cm1shmnn0002qx9p4c6v8b2de"
namestring

Nombre del turno.

"Mañana"
isDayOffboolean

Día libre marcado en el calendario.

false
startTimestring | null

Entrada prevista (HH:mm, hora local).

"09:00"
endTimestring | null

Salida prevista (HH:mm, hora local).

"17:00"
hoursnumber | null

Horas previstas.

7.5
notesstring | 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

CampoTipoDescripciónEjemplo
object"holiday"

Tipo de objeto: siempre "holiday".

"holiday"
idstring

Id del festivo.

"cm1hol12o0004qx2b7n5c9d3f"
datestring

Día (YYYY-MM-DD, zona de la empresa).

"2026-10-12"
namestring

Nombre.

"Fiesta Nacional de España"
typeNATIONAL | REGIONAL | LOCAL | COMPANY

Nacional, autonómico, local o propio de la empresa.

"NATIONAL"
scopeCOMPANY | LOCATION

COMPANY = de toda la empresa; LOCATION = solo de un centro.

"COMPANY"
locationIdstring | null

Centro al que aplica (solo con scope LOCATION).

null
provincestring | null

Provincia (festivos autonómicos).

null
workingHoursnumber

Horas de trabajo previstas ese día (0 = festivo completo; más = jornada reducida).

0
recurrentboolean

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

CampoTipoDescripciónEjemplo
object"export"

Tipo de objeto: siempre "export".

"export"
idstring

Id de la exportación.

"cm1exp7t0005qx6c3m9b2v8zq"
statuspending | processing | completed | failed | expired

pending → processing → completed (o failed). expired = el archivo ya se borró.

"completed"
datasetclocking | balance | absence | hour-bank | location-hours

Informe pedido.

"clocking"
formatcsv | xlsx | pdf

Formato del archivo.

"pdf"
fromstring | null

Primer día del periodo.

"2026-09-01"
tostring | null

Último día del periodo.

"2026-09-30"
progressinteger

Progreso, de 0 a 100.

100
rowsinteger | null

Filas de datos del informe (cuando termina).

412
sizeBytesinteger | null

Tamaño del archivo en bytes.

183422
downloadUrlstring | 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…
errorstring | null

Qué falló, si status es failed.

null
createdAtstring

Cuándo se pidió (UTC).

"2026-10-01T07:00:03.000Z"
completedAtstring | null

Cuándo terminó (UTC).

"2026-10-01T07:00:41.000Z"
expiresAtstring

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

CampoTipoDescripciónEjemplo
object"webhookEndpoint"

Tipo de objeto: siempre "webhookEndpoint".

"webhookEndpoint"
idstring

Id del webhook.

"cm1whk4r0006qx8v2n6c3b7xy"
urlstring

URL https que recibe los eventos.

"https://erp.example.com/webhooks/fichme"
descriptionstring | null

Descripción libre.

"ERP de nóminas"
eventsstring[]

Tipos suscritos, o ["*"] para todos.

["clockEntry.created","leaveRequest.approved"]
statusACTIVE | PAUSED | DISABLED

DISABLED = desactivado automáticamente tras 72 h fallando (reactívalo con PATCH status ACTIVE).

"ACTIVE"
failingSincestring | null

Desde cuándo falla sin interrupción (UTC), o null.

null
disabledReasonstring | null

Por qué se desactivó.

null
secretLast4string

Últimos 4 caracteres del secreto, para reconocerlo.

"hJ2l"
createdAtstring

Alta (UTC).

"2026-09-10T09:00:00.000Z"
updatedAtstring

Último cambio (UTC).

"2026-09-10T09:00:00.000Z"
lastDeliveryobject | null

La entrega más reciente, o null si aún no hubo ninguna.

Campos
CampoTipoDescripciónEjemplo
statusDELIVERED | FAILED | PENDING

Resultado de la última entrega.

"DELIVERED"
httpStatusinteger | null

Código HTTP que respondió tu servidor.

200
atstring

Cuándo (UTC).

"2026-09-15T06:58:33.000Z"
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

CampoTipoDescripciónEjemplo
endpointobject

El webhook (WebhookEndpoint).

Campos
CampoTipoDescripciónEjemplo
object"webhookEndpoint"

Tipo de objeto: siempre "webhookEndpoint".

"webhookEndpoint"
idstring

Id del webhook.

"cm1whk4r0006qx8v2n6c3b7xy"
urlstring

URL https que recibe los eventos.

"https://erp.example.com/webhooks/fichme"
descriptionstring | null

Descripción libre.

"ERP de nóminas"
eventsstring[]

Tipos suscritos, o ["*"] para todos.

["clockEntry.created","leaveRequest.approved"]
statusACTIVE | PAUSED | DISABLED

DISABLED = desactivado automáticamente tras 72 h fallando (reactívalo con PATCH status ACTIVE).

"ACTIVE"
failingSincestring | null

Desde cuándo falla sin interrupción (UTC), o null.

null
disabledReasonstring | null

Por qué se desactivó.

null
secretLast4string

Últimos 4 caracteres del secreto, para reconocerlo.

"hJ2l"
createdAtstring

Alta (UTC).

"2026-09-10T09:00:00.000Z"
updatedAtstring

Último cambio (UTC).

"2026-09-10T09:00:00.000Z"
lastDeliveryobject | null

La entrega más reciente, o null si aún no hubo ninguna.

Campos
CampoTipoDescripciónEjemplo
statusDELIVERED | FAILED | PENDING

Resultado de la última entrega.

"DELIVERED"
httpStatusinteger | null

Código HTTP que respondió tu servidor.

200
atstring

Cuándo (UTC).

"2026-09-15T06:58:33.000Z"
secretstring

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

CampoTipoDescripciónEjemplo
object"webhookDelivery"

Tipo de objeto: siempre "webhookDelivery".

"webhookDelivery"
idstring

Id de la entrega.

"48213"
eventIdstring

Id del evento (el id del cuerpo): deduplica por él.

"evt_01K5Q8Z3M2X7C9V4B6N1P8R0TQ"
eventTypestring

Tipo de evento.

"clockEntry.created"
statusDELIVERED | FAILED | PENDING

DELIVERED = tu servidor respondió 2xx; FAILED = se agotaron los intentos.

"DELIVERED"
attemptsinteger

Intentos hechos.

1
lastStatusinteger | null

Código HTTP del último intento.

200
lastErrorstring | null

Error del último intento (timeout, TLS…).

null
createdAtstring

Cuándo se generó el evento (UTC).

"2026-09-15T06:58:32.000Z"
deliveredAtstring | null

Cuándo se entregó (UTC).

"2026-09-15T06:58:33.000Z"
nextAttemptAtstring | 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

CampoTipoDescripciónEjemplo
deliveryIdstring

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).