Skip to main content
A record is one row in an object. Its values live in fields, keyed by property key. See How the schema works. Everywhere an endpoint takes an {object} path segment you may pass the object’s id or its key (obj_3Ab… or contacts). The display name is not accepted.

Attributes

string
Unique identifier, prefixed rec_.
string
The key of the object this record belongs to, e.g. contacts.
string
The id of that object.
string | null
The record’s display name, derived from the object’s primary property.
object
The record’s values, keyed by each property’s key. The keys depend on the object’s schema; a property with no value is null rather than absent, so every record in a list has identical keys. The shape of each value depends on the property’s type. See How the schema works.A notes-type field holds a doc_… document id, not the text. Retrieve the record with ?expand=notes to inline it as markdown in one call (needs documents:read), or fetch the document by its id.
string | null
ISO 8601 creation timestamp.
string | null
ISO 8601 timestamp of the last change.
On create and update, a field key that matches no property doesn’t fail the request. The record is written and the key is reported back in ignoredFields. A typo’d key therefore looks just like a successful write, so check ignoredFields on your first run.

Writing select, status and multi-select fields

Send an option’s label, as it appears in Custral, or its opt_… id. Labels match case-insensitively. A multi-select takes an array:
A label the property has no option for fails the request with option_not_found, and the message names the field and the label (Stage has no option "Comitment".). Nothing is written, and the API never creates an option for you; add it in Custral first. A single-choice field sent several values fails the same way. Send null (or []) to clear the field.

Writing the same record twice

POST /v1/records/{object}/upsert takes matchOn (the property that identifies the record) beside fields. The record whose matchOn property holds that value is updated, or created when none does, so a sync can send the same record on every run without listing first:
Fields that should only be set on a new record, such as a first stage or an owner, go in createFields. They are written when the upsert creates the record and ignored when it updates one, so a re-run never resets what your team changed:
The response says which happened ("action": "created" or "updated"). The match is exact, on a text, email, phone or website property. When two records already hold the value, the call fails with match_ambiguous and writes nothing; update the one you mean by its id.

Querying the list

GET /v1/records/{object} (see List records) is more than a dump. You can look records up by any property, search, sort, and page two ways.
query
Filter by a property, this is how you fetch by your own id property, an email, or any field instead of the rec_ id. filter[email]=jane@acme.com matches exactly; filter[amount][gte]=100 uses an operator (eq neq gt lt gte lte contains notContains startsWith endsWith empty notEmpty). Multiple properties AND together. A <key> is a property key (or created_at / updated_at). empty / notEmpty ignore their value (filter[email][empty]=true). A filter is never silently dropped. An unknown property answers property_invalid, an unsupported operator filter_operator_invalid, and a malformed or repeated filter parameter filter_invalid; each error’s message and data.parameter name the parameter at fault, e.g. filter[nosuchproperty].
query
Full-text search across the object’s records, relevance-ranked. When set, filter / sort / cursor are ignored.
query
sort=amount:desc, a property key (or created_at / updated_at) and an optional :asc / :desc (default asc).
query
Stable pagination for large sets. Each response carries nextCursor / prevCursor; pass one back as cursor=… for the next page. Mutually exclusive with offset. Pick one paging mode. (Offset paging still works for small sets.)
Look up by a property, newest first