xYago APIv1

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

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 /employeesQué significa / qué hacer
201Creado ahora.
409 external_id_already_existsYa 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_progressOtra petición lo está creando ahora mismo. Espera unos segundos y consulta por external_id.
502 / timeoutEstado 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ónComportamiento al repetirla
PATCH /employees/{id}Idempotente: aplicar los mismos campos dos veces deja el mismo resultado.
POST /employees/{id}/deactivateIdempotente: si ya está inactivo responde 200 sin cambios ni webhook.
POST /employees/importIdempotente 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 /areasEl nombre es único bajo el mismo padre: un reintento responde 409 area_already_exists; búscala con GET /areas.
POST /time-entriesUn 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|rejectUn reintento responde 409 vacation_already_decided; comprueba el status con GET /vacations.
POST /vacationsNo 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).