> ## Documentation Index
> Fetch the complete documentation index at: https://flow9.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Working with records

> One uniform surface covers every entity — built-in ones like leads and deals, and anything the tenant has defined themselves.

One uniform surface covers every entity — built-in ones like leads and deals, and
anything the tenant has defined themselves.

```
GET    /v1/objects/{object}/records        list
POST   /v1/objects/{object}/records        create
GET    /v1/objects/{object}/records/{id}   read one
PATCH  /v1/objects/{object}/records/{id}   update
DELETE /v1/objects/{object}/records/{id}   delete
```

`{object}` is whatever [`GET /v1/objects`](/api-reference/objects) returned — `leads`, or
`object:property` (URL-encoded as `object%3Aproperty`).

Read [the object model](/api-reference/objects) first. The field names, types and which ones
are required all come from the object's schema, and they differ per tenant.

***

## Listing and paging

```bash theme={null}
curl -H "x-api-key: $KEY" \
  "https://<host>/functions/v1/public-api/v1/objects/leads/records?limit=25"
```

```json theme={null}
{
  "success": true,
  "data": {
    "object": "leads",
    "records": [ { "id": "…", "name": "Acme", "email": "a@b.com", "city": "Dubai" } ],
    "next_cursor": "eyJ0IjoiMjAyNi0wNy0yMlQxMDowMDowMC4wMDBaIiwiaSI6IjExMSJ9",
    "has_more": true
  }
}
```

To get the next page, pass `next_cursor` back:

```
?limit=25&cursor=eyJ0IjoiMjAyNi0wNy0yMlQxMDowMDowMC4wMDBaIiwiaSI6IjExMSJ9
```

When `next_cursor` is `null` you have reached the end. That is the only signal
you need — do not compare counts.

**`limit`** is 1–100 and defaults to 25. Out-of-range values are clamped rather
than rejected.

### Treat the cursor as opaque

It is a base64 blob and its contents are not part of the contract. Do not parse
it, increment it, or build one yourself.

The reason it exists in this form: paging is anchored to *a record*, not to a
position in the list. So if records are created or deleted while you are paging,
you still will not see the same record twice or miss one. Offset paging (`?page=2`)
cannot promise that — one insert shifts every row down and the last row of page 1
silently reappears at the top of page 2.

If you are syncing data, that guarantee is the difference between a clean sync and
duplicated records.

***

## Creating

```bash theme={null}
curl -X POST \
  -H "x-api-key: $KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "content-type: application/json" \
  -d '{"name":"Acme Ltd","email":"hello@acme.com","city":"Dubai"}' \
  "https://<host>/functions/v1/public-api/v1/objects/leads/records"
```

**`Idempotency-Key` is required.** Send a unique value per logical create and
reuse it on retries: if the first attempt actually succeeded but the response
never reached you, retrying with the same key returns the original record instead
of creating a second one. Omitting the header returns `428`.

Built-in fields and custom fields are sent the same way, flat in the body. You do
not need to know which is which.

***

## Updating

`PATCH` is partial: omit what you are not changing.

Custom fields are **merged**, not replaced — patching one custom field leaves the
others intact.

***

## Validation

A bad payload returns `422`, and the response names every field that is wrong:

```json theme={null}
{
  "success": false,
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "Validation failed for \"status\": Field \"status\" must be one of: new, won",
    "details": {
      "fields": [
        { "field": "status", "code": "INVALID_OPTION", "message": "…must be one of: new, won" },
        { "field": "email",  "code": "REQUIRED",       "message": "Field \"email\" is required" }
      ]
    }
  }
}
```

All problems are reported at once, so you are not fixing one field per round trip.

Things worth knowing:

* **Unknown fields are rejected, not ignored.** A typo fails loudly instead of
  silently dropping your data.
* **`id`, `created_at` and `updated_at` are read-only.** Sending them is an error
  rather than a no-op, so you never believe you set an id you did not.
* **`required` is per tenant.** A field that is optional for one customer can be
  mandatory for another. Read the schema; do not assume.
* Numeric strings (`"42"`) are accepted for number fields. `"yes"` is not a
  boolean; `true` and `"true"` are.

***

## Creating many at once

```
POST /v1/objects/{object}/records:batch
```

