# Search recipes

> Copy-paste Kustomer search queries for common tasks, plus patterns for exporting beyond the 100-page limit, incremental sync and counting.

Source: https://developer.kustomer.com/docs/guides/search-recipes

Ready-to-use bodies for [`POST /v1/customers/search`](https://developer.kustomer.com/api/core-resources/create-customer-search). See [Search](https://developer.kustomer.com/docs/concepts/search) for the full syntax and every attribute.

## Conversations

**Open, high priority**

```json
{
  "queryContext": "conversation",
  "and": [
    { "conversation_status": { "equals": "open" } },
    { "conversation_priority": { "gte": 4 } }
  ]
}
```

**Unassigned**

```json
{
  "queryContext": "conversation",
  "and": [
    { "conversation_status": { "in": ["open", "snoozed"] } },
    { "conversation_assigned_users": { "not_set": "" } }
  ]
}
```

**Created this week (local time)**

```json
{
  "queryContext": "conversation",
  "and": [{ "conversation_created_at": { "gte": "2026-10-19" } }],
  "timeZone": "America/Los_Angeles"
}
```

**By custom attribute**

```json
{
  "queryContext": "conversation",
  "and": [{ "conversation_custom_orderNumberStr": { "equals": "4417" } }]
}
```

## Customers

**By email domain**

```json
{
  "queryContext": "customer",
  "and": [{ "customer_email": { "contains": "@acme.example" } }]
}
```

**With an open conversation**

```json
{
  "queryContext": "customer",
  "and": [{ "conversation_status": { "equals": "open" } }],
  "sort": [{ "customer_last_message_at": "desc" }]
}
```

**VIPs not seen in 30 days**

```json
{
  "queryContext": "customer",
  "and": [
    { "customer_custom_loyaltyTierStr": { "equals": "gold" } },
    { "customer_last_seen_at": { "lt": "2026-09-21" } }
  ]
}
```

## Messages and custom objects

**Messages mentioning a refund**

```json
{
  "queryContext": "message",
  "and": [
    { "message_body": { "contains": "refund" } },
    { "message_direction": { "equals": "in" } },
    { "message_created_at": { "gte": "2026-10-01" } }
  ]
}
```

**Orders by status**

```json
{
  "queryContext": "kobject_ecomm-order",
  "and": [{ "kobject_custom_ecomm-order_orderStatusStr": { "equals": "shipped" } }]
}
```

Check attribute names against the [standard attributes tables](https://developer.kustomer.com/docs/concepts/search#standard-attributes-json-schema). A misspelled attribute returns an error or no results.

## Export more than the page limit

Search stops at **10,000 results** per query: a request where `page × pageSize` is over 10,000 returns `400`, so with `pageSize=500` the last page you can read is 20. To export everything, split the query into time windows on `updated_at` and page through each window. Make each window small enough to stay under the limit.

```ts title="Node.js"
async function* exportConversations(from: Date, to: Date, windowHours = 24) {
  for (let start = from; start < to; start = new Date(start.getTime() + windowHours * 3_600_000)) {
    const end = new Date(Math.min(start.getTime() + windowHours * 3_600_000, to.getTime()))
    const body = {
      queryContext: 'conversation',
      and: [
        { conversation_updated_at: { gte: start.toISOString() } },
        { conversation_updated_at: { lt: end.toISOString() } },
      ],
    }
    for (let page = 1; ; page++) {
      const res = await kfetch(`/v1/customers/search?page=${page}&pageSize=500`, { method: 'POST', body: JSON.stringify(body) })
      const { data, meta } = await res.json()
      yield* data
      if (page >= (meta.totalPages ?? 0)) break
      if (page * 500 >= 10_000) throw new Error(`Window ${start.toISOString()} has more than 10,000 results; use a smaller window`)
    }
  }
}
```

UTC timestamps (with `Z`) are used exactly, so adjacent windows with `gte` and `lt` never overlap or leave gaps.

## Incremental sync

After a full export, only fetch what changed since your last run:

```ts title="Node.js"
const since = await store.get('lastRun') // ISO timestamp
const runStartedAt = new Date().toISOString()

for await (const c of exportConversations(new Date(since), new Date(runStartedAt), 6)) await upsertLocal(c)

await store.set('lastRun', runStartedAt)
```

Store the time the run **started**, not finished, so records updated during the run are picked up next time. Search updates asynchronously, so overlap windows by a few minutes if you can tolerate duplicates.

## Count without fetching

Use `pageSize=1` and read `meta.total`:

```bash
curl -X POST "https://acme.api.kustomerapp.com/v1/customers/search?pageSize=1" \
  -H "Authorization: Bearer $KUSTOMER_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "queryContext": "conversation", "and": [{ "conversation_status": { "equals": "open" } }] }' \
  | jq '.meta.total'
```

## IDs only

For large syncs where you'll fetch records separately, add `?idsOnly=true` to return only matching IDs.

## Records older than two years

Search only covers records updated in the last two years. Send the same body to [archive search](https://developer.kustomer.com/api/core-resources/create-customer-archive-search) for older data.

## Remember the limits

- Machine users can make **100 search requests per minute**.
- Sorting only works on **customer attributes**.
- Results reflect the search index, which can lag writes by a moment.
