Source: https://www.puesto.dev/docs/api/jobs/search-jobs
Markdown source: https://www.puesto.dev/docs/api/jobs/search-jobs.md
Description: Search the public job catalog with cursor-based pagination. Returns a page of job summaries and a `next_cursor` for the following page. Add `q` for full-text search (Google-style: quoted `"phrases"`, `OR`, `-exclusion`; a pure-exclusion query is rejected). Implicit AND binds tighter than `OR` and a `-term` attaches only to its adjacent group, so `"machine learning" OR "data scientist" -senior` means `A OR (B AND NOT senior)` and can still return Senior ML titles; to exclude everywhere, distribute the term: `"machine learning" -senior OR "data scientist" -senior`. By default `q` matches the job title only (precise — best for common role terms like `developer`/`engineer`, which otherwise match description boilerplate); pass `q_scope=full` to also search the description (broader reach — best for niche skills or tools named only in the requirements text). A phrase query under `q_scope=full` combined with very narrow filters (a small country, a rare language) can hit the search timeout; the default title scope is immune, so prefer it for phrase precision. `q` composes as a filter with the standard sorts: `sort=posted_at` (default) and `sort=first_seen_at` are descending keyset sorts with unbounded cursor pagination, applying `q` as a filter over the newest-first keyset. Symbol-bearing tech tokens (`c++`, `c#`, `.net`) are matched as a title substring rather than full-text (they collapse under English tokenization); a mixed query such as `python c++` still runs as full-text. If a full-text search matches nothing, one title-substring fallback runs automatically and the response is flagged `fuzzy: true`. `q` respects the default 90-day freshness window unless a date filter is supplied. A well-formed but overly broad `q` that exceeds the search time budget returns 422 (narrow the query); a malformed `q` (too complex, or only exclusions) returns 400.

# Search jobs

`GET /api/v1/jobs`

Search the public job catalog with cursor-based pagination. Returns a page of job summaries and a `next_cursor` for the following page. Add `q` for full-text search (Google-style: quoted `"phrases"`, `OR`, `-exclusion`; a pure-exclusion query is rejected). Implicit AND binds tighter than `OR` and a `-term` attaches only to its adjacent group, so `"machine learning" OR "data scientist" -senior` means `A OR (B AND NOT senior)` and can still return Senior ML titles; to exclude everywhere, distribute the term: `"machine learning" -senior OR "data scientist" -senior`. By default `q` matches the job title only (precise — best for common role terms like `developer`/`engineer`, which otherwise match description boilerplate); pass `q_scope=full` to also search the description (broader reach — best for niche skills or tools named only in the requirements text). A phrase query under `q_scope=full` combined with very narrow filters (a small country, a rare language) can hit the search timeout; the default title scope is immune, so prefer it for phrase precision. `q` composes as a filter with the standard sorts: `sort=posted_at` (default) and `sort=first_seen_at` are descending keyset sorts with unbounded cursor pagination, applying `q` as a filter over the newest-first keyset. Symbol-bearing tech tokens (`c++`, `c#`, `.net`) are matched as a title substring rather than full-text (they collapse under English tokenization); a mixed query such as `python c++` still runs as full-text. If a full-text search matches nothing, one title-substring fallback runs automatically and the response is flagged `fuzzy: true`. `q` respects the default 90-day freshness window unless a date filter is supplied. A well-formed but overly broad `q` that exceeds the search time budget returns 422 (narrow the query); a malformed `q` (too complex, or only exclusions) returns 400.

## Parameters

