# Search open jobs

`POST https://trawle.com/v1/jobs/search`

Open jobs matching a `JobQuery`, newest first (or by pay). Paginate with `cursor`. For a daily "what's new" run, store `nextSince` and pass it back as `postedSince` next time. Data refreshes once a day.

## Request body

`application/json`, required (SearchJobsRequest)

- `query` (object, required, JobQuery): A job search. Values inside one filter are OR, filters are AND. The same object is used by the website's links and saved views.
  - `q` (string, optional): Free text. Every word must start a word of the job title or department (any order, `react` matches "React/TypeScript"), or every word must start a word of the company name. 2 to 100 characters. Example: `react`.
  - `roles` (array<string>, optional): Role slugs, any of them. Look slugs up with `GET /v1/roles?q=` (MCP `find_occupation`). 1 to 20 items, items: pattern ^[a-z0-9][a-z0-9-]{0,79}$. Example: `["frontend-engineer"]`.
  - `families` (array<string>, optional): Job family slugs (any role in the field). List them with `GET /v1/families`. 1 to 10 items, items: pattern ^[a-z0-9][a-z0-9-]{0,79}$. Example: `["software-engineering"]`.
  - `places` (array<integer>, optional): Where the person wants to work from, as GeoNames ids (multi-country regions are negative, `-1` is worldwide). Matches onsite/hybrid jobs located in the place plus remote jobs open to people living there. Look ids up with `GET /v1/places?q=` (MCP `resolve_location`). 1 to 20 items. Example: `[2618425]`.
  - `companies` (array<string>, optional): Company slugs. Look them up with `GET /v1/companies?q=`. 1 to 50 items, items: pattern ^[a-z0-9][a-z0-9-]{0,79}$. Example: `["stripe"]`.
  - `workplace` (array<string>, optional): One of: `remote`, `hybrid`, `onsite`. At least 1 item.
  - `seniority` (array<string>, optional): One of: `intern`, `entry`, `mid`, `senior`, `lead`, `principal`, `director`, `vp`, `executive`. At least 1 item.
  - `employment` (array<string>, optional): One of: `full_time`, `part_time`, `contract`, `temporary`, `internship`, `apprenticeship`, `seasonal`, `volunteer`. At least 1 item.
  - `postingLanguages` (array<string>, optional): Languages the posting is written in, ISO 639-1, any of them. Detected from the description; jobs without one never match. Not the languages a job requires (`JobDetail.languages`). 1 to 10 items, items: pattern ^[a-z]{2}$. Example: `["en"]`.
  - `salary` (object, optional, SalaryRange): Annual pay range, both ends inclusive, either end open (null). Jobs match when their own range overlaps it after converting currencies; jobs without pay never match.
    - `min` (integer | null, required): At least 0. Example: `60000`.
    - `max` (integer | null, required): At least 0.
    - `currency` (string, required): One of: `USD`, `EUR`, `GBP`, `DKK`, `SEK`, `NOK`, `CHF`, `CAD`, `AUD`.
  - `pay` (string, optional): `listed`: only jobs that show pay. One of: `listed`.
  - `posted` (string, optional): First seen by Trawle within this window. For scheduled agents, prefer `postedSince` with the `nextSince` of the previous run. One of: `1d`, `3d`, `7d`, `30d`.
  - `reposts` (string, optional): Reposts are jobs seen before under another id, or closed and reopened. `hide` keeps only originals. One of: `hide`, `only`.
  - `sort` (string, optional): `newest` (default) or `pay` (highest annual pay first; only jobs that show pay). One of: `newest`, `pay`.
- `cursor` (string | null, optional): `nextCursor` from the previous page.
- `limit` (integer, optional): Default 25. 1 to 100.
- `postedSince` (string<date-time>, optional): Only jobs first seen at or after this instant. Scheduled agents pass the `nextSince` of their previous run to get only new jobs. Example: `2026-10-31T06:12:00.000Z`.

## Responses

### 200: A page of jobs.

Schema: SearchJobsResult

- `jobs` (array<object>, required, JobSummary)
  - `id` (string, required): Stable public id; never changes across scans. Example: `1234567`.
  - `title` (string, required)
  - `company` (object, required, CompanyRef)
    - `slug` (string, required): Example: `stripe`.
    - `name` (string, required): Example: `Stripe`.
    - `domain` (string | null, required): Example: `stripe.com`.
  - `role` (object | null, required, Ref)
    - `slug` (string, required)
    - `name` (string, required)
  - `family` (object | null, required, Ref)
    - `slug` (string, required)
    - `name` (string, required)
  - `locations` (array<object>, required, PlaceRef): Where the job is located (or, for remote jobs, who may apply), most specific first.
    - `id` (integer, required): GeoNames id (regions are negative).
    - `name` (string, required): Example: `Copenhagen`.
    - `country` (string | null, required): ISO 3166-1 alpha-2 country code; null for multi-country regions. Example: `DK`.
  - `workplace` (string | null, required): One of: `remote`, `hybrid`, `onsite`.
  - `remoteScope` (string | null, required): For remote jobs: who may apply, in words ("Anywhere", "United States only").
  - `seniority` (string | null, required): One of: `intern`, `entry`, `mid`, `senior`, `lead`, `principal`, `director`, `vp`, `executive`.
  - `employmentType` (string | null, required): One of: `full_time`, `part_time`, `contract`, `temporary`, `internship`, `apprenticeship`, `seasonal`, `volunteer`.
  - `salary` (object | null, required, Salary): Pay as the employer listed it (not converted).
    - `min` (integer | null, required)
    - `max` (integer | null, required)
    - `currency` (string | null, required): ISO 4217 code. Example: `EUR`.
    - `period` (string, required): One of: `year`, `month`, `week`, `day`, `hour`.
  - `firstSeenAt` (string<date-time>, required): First seen by Trawle under this id. Example: `2026-10-31T06:12:00.000Z`.
  - `lastSeenAt` (string<date-time>, required): Last time a daily scan saw it open on the employer's site. Example: `2026-10-31T06:12:00.000Z`.
  - `postedAt` (string<date-time> | null, required): "Date posted" from the employer's ATS, when it has one. Often unreliable. Example: `2026-10-31T06:12:00.000Z`.
  - `openSince` (string<date-time>, required): First time any posting of this job (reposts included) was seen; days open count from here. Example: `2026-10-31T06:12:00.000Z`.
  - `repostCount` (integer, required): Earlier postings of this job under other ids, plus reopenings. 0 = original.
  - `closedAt` (string<date-time> | null, required): When it disappeared; null while open. Example: `2026-10-31T06:12:00.000Z`.
  - `applyUrl` (string, required): The employer's own posting (their ATS).
  - `url` (string, required): This job on Trawle.
