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

# Query Records for Sync

> Use `_query`, filters, sorting, and cursors to build reliable sync jobs.

<Warning>
  **Restricted access.** Public API v2 is currently available only to allowlisted
  organizations. Requests from workspaces that have not been enabled are rejected.
  Contact your Reevo representative to have your organization added to the allowlist.
</Warning>

# Query Records for Sync

Use `POST /api/v2/public/<record_type>/_query` when you need filtered, sorted, paginated record reads.

## Basic query

```json theme={null}
{
  "filters": [
    {
      "field": "primary_email",
      "operator": "eq",
      "value": "sarah.chen@acme.com"
    }
  ],
  "limit": 50
}
```

Top-level filters are combined with `and`.

## Sort and paginate

```json theme={null}
{
  "filters": [
    {
      "field": "updated_at",
      "operator": "gte",
      "value": "2026-06-01T00:00:00Z"
    }
  ],
  "sort": [
    {
      "field": "updated_at",
      "direction": "asc"
    }
  ],
  "limit": 100
}
```

When the response includes `next_cursor`, send it back in the next request. You MUST repeat the identical `filters` and `sort` from the original query alongside `cursor` (and `limit`), because the cursor is bound to the exact filter and sort that created it. Omitting them is treated as a filter/sort change and rejected as `invalid_cursor`:

```json theme={null}
{
  "filters": [
    {
      "field": "updated_at",
      "operator": "gte",
      "value": "2026-06-01T00:00:00Z"
    }
  ],
  "sort": [
    {
      "field": "updated_at",
      "direction": "asc"
    }
  ],
  "cursor": "<next_cursor>",
  "limit": 100
}
```

Cursor rules:

* Treat cursors as opaque strings.
* Do not parse or modify cursor values.
* Cursors are tied to the filter and sort used to create them.
* If a cursor becomes invalid, restart the query from the first page.

## Composite filters

Use `or` and `not` blocks for more complex queries.

```json theme={null}
{
  "filters": [
    {
      "or": [
        {
          "field": "primary_email",
          "operator": "contains",
          "value": "@acme.com"
        },
        {
          "field": "title",
          "operator": "contains",
          "value": "founder"
        }
      ]
    }
  ]
}
```

## Operator guide

| Operator family  | Operators                              | Typical fields                                          |
| ---------------- | -------------------------------------- | ------------------------------------------------------- |
| Equality         | `eq`, `ne`, `in`, `not_in`             | IDs, strings, emails, select values, stages, references |
| String matching  | `contains`, `starts_with`, `ends_with` | Text, email, URL, phone values                          |
| Ordering         | `gt`, `gte`, `lt`, `lte`, `between`    | Number, currency, percent, timestamp, date, time        |
| Array membership | `contains_any`, `contains_none`        | Array-valued fields such as multi-select and labels     |
| Empty checks     | `exists`, `not_exists`                 | Any nullable field                                      |

Use `GET /api/v2/public/_schema/{record_type}` to confirm which operators a given field supports — operator availability is gated per value type.

The schema endpoint describes which fields are filterable and sortable. Invalid field/operator combinations return `invalid_filter_operator` or `invalid_filter_field`.

### Operator notes

A few operators have semantics that are not obvious from the table:

* **`exists` / `not_exists`** require an explicit boolean `value`. Send `{ "field": "title", "operator": "exists", "value": true }`; the bodiless form (no `value`) is rejected with `invalid_filter_value`.
* **`between`** is half-open `[low, high)`: the lower bound is inclusive and the upper bound is exclusive. A row whose value exactly equals the upper bound is not matched.
* **`contains` / `starts_with` / `ends_with`** match case-insensitively — `contains "DEAL"` matches the same rows as `contains "deal"`.

### Filtering compound fields

Compound fields such as `stage` and other `select` fields cannot be filtered directly — `{ "field": "stage", ... }` returns `invalid_filter_field`, whose `details.filterable_subfields` names the one usable subfield: `stage.id`. Filter by the option's **id**, using the raw option UUID:

```json theme={null}
{
  "filters": [
    {
      "field": "stage.id",
      "operator": "in",
      "value": [
        "52f492e9-fcc1-44b5-b5dc-cd826e6af18d",
        "7b1c8d90-2e34-4a56-9f01-a2b3c4d5e6f7"
      ]
    }
  ]
}
```

`<field>.id` is the only filterable subfield of a select or stage field; name-based subfields such as `stage.api_name` and `stage.display_name` are not filterable. A non-UUID value returns `field_value_invalid`. Dot-path filters may still traverse up to three hops; deeper paths return `path_too_deep`.

## Common sync pattern

1. Query by `updated_at` from your last successful sync timestamp.
2. Sort by `updated_at` ascending.
3. Process every page until `next_cursor` is `null`.
4. Persist the latest processed `updated_at` and record ID in your own system.
5. On failure, restart from the last checkpoint instead of trying to reuse an old cursor indefinitely.

## Common errors

| Error code                | Usually means                                                                               | How to recover                                 |
| ------------------------- | ------------------------------------------------------------------------------------------- | ---------------------------------------------- |
| `invalid_filter_field`    | Field does not exist or is not filterable                                                   | Check `/_schema/{record_type}`                 |
| `invalid_filter_operator` | Operator is not valid for that field type                                                   | Use an operator compatible with the value type |
| `invalid_filter_value`    | Value shape does not match the operator or field                                            | Coerce the value before sending                |
| `filter_depth_exceeded`   | Logical (`and`/`or`/`not`) nesting is too deep (more than 4 levels)                         | Flatten the filter tree                        |
| `path_too_deep`           | Dot-path traversal is too deep (more than 3 hops)                                           | Reduce the relationship path depth             |
| `sort_too_many_fields`    | Too many explicit sort fields                                                               | Use no more than three explicit sort fields    |
| `unsortable_field`        | Field is not sortable                                                                       | Choose a sortable field from schema            |
| `invalid_cursor`          | Cursor was malformed, carried an unsupported version, or was used with changed filters/sort | Restart pagination                             |
| `limit_out_of_range`      | `limit` is outside the accepted range                                                       | Use `1` through `100`                          |
