Importación de empleados
Carga o sincroniza hasta 500 empleados por petición. xYago procesa el fichero en segundo plano y te devuelve el resultado fila a fila. Ideal para la carga inicial desde el ERP y para sincronizaciones nocturnas.
Flujo
- Encola la importación:
POST /employees/importcon{"rows": [...]}(1 a 500 filas). Respuesta202conjob_id. - Consulta el estado con
GET /employees/import/{job_id}cada 2-5 segundos hasta questatusseacompletedofailed. - Revisa
results: cada fila indicacreated,updatedofailedcon su error. Corrige las fallidas en el ERP y reenvíalas.
Cada fila
Una fila tiene la forma del cuerpo de POST /employees (ver la
referencia) y external_id es
obligatorio: es la clave del upsert.
- external_id desconocido → alta. La fila necesita todos los campos
obligatorios del alta:
first_name,phone_number,job_title,contract_type,hire_date,work_shiftsycontract_end_datesalvo contratos indefinidos. - external_id ya asociado → edición parcial: solo se cambian los campos
enviados;
work_shiftsyvacation_daysse ignoran. - Una fila con error no detiene el resto.
Ejemplo
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) # lista de dicts, máx. 500 por llamada
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")}");
Resultado
{
"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"
}
]
}
status | queued → processing → completed. failed indica que el job entero no se pudo procesar (no que alguna fila falle): reenvíalo. |
total_rows, processed_rows | Progreso. |
created, updated, failed | Contadores por resultado. |
results[] | Una entrada por fila: row_index (posición en rows, desde 0), external_id, status, employee_id y, si falló, error_code y error_message. |
Errores por fila
Además de cualquier error de POST/PATCH /employees
(employee_already_exists, email_already_registered,
invalid_employee_data, upstream_error... ver
errores):
invalid_row | La fila no es un objeto JSON. |
invalid_external_id | Falta external_id o no tiene un formato válido. |
duplicate_external_id | El external_id aparece más de una vez en la misma importación. |
validation_error | La fila no cumple el esquema de POST /employees (o de PATCH). |
Reglas útiles
- Reenviar es seguro: el mismo fichero actualiza en lugar de duplicar. Si el
job idéntico sigue en cola o en proceso, recibes el mismo
job_id. - Más de 500 empleados: divide en lotes y envíalos uno detrás de otro (espera a que termine cada job).
- Cada fila creada o editada emite su webhook
employee.created/employee.updated. - Un job solo es visible con claves de la misma empresa y del mismo entorno (live/test);
si no,
404. - Si la cola no está disponible recibirás
503 import_queue_unavailable: reintenta más tarde.