- `total` (integer, required): Matches, capped at 10,000 (`totalCapped`).
- `totalCapped` (boolean, required)
- `nextCursor` (string | null, required): Next page; null on the last.
- `labels` (object, required): Names for the ids and slugs in the query, keyed `filter:value` (`places:2618425` → `Copenhagen`). Unknown values are absent and matched nothing.
- `dataDate` (string<date-time>, required): When the data was last refreshed (daily). Example: `2026-10-31T06:12:00.000Z`.
- `nextSince` (string<date-time>, required): Pass as `postedSince` on the next scheduled run to get only jobs that are new since this one. Example: `2026-10-31T06:12:00.000Z`.

### 400: Invalid request: see `error.issues` and `error.hint`.

Schema: Error

- `error` (object, required)
  - `code` (string, required): Machine-readable: `invalid_request`, `not_found`, `forbidden`, `rate_limited`, `internal`. Example: `invalid_request`.
  - `message` (string, required): Example: `places: expected an array of GeoNames ids`.
  - `hint` (string, optional): What to do next, e.g. which lookup to call first. Example: `Resolve place names to ids with GET /v1/places?q=`.
  - `issues` (array<object>, optional): Validation problems, one per invalid field.
    - `path` (string, required)
    - `message` (string, required)

## Examples

### cURL

```bash
curl -X POST "https://trawle.com/v1/jobs/search" \
  -H "Content-Type: application/json" \
  -d '{
  "query": {
    "roles": ["frontend-engineer"],
    "places": [2618425],
    "workplace": ["hybrid", "remote"]
  },
  "limit": 25
}'
```

### TypeScript

```ts
import createClient from "openapi-fetch";

// npx openapi-typescript https://developers.trawle.com/openapi.json -o trawle.d.ts
import type { paths } from "./trawle";

const trawle = createClient<paths>({ baseUrl: "https://trawle.com" });

const { data, error } = await trawle.POST("/v1/jobs/search", {
  body: {
    query: {
      roles: ["frontend-engineer"],
      places: [2618425],
      workplace: ["hybrid", "remote"],
    },
    limit: 25,
  },
});
```

### Python

```python
import requests

response = requests.post(
    "https://trawle.com/v1/jobs/search",
    json={
        "query": {
            "roles": ["frontend-engineer"],
            "places": [2618425],
            "workplace": ["hybrid", "remote"],
        },
        "limit": 25,
    },
)
response.raise_for_status()
data = response.json()
```

### Response 200

```json
{
  "jobs": [
    {
      "id": "4419023",
      "title": "Senior Frontend Engineer",
      "company": {
        "slug": "lumen-labs",
        "name": "Lumen Labs",
        "domain": "lumenlabs.example"
      },
      "role": {
        "slug": "frontend-engineer",
        "name": "Frontend Engineer"
      },
      "family": {
        "slug": "software-engineering",
        "name": "Software Engineering"
      },
      "locations": [
        {
          "id": 2618425,
          "name": "Copenhagen",
          "country": "DK"
        }
      ],
      "workplace": "hybrid",
      "remoteScope": null,
      "seniority": "senior",
      "employmentType": "full_time",
      "salary": {
        "min": 720000,
        "max": 900000,
        "currency": "DKK",
        "period": "year"
      },
      "firstSeenAt": "2026-10-30T06:12:00.000Z",
      "lastSeenAt": "2026-10-31T06:12:00.000Z",
      "postedAt": "2026-10-29T00:00:00.000Z",
      "openSince": "2026-09-14T06:10:00.000Z",
      "repostCount": 1,
      "closedAt": null,
      "applyUrl": "https://job-boards.greenhouse.io/lumenlabs/jobs/7712001",
      "url": "https://trawle.com/jobs/4419023"
    }
  ],
  "total": 38,
  "totalCapped": false,
  "nextCursor": "MjAyNi0xMC0zMFQwNjoxMjowMC4wMDBafDQ0MTkwMjM",
  "labels": {
    "roles:frontend-engineer": "Frontend Engineer",
    "places:2618425": "Copenhagen"
  },
  "dataDate": "2026-10-31T06:12:00.000Z",
  "nextSince": "2026-10-31T09:30:00.000Z"
}
```

### Response 400

```json
{
  "error": {
    "code": "invalid_request",
    "message": "query.places.0: Invalid input: expected number, received string",
    "hint": "Fix the fields in `issues`. Ids for `places`, `roles` and `companies` come from the lookup endpoints.",
    "issues": [
      {
        "path": "query.places.0",
        "message": "Invalid input: expected number, received string"
      }
    ]
  }
}
```