### Query parameters

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `title` | string | No | Case-insensitive substring match on the job title (3-200 characters). |
| `q` | string | No | Full-text search across the job. Google-style syntax: quoted `"exact phrase"`, `OR`, and `-term` exclusion (a query that is ONLY exclusions is rejected with 400). Operator precedence: implicit AND binds tighter than `OR`, and a `-term` attaches only to its adjacent group — so `"machine learning" OR "data scientist" -senior` parses as `A OR (B AND NOT senior)` and CAN still return Senior ML titles. To exclude a term everywhere, distribute it across each branch: `"machine learning" -senior OR "data scientist" -senior`. By default `q` matches the job TITLE only (precise — best for common role terms); pass `q_scope=full` to also search the description. Phrase queries under `q_scope=full` combined with very narrow filters (a small country, a rare language) can hit the search timeout; the default title scope is immune, so prefer it for phrase precision. `q` composes as a filter with the standard `sort`/`cursor` pagination (newest-first over the matches) and respects the default 90-day freshness window unless a date filter is given. Symbol-bearing tech tokens (`c++`, `c#`, `.net`) are matched as a title substring instead of full-text (they collapse under English tokenization). If a full-text search yields no rows a single title-substring fallback runs and its results are flagged `fuzzy: true`. |
| `q_scope` | string | No | Which fields `q` searches. `title` (default) matches the job title only — precise, best for common role terms (e.g. `developer`, `engineer`), which otherwise match description boilerplate. `full` also searches the description — broader reach, best for niche skills or tools that appear only in the requirements text (a language or framework not named in the title). Only affects results when `q` is set (ignored otherwise). |
| `country` | string[] | No | Filter by ISO 3166-1 alpha-2 country code(s). Repeat the parameter or comma-separate values (e.g. `country=us,de`). A job matches if it is tagged with any of the given countries. Up to 20 values; case-insensitive. |
| `state` | string[] | No | Filter by state / region reference code(s). Repeat the parameter or comma-separate values (e.g. `state=CA,NY`). A job matches if it is tagged with any of the given regions. Up to 20 values; case-insensitive. |
| `city` | string | No | Case-insensitive substring match on the city name. |
| `workplace` | string | No | Filter by workplace arrangement. |
| `employment_type` | string | No | Filter by employment type. |
| `level` | string | No | Filter by seniority level. |
| `language` | string | No | Filter by ISO 639-1 language code (2 letters, case-insensitive). |
| `salary_min` | number | No | Keep jobs whose stated salary range reaches at least this amount. Jobs that state only a minimum (no maximum) match when that minimum is at least this value. |
| `salary_max` | number | No | Keep jobs whose stated salary range starts at or below this amount. |
| `salary_currency` | string | No | Filter by ISO 4217 currency code (3 letters, case-insensitive). |
| `has_salary` | boolean | No | When `true`, return only jobs that specify a salary; when `false`, only jobs without a salary. |
| `company_id` | integer | No | Filter by the numeric company id. |
| `status` | string | No | Filter by listing status. `open` (default) returns active listings; `closed` returns delisted ones. |
| `posted_since` | string | No | Only jobs posted at or after this ISO-8601 timestamp, including a UTC offset (e.g. `2024-01-01T00:00:00+00:00`). When none of `posted_since`, `posted_until`, or `first_seen_since` are provided and `status` is `open`, results default to the last 90 days. |
| `posted_until` | string | No | Only jobs posted at or before this ISO-8601 timestamp, including a UTC offset (e.g. `2024-01-01T00:00:00+00:00`). |
| `first_seen_since` | string | No | Only jobs first seen at or after this ISO-8601 timestamp, including a UTC offset (e.g. `2024-01-01T00:00:00+00:00`). |
| `sort` | string | No | Sort field for the descending keyset pagination. Defaults to `posted_at`. A full-text `q` composes with either sort as a filter. |
| `limit` | integer | No | Maximum number of jobs to return. Defaults to 100; values above 500 are clamped to 500. |
| `cursor` | string | No | Opaque pagination cursor taken from a previous response `next_cursor`. Must be used with the same `sort` value that produced it. |
| `description` | string | No | Include the job description in each result: `off` (default, omitted), `text` (markdown as `description_md`), or `html` (`description_html`). |

## Response

A page of matching jobs.

### Response fields

| Field | Type | Description |
| --- | --- | --- |
| `data` | object[] | The page of matching jobs. |
| `next_cursor` | string \| null | Cursor for the next page, or null when there are no more results. Pass it as `cursor` on the next request with the same `sort`. |
| `limit` | integer | The effective page size applied to this response. |
| `fuzzy` | boolean | Present and `true` only when the full-text query matched no jobs and these results come from the title-substring fallback lane. Absent otherwise. |

## Authentication

Send your secret API key (prefixed `sk_`) as a bearer token in the `Authorization` header. See [Authentication](https://www.puesto.dev/docs/authentication.md).

## Errors

Errors use the RFC 7807 `application/problem+json` format. See [Errors](https://www.puesto.dev/docs/errors.md).

## Example request

```bash
curl -X GET "https://www.puesto.dev/api/v1/jobs" \
  -H "Authorization: Bearer sk_your_key_here"
```

Full spec: https://www.puesto.dev/api/v1/openapi.json