Up to **100** items per request, as a bare array or `{ "records": [...] }`.
`Idempotency-Key` is required, as it is for a single create.

```bash theme={null}
curl -X POST   -H "x-api-key: $KEY"   -H "Idempotency-Key: $(uuidgen)"   -H "content-type: application/json"   -d '[{"name":"Acme"},{"name":"Globex"}]'   "https://<host>/functions/v1/public-api/v1/objects/leads/records:batch"
```

### Partial success is normal — always read `failed`

```json theme={null}
{
  "success": true,
  "data": {
    "object": "leads",
    "succeeded_count": 97,
    "failed_count": 3,
    "succeeded": [ { "index": 0, "record": { "id": "…", "name": "Acme" } } ],
    "failed": [
      { "index": 10, "code": "VALIDATION_ERROR", "message": "email: Field \"email\" must be a valid email address" },
      { "index": 40, "code": "VALIDATION_ERROR", "message": "mobile: Field \"mobile\" is required" }
    ]
  }
}
```

**The response is `200` even when some items failed.** One bad row in a nightly
import should not throw away the other 99 — but it does mean the HTTP status
alone no longer tells you whether everything was written. `failed` is always
present (empty when nothing failed), so check it every time.

`index` is the position in the array **you sent**. It is the only way to find the
offending row in your own data, since a record that failed to be created has no id.

### What fails the whole request instead

These return a 4xx and write **nothing**:

|                                           |                                                            |
| ----------------------------------------- | ---------------------------------------------------------- |
| More than 100 items                       | `422`, naming the limit and confirming nothing was created |
| Empty array, or not a list                | `422`                                                      |
| Key lacks the create scope for the object | `403`                                                      |
| Object not visible to the key             | `404`                                                      |
| `Idempotency-Key` missing                 | `428`                                                      |

### Duplicates within a batch

If the workspace has duplicate checking configured, items that duplicate an
**earlier item in the same batch** on the configured fields are reported as
`DUPLICATE_IN_BATCH` instead of being written. The first occurrence wins, and the
message tells you which index it collided with.

This only looks *within the request*. It is not a check against records already
in the CRM.

### Retrying

Reuse the same `Idempotency-Key` when you retry a batch that timed out. The
replay returns without creating anything new.

If some items failed on validation, fix those rows and send them as a **new**
batch with a **new** key — reusing the old key replays the old outcome rather
than processing your corrections.

### A note for SDK authors

Wrap this so callers cannot ignore `failed`. Returning a plain response object
invites `if (res.success)` and silent data loss. Prefer either raising on any
failure, or returning a result type that forces the caller to handle both lists.

***

## Filtering and sorting

### Sorting

```
?sort=-created_at,name
```

Comma-separated field names, at most 3. A leading `-` means descending. Only
fields the object's schema publishes can be sorted on; anything else is a `400`
naming the field.

### Filtering

`filter` takes a JSON object, URL-encoded.

A single condition:

```json theme={null}
{ "field": "priority", "op": "in", "value": [1, 2] }
```

Combine with `and` / `or`, nested at most **3** levels deep:

```json theme={null}
{ "and": [
    { "field": "converted", "op": "eq", "value": false },
    { "or": [
        { "field": "priority", "op": "eq", "value": 1 },
        { "field": "name", "op": "contains", "value": "acme" }
    ]}
]}
```

| Operator                 | Meaning                                                |
| ------------------------ | ------------------------------------------------------ |
| `eq`, `neq`              | equals / not equals                                    |
| `gt`, `gte`, `lt`, `lte` | comparisons                                            |
| `contains`               | case-insensitive substring. Text fields only.          |
| `in`                     | value is in the given non-empty array                  |
| `is_null`                | `value: true` (default) for null, `false` for not-null |

**Values must match the field's type.** The schema tells you what each field is —
`priority` on leads is a `number`, so `{"op":"eq","value":"High"}` is a `400`, not
a silent empty result. This is checked before the query runs, so a type mistake
never reaches the database.

**Unknown fields are a `400` naming the field**, rather than being ignored. An
ignored filter would return more rows than you asked for and look successful,
which is the worst possible failure for a sync.

