> ## Documentation Index
> Fetch the complete documentation index at: https://docs.chatsailer.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Sincronización

> Mantén otro sistema al día con un workspace de Sailer: cambios, eliminaciones, tus propios ids y escrituras masivas.

Un sync bidireccional necesita cuatro cosas de una API: leer solo lo que
cambió, saber qué se eliminó, una clave que tú controlas y una forma de escribir
muchos registros a la vez. Sailer tiene las cuatro para contactos,
organizaciones y negocios.

## Lee lo que cambió

Lista con `updated_since` en el momento en que **empezó** tu ejecución anterior,
y `sort=updated_at`:

```bash theme={null}
curl "https://api.chatsailer.com/v1/contacts?updated_since=2026-09-30T00:00:00Z&sort=updated_at&limit=200" \
  -H "Authorization: Bearer $SAILER_API_TOKEN"
```

`updated_at` cambia cada vez que algo del registro cambia — un campo estándar,
un campo personalizado o sus etiquetas. La hora debe llevar zona horaria;
`2026-09-30T00:00:00` sin zona devuelve `400`.

Sigue el cursor hasta el final:

```python theme={null}
params = {"updated_since": last_run_started_at, "sort": "updated_at", "limit": 200}
while True:
    page = httpx.get(f"{BASE}/v1/contacts", params=params, headers=headers).json()
    for contact in page["data"]:
        upsert_locally(contact)
    if page["links"]["next"] is None:
        break
    params["cursor"] = page["links"]["next"]
```

El cursor queda anclado en el último registro que recibiste, así que un registro
creado o editado mientras paginas nunca aparece dos veces ni se salta. Un
registro editado a mitad del recorrido simplemente vuelve a aparecer más
adelante en el mismo recorrido, con su `updated_at` nuevo.

Filtra un listado con parámetros de query — repite uno para hacer O entre sus
valores, combina distintos para hacer Y:

```bash theme={null}
# Contactos ganados o perdidos de una organización
curl "https://api.chatsailer.com/v1/contacts?status=won&status=lost&organization_id=org_..."
```

Para condiciones anidadas, rangos o filtros entre relaciones, usa
`POST /v1/contacts/search`, que devuelve los mismos objetos.

## Lee lo que se eliminó

Las eliminaciones tienen su propio feed, de la más antigua a la más reciente:

```bash theme={null}
curl "https://api.chatsailer.com/v1/deleted-records?resource=contact&since=2026-09-30T00:00:00Z" \
  -H "Authorization: Bearer $SAILER_API_TOKEN"
```

```json theme={null}
{
  "data": [
    {
      "object": "deleted_record",
      "resource": "contact",
      "id": "con_9f2ac41d8b7e4a51b0d3e6f7a1c2d3e4",
      "external_id": "crm-48213",
      "deleted_at": "2026-09-30T14:02:11Z"
    }
  ],
  "meta": { "limit": 50, "has_more": false },
  "links": { "next": null }
}
```

Lista registros eliminados por esta API, en la app de Sailer o por un CRM
conectado. Requiere el scope de lectura del recurso, por ejemplo
`contacts:read`.

`DELETE /v1/contacts/{id}` (y lo mismo en organizaciones y negocios) saca un
registro de todos los listados y búsquedas **sin borrar nada**: sus
conversaciones e historial se conservan. Hacer upsert del mismo contacto, o un
mensaje nuevo suyo, lo restaura, y vuelve a aparecer en tu próxima lectura con
`updated_since`.

## Usa tus propios ids

Define `external_id` con el id de tu sistema al crear o actualizar un registro.
Es único por workspace y por recurso, vuelve en cada lectura y se puede usar
como filtro:

```bash theme={null}
curl "https://api.chatsailer.com/v1/contacts?external_id=crm-48213"
```

`POST /v1/contacts/upsert` busca primero por `external_id` y después por
`phone`:

| Lo que envías | Lo que pasa |
| - | - |
| Un `external_id` que ningún contacto tiene, un teléfono nuevo | Se crea un contacto con tu `external_id` (`201`) |
| Un `external_id` que un contacto tiene, el mismo teléfono | Ese contacto se actualiza (`200`) |
| Un teléfono de un contacto que aún no tiene `external_id` | Ese contacto se actualiza y recibe tu `external_id` (`200`) |
| Un `external_id` de un contacto y un teléfono de otro | `409 external_id_conflict`: las dos claves no coinciden |
| Un `external_id` de un contacto con otro teléfono | `409 phone_mismatch`: el upsert nunca cambia un teléfono |

## Escribe en masa

`POST /v1/contacts/batch` hace upsert de hasta 100 contactos en una llamada.
Cada ítem se escribe exactamente como en `POST /v1/contacts/upsert`, en orden, y
recibe su propio resultado — un ítem que falla nunca deshace los demás:

```json theme={null}
{
  "object": "batch_result",
  "data": [
    { "index": 0, "status": "created", "contact": { "object": "contact", "id": "con_..." } },
    { "index": 1, "status": "updated", "contact": { "object": "contact", "id": "con_..." } },
    {
      "index": 2,
      "status": "failed",
      "error": { "type": "invalid_request_error", "code": "validation_failed", "message": "..." }
    }
  ],
  "created": 1,
  "updated": 1,
  "failed": 1
}
```

La llamada responde `200` siempre que el lote en sí sea válido; lee el `status`
de cada ítem. Envía un header `Idempotency-Key` para que un lote reenviado no se
aplique dos veces.

## Todo junto

1. Anota la hora y lee los cambios con `updated_since` en el inicio de la
   ejecución anterior.
2. Lee `GET /v1/deleted-records` con `since` en la misma hora y elimina esos
   registros de tu lado.
3. Envía tus cambios con `POST /v1/contacts/batch`, por la clave `external_id`,
   con un `Idempotency-Key`.
4. Guarda la hora del paso 1 como el inicio de esta ejecución.

Vigila `X-RateLimit-Remaining` en cada respuesta y baja el ritmo antes de llegar
a cero.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.