Idempotencia y external_id
Las redes fallan: un timeout no te dice si la escritura se aplicó. Esta
página explica cómo reintentar sin crear duplicados, con external_id como pieza
central.
external_id: el id de tu ERP en xYago
- Es tu identificador del empleado (código de empleado en Siesa, número de ficha...).
Formato: 1 a 128 caracteres
A-Z a-z 0-9 . _ : @ / -. - Es único por empresa: dos empleados no pueden compartirlo y un empleado solo tiene uno.
- Se asigna en el alta (
POST /employees) o después conPATCH /employees/{id}, y es la clave del upsert de la importación. - Búscalo con
GET /employees?external_id=SIESA-000123: devuelve 0 o 1 resultado.
Guarda también el id de xYago en tu ERP cuando lo recibas; así evitas una
búsqueda en cada edición.
Altas de empleados sin duplicados
Envía siempre external_id en el alta. xYago lo reserva antes
de crear el empleado, así que dos peticiones simultáneas con el mismo
external_id no crean dos empleados:
Respuesta a un reintento de POST /employees | Qué significa / qué hacer |
|---|---|
201 | Creado ahora. |
409 external_id_already_exists | Ya se había creado (p. ej. el primer intento sí llegó). Obtén el empleado con GET /employees?external_id=... y trátalo como éxito. |
409 external_id_in_progress | Otra petición lo está creando ahora mismo. Espera unos segundos y consulta por external_id. |
502 / timeout | Estado desconocido: consulta por external_id; si no existe, reintenta el alta. |
Python
def upsert_employee(session, employee):
"""Crea o actualiza en xYago un empleado del ERP (dict con external_id)."""
base = "https://api.xyago.com/v1/employees"
found = session.get(base, params={"external_id": employee["external_id"]}, timeout=30)
found.raise_for_status()
results = found.json()["results"]
if results:
changes = {k: v for k, v in employee.items() if k not in ("work_shifts", "vacation_days")}
response = session.patch(f"{base}/{results[0]['id']}", json=changes, timeout=30)
else:
response = session.post(base, json=employee, timeout=30)
if response.status_code == 409 and response.json()["error"]["code"] == "external_id_already_exists":
return upsert_employee(session, employee) # lo creó un intento anterior
response.raise_for_status()
return response.json()
C#
// Pseudocódigo con HttpClient + System.Net.Http.Json
var found = await http.GetFromJsonAsync<JsonElement>(
$"employees?external_id={Uri.EscapeDataString(employee.ExternalId)}");
var results = found.GetProperty("results");
HttpResponseMessage response = results.GetArrayLength() > 0
? await http.PatchAsJsonAsync($"employees/{results[0].GetProperty("id")}", employeeChanges)
: await http.PostAsJsonAsync("employees", newEmployee);
// 409 external_id_already_exists en el POST => ya existe: repite la búsqueda y haz PATCH.
Resto de escrituras
| Operación | Comportamiento al repetirla |
|---|---|
PATCH /employees/{id} | Idempotente: aplicar los mismos campos dos veces deja el mismo resultado. |
POST /employees/{id}/deactivate | Idempotente: si ya está inactivo responde 200 sin cambios ni webhook. |
POST /employees/import | Idempotente por external_id: reenviar el mismo fichero actualiza en lugar de duplicar; si el job idéntico sigue en cola o en proceso se devuelve el mismo job_id. |
POST /areas | El nombre es único bajo el mismo padre: un reintento responde 409 area_already_exists; búscala con GET /areas. |
POST /time-entries | Un tramo que se solapa con otro del empleado responde 409 time_entry_overlap: si es el mismo tramo, el primer intento se aplicó. |
PATCH /vacations/{id}/approve|reject | Un reintento responde 409 vacation_already_decided; comprueba el status con GET /vacations. |
POST /vacations | No es idempotente: un reintento puede crear una segunda solicitud pendiente. Antes de reintentar, busca con GET /vacations?employee_id=...&status=pending una solicitud con las mismas fechas. |
Las APIs de recursos humanos no usan la cabecera
Idempotency-Key: usa external_id y las comprobaciones de esta
tabla. POST /messages sí la exige (ver Mensajes).