xYago APIv1

Employee import

Load or sync up to 500 employees per request. xYago processes the file in the background and reports the result row by row. Ideal for the initial load from the ERP and for nightly syncs.

Flow

  1. Enqueue the import: POST /employees/import with {"rows": [...]} (1 to 500 rows). Response 202 with job_id.
  2. Poll GET /employees/import/{job_id} every 2-5 seconds until status is completed or failed.
  3. Check results: each row says created, updated or failed with its error. Fix failed rows in the ERP and resend them.

Each row

A row has the shape of the POST /employees body (see the reference) and external_id is required: it is the upsert key.

Example

curl

curl -X POST "https://api.xyago.com/v1/employees/import" \
  -H "Authorization: Bearer $XYAGO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "rows": [
      {
        "external_id": "SIESA-000123",
        "first_name": "Valentina",
        "last_name": "Rojas Castaño",
        "phone_number": "+573005550123",
        "email": "valentina.rojas@example.com",
        "document_number": "1020304050",
        "job_title": "Auxiliar de bodega",
        "contract_type": "Full-time fixed-term",
        "hire_date": "2026-02-01",
        "contract_end_date": "2027-01-31",
        "work_shifts": [{"day": "Monday", "start": "07:00", "end": "16:00"}]
      },
      {"external_id": "SIESA-000087", "job_title": "Conductor de reparto"}
    ]
  }'
# => 202 {"job_id": "0b8c6d4e-2f1a-4c3b-9e5d-7a6b8c9d0e1f", "status": "queued"}

curl "https://api.xyago.com/v1/employees/import/0b8c6d4e-2f1a-4c3b-9e5d-7a6b8c9d0e1f" \
  -H "Authorization: Bearer $XYAGO_API_KEY"

Python

import os
import time

import requests

BASE_URL = "https://api.xyago.com/v1"
session = requests.Session()
session.headers["Authorization"] = f"Bearer {os.environ['XYAGO_API_KEY']}"


def import_employees(rows):
    response = session.post(f"{BASE_URL}/employees/import", json={"rows": rows}, timeout=60)
    response.raise_for_status()
    job_id = response.json()["job_id"]
    while True:
        job = session.get(f"{BASE_URL}/employees/import/{job_id}", timeout=30).json()
        if job["status"] in ("completed", "failed"):
            return job
        time.sleep(3)


job = import_employees(rows_from_erp)  # list of dicts, max. 500 per call
for row in job["results"]:
    if row["status"] == "failed":
        print(row["external_id"], row["error_code"], row["error_message"])

C#

var accepted = await http.PostAsJsonAsync("employees/import", new { rows = rowsFromErp });
accepted.EnsureSuccessStatusCode();
var jobId = (await accepted.Content.ReadFromJsonAsync<JsonElement>()).GetProperty("job_id").GetString();

JsonElement job;
do
{
    await Task.Delay(TimeSpan.FromSeconds(3));
    job = await http.GetFromJsonAsync<JsonElement>($"employees/import/{jobId}");
} while (job.GetProperty("status").GetString() is "queued" or "processing");

foreach (var row in job.GetProperty("results").EnumerateArray())
    if (row.GetProperty("status").GetString() == "failed")
        Console.WriteLine($"{row.GetProperty("external_id")}: {row.GetProperty("error_code")}");

Result

{
  "job_id": "0b8c6d4e-2f1a-4c3b-9e5d-7a6b8c9d0e1f",
  "status": "completed",
  "total_rows": 3,
  "processed_rows": 3,
  "created": 1,
  "updated": 1,
  "failed": 1,
  "created_at": "2026-03-02T15:00:00Z",
  "started_at": "2026-03-02T15:00:01Z",
  "finished_at": "2026-03-02T15:00:04Z",
  "results": [
    {
      "row_index": 0,
      "external_id": "SIESA-000123",
      "status": "created",
      "employee_id": "2c9a4e71-5b3d-4f8e-a6c2-7d1e0f9b3a54",
      "error_code": null,
      "error_message": null
    },
    {
      "row_index": 1,
      "external_id": "SIESA-000087",
      "status": "updated",
      "employee_id": "5e7b1d93-2a4c-4b6e-8f0a-3c5d7e9f1b26",
      "error_code": null,
      "error_message": null
    },
    {
      "row_index": 2,
      "external_id": "SIESA-000150",
      "status": "failed",
      "employee_id": null,
      "error_code": "validation_error",
      "error_message": "phone_number: Field required; job_title: Field required"
    }
  ]
}
statusqueued → processing → completed. failed means the whole job could not be processed (not that some row failed): resend it.
total_rows, processed_rowsProgress.
created, updated, failedCounters by outcome.
results[]One entry per row: row_index (position in rows, from 0), external_id, status, employee_id and, if it failed, error_code and error_message.

Row errors

Besides any POST/PATCH /employees error (employee_already_exists, email_already_registered, invalid_employee_data, upstream_error... see errors):

invalid_rowThe row is not a JSON object.
invalid_external_idexternal_id is missing or malformed.
duplicate_external_idThe external_id appears more than once in the same import.
validation_errorThe row does not match the POST /employees (or PATCH) schema.

Useful rules