Portal del desarrollador · en preparación
Guías por caso de uso
Cinco tareas habituales, cada una con curl y con la librería de cada lenguaje. Dice también lo que la API no hace.
Alta de personas: lo que sí se puede hacer por API
La API no da de alta, modifica ni da de baja personas: no existe ninguna ruta para ello. Las altas se hacen en la aplicación o con la importación de personas; por API se lee la plantilla para mantener sincronizado tu programa de nóminas o de recursos humanos.
Ámbito de la clave: personas:leer (y personas:nif si necesitas el NIF completo).
- Sin personas:nif, el NIF sale enmascarado (***5678Z).
- Para dar de alta personas usa la aplicación, que admite el alta masiva desde una plantilla CSV; no hay fecha para altas por API.
curl
Rutas y parámetros comprobados contra el contrato; no se han lanzado contra el servicio real, que aún no existe.
export ERJ_CLAVE="erj_test_..." # tu clave de pruebas
curl -H "Authorization: Bearer $ERJ_CLAVE" \
"https://app.elregistrodejornada.com/api/v1/personas?estado=alta&limit=50"
# La respuesta trae "datos" y "siguiente" (cursor). Para la página siguiente, el mismo filtro más ese cursor:
curl -H "Authorization: Bearer $ERJ_CLAVE" \
"https://app.elregistrodejornada.com/api/v1/personas?estado=alta&limit=50&cursor=EL_CURSOR_DE_SIGUIENTE"TypeScript
Ejecutado contra un servidor local falso.
import { ClienteErj } from './sdk/typescript/src/index.ts';
const cliente = new ClienteErj({ clave: process.env.ERJ_CLAVE!, urlBase: process.env.ERJ_URL_BASE });
// iterar sigue el cursor hasta la última página
for await (const persona of cliente.personas.iterar({ estado: 'alta', limit: 50 })) {
console.log(persona.id, persona.nombre);
}Python
Ejecutado contra un servidor local falso.
import os
import sys
sys.path.insert(0, "sdk/python")
from erj import Cliente
opciones = {"url_base": os.environ["ERJ_URL_BASE"]} if os.environ.get("ERJ_URL_BASE") else {}
cliente = Cliente(os.environ["ERJ_CLAVE"], **opciones)
# iterar sigue el cursor hasta la última página
for persona in cliente.personas.iterar(estado="alta", limit=50):
print(persona["id"], persona["nombre"])C#
Escrito con la librería de C#, sin compilar ni ejecutar (no hay SDK de .NET en el equipo de desarrollo).
using ElRegistroJornada;
var cliente = new ClienteErj(new OpcionesCliente { Clave = Environment.GetEnvironmentVariable("ERJ_CLAVE")! });
// IterarAsync sigue el cursor hasta la última página
await foreach (var persona in cliente.Personas.IterarAsync(new Dictionary<string, string?> { ["estado"] = "alta", ["limit"] = "50" }))
Console.WriteLine($"{persona["id"]} {persona["nombre"]}");PHP
Escrito con la librería de PHP, sin ejecutar (no hay intérprete de PHP en el equipo de desarrollo).
<?php
require 'sdk/php/src/ClienteErj.php';
use ElRegistroJornada\ClienteErj;
$cliente = new ClienteErj(getenv('ERJ_CLAVE') ?: '');
// iterar sigue el cursor hasta la última página
foreach ($cliente->personas->iterar(['estado' => 'alta', 'limit' => 50]) as $persona) {
echo $persona['id'], ' ', $persona['nombre'] ?? '', "\n";
}Leer fichajes de un periodo
Los fichajes se leen por tramos de como mucho 31 días por llamada. Cada fichaje lleva su secuencia y su huella en la cadena de integridad. Sin parámetros, devuelve los últimos 7 días.
Ámbito de la clave: fichajes:leer (y fichajes:posicion para ver posición y nota).
- Más de 31 días en una llamada da 422 rango_demasiado_amplio: pide varios tramos.
- Sin fichajes:posicion no salen la posición ni la nota (minimización de datos).
- La lista de la API no sustituye al verificador independiente: es una proyección de lectura. Para recalcular la cadena sin fiarte del servidor, usa el paquete pericial.
curl
Rutas y parámetros comprobados contra el contrato; no se han lanzado contra el servicio real, que aún no existe.
curl -G -H "Authorization: Bearer $ERJ_CLAVE" \
--data-urlencode "persona=per_1" \
--data-urlencode "desde=2026-10-01" \
--data-urlencode "hasta=2026-10-08" \
--data-urlencode "limit=100" \
"https://app.elregistrodejornada.com/api/v1/fichajes"TypeScript
Ejecutado contra un servidor local falso.
import { ClienteErj, ErrorApi } from './sdk/typescript/src/index.ts';
const cliente = new ClienteErj({ clave: process.env.ERJ_CLAVE!, urlBase: process.env.ERJ_URL_BASE });
try {
for await (const f of cliente.fichajes.iterar({ persona: 'per_1', desde: '2026-10-01', hasta: '2026-10-08', limit: 100 })) {
console.log(f.secuencia, f.tipo, f.ocurridoEn);
}
} catch (e) {
if (e instanceof ErrorApi && e.codigo === 'rango_demasiado_amplio') console.error('Pide tramos de 31 días como mucho.');
else throw e;
}Python
Ejecutado contra un servidor local falso.
import os
import sys
sys.path.insert(0, "sdk/python")
from erj import Cliente, ErrorApi
opciones = {"url_base": os.environ["ERJ_URL_BASE"]} if os.environ.get("ERJ_URL_BASE") else {}
cliente = Cliente(os.environ["ERJ_CLAVE"], **opciones)
try:
for f in cliente.fichajes.iterar(persona="per_1", desde="2026-10-01", hasta="2026-10-08", limit=100):
print(f["secuencia"], f["tipo"], f["ocurridoEn"])
except ErrorApi as e:
if e.codigo != "rango_demasiado_amplio":
raise
print("Pide tramos de 31 días como mucho.")C#
Escrito con la librería de C#, sin compilar ni ejecutar (no hay SDK de .NET en el equipo de desarrollo).
using ElRegistroJornada;
var cliente = new ClienteErj(new OpcionesCliente { Clave = Environment.GetEnvironmentVariable("ERJ_CLAVE")! });
try
{
var filtros = new Dictionary<string, string?> { ["persona"] = "per_1", ["desde"] = "2026-10-01", ["hasta"] = "2026-10-08", ["limit"] = "100" };
await foreach (var f in cliente.Fichajes.IterarAsync(filtros))
Console.WriteLine($"{f["secuencia"]} {f["tipo"]} {f["ocurridoEn"]}");
}
catch (ErrorApi e) when (e.Codigo == "rango_demasiado_amplio")
{
Console.Error.WriteLine("Pide tramos de 31 días como mucho.");
}PHP
Escrito con la librería de PHP, sin ejecutar (no hay intérprete de PHP en el equipo de desarrollo).
<?php
require 'sdk/php/src/ClienteErj.php';
use ElRegistroJornada\ClienteErj;
use ElRegistroJornada\ErrorApi;
$cliente = new ClienteErj(getenv('ERJ_CLAVE') ?: '');
try {
foreach ($cliente->fichajes->iterar(['persona' => 'per_1', 'desde' => '2026-10-01', 'hasta' => '2026-10-08', 'limit' => 100]) as $f) {
echo $f['secuencia'], ' ', $f['tipo'], ' ', $f['ocurridoEn'], "\n";
}
} catch (ErrorApi $e) {
if ($e->codigo !== 'rango_demasiado_amplio') { throw $e; }
fwrite(STDERR, "Pide tramos de 31 días como mucho.\n");
}Añadir un fichaje desde tu sistema (apagado por defecto)
POST /fichajes añade un asiento de entrada o salida con el mismo sellador que la aplicación. Está apagado en todas las empresas y sin fecha: hace falta una opción de la empresa y un ámbito de la clave. Estos ejemplos no lo activan.
Ámbito de la clave: fichajes:escribir (no se concede por defecto ni por OAuth).
- Sin la opción de la empresa, 403 escritura_no_habilitada; sin el ámbito, 403 ambito_insuficiente; con una clave erj_test_, 403 escritura_no_disponible_en_pruebas.
- Idempotency-Key es obligatoria. Con la misma clave y los mismos datos se devuelve el mismo asiento (cabecera Idempotent-Replayed: true) y no se duplica nada.
- La hora la pone el servidor. Los asientos solo se añaden: no hay ruta para modificar ni borrar uno. Una corrección es una propuesta (POST /modificaciones) que la persona acepta o rechaza en la aplicación.
curl
Rutas y parámetros comprobados contra el contrato; no se han lanzado contra el servicio real, que aún no existe.
curl -X POST -H "Authorization: Bearer $ERJ_CLAVE" -H "Content-Type: application/json" \
-H "Idempotency-Key: fichaje-per_1-2026-10-12-entrada" \
-d '{"personaId":"per_1","tipo":"entrada"}' \
"https://app.elregistrodejornada.com/api/v1/fichajes"
# Con la escritura apagada (hoy, en todas las empresas) responde 403 con "code": "escritura_no_habilitada".TypeScript
Ejecutado contra un servidor local falso.
import { ClienteErj, ErrorApi } from './sdk/typescript/src/index.ts';
const cliente = new ClienteErj({ clave: process.env.ERJ_CLAVE!, urlBase: process.env.ERJ_URL_BASE });
const clave = 'fichaje-per_1-2026-10-12-entrada'; // una por intento; la MISMA al reintentar tras un corte
try {
const asiento = await cliente.crearFichaje({ personaId: 'per_1', tipo: 'entrada' }, clave);
console.log(asiento.id, asiento.secuencia);
} catch (e) {
if (e instanceof ErrorApi && e.codigo === 'escritura_no_habilitada') console.log('La escritura está apagada en esta empresa.');
else throw e;
}Python
Ejecutado contra un servidor local falso.
import os
import sys
sys.path.insert(0, "sdk/python")
from erj import Cliente, ErrorApi
opciones = {"url_base": os.environ["ERJ_URL_BASE"]} if os.environ.get("ERJ_URL_BASE") else {}
cliente = Cliente(os.environ["ERJ_CLAVE"], **opciones)
clave = "fichaje-per_1-2026-10-12-entrada" # una por intento; la MISMA al reintentar tras un corte
try:
asiento = cliente.crear_fichaje({"personaId": "per_1", "tipo": "entrada"}, clave)
print(asiento["id"], asiento.get("secuencia"))
except ErrorApi as e:
if e.codigo != "escritura_no_habilitada":
raise
print("La escritura está apagada en esta empresa.")C#
Escrito con la librería de C#, sin compilar ni ejecutar (no hay SDK de .NET en el equipo de desarrollo).
using System.Text.Json.Nodes;
using ElRegistroJornada;
var cliente = new ClienteErj(new OpcionesCliente { Clave = Environment.GetEnvironmentVariable("ERJ_CLAVE")! });
var clave = "fichaje-per_1-2026-10-12-entrada"; // una por intento; la MISMA al reintentar tras un corte
try
{
var asiento = await cliente.CrearFichajeAsync(new JsonObject { ["personaId"] = "per_1", ["tipo"] = "entrada" }, clave);
Console.WriteLine($"{asiento["id"]} {asiento["secuencia"]}");
}
catch (ErrorApi e) when (e.Codigo == "escritura_no_habilitada")
{
Console.WriteLine("La escritura está apagada en esta empresa.");
}PHP
Escrito con la librería de PHP, sin ejecutar (no hay intérprete de PHP en el equipo de desarrollo).
<?php
require 'sdk/php/src/ClienteErj.php';
use ElRegistroJornada\ClienteErj;
use ElRegistroJornada\ErrorApi;
$cliente = new ClienteErj(getenv('ERJ_CLAVE') ?: '');
$clave = 'fichaje-per_1-2026-10-12-entrada'; // una por intento; la MISMA al reintentar tras un corte
try {
$asiento = $cliente->crearFichaje(['personaId' => 'per_1', 'tipo' => 'entrada'], $clave);
echo $asiento['id'], ' ', $asiento['secuencia'] ?? '', "\n";
} catch (ErrorApi $e) {
if ($e->codigo !== 'escritura_no_habilitada') { throw $e; }
echo "La escritura está apagada en esta empresa.\n";
}Informes: consultar los generados y pedir una exportación asíncrona
GET /informes lista los informes y exportaciones de la empresa con su estado y los metadatos del archivo. POST /informes pide una exportación de asientos como trabajo asíncrono (responde 202 sin esperar); se consulta hasta que termina y se descarga el CSV con un enlace firmado. Pedirla exige el ámbito informes:generar, que solo se concede en los planes de pago de la API (en el resto da 403 plan_sin_informes), con un tope diario por empresa; el informe lo hace el servidor en su pasada diaria, así que puede tardar hasta un día.
Ámbito de la clave: informes:leer (listar y consultar), informes:generar más fichajes:leer y personas:leer (generar y descargar; solo planes de pago de la API, desde el panel y nunca por OAuth).
- El ámbito informes:generar solo existe en los planes de pago de la API. Y las escrituras de la API (POST /fichajes, /ausencias y /modificaciones) siguen apagadas en todas las empresas. Este caso de uso no las activa; los ejemplos de generar valen para cuando tu plan lo incluya.
- POST /informes no modifica ningún dato (lee lo que la clave ya podría leer), pero Idempotency-Key es obligatoria: con la misma clave y los mismos datos se devuelve el mismo informe (200 e Idempotent-Replayed: true); con otros datos, 409 idempotency_key_reutilizada. La librería genera una si no la das y la mantiene en los reintentos.
- Hoy solo existe tipo exportacion: los asientos entre desde y hasta (como mucho 366 días), en CSV y sin el documento de identidad. Una clave erj_test_ no genera (403 generacion_no_disponible_en_pruebas).
- Estados: en_cola, en_proceso, terminado, fallido y caducado. La librería espera con intervalos crecientes (1 s, 2 s, 4 s... hasta 15 s) y un plazo (5 minutos por defecto); si acaba fallido o caducado lanza un error de informe, y si pasa el plazo, uno de espera agotada (el informe sigue en marcha). Cada consulta cuenta contra tus límites de uso.
- Límite de informes sin terminar: 2 por clave en el plan Esencial y 3 en el Ampliada (y 3 entre todas las claves de la empresa); si no, 429 demasiados_informes_abiertos, que no trae Retry-After y la librería no reintenta.
- El enlace de descarga vale 15 minutos como mucho, solo para la clave que generó el informe y solo contra la URL de la API, con su cabecera Authorization. Otra clave de tu empresa recibe 404 informe_no_encontrado. La librería lo usa solo contra su URL base.
- Para comprobar el cumplimiento de un cuadrante o de unas jornadas sin guardar nada, existe POST /cumplimiento/validar (solo calcula): ver la referencia.
curl
Rutas y parámetros comprobados contra el contrato; no se han lanzado contra el servicio real, que aún no existe.
# 1) Los informes que ya existen (informes:leer):
curl -G -H "Authorization: Bearer $ERJ_CLAVE" \
--data-urlencode "estado=terminado" \
--data-urlencode "desde=2026-10-01" \
"https://app.elregistrodejornada.com/api/v1/informes"
# 2) Pedir una exportación (informes:generar: hoy, 403 ambito_insuficiente). Responde 202 con "estado": "en_cola":
curl -X POST -H "Authorization: Bearer $ERJ_CLAVE" -H "Content-Type: application/json" \
-H "Idempotency-Key: exportacion-2026-10" \
-d '{"tipo":"exportacion","desde":"2026-10-01","hasta":"2026-10-31"}' \
"https://app.elregistrodejornada.com/api/v1/informes"
# 3) Consultar hasta que "estado" sea "terminado"; entonces trae "descarga" (enlace firmado, 15 minutos):
curl -H "Authorization: Bearer $ERJ_CLAVE" "https://app.elregistrodejornada.com/api/v1/informes/inf_1"
# 4) Descargar el CSV con ese enlace y la MISMA clave (otra clave recibe 404):
curl -H "Authorization: Bearer $ERJ_CLAVE" -o exportacion-2026-10.csv \
"https://app.elregistrodejornada.com<valor del campo descarga>"TypeScript
Ejecutado contra un servidor local falso.
import { writeFile } from 'node:fs/promises';
import { ClienteErj, ErrorApi, ErrorEsperaAgotada, ErrorInforme } from './sdk/typescript/src/index.ts';
const cliente = new ClienteErj({ clave: process.env.ERJ_CLAVE!, urlBase: process.env.ERJ_URL_BASE });
// 1) Los informes que ya existen (informes:leer).
for await (const informe of cliente.informes.iterar({ estado: 'terminado', desde: '2026-10-01' })) {
console.log(informe.id, informe.tipo, informe.estado);
}
// 2) Pedir una exportación, esperar a que termine y descargar el CSV (informes:generar; solo planes de pago).
const clave = 'exportacion-2026-10'; // una por informe que quieres; la MISMA al reintentar tras un corte
try {
const creado = await cliente.generarInforme({ tipo: 'exportacion', desde: '2026-10-01', hasta: '2026-10-31' }, clave);
const listo = await cliente.esperarInforme(creado, { plazoMs: 120_000 });
await writeFile('exportacion-2026-10.csv', await cliente.descargarInforme(listo));
console.log('CSV guardado:', listo.archivo?.nombre);
} catch (e) {
if (e instanceof ErrorApi && e.codigo === 'ambito_insuficiente') console.log('Esta clave no tiene informes:generar (solo en planes de pago de la API).');
else if (e instanceof ErrorApi && e.codigo === 'demasiados_informes_abiertos') console.log('Hay demasiados informes sin terminar: espera a que acabe alguno.');
else if (e instanceof ErrorEsperaAgotada) console.log('Sigue en marcha; vuelve a esperarlo con el id', e.informe.id);
else if (e instanceof ErrorInforme) console.log('No se puede entregar:', e.codigo);
else throw e;
}Python
Ejecutado contra un servidor local falso.
import os
import sys
sys.path.insert(0, "sdk/python")
from erj import Cliente, ErrorApi, ErrorEsperaAgotada, ErrorInforme
opciones = {"url_base": os.environ["ERJ_URL_BASE"]} if os.environ.get("ERJ_URL_BASE") else {}
cliente = Cliente(os.environ["ERJ_CLAVE"], **opciones)
# 1) Los informes que ya existen (informes:leer).
for informe in cliente.informes.iterar(estado="terminado", desde="2026-10-01"):
print(informe["id"], informe["tipo"], informe["estado"])
# 2) Pedir una exportación, esperar a que termine y descargar el CSV (informes:generar; solo planes de pago).
clave = "exportacion-2026-10" # una por informe que quieres; la MISMA al reintentar tras un corte
try:
creado = cliente.generar_informe({"tipo": "exportacion", "desde": "2026-10-01", "hasta": "2026-10-31"}, clave)
listo = cliente.esperar_informe(creado, plazo=120)
with open("exportacion-2026-10.csv", "w", encoding="utf-8", newline="") as f:
f.write(cliente.descargar_informe(listo))
print("CSV guardado:", listo["archivo"]["nombre"])
except ErrorApi as e:
if e.codigo == "ambito_insuficiente":
print("Esta clave no tiene informes:generar (solo en planes de pago de la API).")
elif e.codigo == "demasiados_informes_abiertos":
print("Hay demasiados informes sin terminar: espera a que acabe alguno.")
else:
raise
except ErrorEsperaAgotada as e:
print("Sigue en marcha; vuelve a esperarlo con el id", e.informe["id"])
except ErrorInforme as e:
print("No se puede entregar:", e.codigo)C#
Escrito con la librería de C#, sin compilar ni ejecutar (no hay SDK de .NET en el equipo de desarrollo).
using System.Text.Json.Nodes;
using ElRegistroJornada;
var cliente = new ClienteErj(new OpcionesCliente { Clave = Environment.GetEnvironmentVariable("ERJ_CLAVE")! });
// 1) Los informes que ya existen (informes:leer).
await foreach (var informe in cliente.Informes.IterarAsync(new Dictionary<string, string?> { ["estado"] = "terminado", ["desde"] = "2026-10-01" }))
Console.WriteLine($"{informe["id"]} {informe["tipo"]} {informe["estado"]}");
// 2) Pedir una exportación, esperar a que termine y descargar el CSV (informes:generar; solo planes de pago).
var clave = "exportacion-2026-10"; // una por informe que quieres; la MISMA al reintentar tras un corte
try
{
var creado = await cliente.GenerarInformeAsync(new JsonObject { ["tipo"] = "exportacion", ["desde"] = "2026-10-01", ["hasta"] = "2026-10-31" }, clave);
var listo = await cliente.EsperarInformeAsync(creado, new OpcionesEspera { Plazo = TimeSpan.FromMinutes(2) });
await File.WriteAllTextAsync("exportacion-2026-10.csv", await cliente.DescargarInformeAsync(listo));
Console.WriteLine($"CSV guardado: {listo["archivo"]?["nombre"]}");
}
catch (ErrorApi e) when (e.Codigo == "ambito_insuficiente")
{
Console.WriteLine("Esta clave no tiene informes:generar (solo en planes de pago de la API).");
}
catch (ErrorApi e) when (e.Codigo == "demasiados_informes_abiertos")
{
Console.WriteLine("Hay demasiados informes sin terminar: espera a que acabe alguno.");
}
catch (ErrorEsperaAgotada e)
{
Console.WriteLine($"Sigue en marcha; vuelve a esperarlo con el id {e.Informe["id"]}");
}
catch (ErrorInforme e)
{
Console.WriteLine($"No se puede entregar: {e.Codigo}");
}PHP
Escrito con la librería de PHP, sin ejecutar (no hay intérprete de PHP en el equipo de desarrollo).
<?php
require 'sdk/php/src/ClienteErj.php';
use ElRegistroJornada\ClienteErj;
use ElRegistroJornada\ErrorApi;
use ElRegistroJornada\ErrorEsperaAgotada;
use ElRegistroJornada\ErrorInforme;
$cliente = new ClienteErj(getenv('ERJ_CLAVE') ?: '');
// 1) Los informes que ya existen (informes:leer).
foreach ($cliente->informes->iterar(['estado' => 'terminado', 'desde' => '2026-10-01']) as $informe) {
echo $informe['id'], ' ', $informe['tipo'], ' ', $informe['estado'], "\n";
}
// 2) Pedir una exportación, esperar a que termine y descargar el CSV (informes:generar; solo planes de pago).
$clave = 'exportacion-2026-10'; // una por informe que quieres; la MISMA al reintentar tras un corte
try {
$creado = $cliente->generarInforme(['tipo' => 'exportacion', 'desde' => '2026-10-01', 'hasta' => '2026-10-31'], $clave);
$listo = $cliente->esperarInforme($creado, 120.0);
file_put_contents('exportacion-2026-10.csv', $cliente->descargarInforme($listo));
echo 'CSV guardado: ', $listo['archivo']['nombre'] ?? '', "\n";
} catch (ErrorApi $e) {
if ($e->codigo === 'ambito_insuficiente') { echo "Esta clave no tiene informes:generar (solo en planes de pago de la API).\n"; }
elseif ($e->codigo === 'demasiados_informes_abiertos') { echo "Hay demasiados informes sin terminar: espera a que acabe alguno.\n"; }
else { throw $e; }
} catch (ErrorEsperaAgotada $e) {
echo 'Sigue en marcha; vuelve a esperarlo con el id ', $e->informe['id'], "\n";
} catch (ErrorInforme $e) {
echo 'No se puede entregar: ', $e->codigo, "\n";
}Recibir webhooks y verificar la firma
Las suscripciones se crean en la aplicación (Más → Integraciones); la API no tiene rutas para gestionarlas. Cada entrega lleva ERJ-Timestamp y ERJ-Signature (v1= más el HMAC-SHA256 de «timestamp.cuerpo» con el secreto del webhook). Verifica siempre sobre el cuerpo exacto recibido.
Ámbito de la clave: Ninguno: es el servidor quien llama a tu URL (https, puerto 443).
- Rechaza lo que tenga más de 300 segundos y deduplica por ERJ-Event-Id: la entrega es «al menos una vez».
- Responde 2xx enseguida. Cualquier otra respuesta se reintenta hasta siete veces dentro de las 24 horas siguientes.
- El secreto va en una variable de entorno, nunca en el repositorio. Los eventos llevan solo identificadores y fechas.
curl
Rutas y parámetros comprobados contra el contrato; no se han lanzado contra el servicio real, que aún no existe.
# Simula una entrega a TU receptor local (no a nuestro servidor) con una firma válida:
export ERJ_WEBHOOK_SECRET="whsec_ejemplo_no_usar_en_produccion"
CUERPO='{"id":"evt_1","tipo":"fichaje.creado","empresaId":"emp_1","creadoEn":"2026-10-12T08:00:00.000Z","datos":{}}'
TS=$(date +%s)
FIRMA="v1=$(printf '%s.%s' "$TS" "$CUERPO" | openssl dgst -sha256 -hmac "$ERJ_WEBHOOK_SECRET" -hex | sed 's/^.* //')"
curl -X POST http://localhost:8080/webhooks/erj \
-H "Content-Type: application/json" -H "ERJ-Timestamp: $TS" -H "ERJ-Signature: $FIRMA" \
-d "$CUERPO"TypeScript
Ejecutado contra un servidor local falso.
import { verificarWebhook } from './sdk/typescript/src/index.ts';
// `cuerpo` es el texto EXACTO recibido; `cabeceras` las de la petición, con los nombres en minúsculas (Node).
export function comprobarEntrega(cuerpo: string, cabeceras: Record<string, string | undefined>) {
const r = verificarWebhook(process.env.ERJ_WEBHOOK_SECRET!, { timestamp: cabeceras["erj-timestamp"], firma: cabeceras["erj-signature"] }, cuerpo);
if (!r.ok) throw new Error(`Entrega rechazada: ${r.motivo}`); // sin_cabeceras | timestamp_invalido | fuera_de_tolerancia | firma_no_valida
return JSON.parse(cuerpo); // deduplica por evento.id y responde 200 enseguida
}Python
Ejecutado contra un servidor local falso.
import json
import os
import sys
sys.path.insert(0, "sdk/python")
from erj import verificar_webhook
def comprobar_entrega(cuerpo: bytes, cabeceras: dict):
"""`cuerpo` son los bytes EXACTOS recibidos; `cabeceras` las de la petición."""
r = verificar_webhook(os.environ["ERJ_WEBHOOK_SECRET"], cabeceras.get("ERJ-Timestamp"), cabeceras.get("ERJ-Signature"), cuerpo)
if not r.ok:
raise ValueError(f"Entrega rechazada: {r.motivo}") # sin_cabeceras | timestamp_invalido | fuera_de_tolerancia | firma_no_valida
return json.loads(cuerpo) # deduplica por evento["id"] y responde 200 enseguidaC#
Escrito con la librería de C#, sin compilar ni ejecutar (no hay SDK de .NET en el equipo de desarrollo).
using ElRegistroJornada;
// En una API mínima de ASP.NET Core; `cuerpo` es el texto EXACTO recibido, sin deserializar antes.
var r = Webhooks.Verificar(Environment.GetEnvironmentVariable("ERJ_WEBHOOK_SECRET")!, Request.Headers["ERJ-Timestamp"], Request.Headers["ERJ-Signature"], cuerpo);
if (!r.Ok) return Results.BadRequest(r.Motivo); // sin_cabeceras | timestamp_invalido | fuera_de_tolerancia | firma_no_valida
// deduplica por el id del evento y responde 200 enseguidaPHP
Escrito con la librería de PHP, sin ejecutar (no hay intérprete de PHP en el equipo de desarrollo).
<?php
require 'sdk/php/src/ClienteErj.php';
use ElRegistroJornada\Webhooks;
$cuerpo = file_get_contents('php://input'); // el texto EXACTO, sin json_decode antes
$r = Webhooks::verificar(getenv('ERJ_WEBHOOK_SECRET') ?: '', $_SERVER['HTTP_ERJ_TIMESTAMP'] ?? null, $_SERVER['HTTP_ERJ_SIGNATURE'] ?? null, $cuerpo);
if (!$r->ok) { http_response_code(400); exit($r->motivo); } // sin_cabeceras | timestamp_invalido | fuera_de_tolerancia | firma_no_valida
// deduplica por el id del evento y responde 200 enseguida
http_response_code(200);Qué falta
Lo que todavía no existe
- Servicio abierto y entorno de pruebas con dominio propio.
- Alta, modificación y baja de personas por API.
- Librerías publicadas en los gestores de paquetes; la de C# y la de PHP, sin ejecutar.
- Generar informes y descargar su archivo por API con una clave real: el ámbito
informes:generarexiste, pero no se concede todavía a ninguna clave (es decisión del titular). Las librerías ya traen los métodos y se prueban contra un servidor falso.