# Search open jobs (cacheable)

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

The same results and cursors as POST /v1/jobs/search. Pass query as a JSON-encoded JobQuery. Results never depend on cookies or viewer location. Use POST for queries too large for a URL.

## Authentication

- Header `x-api-key`: Read-only API key from https://trawle.com/account#api-keys. 1,000 requests per daily window. Anonymous requests are also supported with a per-IP limit.

## Query parameters

- `query` (string, optional): JSON-encoded JobQuery; default {}. At most 6000 characters.
- `cursor` (string, optional): 1 to 2000 characters.
- `limit` (integer, optional): 1 to 100.
- `postedSince` (string<date-time>, optional)

## Responses

### 200: A page of jobs.

Schema: SearchJobsResult

- `jobs` (array<object>, required, JobSummary)
  - `id` (string, required): Stable public id; never changes across scans. Example: `019a3b6e-8f2c-7d41-9b0e-5c3f7a2d4e81`.
  - `title` (string, required)
  - `postingId` (string, optional): The concrete source posting whose facts and application link are shown.
  - `postingCount` (integer, optional): Currently publishable source postings grouped into this job; not a headcount estimate. At least 0.
  - `department` (string | null, optional): Department or team as supplied by the employer; null when not stated. Default `null`.
  - `requirements` (array<string>, optional): Up to three key must-have qualifications extracted from the posting. Preferred qualifications are excluded. Empty when not yet extracted or none are explicitly stated; not an exhaustive list. Default `[]`. At most 3 items, items: 1 to 160 characters.
  - `summary` (string | null, optional): One-paragraph summary (generated). Default `null`.
  - `experienceYears` (object | null, optional): Default `null`.
    - `min` (integer | null, required)
    - `max` (integer | null, required)
  - `company` (object, required, CompanyRef)
    - `slug` (string, required): Example: `stripe`.
    - `name` (string, required): Example: `Stripe`.
    - `domain` (string | null, required): Example: `stripe.com`.
    - `isAgency` (boolean, required): A recruitment or staffing agency posting jobs for its clients, not its own. Job search leaves these out unless `agencies` is set.
  - `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): Legacy earliest observation of a similar posting. Not continuous time open or proof of the same requisition. Example: `2026-10-31T06:12:00.000Z`.
  - `repostCount` (integer, required): Legacy total of possible earlier postings plus reopenings. Use possibleRepostCount and reopenCount separately.
  - `possibleRepostCount` (integer, optional)
  - `reopenCount` (integer, optional)
  - `historyStartedAt` (string<date-time> | null, optional): Detailed scan change capture began here. Earlier changes may be missing; null means capture has not started. Example: `2026-10-31T06:12:00.000Z`.
  - `lastListingChangeAt` (string<date-time> | null, optional): Example: `2026-10-31T06:12:00.000Z`.
  - `relatedPostings` (array<object>, optional): Up to eight earlier matching postings, strongest evidence first. source_identity requires matching URL, ATS and requisition id on a confirmed board move. A similar title/location is only possible; never automatically merge decisions for it. At most 8 items.
    - `id` (string, required)
    - `title` (string, required)
    - `firstSeenAt` (string<date-time>, required): Example: `2026-10-31T06:12:00.000Z`.
    - `confidence` (string, required): One of: `source_identity`, `possible`.
  - `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.
  - `matchReasons` (array<object>, optional): Query-specific evidence returned by search, not a candidate fit score. Classification is not proof of an employer requirement. Absent outside search.
    - `label` (string, required)
    - `evidence` (string, required)
    - `explanation` (string, required)
- `total` (integer, required): Matches, capped at 10,000 (`totalCapped`).
- `totalCapped` (boolean, required)
- `nextCursor` (string | null, required): Next page; null on the last.
- `selectedPlaces` (array<object>, optional, PlaceRef): Resolved query.places, independent of matching jobs. Includes country codes for location-aware currency defaults; unknown ids are omitted and multi-country regions have a null country.
  - `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`.
