# Filters

> Which filters can I send, and how does the suffix grammar work?

Source: https://docs.betterjobs.cc/platform/filters/

Every search takes one `filters` object. The same object works in `POST /v1/jobs/search`, `POST /v1/searches` and in search watches (`POST /v1/watches` with `type: search`). BetterJobs translates it into each provider’s own query language, so you learn one grammar instead of six.

## Build a query

Change the fields and copy the request. The output updates as you type.

## Grammar

A filter name is a field name plus an optional suffix. The suffix says how to compare.

| Suffix | Meaning                                           | Value type         | Example                                 |
| ------ | ------------------------------------------------- | ------------------ | --------------------------------------- |
| `_or`  | Match any value in the list                       | array              | `"country_code_or": ["DE", "AT", "CH"]` |
| `_not` | Exclude any value in the list                     | array              | `"title_not": ["Intern"]`               |
| `_gte` | Greater than or equal to                          | number             | `"salary_min_gte": 90000`               |
| none   | Exact value or special rule (see the table below) | boolean or integer | `"remote": true`                        |

Two rules hold for every request:

- **Different filters combine with AND.** A job must pass every filter you send.
- **Values inside one `_or` list combine with OR.** `title_or: ["Head of RevOps", "Head of Revenue Operations"]` matches either title.

## All filters

| Filter               | Type                     | What it does                                                                         |
| -------------------- | ------------------------ | ------------------------------------------------------------------------------------ |
| `title_or`           | string\[]                | Match any of these title keywords.                                                   |
| `title_not`          | string\[]                | Exclude titles containing any of these.                                              |
| `country_code_or`    | string\[]                | ISO 3166-1 alpha-2 codes, for example `DE`.                                          |
| `posted_within_days` | integer, 1 to 365        | Jobs first seen within this many days.                                               |
| `seniority_or`       | Seniority\[]             | Any of `intern`, `junior`, `mid`, `senior`, `lead`, `director`, `vp`, `c_level`.     |
| `employment_type_or` | EmploymentType\[]        | Any of `full_time`, `part_time`, `contract`, `internship`, `temporary`.              |
| `remote`             | boolean or null          | `true` remote only, `false` non-remote only, `null` or omitted = any.                |
| `salary_min_gte`     | number                   | Keep jobs whose `salary.min` is at least this value (yearly, in the job’s currency). |
| `company_domain_or`  | string\[]                | Company domains, for example `acme-robotics.example`.                                |
| `job_family_or`      | string\[]                | Job families, for example `operations`, `sales`.                                     |
| `include_closed`     | boolean, default `false` | Include jobs with `status: closed`.                                                  |

