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.
2. Link records
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/:idwith{ "external_source": "your_app", "external_id": "u_8812" }(the same on companies and deals). It lands inexternal_idsnext 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.
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 inactor.app_id:
"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_fieldsin aPATCHmerges into what is stored;nullremoves 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
5for"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
404means the record was deleted in Orbit (or is in Recently deleted): unlink it, or wait forcontact.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: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.