Batch Upsert
Create or update up to 500 operators in a single operation, with per-item result reporting.
POST /api/integration/operators/batch-upsert
Creates or updates (upsert) up to 500 operators in a single operation. It is the integration entry point for loading the operator roster (salesmen, activators, call center agents) from an external system.
Auth: Required — operator:batch-upsert permission
Partial success. Once the body passes format validation, each item is
processed independently: the ones that can be persisted are, and the ones that
fail are reported in errors[] with a stable code — a failing item does not
undo the others. Always check summary.failed and errors in the response.
Upsert key
Each item is matched against existing operators by the pair type + external_id. If an operator with that pair exists it is updated (counted in summary.updated); otherwise it is created (summary.created). The same external_id under a different type is a different operator. On update, metadata is merged with the existing one: keys sent overwrite keys with the same name, keys not sent are kept, and sending a key with null removes it. The merge is recursive (RFC 7386 JSON Merge Patch): nested objects are merged level by level, so you can update one key inside a nested object without resending its siblings. Any other value replaces the previous one wholesale — arrays included, which are never merged element by element. Sending metadata: null empties the whole bag.
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
items | object[] | Yes | List of operators to write (minimum 1, maximum 500). |
Each element of items:
| Field | Type | Required | Description |
|---|---|---|---|
type | enum | Yes | Operator type: SALESMAN, ACTIVATOR or CALLCENTER. Part of the upsert key. |
external_id | string | Yes | Identifier of the operator in the external system. Part of the upsert key. |
name | string | Yes | Operator name. |
email | string | No | Email address. Must be a valid email and must not be in use by another operator. |
phone | string | No | Phone number. |
metadata | object | No | Arbitrary JSON. Merged recursively with the existing metadata. |
Example
curl -X POST "https://api.reten.ai/api/integration/operators/batch-upsert" \
-H "x-api-key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"items": [
{
"type": "SALESMAN",
"external_id": "OP-001",
"name": "Juan Pérez",
"email": "juan.perez@example.com",
"phone": "+56912345678",
"metadata": { "zone": "north" }
},
{
"type": "ACTIVATOR",
"external_id": "OP-002",
"name": "Ana López"
},
{
"type": "CALLCENTER",
"external_id": "OP-003",
"name": "Carla Soto",
"email": "juan.perez@example.com"
}
]
}'const response = await fetch(
"https://api.reten.ai/api/integration/operators/batch-upsert",
{
method: "POST",
headers: {
"x-api-key": "YOUR_API_KEY",
"Content-Type": "application/json",
},
body: JSON.stringify({
items: [
{ type: "SALESMAN", external_id: "OP-001", name: "Juan Pérez", email: "juan.perez@example.com", phone: "+56912345678", metadata: { zone: "north" } },
{ type: "ACTIVATOR", external_id: "OP-002", name: "Ana López" },
{ type: "CALLCENTER", external_id: "OP-003", name: "Carla Soto", email: "juan.perez@example.com" },
],
}),
},
);
const result = await response.json();import requests
response = requests.post(
"https://api.reten.ai/api/integration/operators/batch-upsert",
headers={"x-api-key": "YOUR_API_KEY"},
json={
"items": [
{"type": "SALESMAN", "external_id": "OP-001", "name": "Juan Pérez", "email": "juan.perez@example.com", "phone": "+56912345678", "metadata": {"zone": "north"}},
{"type": "ACTIVATOR", "external_id": "OP-002", "name": "Ana López"},
{"type": "CALLCENTER", "external_id": "OP-003", "name": "Carla Soto", "email": "juan.perez@example.com"},
],
},
)
result = response.json()Response 200 OK
In this example, OP-001 already existed and was updated, OP-002 was created, and OP-003 failed because its email is already used by another operator:
{
"summary": {
"total": 3,
"created": 1,
"updated": 1,
"failed": 1
},
"errors": [
{
"index": 2,
"identifier": "OP-003",
"error": "upsert_error",
"message": "Email juan.perez@example.com already exists in operator 9c4b2e7a-1d5f-4a8b-b3c6-7e8f9a0b1c2d"
}
]
}Response fields
| Field | Type | Description |
|---|---|---|
summary.total | number | Total items submitted. |
summary.created | number | Items inserted (new operators). |
summary.updated | number | Items updated (existing operators modified). |
summary.failed | number | Items that could not be persisted. |
errors | object[] | Detail per failed item (empty if all succeeded). |
errors[].index | number | Position (0-based) of the item within the submitted items array. |
errors[].identifier | string | The external_id of the failed item. |
errors[].error | string | Stable error code (see table below). |
errors[].message | string | Human/log-readable message with the cause. |
Per-item error codes
error | Meaning |
|---|---|
upsert_error | The item could not be persisted; message carries the cause (e.g. an email already used by another operator). |
Errors (for the whole request)
| Status | Description |
|---|---|
400 | items empty or with more than 500 elements; an item that does not match the format (missing required field, invalid type or enum value); or a property not declared here. |
401 | Missing or invalid API key. |
403 | The API key lacks the operator:batch-upsert permission. |