**Values are always treated as literal text**, never as query syntax. Commas,
dots, parentheses, quotes and SQL fragments in a value match themselves and
nothing else — searching for `50%` finds the string "50%", not "50 followed by
anything".

### Filtering on custom fields

Custom fields work the same way. Because they are stored as JSON, comparisons on
them are **textual** — a custom number field sorts and compares lexically
(`"10" < "9"`). If ordering matters on a custom numeric field, pad the values or
keep it in a built-in field.

***

## Notes, tasks and activities on a record

```
GET  /v1/objects/{object}/records/{id}/notes
POST /v1/objects/{object}/records/{id}/notes
GET  /v1/objects/{object}/records/{id}/tasks
POST /v1/objects/{object}/records/{id}/tasks
GET  /v1/objects/{object}/records/{id}/activities
```

These work for any object, built-in or custom. Everything you create here shows
up on the record inside the CRM — the API writes to the same tables the app
reads, which differ per object behind the scenes.

### Notes

```bash theme={null}
curl -X POST -H "x-api-key: $KEY" -H "content-type: application/json"   -d '{"content":"Spoke to the client, sending a quote."}'   ".../v1/objects/leads/records/$LEAD_ID/notes"
```

Only `content` is required.

### Tasks

```bash theme={null}
curl -X POST -H "x-api-key: $KEY" -H "content-type: application/json"   -d '{"title":"Follow up","task_type":"Call","scheduled_at":"2026-08-01T09:00:00Z"}'   ".../v1/objects/deals/records/$DEAL_ID/tasks"
```

| Field                     | Required | Notes                                                                                                               |
| ------------------------- | -------- | ------------------------------------------------------------------------------------------------------------------- |
| `title`                   | yes      |                                                                                                                     |
| `scheduled_at`            | yes      | ISO 8601. Required rather than defaulted — a task silently scheduled for "now" would fire its reminder immediately. |
| `task_type`               | no       | The **name** of a task type in the workspace. An unrecognised one is a `422`.                                       |
| `description`, `priority` | no       |                                                                                                                     |

Creating a task also creates its link to the record, so it appears on that
record's task list in the app — not just in a global task list.

The task is owned by and attributed to the user your API key belongs to. A key
with no owning user cannot create tasks or notes; re-create the key from a user
account.

### Activities

Read-only here. Activities are logged by the CRM's own workflows; this endpoint
lets you read the interaction history for a record. Use
[`/v1/activity-types`](/api-reference/introduction) to discover the types a workspace has.

### Errors

Same rules as the record itself: a record you cannot see returns `404` for its
notes, tasks and activities too — including on write. There is no way to attach
anything to a record you could not have read.

***

## Errors, and the 403/404 distinction

| Status | Meaning                                                        |
| ------ | -------------------------------------------------------------- |
| `401`  | Missing or invalid key.                                        |
| `403`  | You can see this object, but your key lacks that action on it. |
| `404`  | The object or record does not exist **for you**.               |
| `409`  | A record with these values already exists.                     |
| `422`  | Validation failed.                                             |
| `428`  | `Idempotency-Key` missing on a create.                         |
| `429`  | Rate limited.                                                  |

The difference between 403 and 404 is deliberate and useful:

* **404** — you cannot *see* it. Either it does not exist, or it belongs to another
  tenant, or it falls outside your key's data scope (own / team / branch). These
  are indistinguishable on purpose: telling them apart would confirm that someone
  else's record exists.
* **403** — you *can* see it, but you are not allowed to do that to it. For example
  reading a lead works, but deleting it needs `leads:delete` on your key.

So: **404 → check the id and your data scope. 403 → add a scope to your key.**

***

## Suggested sync loop

1. `GET /v1/objects` to discover what you can access.
2. `GET /v1/objects/{object}/schema` for each object you touch, and cache it.
3. Page through records with `cursor` until `next_cursor` is null.
4. Write with `Idempotency-Key`, retrying the *same* key on network failure.
5. Re-fetch the schema when a write starts failing with `422` — a tenant may have
   added a mandatory field.

***

## Related

* [The object model](/api-reference/objects) — discovering objects and fields
* [Idempotency](/api-reference/idempotency)
* [Authentication and scopes](/api-reference/authentication)
* [Errors](/api-reference/errors)