- `labels` (object, required): Names for the ids and slugs in the query, keyed `filter:value` (`places:2618425` → `Copenhagen`). Excluded companies use `companies:slug` too. Unknown inclusions match nothing; unknown exclusions remove nothing. Unknown labels are absent.
- `dataDate` (string<date-time>, required): When the data was last refreshed (daily). Example: `2026-10-31T06:12:00.000Z`.
- `nextSince` (string<date-time>, required): Safe discovery boundary, not response time. Stable across cached pages and pagination. Save only after finishing all pages; pass unchanged as postedSince next run and deduplicate by job id because boundaries overlap. May remain unchanged while scans are incomplete; Unix epoch before a safe scan exists. 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)

### 401: Invalid, expired or revoked API key.

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)

### 429: Rate limit reached. Respect Retry-After and cache results.

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 "https://trawle.com/v1/jobs/search" \
  -H "x-api-key: $TRAWLE_API_KEY"
```

### 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",
  headers: { "x-api-key": process.env.TRAWLE_API_KEY },
});

const { data, error } = await trawle.GET("/v1/jobs/search");
```

### Python

```python
import os
import requests

response = requests.get(
    "https://trawle.com/v1/jobs/search",
    headers={"x-api-key": os.environ["TRAWLE_API_KEY"]},
)
response.raise_for_status()
data = response.json()
```

### Response 200

```json
{
  "jobs": [
    {
      "id": "019a3b6e-8f2c-7d41-9b0e-5c3f7a2d4e81",
      "title": "string",
      "postingId": "string",
      "postingCount": 0,
      "department": null,
      "requirements": [],
      "summary": null,
      "experienceYears": null,
      "company": {
        "slug": "stripe",
        "name": "Stripe",
        "domain": "stripe.com",
        "isAgency": true
      },
      "role": {
        "slug": "string",
        "name": "string"
      },
      "family": {
        "slug": "string",
        "name": "string"
      },
      "locations": [
        {
          "id": 0,
          "name": "Copenhagen",
          "country": "DK"
        }
      ],
      "workplace": "remote",
      "remoteScope": "string",
      "seniority": "intern",
      "employmentType": "full_time",
      "salary": {
        "min": 0,
        "max": 0,
        "currency": "EUR",
        "period": "year"
      },
      "firstSeenAt": "2026-10-31T06:12:00.000Z",
      "lastSeenAt": "2026-10-31T06:12:00.000Z",
      "postedAt": "2026-10-31T06:12:00.000Z",
      "openSince": "2026-10-31T06:12:00.000Z",
      "repostCount": 0,
      "possibleRepostCount": 0,
      "reopenCount": 0,
      "historyStartedAt": "2026-10-31T06:12:00.000Z",
      "lastListingChangeAt": "2026-10-31T06:12:00.000Z",
      "relatedPostings": [
        {
          "id": "string",
          "title": "string",
          "firstSeenAt": "2026-10-31T06:12:00.000Z",
          "confidence": "source_identity"
        }
      ],
      "closedAt": "2026-10-31T06:12:00.000Z",
      "applyUrl": "string",
      "url": "string",
      "matchReasons": [
        {
          "label": "string",
          "evidence": "string",
          "explanation": "string"
        }
      ]
    }
  ],
  "total": 0,
  "totalCapped": true,
  "nextCursor": "string",
  "selectedPlaces": [
    {
      "id": 0,
      "name": "Copenhagen",
      "country": "DK"
    }
  ],
  "labels": {
    "key": "string"
  },
  "dataDate": "2026-10-31T06:12:00.000Z",
  "nextSince": "2026-10-31T06:12: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"
      }
    ]
  }
}
```

### Response 401

```json
{
  "error": {
    "code": "unauthorized",
    "message": "Sign in to use this",
    "hint": "Sign in at /sign-in; requests carry the session cookie."
  }
}
```

### Response 429

```json
{
  "error": {
    "code": "invalid_request",
    "message": "places: expected an array of GeoNames ids",
    "hint": "Resolve place names to ids with GET /v1/places?q=",
    "issues": [
      {
        "path": "string",
        "message": "string"
      }
    ]
  }
}
```
