Skip to main content
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 familyOperatorsTypical fields
Equalityeq, ne, in, not_inIDs, strings, emails, select values, stages, references
String matchingcontains, starts_with, ends_withText, email, URL, phone values
Orderinggt, gte, lt, lte, betweenNumber, currency, percent, timestamp, date, time
Array membershipcontains_any, contains_noneArray-valued fields such as multi-select and labels
Empty checksexists, not_existsAny 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

  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 codeUsually meansHow to recover
invalid_filter_fieldField does not exist or is not filterableCheck /_schema/{record_type}
invalid_filter_operatorOperator is not valid for that field typeUse an operator compatible with the value type
invalid_filter_valueValue shape does not match the operator or fieldCoerce the value before sending
filter_depth_exceededLogical (and/or/not) nesting is too deep (more than 4 levels)Flatten the filter tree
path_too_deepDot-path traversal is too deep (more than 3 hops)Reduce the relationship path depth
sort_too_many_fieldsToo many explicit sort fieldsUse no more than three explicit sort fields
unsortable_fieldField is not sortableChoose a sortable field from schema
invalid_cursorCursor was malformed, carried an unsupported version, or was used with changed filters/sortRestart pagination
limit_out_of_rangelimit is outside the accepted rangeUse 1 through 100