Skip to main content
Every record in the CRM belongs to an object. Some objects ship with the product; others are created by the tenant. The API exposes both through the same two endpoints, so an integration can discover what exists and what shape it has without anything being hard-coded.
If you are building anything that has to survive a customer adding a field, start here rather than hard-coding a field list.

Objects

An object is either built-in or custom. Custom objects are always prefixed with object:. That prefix is part of the name — you pass it verbatim in the path, URL-encoded:

Listing objects

The list is filtered by your key’s scopes. An object your key cannot view is simply absent — it is not listed and marked inaccessible. This is deliberate: if inaccessible objects were listed, the registry would tell you which objects a tenant has configured, which is information a key without the matching scope is not entitled to. So GET /v1/objects answers “what can this key work with”, not “what exists”. Two keys on the same tenant can legitimately get different lists.

Schemas

The field descriptor

Types: text, textarea, number, boolean, date, datetime, email, phone, url, currency, select. That list is closed. An internal type we do not publish is reported as text rather than passed through, so a UI-side change cannot alter your contract.

Built-in and custom fields look identical

Internally this CRM has two different field systems with different column names. You will never see that. Both are mapped to the descriptor above, and the only way to tell them apart is the custom flag — which is there because you may want to treat tenant fields differently in your UI, not because they behave differently.

What a tenant can change

Two things move under you. Both are why you should read the schema rather than assume one. A built-in field can be turned off. A disabled field is absent from the schema. It is not returned as enabled: false — if it is not in fields, do not send it. A built-in field can be made mandatory. A field that is optional by default comes back with "required": true for that tenant. Two tenants can therefore return different required values for the same field, and neither is wrong. Custom fields can be added and removed at any time.

Name collisions

If a tenant creates a custom field whose name matches a built-in, the built-in wins and the custom one is not published. Otherwise a custom email of type text could shadow the real email field and the published type would stop matching what writes actually accept.

Errors

The 404 is deliberately ambiguous. Distinguishing “no such object” from “not allowed” would let a key probe for which objects a tenant has configured, which is the same reason record fetches return 404 instead of 403. If you get a 404 for an object you expect, check your key’s scopes before assuming the object is missing.

Suggested integration flow

  1. Call GET /v1/objects once at setup, and cache it.
  2. Call GET /v1/objects/{object}/schema for each object you write to.
  3. Build your field mapping from name, validating against type, required and options.
  4. Re-fetch the schema periodically, or when a write fails validation — a tenant may have added a mandatory field since you last looked.
Do not hard-code field lists. A tenant adding one mandatory custom field is enough to start failing every write from an integration that assumes a fixed shape.

Managing fields (provisioning)

Requires the settings:configure scope — a key without it gets 403. This writes the tenant’s real schema, so a field created here appears on the CRM’s forms immediately.
  • name is normalised to lowercase with underscores. Reserved names (id, created_at, company_id, …) and names that collide with an existing field are rejected.
  • label is stripped of all markup before storage. A label renders on every form in the workspace, so it is never allowed to carry HTML.
  • A select must supply options; any other type must not.

Type and name are immutable

PATCH can change label, required and options — nothing else. Sending type or name returns 422, even if the value matches what is already stored. The reason is that values already exist. Flipping text to number converts nothing; it just leaves stored values that no longer match the declared type, so reads start failing for records nobody touched. If you need a different type, create a new field.

Retiring a field

There is no delete. Send {"enabled": false} instead:
The field disappears from the schema — so it stops being published, stops being accepted on writes, and stops appearing on forms — but every value already stored against it is kept. Send {"enabled": true} to bring it back. A hard delete would orphan that data with no way back, which is why the API does not offer one. Fields on custom objects cannot currently be disabled this way (there is no such flag on their storage); remove them in the CRM instead.