xYago APIv1

Idempotency

Networks fail: a timeout does not tell you whether a write was applied. This page explains how to retry without creating duplicates, with external_id at the core.

external_id: your ERP id in xYago

Store the xYago id in your ERP as well when you receive it, to avoid a lookup on every update.

Creating employees without duplicates

Always send external_id on create. xYago reserves it before creating the employee, so two concurrent requests with the same external_id never create two employees:

Response to a retried POST /employeesMeaning / what to do
201Created now.
409 external_id_already_existsIt already exists (e.g. the first attempt did arrive). Fetch it with GET /employees?external_id=... and treat it as success.
409 external_id_in_progressAnother request is creating it right now. Wait a few seconds and look it up by external_id.
502 / timeoutUnknown outcome: look it up by external_id; if it doesn't exist, retry the create.

Python

def upsert_employee(session, employee):
    """Create or update in xYago an ERP employee (dict with 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)  # a previous attempt created it
    response.raise_for_status()
    return response.json()

C#

// Sketch with 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 on POST => it exists: look it up again and PATCH.

Other writes

OperationBehaviour when repeated
PATCH /employees/{id}Idempotent: applying the same fields twice gives the same result.
POST /employees/{id}/deactivateIdempotent: if already inactive it returns 200 with no change and no webhook.
POST /employees/importIdempotent by external_id: resending the same file updates instead of duplicating; if the identical job is still queued or running you get the same job_id.
POST /areasThe name is unique under the same parent: a retry returns 409 area_already_exists; find it with GET /areas.
POST /time-entriesAn entry overlapping another entry of the employee returns 409 time_entry_overlap: if it is the same entry, the first attempt was applied.
PATCH /vacations/{id}/approve|rejectA retry returns 409 vacation_already_decided; check the status with GET /vacations.
POST /vacationsNot idempotent: a retry may create a second pending request. Before retrying, look for a request with the same dates via GET /vacations?employee_id=...&status=pending.

The HR APIs do not use the Idempotency-Key header: use external_id and the checks above. POST /messages does require it (see Messages).