Custom objects and fields
Model your own entities and fields, consistently across REST, GraphQL and webhooks.
Custom fields on standard objects
Define fields on leads, contacts, accounts, opportunities, products and quotes
with POST /v1/custom-fields. Read a record's values with
GET /v1/custom-fields/values/{entityType}/{entityId}. Formula and roll-up
fields are computed when read and can't be written.
On the core list endpoints
GET /v1/leads, /contacts, /accounts and /opportunities accept:
| Parameter | Effect |
|---|---|
withCustomFields=true | Each row gains customFields: { <apiName>: value } (stored fields you can read) |
sortBy=customFields.<apiName> | Sort by a custom field (server-side, across every page) |
customFields=[{"field":"region","operator":"eq","value":"EMEA"}] | Filter by custom fields (JSON, up to 10, ANDed) |
Operators: eq, neq, contains, gt, gte, lt, lte, is_empty,
is_not_empty. Filters and sorts are ANDed onto the list's own visibility, so
they only ever narrow it. Sorting or filtering resolves the matching ids first and
refuses with 400 CUSTOM_FIELD_SORT_TOO_BROAD / CUSTOM_FIELD_FILTER_TOO_BROAD
beyond 10,000; add another filter. A field your profile can't read is
403 FIELD_NOT_READABLE; formula and roll-up fields can't be sorted or filtered.
Write values on one record with
PUT /v1/record-custom-fields/{LEAD|CONTACT|ACCOUNT|OPPORTUNITY}/{id} and
{"values": {"region": "EMEA", "renewal_date": "2026-12-01"}} (null clears a
value). The same rule as the record's own update applies: you own it, it's shared
with you for edit, or you're an admin; plus your profile's edit permission on the
object and field security on each field (403 FIELD_NOT_EDITABLE).
Field-level security keys for these fields are customFields.<apiName>.
Custom objects
| Operation | Endpoint |
|---|---|
| List definitions | GET /v1/custom-objects |
| Create a definition | POST /v1/custom-objects |
| Add a field | POST /v1/custom-objects/{object}/fields |
| List records | GET /v1/custom-objects/{object}/records |
| Create a record | POST /v1/custom-objects/{object}/records with {"data": {"title": "Q4 rollout", "budget": 15000}} |
| Get a record | GET /v1/custom-objects/{object}/records/{recordId} |
| Update a record | PATCH /v1/custom-objects/{object}/records/{recordId} (merges data) |
| Update one field on many records | PATCH /v1/custom-objects/{object}/records with {"ids": [...], "field": "status", "value": "Done"} (≤200 ids; each goes through the normal update) |
| Delete a record | DELETE /v1/custom-objects/{object}/records/{recordId} |
| Child related lists of a record | GET /v1/custom-objects/{object}/records/{recordId}/related |
| Custom-object related lists of a standard record | GET /v1/custom-objects/related/{entityType}/{entityId} |
{object} is the object's id or API name.
Listing records
GET …/records?page=1&limit=25&sortBy=budget&sortOrder=desc&search=rollout&filters=<json>&owner=me&ids=a,b
returns { data, pagination: { page, limit, total, totalPages, hasMore } }.
filters is a JSON array of { "field", "operator", "value" } using the
operators above (plus not_contains, starts_with, between, in, not_in);
sortBy takes a field API name or createdAt / updatedAt / ownerName.
Each record carries id, data, ownerId, owner, timestamps and
lookups: { <field>: { id, object, label } } for its lookup fields.
Relationships
LOOKUP and MASTER_DETAIL fields point at a standard object (LEAD,
CONTACT, ACCOUNT, OPPORTUNITY, TASK, CASE) or another custom object
(custom:<apiName>); the value in data is the target record's id. A detail
record of a MASTER_DETAIL is visible to whoever can see its master. The
related endpoints group the records that point at a record by object and field,
with the first few rows and a total each.
Permissions
Records follow the same rules as the core objects: the custom:<apiName> module
on the caller's profile (read / create / edit / delete and OWN / TEAM / ALL data
access), the owner's reporting tree, record shares and master-detail
inheritance. Field-level security uses custom:<apiName> as the entity and the
field API name as the field; hidden fields are left out of responses and
writes to read-only fields are 403 FIELD_NOT_EDITABLE. API keys need
READ_CUSTOM_OBJECTS or WRITE_CUSTOM_OBJECTS.
Field history
Fields with trackHistory record every change. GET /v1/custom-object-history/{object}/{recordId}
returns { data: [{ field, oldValue, newValue, changedBy, changedByName, changedAt }] },
newest first, for anyone who can read the record (fields hidden by their profile are
left out). The admin audit trail has the same rows under entity type custom:<apiName>.
Page layouts and saved views
Use custom:<apiName> wherever the API takes an object type: page layouts
(PUT /v1/page-layouts with objectType: "custom:project"; built-in tabs
details, related, activity; rail fields are the object's field API names),
saved list views (entity: "custom:project") and recently viewed records.
Reports and dashboards
POST /v1/custom-reports/preview and saved reports accept type: "custom:<apiName>";
GET /v1/custom-reports/fields/custom:<apiName> lists its fields. Standard report
types also offer their custom fields as cf:<apiName> (columns, filters, group by
and numeric aggregates). Dashboard widgets take dataSource: "custom:<apiName>".
Importing
Send rows to the Bulk API with object: "custom:<apiName>".
To turn names into lookup ids first,
POST /v1/import-export/custom-objects/{apiName}/resolve-lookups with
{"field": "account", "values": ["Acme", "[email protected]"]} returns
{ resolved: { value: id }, unresolved, ambiguous }, limited to records you can see.
One record shape everywhere
GraphQL and webhook payloads share one record shape, so a record looks the same whichever way you receive it:
Standard records carry customFields keyed by field API name. Dates are ISO
strings, money is a number, and secrets are never included.
See GraphQL for typed per-object queries.