Skip to main content
A two-way sync lets a workspace map fields between Orbit and your app (“Orbit field” on one side, “your field” on the other), choose which way each one flows, and then forget about it: a change on either side shows up on the other within about a minute. The mapping screen and the sync logic live in your app. Orbit gives you the pieces: the workspace’s fields, the records, change events, a way to tell your own writes apart, and a feed to catch up on anything you missed.

Scopes

Adding scopes to an approved app is a change request; workspaces that installed the app before grant the new scopes when they reconnect.

1. Show the workspace’s fields

GET /api/v1/crm/schema returns every standard and custom field per object with its type and options (a select’s choices, a teammate field’s members). That is the Orbit column of your mapping table. Read it when the mapping screen opens, and again before a sync run, rather than caching it for good: workspaces rename, add and delete fields. On companies and deals a write that names a field that no longer exists answers 400; on contacts the key is kept as a plain value with no field behind it, so check the schema rather than relying on an error. Flag that mapping row instead of retrying. Your app can also create the fields it needs (POST /api/v1/crm/fields, crm:write), say a “Last ticket status” field filled from your side. Match records once, then keep both ids:
  • Contacts by email: GET /api/v1/contacts?email=ada@example.com (exact, any case).
  • Companies by domain: GET /api/v1/companies?domain=acme.com.
  • Then store your record’s id on the Orbit record: PATCH /api/v1/contacts/:id with { "external_source": "your_app", "external_id": "u_8812" } (the same on companies and deals). It lands in external_ids next to any other system’s id, comes back on every read and every webhook, and finds the record later: GET /api/v1/contacts?external_source=your_app&external_id=u_8812.
Keep your own map from your ids to Orbit ids as well; external_ids is the fallback when your map and Orbit disagree.

3. Hear about changes

Subscribe the workspace once, after it connects:
contact.updated and the other *.updated events list what changed in data.changes as { field: { from, to } }. Custom fields arrive as one entry, custom_fields, with the whole object before and after: compare the keys your mapping uses. Deliveries are signed (check X-Orbit-Signature), at least once (de-duplicate on the envelope’s id) and not ordered (use occurred_at). See Webhooks and Events.

4. Skip your own writes

Every change your app makes through the API raises the same events as anyone else’s, and you receive them. They carry your app’s id in actor.app_id:
Without this check, two systems that store a value slightly differently ("5" and 5, a phone number with and without formatting) overwrite each other for ever. Orbit also raises no event for a write that changes nothing, so an echo that slips through stops after one round.

5. Write to Orbit

  • Send only the mapped fields that changed. custom_fields in a PATCH merges into what is stored; null removes a key.
  • Orbit normalizes some values: emails are lowercased, phones are stored in E.164 for the workspace’s country when they parse, values are converted to their field’s type (a number field stores 5 for "5"), and a select value must be one of the field’s options. Keep what the response returns as your copy of the Orbit side, so the next comparison is against what Orbit actually holds.
  • A 404 means the record was deleted in Orbit (or is in Recently deleted): unlink it, or wait for contact.restored.

6. Catch up

Webhook deliveries give up after their retries, and your endpoint will be down sometimes. Read the changes feed every so often, say every 15 minutes, and apply whatever you missed:
Start each run from when the previous run started, minus a few minutes, and treat records you already applied as no-ops. The feed lists records changed by anyone, your app included: compare against your copy rather than writing back what you already have.

7. The first sync

Moving a workspace’s existing records in one go is an import: POST /api/v1/contacts/import (and /companies/import, /deals/import) takes 1,000 rows a call, with external_source and an external_id per row so a re-run updates instead of duplicating. Imported records are not sent to webhooks unless the workspace turned that on, so your first sync does not come back to you as thousands of events. Use the imports rather than single creates for this: single creates are capped per hour (below).

Direction and conflicts

These are your app’s settings, per mapping row; Orbit applies whatever you write. For the first sync, when both sides already hold a value, ask the workspace which side to keep. “Latest change wins” needs a time per field: Orbit keeps updated_at per record, not per field, so record the occurred_at of each change event per mapped field and compare those.

Limits

A 429 carries Retry-After. Read limits and the rest are on Rate limits.