Skip to main content
A SaaS company’s CRM needs to know who signed up, which account they belong to, what plan it is on and when they were last seen. Orbit keeps that in two places, the way Attio’s Users and Workspaces objects do, fitted to one rule: a person is a contact.
  • Users are the people who use your product. A user is the contact with the login email, created when none has it. The account in your product (its id there, when they signed up, when they were last seen, which workspaces they are in and with what role) hangs off the contact, and what your product knows about the person lands on the contact as fields in the Product group. So product usage sits next to the emails, calls, meetings and deals your team already has with that person, and it works in every filter, saved view, lead score, workflow, sequence and report that reads contact fields.
  • Workspaces are the accounts in your product (teams, organizations, tenants): their id and name, their domain, their plan, seats and MRR as custom fields, and their members. A workspace is its own object, linked to the company with its domain, because an account is not a company: one company can own several, and a gmail.com sign-up has none.
Users are not your Orbit teammates, and workspaces are not Orbit workspaces. Teammates are team_members (GET /api/v1/crm/schema); your own Orbit workspace is team in GET /api/v1/me. Everything on this page is about your product’s people and accounts.

Set up in ten minutes

1

Create an API key

Settings → API keys, preset CRM agent (it carries contacts:write, companies:write and crm:write). Users use the contacts scopes, workspaces the companies scopes, and adding workspace fields needs crm:write. An OAuth app or an MCP client with the same scopes works the same way.
2

Map your ids

Every write is matched on the id your system already has, so nothing in Orbit needs to exist first. The ids are text (numbers are stored as text, up to 255 characters).You never send Orbit ids. contact_id and company_id are optional overrides for the rare account that should attach to a specific contact or company; otherwise the email and the domain decide. There is no separate “company id” to send for a user: the user’s workspaces carry the account, and the account’s company reaches the user’s contact by itself (see how a user connects to everything else).
3

Send a user when someone signs up

PUT /api/v1/users creates or updates the user by user_id. The first write turns Users on in the app.
Orbit finds the contact with that email (as its primary or another address) or creates one named Ada Lovelace, attaches the account to it, writes seat_type on the contact, adds the account to workspace w_301 (creating a bare workspace with that id if it has never been seen) and fills the contact’s Product fields. The answer carries the account, its workspaces and its contact, with created: true and a 201 the first time.
4

Send a workspace when an account is created or its plan changes

Define the account fields once, under Settings → Users & workspaces or with the API; a workspace write that names a field you haven’t defined is refused with 400, so a typo never creates a field by accident.
Define Seats and MRR the same way with "field_type": "number" (field keys are the lowercased labels: plan, seats, mrr). Then PUT /api/v1/workspaces by workspace_id:
Orbit links the company with acme.com, creating it when there is none (a setting), and fills the workspace. Send users on a workspace, or workspaces on a user, whichever side your code finds easier: they are the same links, and a sent list replaces the stored memberships.
5

Backfill, and keep last seen fresh, in batches

POST /api/v1/users/import and POST /api/v1/workspaces/import take up to 1,000 rows per call, each row shaped like a PUT body, matched the same way. Use them for the initial load and for anything that changes often. In particular, don’t call PUT on every sign-in: a key may make 300 writes a minute, and sign-ins exceed that quickly. Send sign-ups live and last_seen_at hourly or nightly from your database or warehouse, one import call per thousand users. Re-running the same file is safe; imports count against a budget of 100,000 rows an hour.
The answer lists created, updated, skipped, the rows refused and why, and the Orbit id of every row. Every write is keyed by your id, so a replay never duplicates a record; but workspaces and users replace the stored memberships with what the write says, so send a record’s writes in order (don’t replay an old snapshot after a newer one), or leave memberships to the one job that holds the truth. If you already send Segment-style identify and group calls, map identify to a user (userId → user_id, traits.email → email, the other traits to custom_fields) and group to a workspace (groupId → workspace_id, traits.website or traits.domain → domain, traits.name → name), with the user’s workspaces carrying the groupId. That is the mapping Attio’s Segment destination uses.
6

See it in Orbit