Enum values for `seniority_or` and `employment_type_or` are listed on [Taxonomies](https://docs.betterjobs.cc/data/taxonomies.md). What each job field means is on the [field dictionary](https://docs.betterjobs.cc/data/field-dictionary.md).

> posted\_within\_days uses first\_seen\_at
>
> `posted_within_days` filters on when any source **first saw** the job (`first_seen_at`), not on `posted_at`. Many postings have no employer post date, so `posted_at` can be `null`. Filtering on first sighting keeps those jobs in your results.

> salary\_min\_gte drops jobs without pay
>
> When you set `salary_min_gte`, jobs whose `salary` is `null` are excluded. Pay is unknown for those jobs, not low. Leave the filter out if you want them.

> remote: false is not 'any'
>
> `remote: false` keeps only on-site and hybrid jobs. To accept any work mode, omit `remote` or send `null`.

## Examples

The landing-page query: Head of RevOps in Germany, Austria and Switzerland, first seen in the last 7 days.

**curl**

```bash
curl https://api.betterjobs.cc/v1/jobs/search \
  -H "Authorization: Bearer $BETTERJOBS_API_KEY" \
  -H "BetterJobs-Version: 2026-10-01" \
  -H "Content-Type: application/json" \
  -d '{
    "filters": {
      "title_or": ["Head of RevOps", "Head of Revenue Operations"],
      "title_not": ["Intern"],
      "country_code_or": ["DE", "AT", "CH"],
      "posted_within_days": 7
    },
    "limit": 25
  }'
```

**Python**

```python
import os
import requests


resp = requests.post(
    "https://api.betterjobs.cc/v1/jobs/search",
    headers={
        "Authorization": f"Bearer {os.environ['BETTERJOBS_API_KEY']}",
        "BetterJobs-Version": "2026-10-01",
    },
    json={
        "filters": {
            "title_or": ["Head of RevOps", "Head of Revenue Operations"],
            "title_not": ["Intern"],
            "country_code_or": ["DE", "AT", "CH"],
            "posted_within_days": 7,
        },
        "limit": 25,
    },
    timeout=30,
)
resp.raise_for_status()
jobs = resp.json()["data"]
```

**TypeScript**

```ts
const res = await fetch("https://api.betterjobs.cc/v1/jobs/search", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.BETTERJOBS_API_KEY}`,
    "BetterJobs-Version": "2026-10-01",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    filters: {
      title_or: ["Head of RevOps", "Head of Revenue Operations"],
      title_not: ["Intern"],
      country_code_or: ["DE", "AT", "CH"],
      posted_within_days: 7,
    },
    limit: 25,
  }),
});
if (!res.ok) throw new Error(`BetterJobs ${res.status}: ${await res.text()}`);
const { data: jobs } = await res.json();
```

More `filters` objects you can drop into the same request:

```json
{ "seniority_or": ["senior", "lead"], "remote": true, "salary_min_gte": 90000 }
```

Senior or lead remote roles that state a minimum of at least 90,000 per year.

```json
{ "company_domain_or": ["acme-robotics.example", "northwind.example"], "job_family_or": ["sales"] }
```

Sales jobs at two named companies. Pair it with a [watch](https://docs.betterjobs.cc/data/events.md) to hear about new ones.

```json
{ "company_domain_or": ["acme-robotics.example"], "include_closed": true, "posted_within_days": 90 }
```

Every job, open or closed, a company listed in the last 90 days. Useful to measure hiring history.

## Unknown filters are rejected

A filter name that does not exist returns `400` with code `unknown_filter`. BetterJobs never silently ignores a filter, because an ignored filter returns a wider, more expensive result than you asked for. `error.param` names the bad field:

```json
{
  "error": {
    "type": "invalid_request_error",
    "code": "unknown_filter",
    "message": "Unknown filter field 'job_title_or'. Did you mean 'title_or'?",
    "param": "filters.job_title_or",
    "doc_url": "https://docs.betterjobs.cc/platform/errors/#unknown_filter",
    "request_id": "req_4Bn8CxV2zA"
  }
}
```

A rejected request is never charged. See [`unknown_filter`](https://docs.betterjobs.cc/platform/errors.md#unknown_filter) in the error catalog.

> Coming from TheirStack?
>
> Per TheirStack docs, TheirStack uses a similar suffix grammar (`_or`, `_not`, `_gte`/`_lte`, `_max_age_days`) but with its own field names, such as `job_title`. BetterJobs uses `title_or`, not `job_title_or`, and `posted_within_days` in place of a max-age suffix. The [migration guide](https://docs.betterjobs.cc/guides/migrate-from-theirstack.md) maps each filter.

## Check the cost before you fetch

Add `"dry_run": true` to any `POST /v1/jobs/search` body. You get a free estimate (`expected_unique_jobs_range`, `providers_planned`, `credits_range`) and nothing is fetched or charged. Set `waterfall.max_credits` to cap what a real request can spend. See [Credits and billing](https://docs.betterjobs.cc/concepts/credits-and-billing.md).

## Related

- [Pagination](https://docs.betterjobs.cc/platform/pagination.md): read more than one page of results.
- [Async searches](https://docs.betterjobs.cc/platform/async-searches.md): run filters over up to 10,000 jobs.
- [Choose a strategy](https://docs.betterjobs.cc/guides/choose-a-strategy.md): decide which providers a filter runs against.
