# Filtering

List operations accept filters so you fetch only what you need. This page covers the common date filters, the filters of each list and how filters combine with pagination.

## How filters work

- Filters are query parameters, for example `GET /v1/calls?status=ended&agent_id=agent_...`.
- Every filter narrows the result: several filters together return only objects that match **all** of them.
- Each filter may be given once. An unknown parameter, a repeated one or an invalid value answers [`validation_error`](/errors/validation_error), with the parameter named in `details.errors`; an invalid phone number (`to`, `phone_number`) answers [`invalid_phone_number`](/errors/invalid_phone_number).
- Values are matched exactly, with the formats below. There is no free-text search.
- **URL-encode values.** In particular, the `+` of an E.164 number must be sent as `%2B` (`to=%2B12025550100`); a bare `+` in a query string means a space. `URLSearchParams` in JavaScript, `params=` in Python `requests` and `curl -G --data-urlencode` do this for you.

## Date filters

| Parameter | Meaning |
| --- | --- |
| `created_after` | Only objects created **at or after** this time. |
| `created_before` | Only objects created **before** this time. |

Both take an RFC 3339 timestamp, for example `2026-10-21T00:00:00.000Z`; use UTC. Together they select a half-open window, so consecutive windows (`created_after=2026-10-20T00:00:00.000Z&created_before=2026-10-21T00:00:00.000Z`, then the next day) never overlap or miss an object. For calls, `created_at` is when the call was accepted.

## Resource filters

- [`GET /calls`](/api/calls#list-calls): `created_after`, `created_before`; `agent_id`; `campaign_id`; `to` (E.164); `status` (`queued`, `ringing`, `in_progress`, `ended`, `unknown`); `outcome` (`completed`, `no_response`, `no_answer`, `busy`, `failed`, `unknown`; matches ended calls only)
- [`GET /campaigns`](/api/campaigns#list-campaigns): `created_after`, `created_before`; `status` (`draft`, `running`, `paused`, `stopped`, `completed`, `failed`, `unknown`)
- [`GET /campaigns/{campaign_id}/recipients`](/api/campaigns#list-recipients): `status` (`pending`, `in_progress`, `completed`, `failed`, `suppressed`, `cancelled`, `unknown`)
- [`GET /contacts`](/api/contacts#list-contacts): `created_after`, `created_before`; `phone_number` (E.164, matched the same way as the do-not-call check); `consent_granted` (`true` or `false`); `do_not_call` (`true` or `false`)
- [`GET /dnc-entries`](/api/do-not-call-list#list-or-look-up-do-not-call-entries): `phone_number` (E.164): one entry if the number is on the list, none if it is not; formatting is ignored
- [`GET /webhook-endpoints/{webhook_endpoint_id}/deliveries`](/api/webhook-endpoints#list-deliveries): `status` (`pending`, `retrying`, `succeeded`, `failed`)
- [`GET /agents`](/api/agents#list-agents), [`GET /phone-numbers`](/api/phone-numbers#list-phone-numbers), [`GET /webhook-endpoints`](/api/webhook-endpoints#list-webhook-endpoints): None (only `limit` and `cursor`)

Every list also takes `limit` and `cursor` ([Pagination](/fundamentals/pagination)). All lists only ever return objects of your key's workspace and mode.

Some useful combinations:

- A call you may have placed after a timeout: `GET /v1/calls?to=%2B12025550100&created_after=<just before the first attempt>` and match your `metadata`
- Yesterday's answered calls of one agent: `GET /v1/calls?agent_id=agent_...&outcome=completed&created_after=...&created_before=...`
- Whether a number is on the do-not-call list: `GET /v1/dnc-entries?phone_number=%2B12025550100`
- Contacts that still need consent: `GET /v1/contacts?consent_granted=false`
- Recipients a campaign left out: `GET /v1/campaigns/cmp_.../recipients?status=suppressed`
- Failed webhook deliveries to retry: `GET /v1/webhook-endpoints/we_.../deliveries?status=failed`

## Example

Ended calls to `+12025550100` created since 21 October 2026, 100 per page. The `+` is sent as `%2B` and the colons of the timestamp as `%3A`:

```bash
curl "https://voice-api.relationsai.com/v1/calls?to=%2B12025550100&status=ended&created_after=2026-10-21T00%3A00%3A00.000Z&limit=100" \
     -H "Authorization: Bearer <token>"
```
```js
const response = await fetch("https://voice-api.relationsai.com/v1/calls?to=%2B12025550100&status=ended&created_after=2026-10-21T00%3A00%3A00.000Z&limit=100", {
  headers: {
    Authorization: "Bearer <token>",
  },
});
console.log(response.status, await response.json());
```
```python

response = requests.get(
    "https://voice-api.relationsai.com/v1/calls?to=%2B12025550100&status=ended&created_after=2026-10-21T00%3A00%3A00.000Z&limit=100",
    headers={
        "Authorization": "Bearer <token>",
    },
    timeout=30,
)
print(response.status_code, response.json())
```

## Filters and cursors

A cursor is bound to the filters of the request that produced it. Keep exactly the same filters on every page of one listing; a cursor sent with different filters answers [`invalid_cursor`](/errors/invalid_cursor). To change filters, start again without a cursor.

Filtered lists can return pages with fewer objects than `limit` while `has_more` is still `true`, especially when filtering calls by `status`. Keep following `next_cursor` until `has_more` is `false`.

Status and outcome are refreshed as calls and campaigns progress, typically within seconds and occasionally a minute or more behind. A call filtered by `status=ended` today may not have matched a minute ago: for exports, filter on `created_before` a few minutes in the past, or wait for `settled`.