Contacts gains a shared Product users view (everyone with a product user id, most recently seen first) and the five Product fields as columns and filters. A contact page shows In your product: the account, its dates, its workspaces and roles. Workspaces appears under More tools with a page per account, and a company page shows Workspaces in your product. GET /api/v1/users?user_id=u_8812 answers the account with its contact, for a look-up from your side.

What lands where

On the contact

The five Product fields are contact custom fields that Orbit defines on first sync and keeps up to date from the contact’s accounts. They are read-only in spirit: a value typed by hand is overwritten by the next sync. Because they are contact fields, “users who haven’t signed in for 30 days” is a Contacts filter, a saved view, a lead scoring rule, a Field changed workflow trigger, a sequence audience or an export, with no extra wiring. Anything else your product knows about a person (seat type, usage counts, sign-up source, first and last UTM) goes in custom_fields on the user write and lands on the contact too, so it gets the same treatment. Define those fields with POST /api/v1/crm/fields (object: "contact") or send them and they are kept as text.

On the account

GET /api/v1/users/{id} and the lists carry the account itself: user_id, email, name, avatar_url, contact_id, signed_up_at, last_seen_at, its workspaces with roles, and the contact it belongs to. A contact can hold more than one account (one person, two logins); a merge of two contacts pulls both onto the kept contact.

On the workspace

Built-ins: workspace_id, name, domain, company_id, avatar_url, signed_up_at, last_seen_at, its users with roles. Custom fields are the workspace’s own, defined under Settings → Users & workspaces or with POST /api/v1/workspaces/fields in any of the custom field types; a write that names an undefined field is refused with 400. Plan, subscription dates, billing status, MRR / ARR, seats and usage belong here: they describe the account, not a person.

How a user connects to everything else

Because a user is a contact, it is connected to every object a contact is, and the sync makes the links for you. Each row is a link, how it is made, and how you use it from the API. Two examples. Log a call that happened in your support tool for a product user, naming them by your id:
Open an expansion deal for the account, on the contact the account belongs to and its company:
The ids are the contact_id and the workspace’s company_id from the user and workspace reads. In the app, a contact page shows In your product (the account, its dates and its workspaces with roles) and a company page shows Workspaces in your product and, in its People panel, the users of its workspaces. Merging two companies moves the merged company’s workspaces to the survivor; merging two contacts moves the accounts. What a product does not get today: a direct workspace-to-deal link (use the company), and notes or tasks on a workspace itself (put them on the company or the contact).

Endpoints

Users use the contacts:* scopes and workspaces the companies:* scopes, so an existing CRM key or app works. Workspace field endpoints need crm:read / crm:write. Deleting an account needs contacts:delete and a workspace companies:delete; both are for good, since the product can send the record again, and the contact and company stay. A membership is "w_301" or { "workspace_id": "w_301", "role": "admin", "joined_at": "2026-03-14T09:30:00Z" } on a user (user_id on a workspace). signed_up_at and last_seen_at take ISO 8601 or Unix milliseconds; the ids are text of up to 255 characters, numbers stored as text. Lists use page / per_page (up to 100) and answer { data, meta }; updated_since lists the records changed at or after an instant, oldest first, for a catch-up sync. Import rows count against the workspace’s hourly import budget. The schema (GET /api/v1/crm/schema) lists objects.user (with contact_fields) and objects.workspace (with its custom_fields). A user reads as:

Webhooks

Subscribe to user.created, user.updated, user.deleted (with contacts:read) for the account itself, and workspace.created, workspace.updated, workspace.user_added, workspace.user_removed, workspace.deleted (with companies:read). Because the product fields live on the contact, contact.updated also fires when a contact’s last seen moves or they join a workspace, with custom_fields in changes, exactly as it does for a dialer call. See Webhook events.

MCP

AI agents get the same through five tools: list_product_users, upsert_product_user, list_product_workspaces, upsert_product_workspace and create_workspace_field, with the settings in update_crm_settings (object: "product"). Because a user is a contact, get_contact and list_contacts already show an agent the product fields. See Tools.

How this compares

What is not here yet: product events (an activity feed of what each user did), workflow triggers on workspace changes, and rollups from workspaces onto companies (plan and MRR on the company). Webhooks cover the second today; the third is next.