Skip to main content
A two-way sync needs four things from an API: a way to read only what changed, a way to learn what was deleted, a key you control, and a way to write many records at once. Sailer has all four for contacts, organizations and deals.

Read what changed

List with updated_since set to the moment your previous run started, and sort=updated_at:
updated_at moves whenever anything about the record changes — a built-in field, a custom field, or its tags. Timestamps must carry a timezone offset; 2026-09-30T00:00:00 without one is a 400. Follow the cursor to the end:
Cursors are anchored on the last record you received, so a record created or edited while you are paging is never served twice and never skipped. A record edited mid-walk simply reappears later in the same walk, with its new updated_at. Narrow a list with query parameters — repeat one to OR its values, combine different ones to AND them:
For nested conditions, ranges or filters across relationships, use POST /v1/contacts/search, which returns the same objects.

Read what was deleted

Deletions are a feed of their own, oldest first:
It lists records deleted through this API, in the Sailer app, or by a connected CRM. It needs the resource’s read scope, e.g. contacts:read. DELETE /v1/contacts/{id} (and the same on organizations and deals) removes a record from every list and lookup without erasing anything: its conversations and history are kept. Upserting the same contact again, or a new message from them, restores it, and it reappears in your next updated_since read.

Use your own ids

Set external_id to your system’s id when you create or update a record. It is unique per workspace and resource, comes back on every read, and can be used as a filter:
POST /v1/contacts/upsert matches on external_id first, then on phone:

Write in bulk

POST /v1/contacts/batch upserts up to 100 contacts in one call. Each item is written exactly like POST /v1/contacts/upsert, in order, and gets its own result — one failing item never undoes the others:
The call answers 200 whenever the batch itself was valid; read each item’s status. Send an Idempotency-Key header so a retried batch is not applied twice.

Putting it together

  1. Record the time, then read changes with updated_since set to your previous run’s start time.
  2. Read GET /v1/deleted-records with since set to the same time, and remove those records on your side.
  3. Push your own changes with POST /v1/contacts/batch, keyed by external_id, with an Idempotency-Key.
  4. Store the time from step 1 as the start of this run.
Watch X-RateLimit-Remaining on every response and slow down before you reach zero.