---
title: Search
url: https://www.tines.com/stories/docs/api/records/search/
updated: 2026-09-23T08:49:56+00:00
description: List and filter records of a single record type. Search supports filtering, sorting, sparse fieldsets, relationship includes, time-range narrowing, and keyset pagination.
---

*[tines.com](https://www.tines.com/llms.txt) › [Docs](https://www.tines.com/stories/docs/llms.txt) › [Tines API](https://www.tines.com/stories/docs/api/llms.txt) › [Records](https://www.tines.com/llm/stories/docs/api/records.md)*

# Search

*[View on tines.com](https://www.tines.com/stories/docs/api/records/search/)*

## Description

List and filter records of a single record type. Search supports filtering, sorting, sparse fieldsets, relationship includes, time-range narrowing, and keyset pagination.

This endpoint supersedes the v1 [List](/api/records/list) record endpoint: use Search to retrieve and filter individual records, and the [Aggregate records API](/api/records/aggregate) to compute counts, sums, and grouped breakdowns.

Despite the `POST` verb, Search is a read — no records are created. The search criteria travel in the request body because they are richer than a query string comfortably carries.

To discover the `record_type_id` and field IDs to reference, use the [List record types API](/api/records/record_types/list).

## Request

HTTP Method: **POST**

URL: `https://<tenant-domain>/api/v2/records/search`

The request body is a JSON object with the following fields:

| Body parameter   | Required | Description                                                                                                                               |
| ---------------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------- |
| record_type_id   | Yes\*    | Numeric record type ID. Provide this or `record_type_name`.                                                                               |
| record_type_name | Yes\*    | Record type name or slug. An alternative to `record_type_id`; if the name matches multiple types, use `record_type_id`.                   |
| filter           | No       | A filter tree that narrows which records are returned (see [Filters](#filters)).                                                          |
| sort             | No       | An array of sort keys applied in priority order (see [Sort](#sort)).                                                                      |
| fields           | No       | Sparse fieldset: `{ "record": "status,severity" }` (or `{ "record": ["status", "severity"] }`). Omit for all fields.                      |
| include          | No       | An array of accepted to-one relationships: `record_type` and/or `parent_record` (see [Includes](#includes)).                              |
| ids              | No       | Hydrate specific record IDs (max 500). Cannot be combined with `after`. When `limit` is omitted, all unique IDs are returned in one page. |
| time_range       | No       | An array of up to two timestamp windows, `AND`ed together, narrowing which records match (see [Time range](#time-range)).                 |
| timezone         | No       | IANA timezone name (for example `America/New_York`) applied to time-range presets and timestamp output. Defaults to UTC.                  |
| limit            | No       | Page size, between 1 and 500. Defaults to 20, or the number of unique `ids` when `ids` is supplied.                                       |
| after            | No       | Opaque forward pagination cursor from a previous response's `page.next_page_token`.                                                       |
| include_total    | No       | When `true`, include `page.total` (the count of all matching records). Defaults to `false`.                                               |

\*Provide at least one of `record_type_id` or `record_type_name`. If you send both, `record_type_id` takes precedence and `record_type_name` is ignored. If `record_type_name` matches more than one record type, the request fails with a `400`; supply `record_type_id` to disambiguate.

An unknown body parameter returns a `400` — a misspelled key such as `filters` (v1's name) is rejected rather than silently ignored.

### Field identification

Every field reference in `filter` and `sort` accepts **one of**:

| Key        | Type    | Description                                                                                                                                         |
| ---------- | ------- | --------------------------------------------------------------------------------------------------------------------------------------------------- |
| field_id   | Integer | The numeric ID of the record field. Stable across renames — recommended for automation.                                                             |
| field_name | String  | The exact, case-sensitive name or canonical snake-case slug of the field within the record type. More readable, but breaks if the field is renamed. |

**Tip:** Use `field_id` for stable automation that won't break if fields are renamed. Use `field_name` for ad-hoc queries where readability matters.

### Filters

The `filter` field is a **filter tree**. It is either a single condition:

```json
{ "field_name": "Status", "operator": "EQUAL", "value": "open" }
```

or an AND group of conditions:

```json
{
  "op": "AND",
  "nodes": [
    { "field_name": "Status", "operator": "EQUAL", "value": "open" },
    { "field_id": 102, "operator": "IS_ANY_OF", "value": ["high", "critical"] }
  ]
}
```

Only `AND` is supported. Groups may nest, up to a maximum depth of 4 and 50 total nodes.

Each condition has:

| Key                        | Required | Description                                                                                                                                            |
| -------------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ |
| field_id **or** field_name | Yes      | The field to filter on.                                                                                                                                |
| operator                   | Yes      | See the operator matrix below.                                                                                                                         |
| value                      | Depends  | The value to compare against. For `IS_ANY_OF`/`IS_NONE_OF`, provide an array. For `IS_EMPTY`, `IS_NOT_EMPTY`, `IS_TRUE`, and `IS_FALSE`, omit `value`. |

Operators are gated by the field's `result_type`:

| Operator                                                                         | Applicable field types                                |
| -------------------------------------------------------------------------------- | ----------------------------------------------------- |
| `EQUAL`, `NOT_EQUAL`                                                             | `TEXT`, `TEXT_ENUM`, `NUMBER`, `TIMESTAMP`            |
| `GREATER_THAN`, `GREATER_THAN_OR_EQUAL_TO`, `LESS_THAN`, `LESS_THAN_OR_EQUAL_TO` | `NUMBER`, `TIMESTAMP`                                 |
| `IS_ANY_OF`, `IS_NONE_OF`                                                        | `TEXT`, `TEXT_ENUM`, `NUMBER`                         |
| `CONTAINS`, `STARTS_WITH`, `ENDS_WITH`                                           | `TEXT`, `TEXT_ENUM`                                   |
| `IS_EMPTY`, `IS_NOT_EMPTY`                                                       | `TEXT`, `TEXT_ENUM`, `NUMBER`, `TIMESTAMP`, `BOOLEAN` |
| `IS_TRUE`, `IS_FALSE`                                                            | `BOOLEAN`                                             |

`ARTIFACT` and `JSON` fields cannot be used in filters.

### Sort

The optional `sort` orders the returned records. It is an array of sort keys applied in priority order: records are ordered by the first key, ties broken by the next, and so on. When omitted, records are ordered newest first. Record `id` is always appended as a final tiebreaker, so paging stays stable even when your keys do not fully disambiguate.

Each key has:

| Key                        | Required | Description                                                                                        |
| -------------------------- | -------- | -------------------------------------------------------------------------------------------------- |
| field_id **or** field_name | Yes      | The field to sort by. `created_at` is accepted as a `field_name` alias for the creation timestamp. |
| direction                  | No       | `ASC` or `DESC` (case-insensitive). Defaults to `ASC`.                                             |

Sorting is supported on `TEXT`, `TEXT_ENUM`, `NUMBER`, `BOOLEAN`, and `TIMESTAMP` fields; sorting on an `ARTIFACT` or `JSON` field returns a `400`. A field may appear only once — repeating it returns a `400` — and an unknown key member is rejected with a `Did you mean?` hint.

```json
[
  { "field_name": "Severity", "direction": "DESC" },
  { "field_name": "created_at", "direction": "ASC" }
]
```

The pagination cursor pins the sort it was created under: replaying an `after` token with a different `sort` returns a `400`. When you supply `sort`, the response's `applied.sort` echoes those resolved keys with each field represented by its `field_id`. When `sort` is omitted, `applied.sort` is an empty array even though the default newest-first order is used.

### Time range

The optional `time_range` narrows records by timestamp. It is an array of up to two windows, `AND`ed together, with at most one window per `field`. Each window is an object:

| Key                | Description                                                                                                                          |
| ------------------ | ------------------------------------------------------------------------------------------------------------------------------------ |
| field              | Timestamp column to filter: `created_at` (default) or `updated_at`.                                                                  |
| rolling_date_range | A rolling preset: `TODAY`, `YESTERDAY`, `LAST_7_DAYS`, `LAST_31_DAYS`, `LAST_365_DAYS`, or `ALL_TIME` (`ALL_TIME` applies no bound). |
| range_start        | Inclusive ISO 8601 lower bound of a custom range.                                                                                    |
| range_end          | Inclusive ISO 8601 upper bound of a custom range.                                                                                    |

Within a window, supply either a `rolling_date_range` preset or `range_start`/`range_end` bounds, not both, and `range_start` must not be after `range_end` — otherwise the request returns a `400`. Equal bounds select that single instant. A second window on the same `field` also returns a `400`. Use one window per column to combine conditions, for example "created before 2026 and updated in the last seven days":

```json
[
  { "field": "created_at", "range_end": "2026-01-01T00:00:00Z" },
  { "field": "updated_at", "rolling_date_range": "LAST_7_DAYS" }
]
```

### Includes

Search accepts these to-one `include` values:

| Value         | Effect                                                                                                                            |
| ------------- | --------------------------------------------------------------------------------------------------------------------------------- |
| record_type   | Embeds the record type once under the top-level `included` object (every record on a page shares one record type).                |
| parent_record | The bare parent reference is always returned in each record's `relationships`; supplying this value does not change the response. |

The to-many relationships (`child_records`, `cases`) are not available on Search — use the [List child records API](/api/records/child_records) and [List linked cases API](/api/records/cases), or the `include` on the [Get record API](/api/records/get).

### Sparse fieldsets

By default every field of the record type is returned. To return only some fields, set `fields` to `{ "record": "<comma-separated field keys>" }`. The keys are the same slug keys that appear in each record's `fields` object. An unknown key returns a `400`.

### Example request

```bash
curl --proto '=https' --tlsv1.2 \
  -X POST \
  "https://<tenant-domain>/api/v2/records/search" \
  -H 'Content-Type: application/json' \
  -H 'Authorization: Bearer <<CREDENTIAL.tines_api_key>>' \
  -d '{
    "record_type_id": 42,
    "filter": {
      "field_name": "Status",
      "operator": "EQUAL",
      "value": "open"
    },
    "sort": [
      { "field_name": "Severity", "direction": "DESC" }
    ],
    "include": ["record_type"],
    "include_total": true,
    "limit": 20
  }'
```

### Pagination

The Records v2 API uses keyset (cursor) pagination rather than the `page`/`per_page` numbering of v1. When more results exist, the response's `page` object contains a `next_page_token`; pass it back as the `after` body parameter to fetch the next page. On the last page, `page` has no `next_page_token`.

```bash
curl --proto '=https' --tlsv1.2 \
  -X POST \
  "https://<tenant-domain>/api/v2/records/search" \
  -H 'Content-Type: application/json' \
  -H 'Authorization: Bearer <<CREDENTIAL.tines_api_key>>' \
  -d '{ "record_type_id": 42, "after": "<<next_page_token>>", "limit": 20 }'
```

## Response

A successful request returns a JSON object with a `data` array of records, a `field_ids` map, a `page` pagination cursor, and (when requested) `included` relationships and an `applied` echo.

### Field description

The response contains these top-level fields:

| Field     | Description                                                                                                   |
| --------- | ------------------------------------------------------------------------------------------------------------- |
| data      | Array of record objects (see below).                                                                          |
| field_ids | Map of each returned field's slug key to its immutable field ID, so callers can pin to the ID across renames. |
| included  | Present only when `include` is used. `record_type` is a one-element array of `{ id, name, slug }`.            |
| page      | Pagination cursor and optional total. Empty on the last page (see [Pagination](#pagination)).                 |
| applied   | Echoes the filter and sort actually applied, so you can diff them against what you sent.                      |

Each object in `data` contains:

| Field         | Description                                                                                                                                                                                                                                                  |
| ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| id            | Integer ID of the record.                                                                                                                                                                                                                                    |
| fields        | Field values keyed by field slug (`field_<id>` when two fields share a slug). Scalar fields hold their typed value; `TIMESTAMP` fields are ISO 8601 strings; artifact-backed fields return a handle (see [Artifact-backed fields](#artifact-backed-fields)). |
| relationships | `record_type` (`{ "id" }`) and `parent_record` (`{ "id" }`, or `null` when the record has no parent).                                                                                                                                                        |
| created_at    | ISO 8601 creation timestamp (UTC unless a `timezone` is supplied).                                                                                                                                                                                           |
| updated_at    | ISO 8601 last-updated timestamp (UTC unless a `timezone` is supplied).                                                                                                                                                                                       |

The `page` object contains:

| Field           | Description                                                                            |
| --------------- | -------------------------------------------------------------------------------------- |
| next_page_token | Opaque forward cursor; present only when another page exists. Pass it back as `after`. |
| total           | Total matching records; present only when `include_total` is `true`.                   |

### Artifact-backed fields

`JSON` and `ARTIFACT` field values are never inlined by Search, whatever their size. Instead, each is returned as a handle:

```json
{ "expandable": true, "href": "/api/v2/records/<id>/fields/<field_id>" }
```

Follow the `href` with the [Get record field API](/api/records/fields/get) to retrieve the full value. (The single-record [Get record API](/api/records/get) can also hydrate these fields inline via its `expand` parameter.)

### Sample response

```json
{
  "data": [
    {
      "id": 555,
      "fields": {
        "status": "open",
        "severity": "high",
        "payload": {
          "expandable": true,
          "href": "/api/v2/records/555/fields/103"
        }
      },
      "relationships": {
        "record_type": {
          "id": 42
        },
        "parent_record": null
      },
      "created_at": "2026-07-02T00:00:00Z",
      "updated_at": "2026-07-02T00:00:00Z"
    }
  ],
  "field_ids": {
    "status": 101,
    "severity": 102,
    "payload": 103
  },
  "included": {
    "record_type": [
      {
        "id": 42,
        "name": "Alerts",
        "slug": "alerts"
      }
    ]
  },
  "page": {
    "total": 1
  },
  "applied": {
    "filter": {
      "field_id": 101,
      "operator": "EQUAL",
      "value": "open"
    },
    "sort": [
      {
        "field_id": 102,
        "direction": "DESC"
      }
    ]
  }
}
```

## Errors

Errors are returned as a JSON object with an `error` field, along with an appropriate HTTP status code. Every Records v2 endpoint uses this shared shape:

```json
{
  "error": {
    "type": "bad_request",
    "message": "record_type_id or record_type_name is required",
    "field": "record_type_id"
  }
}
```

| Status | Meaning                                                                                                          |
| ------ | ---------------------------------------------------------------------------------------------------------------- |
| 400    | Invalid request — malformed body, unknown parameter, invalid timezone, disallowed operator, or invalid cursor.   |
| 403    | The tenant does not have access to records.                                                                      |
| 404    | Record type not found, or the calling token does not have access to it.                                          |
| 422    | A field value could not be read for the requested representation.                                                |
| 503    | Search is temporarily unavailable. Retry the request or restart from the first page using the response's `hint`. |

The `error` object always carries `type` and `message`; validation errors add a `field` pointer, and some add a `hint` suggesting a correction.
