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.
Query Records for Sync
Use POST /api/v2/public/<record_type>/_query when you need filtered, sorted, paginated record reads.
Basic query
{
"filters": [
{
"field": "primary_email",
"operator": "eq",
"value": "sarah.chen@acme.com"
}
],
"limit": 50
}
Top-level filters are combined with and.
Sort and paginate
{
"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:
{
"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.
{
"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:
{
"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
- Query by
updated_at from your last successful sync timestamp.
- Sort by
updated_at ascending.
- Process every page until
next_cursor is null.
- Persist the latest processed
updated_at and record ID in your own system.
- 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 |