# Trawle for developers Search open jobs read straight from employers' own applicant tracking systems, checked every day, with closure and repost history. Trawle is a job index built from employers' own careers pages: Greenhouse, Lever, Ashby, Workday and more than fifteen other applicant tracking systems. Every board is scanned once a day. Jobs that disappear are marked closed, reposts are linked to the original, and every job knows how long it has really been open. You can search it with a JSON API, or let an AI agent search it through the MCP server. ## Where to start - [Quickstart](https://developers.trawle.com/quickstart): your first search, building a query from lookups, reading results. - [Search jobs API](https://developers.trawle.com/api): the reference, generated from the OpenAPI spec. - [MCP for agents](https://developers.trawle.com/mcp): tools and a daily job search prompt for AI assistants (preview). - [Guides](https://developers.trawle.com/guides/daily-new-jobs): daily new-jobs checks, building a `JobQuery`, pagination, errors, freshness, attribution. ## Good to know - Data refreshes once a day. Polling more often returns the same results. - No API key is needed yet. API keys are coming soon; the free tier is planned at about 1,000 requests a day, with attribution. - Results link to the employer's posting (`applyUrl`) and to Trawle (`url`). Show "Data by Trawle" with a link when you display them. - Full job descriptions are not served. Link to `applyUrl` instead. ## Machine-readable - [/llms.txt](https://developers.trawle.com/llms.txt) and [/llms-full.txt](https://developers.trawle.com/llms-full.txt) - [/openapi.json](https://developers.trawle.com/openapi.json): the public OpenAPI 3.1 spec - Every page is also available as Markdown: add `.md` to its URL. --- # Quickstart Make your first search, build a JobQuery from lookups, and read the results. The API lives at `https://trawle.com/v1`. It speaks JSON, needs no key for now, and allows calls from any origin, so you can try it from a terminal or a browser. ## 1. Search `POST /v1/jobs/search` takes a `JobQuery` and returns a page of open jobs, newest first. Free text is the quickest start: ```bash curl -X POST "https://trawle.com/v1/jobs/search" \ -H "Content-Type: application/json" \ -d '{ "query": { "q": "react" }, "limit": 5 }' ``` ```ts const response = await fetch("https://trawle.com/v1/jobs/search", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ query: { q: "react" }, limit: 5 }), }); const result = await response.json(); ``` ```python import requests response = requests.post( "https://trawle.com/v1/jobs/search", json={"query": {"q": "react"}, "limit": 5}, ) result = response.json() ``` The request body, for reference: ```json { "query": { "q": "react" }, "limit": 5 } ``` ## 2. Build a query from lookups Free text matches titles, departments and company names. For precise results, filter by ids instead. Each filter has a lookup endpoint that turns words into ids: | Filter | Lookup | Use | | ----------- | ----------------------------------------------- | -------------------------- | | `places` | [`GET /v1/places?q=`](https://developers.trawle.com/api/resolve-location) | `items[].id` (GeoNames id) | | `roles` | [`GET /v1/roles?q=`](https://developers.trawle.com/api/find-roles) | `items[].slug` | | `companies` | [`GET /v1/companies?q=`](https://developers.trawle.com/api/search-companies) | `items[].slug` | | `families` | [`GET /v1/families`](https://developers.trawle.com/api/list-families) | `items[].slug` | Look up Copenhagen: ```bash curl "https://trawle.com/v1/places?q=copenhagen" ``` ```json { "items": [ { "id": 2618425, "name": "Copenhagen", "country": "DK", "kind": "city", "context": "Capital Region, Denmark" } ] } ``` Then the role, with `GET /v1/roles?q=frontend`, which returns `frontend-engineer`. Put both into the query, and narrow it to hybrid and remote jobs: ```json { "query": { "roles": ["frontend-engineer"], "places": [2618425], "workplace": ["hybrid", "remote"] }, "limit": 25 } ``` Values inside one filter are OR, filters are AND: frontend jobs, in or open to people in Copenhagen, that are hybrid or remote. [Building a JobQuery](https://developers.trawle.com/guides/building-a-job-query) explains every filter. ## 3. Read the results ```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" } ``` A few fields worth knowing: - `applyUrl` is the employer's own posting. Send people there to read the full description and apply. - `url` is the job on Trawle. - `openSince` and `repostCount` tell you how long the job has really been open. This one was first posted in September and reposted once; see [Freshness and reposts](https://developers.trawle.com/guides/freshness-and-reposts). - `labels` names the ids in your query. An id that is missing from `labels` matched nothing. - `nextCursor` fetches the next page ([Pagination](https://developers.trawle.com/guides/pagination)); `nextSince` gets only new jobs on your next run ([Daily new-jobs check](https://developers.trawle.com/guides/daily-new-jobs)). ## 4. Show attribution When you display Trawle results, show "Data by Trawle" with a link to `https://trawle.com` (or to each job's `url`), and link each job to its `applyUrl`. See [Attribution and data policy](https://developers.trawle.com/guides/attribution). ## Typed clients The [OpenAPI spec](https://developers.trawle.com/openapi.json) is complete enough to generate a client. For TypeScript, `openapi-typescript` plus `openapi-fetch` gives typed requests and responses: ```bash npx openapi-typescript https://developers.trawle.com/openapi.json -o trawle.d.ts npm install openapi-fetch ``` ```ts import createClient from "openapi-fetch"; import type { paths } from "./trawle"; const trawle = createClient({ baseUrl: "https://trawle.com" }); const { data, error } = await trawle.POST("/v1/jobs/search", { body: { query: { roles: ["frontend-engineer"], places: [2618425] } }, }); ``` Every page in the [API reference](https://developers.trawle.com/api) has samples for cURL, TypeScript and Python. --- # Daily new-jobs check for agents Run the same search once a day and get only the jobs that are new since the last run, with postedSince and nextSince. The most common thing an agent does with Trawle is a scheduled check: "tell me about new frontend jobs in Copenhagen every morning." The API is built for this. ## How it works Every search response includes `nextSince`, the time of that search. Store it. On the next run, send it back as `postedSince`, and you get only jobs that Trawle first saw after your previous run. ```json { "query": { "roles": ["frontend-engineer"], "places": [2618425] }, "postedSince": "2026-10-30T07:00:00.000Z" } ``` No overlap and no gaps: a job first seen between two runs shows up in exactly one of them. ## Run once a day Trawle scans every employer board once a day, starting early in the morning (UTC). `dataDate` in each response says when the data last changed. Running more often than daily just returns empty results; running less often is fine, because `postedSince` catches up on everything since the stored time. ## A complete run ```ts type State = { since?: string }; async function dailyCheck(state: State) { const jobs = []; let cursor: string | null = null; let nextSince: string | undefined; do { const response = await fetch("https://trawle.com/v1/jobs/search", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ query: { roles: ["frontend-engineer"], places: [2618425] }, postedSince: state.since, cursor, limit: 100, }), }); const page = await response.json(); jobs.push(...page.jobs); cursor = page.nextCursor; nextSince ??= page.nextSince; // keep the first page's value } while (cursor); state.since = nextSince; // store only after the run succeeded return jobs; } ``` On the first run there is no stored time. Leave out `postedSince` and you get every matching open job, or use `query.posted: "1d"` (or `"7d"`) to start with a short window. ## Tips - Store `nextSince` from the first page, and only after the whole run succeeds. If a run fails, the next one picks up from the older time and nothing is lost. - Keep the query identical between runs. A changed query with an old `postedSince` only returns jobs new since then, not every match for the new query. - `postedSince` counts from when Trawle first saw the job (`firstSeenAt`), not the employer's "date posted", which is often unreliable. - Reposts count as new: a job taken down and posted again under a new id has a new `firstSeenAt`. Add `query.reposts: "hide"` to skip them, or check `repostCount`. - The MCP server offers the same pattern as a `daily_job_search` prompt. See [MCP for agents](https://developers.trawle.com/mcp). --- # Building a JobQuery How the filters in a JobQuery combine, how places match remote jobs, and where ids come from. A `JobQuery` is the one object that describes a job search. The same object drives the API, the MCP server, and the links and saved searches on trawle.com. The [schema](https://developers.trawle.com/api/search-jobs) lists every field; this page explains how they combine. ## OR inside a filter, AND across filters ```json { "roles": ["frontend-engineer", "fullstack-engineer"], "places": [2618425, 2673730], "seniority": ["senior", "lead"] } ``` This reads: (frontend **or** full-stack) **and** (Copenhagen **or** Stockholm) **and** (senior **or** lead). An empty query matches every open job. ## Ids come from lookups Filters take ids and slugs, not names, so a query means the same thing every time. | Filter | Takes | Look up with | | ----------- | ------------- | ----------------------------------------------- | | `places` | GeoNames ids | [`GET /v1/places?q=`](https://developers.trawle.com/api/resolve-location) | | `roles` | role slugs | [`GET /v1/roles?q=`](https://developers.trawle.com/api/find-roles) | | `families` | family slugs | [`GET /v1/families`](https://developers.trawle.com/api/list-families) | | `companies` | company slugs | [`GET /v1/companies?q=`](https://developers.trawle.com/api/search-companies) | Role lookups match common job titles as well as role names: `q=react` finds Frontend Engineer. Place lookups return a `context` ("Washington, United States") to tell homonyms apart. Unknown slugs and ids match nothing, never everything, and they are left out of the response's `labels`. If a value you sent is missing from `labels`, look it up again. ## Places: where the person wants to work from `places` describes where the job seeker is, not only where the office is. A place matches: - onsite and hybrid jobs located in that place, and - remote jobs open to people living there, such as a remote job open to "Europe" or "Anywhere". So `places: [2618425]` (Copenhagen) includes a hybrid job in Copenhagen and a remote job open to anyone in the EU, but not a remote job limited to the United States. Each remote job's `remoteScope` says who may apply, in words. To only get one kind, combine with `workplace`: ```json { "places": [2618425], "workplace": ["remote"] } ``` Places can be cities, metro areas, states or regions (`admin1`), countries, or multi-country regions. Multi-country regions such as Europe or EMEA have **negative ids**; `-1` is worldwide. Find them through the same lookup (`GET /v1/places?q=europe`). ## Other filters - `q`: free text over title, department and company name (2 to 100 characters). Use it for things that have no filter, like a technology. - `workplace`, `seniority`, `employment`: fixed values; see the schema. - `salary`: an annual range, for example `{ "min": 60000, "max": null, "currency": "EUR" }`. Jobs match when their own range overlaps it after converting currencies. Jobs that don't show pay never match. - `pay: "listed"`: only jobs that show pay. - `posted`: first seen within `1d`, `3d`, `7d` or `30d`. For scheduled runs prefer `postedSince` ([guide](https://developers.trawle.com/guides/daily-new-jobs)). - `reposts`: `hide` keeps only originals, `only` shows only reposts. - `sort`: `newest` (default) or `pay`, highest annual pay first, which also limits results to jobs that show pay. ```json { "families": ["software-engineering"], "places": [-1], "workplace": ["remote"], "salary": { "min": 100000, "max": null, "currency": "USD" }, "reposts": "hide", "sort": "pay" } ``` --- # Pagination Page through search results with nextCursor. Search results come in pages of up to 100 jobs (`limit`, default 25). Each response has a `nextCursor`; pass it back as `cursor` to get the next page. On the last page `nextCursor` is `null`. ```json { "query": { "roles": ["frontend-engineer"] }, "cursor": "MjAyNi0xMC0zMFQwNjoxMjowMC4wMDBafDQ0MTkwMjM", "limit": 100 } ``` ## Rules - Send the same `query` (including `sort`) and `postedSince` with every page. A cursor from a different search is rejected with `invalid_request`. - Treat the cursor as opaque. Don't build or edit one. - Pages are keyset-based, so they stay stable while you page: you won't see the same job twice or skip one because results shifted. ## Totals `total` is the number of matches, capped at 10,000. When the real number is higher, `totalCapped` is `true`; narrow the query rather than paging through everything. ## Lookups Lookup endpoints (`/v1/places`, `/v1/roles`, `/v1/companies`) aren't paginated. They return the best matches first, up to `limit` (default 8, at most 20). `/v1/families` returns every family. --- # Errors Every error has the same shape, with a machine-readable code and a hint about what to do next. Every error, from every operation, has the same shape: ```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" } ] } } ``` - `code` is stable and machine-readable. Branch on it, not on `message`. - `message` is for people and logs. - `hint`, when present, says what to do next, often which lookup to call first. Agents can act on it directly. - `issues`, on validation errors, lists one problem per invalid field, with a dotted `path` into your request. ## Codes | Code | Status | Meaning | | ----------------- | ------ | -------------------------------------------------------------------- | | `invalid_request` | 400 | The request doesn't match the spec. See `issues`. | | `unauthorized` | 401 | The operation needs you to be signed in (account operations). | | `forbidden` | 403 | The operation isn't public. | | `not_found` | 404 | No job, company or agent with that id or slug, or no such operation. | | `conflict` | 409 | The change conflicts with what exists, such as too many agents. | | `rate_limited` | 429 | Too many requests. Wait and retry. Rate limits arrive with API keys. | | `internal` | 500 | Something went wrong on our side. Retry later. | | `unavailable` | 503 | A dependency is briefly unavailable. Retry later. | ## Not found is not an error in search A search with unknown ids or slugs is valid and matches nothing; it returns `200` with no jobs. Check the response's `labels`: values that were not recognized are missing from it. A closed job is not an error either. `GET /v1/jobs/{id}` returns closed jobs too, with `closedAt` set. --- # Freshness and reposts What firstSeenAt, lastSeenAt, openSince, repostCount and closedAt mean, and how Trawle detects closures and reposts. Trawle reads every employer's board once a day. Comparing one day with the next tells it which jobs are new, which are still open and which have closed. That history is in five fields on every job. | Field | Meaning | | ------------- | ------------------------------------------------------------------------------------------------------------------- | | `firstSeenAt` | First time Trawle saw the job under this id. `postedSince` and `posted` filter on this. | | `lastSeenAt` | Last daily scan that saw it open on the employer's site. For open jobs, that's the latest scan. | | `postedAt` | The employer's own "date posted", when their system has one. Often unreliable: some systems reset it on every edit. | | `openSince` | First time any posting of this job was seen, reposts included. Days open count from here. | | `repostCount` | Earlier postings of the same job under other ids, plus times it closed and reopened. `0` means original. | | `closedAt` | When it disappeared from the employer's site; `null` while open. | ## Closures When a job is gone from the employer's board, Trawle marks it closed (`closedAt`) and drops it from search within a day. `GET /v1/jobs/{id}` still returns it, so links you stored keep working. If a board can't be read on a given day (the site is down, say), its jobs are left as they were rather than closed. ## Reposts Some employers take a job down and post it again under a new id, so it looks fresh. Trawle links the new posting to the old one (same company, title and location) and counts it in `repostCount`. `openSince` carries over, so a job reposted every month still shows how long it has really been open. ```json { "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": null, "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" } ``` This posting is two days old (`firstSeenAt`), but the job has been open since September 14 (`openSince`) and was posted once before. To skip reposts in a search, use `query.reposts: "hide"`; to find only them, `"only"`. ## History Trawle's own history starts at `historyStart` in [`GET /v1/stats`](https://developers.trawle.com/api/get-stats). For jobs that were already open then, `openSince` is the first day Trawle saw them, and `postedAt` is the best hint of their real age. --- # Attribution and data policy Show "Data by Trawle" with a link, link to the employer's posting, and don't republish full descriptions. Trawle's data is free to use in your apps and agents, on a few conditions. ## Attribution Wherever you show Trawle results to people, show **"Data by Trawle"** with a link to `https://trawle.com` or to the job's `url`. A small line under a list is enough: ```html

Data by Trawle

``` Agents that answer in text should say where the jobs came from and include the links. ## Link to the employer Every job has an `applyUrl`, the employer's own posting. Link to it so people read the full description and apply with the employer directly. Don't put your own application flow in between, and don't auto-apply on people's behalf. ## Descriptions The API returns a summary of each job, never the full description. Employers own their descriptions; read them at `applyUrl`. Don't scrape and republish them. ## Use - Cache results for up to a day; the data only changes once a day anyway. - Employers who want their jobs removed can email [hello@trawle.com](mailto:hello@trawle.com). ## Limits and keys No key is needed today. API keys and OAuth for apps acting on someone's behalf are coming soon. The free tier is planned at about **1,000 requests a day**, with attribution. Heavier use will need a key; we'll announce plans before limits apply. --- # MCP for agents The hosted Trawle MCP server gives AI assistants tools to search jobs, resolve places and roles, and run a daily job search. Coming soon. > **Preview.** The MCP server is being built and is not live yet. Names and details below may change before launch. The Trawle MCP server will let AI assistants such as Claude and ChatGPT search Trawle directly. It will be hosted at: ```text https://trawle.com/mcp ``` It uses the same data and the same `JobQuery` as the [API](https://developers.trawle.com/api), and the same rules: data refreshes daily, results link to the employer's posting, and "Data by Trawle" should be shown with them. ## Tools | Tool | Does | API equivalent | | ------------------ | ----------------------------------------------------------------------------- | ----------------------------------------------- | | `search_jobs` | Search open jobs with a `JobQuery`; supports `postedSince` for new-only runs. | [`POST /v1/jobs/search`](https://developers.trawle.com/api/search-jobs) | | `get_job` | One job, open or closed, with its history. | [`GET /v1/jobs/{id}`](https://developers.trawle.com/api/get-job) | | `check_status` | Whether jobs are still open, for a list the user saved earlier. | [`POST /v1/jobs/status`](https://developers.trawle.com/api/check-job-status) | | `resolve_location` | Place names to ids for `places`. | [`GET /v1/places`](https://developers.trawle.com/api/resolve-location) | | `find_occupation` | Job titles to role slugs for `roles`. | [`GET /v1/roles`](https://developers.trawle.com/api/find-roles) | | `get_company` | A company and its hiring activity. | [`GET /v1/companies/{slug}`](https://developers.trawle.com/api/get-company) | | `search_companies` | Company names to slugs for `companies` and `get_company`. | [`GET /v1/companies`](https://developers.trawle.com/api/search-companies) | | `list_families` | Job families for `families`. | [`GET /v1/families`](https://developers.trawle.com/api/list-families) | ## Prompt `daily_job_search` sets up the [daily new-jobs check](https://developers.trawle.com/guides/daily-new-jobs): it resolves the user's places and roles to ids once, runs the search, and keeps `nextSince` so each run only reports jobs that are new. ## Until then Agents can use the API today: it needs no key, and the [OpenAPI spec](https://developers.trawle.com/openapi.json) and [llms.txt](https://developers.trawle.com/llms.txt) describe it in full. --- # Trawle API reference Base URL: `https://trawle.com`. Version 1.0.0. Open jobs straight from employers' own applicant tracking systems, verified daily, with closure and repost history. - Data refreshes once a day; polling more often returns the same results. - Build a `JobQuery` from ids: resolve place names with `GET /v1/places`, roles with `GET /v1/roles`, companies with `GET /v1/companies`. - For a daily "new jobs" check, pass the previous response's `nextSince` as `postedSince`. - Results link to the employer's posting (`applyUrl`) and to Trawle (`url`). Show "Data by Trawle" with a link when you display them. - Full job descriptions are not served; link to `applyUrl`. ## Jobs - [Search open jobs](https://developers.trawle.com/api/search-jobs.md): `POST /v1/jobs/search` - [Check whether jobs are still open](https://developers.trawle.com/api/check-job-status.md): `POST /v1/jobs/status` - [Get a job](https://developers.trawle.com/api/get-job.md): `GET /v1/jobs/{id}` ## Meta - [Index size and freshness](https://developers.trawle.com/api/get-stats.md): `GET /v1/stats` ## Companies - [Get a company and its hiring activity](https://developers.trawle.com/api/get-company.md): `GET /v1/companies/{slug}` - [Find companies by name or domain](https://developers.trawle.com/api/search-companies.md): `GET /v1/companies` ## Lookups - [Find places (GeoNames) for the `places` filter](https://developers.trawle.com/api/resolve-location.md): `GET /v1/places` - [Find roles for the `roles` filter](https://developers.trawle.com/api/find-roles.md): `GET /v1/roles` - [List job families for the `families` filter](https://developers.trawle.com/api/list-families.md): `GET /v1/families` Base URL: `https://trawle.com`. JSON in, JSON out. OpenAPI 3.1 spec: [/openapi.json](https://developers.trawle.com/openapi.json). ## About the API Open jobs straight from employers' own applicant tracking systems, verified daily, with closure and repost history. - Data refreshes once a day; polling more often returns the same results. - Build a `JobQuery` from ids: resolve place names with `GET /v1/places`, roles with `GET /v1/roles`, companies with `GET /v1/companies`. - For a daily "new jobs" check, pass the previous response's `nextSince` as `postedSince`. - Results link to the employer's posting (`applyUrl`) and to Trawle (`url`). Show "Data by Trawle" with a link when you display them. - Full job descriptions are not served; link to `applyUrl`. ## Authentication No key is needed yet, and any origin may call the API from a browser. API keys and OAuth are coming soon; the free tier is planned at about 1,000 requests a day, with attribution. ## Errors Every error has the same shape, `{ error: { code, message, hint?, issues? } }`. See [Errors](https://developers.trawle.com/guides/errors). --- # 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, 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, 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, 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, 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, optional): One of: `remote`, `hybrid`, `onsite`. At least 1 item. - `seniority` (array, optional): One of: `intern`, `entry`, `mid`, `senior`, `lead`, `principal`, `director`, `vp`, `executive`. At least 1 item. - `employment` (array, optional): One of: `full_time`, `part_time`, `contract`, `temporary`, `internship`, `apprenticeship`, `seasonal`, `volunteer`. At least 1 item. - `postingLanguages` (array, 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, 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, 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, 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, required): First seen by Trawle under this id. Example: `2026-10-31T06:12:00.000Z`. - `lastSeenAt` (string, required): Last time a daily scan saw it open on the employer's site. Example: `2026-10-31T06:12:00.000Z`. - `postedAt` (string | null, required): "Date posted" from the employer's ATS, when it has one. Often unreliable. Example: `2026-10-31T06:12:00.000Z`. - `openSince` (string, 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 | 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, required): When the data was last refreshed (daily). Example: `2026-10-31T06:12:00.000Z`. - `nextSince` (string, 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, 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({ 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" } ] } } ``` --- # Check whether jobs are still open `POST https://trawle.com/v1/jobs/status` Bulk status for up to 100 job ids from earlier results: `open`, `closed` (with `closedAt`) or `not_found`. Use it to drop closed jobs from a list you are tracking, instead of fetching each job. Data refreshes once a day. ## Request body `application/json`, required (CheckJobStatusRequest) - `ids` (array, required): Job ids from earlier search results (`jobs[].id`), up to 100. 1 to 100 items, items: 1 to 20 characters. Example: `["1234567","1234568"]`. ## Responses ### 200: One status per id. Schema: CheckJobStatusResult - `items` (array, required, JobStatus): One item per distinct id asked for, in the order given. - `id` (string, required): Example: `1234567`. - `status` (string, required): `open`: still on the employer's site at the last daily scan. `closed`: gone from it. `not_found`: not a Trawle job id (or archived long ago); treat it as closed. One of: `open`, `closed`, `not_found`. - `closedAt` (string | null, required): When it disappeared; null unless closed. Example: `2026-10-31T06:12:00.000Z`. - `lastSeenAt` (string | null, required): Last daily scan that saw it open; null when not found. 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, optional): Validation problems, one per invalid field. - `path` (string, required) - `message` (string, required) ## Examples ### cURL ```bash curl -X POST "https://trawle.com/v1/jobs/status" \ -H "Content-Type: application/json" \ -d '{ "ids": ["1234567", "1234568"] }' ``` ### 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({ baseUrl: "https://trawle.com" }); const { data, error } = await trawle.POST("/v1/jobs/status", { body: { ids: ["1234567", "1234568"], }, }); ``` ### Python ```python import requests response = requests.post( "https://trawle.com/v1/jobs/status", json={ "ids": ["1234567", "1234568"], }, ) response.raise_for_status() data = response.json() ``` ### Response 200 ```json { "items": [ { "id": "1234567", "status": "open", "closedAt": "2026-10-31T06:12:00.000Z", "lastSeenAt": "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" } ] } } ``` --- # Get a job `GET https://trawle.com/v1/jobs/{id}` One job, open or closed, with the company's hiring activity. Closed jobs have `closedAt` set. ## Path parameters - `id` (string, required): 1 to 20 characters. Example: `1234567`. ## Responses ### 200: The job. Schema: JobDetail - `id` (string, required): Stable public id; never changes across scans. Example: `1234567`. - `title` (string, required) - `company` (object, required) - `slug` (string, required): Example: `stripe`. - `name` (string, required): Example: `Stripe`. - `domain` (string | null, required): Example: `stripe.com`. - `openJobs` (integer, required) - `newLast30` (integer, required) - `trend` (array, required): Open jobs per day, oldest first, the last 90 days. - `role` (object | null, required, Ref) - `slug` (string, required) - `name` (string, required) - `family` (object | null, required, Ref) - `slug` (string, required) - `name` (string, required) - `locations` (array, 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, required): First seen by Trawle under this id. Example: `2026-10-31T06:12:00.000Z`. - `lastSeenAt` (string, required): Last time a daily scan saw it open on the employer's site. Example: `2026-10-31T06:12:00.000Z`. - `postedAt` (string | null, required): "Date posted" from the employer's ATS, when it has one. Often unreliable. Example: `2026-10-31T06:12:00.000Z`. - `openSince` (string, 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 | 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. - `department` (string | null, required) - `summary` (string | null, required): One-paragraph summary (generated). - `skills` (array, required) - `experienceYears` (object | null, required) - `min` (integer | null, required) - `max` (integer | null, required) - `visaSponsorship` (string | null, required): One of: `yes`, `no`. - `education` (string | null, required): Minimum required degree, if stated. One of: `none`, `high_school`, `vocational`, `associate`, `bachelor`, `master`, `doctorate`. - `managesPeople` (boolean | null, required): Has direct reports (true), individual contributor (false), or not stated. - `relocation` (boolean | null, required): Relocation support offered (true), explicitly not (false), or not stated. - `clearanceRequired` (boolean | null, required): A government security clearance is required (true), explicitly not (false), or not stated. - `equityOffered` (boolean | null, required): Equity (stock options, RSUs) offered (true), explicitly not (false), or not stated. - `languages` (array, required): Required languages, ISO 639-1. - `postingLanguage` (string | null, required): Language the posting is written in, ISO 639-1 (detected). Example: `en`. - `source` (string, required): The ATS the job was read from ("greenhouse"). Example: `greenhouse`. ### 404: Not found. 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, optional): Validation problems, one per invalid field. - `path` (string, required) - `message` (string, required) ## Examples ### cURL ```bash curl "https://trawle.com/v1/jobs/1234567" ``` ### 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({ baseUrl: "https://trawle.com" }); const { data, error } = await trawle.GET("/v1/jobs/{id}", { params: { path: { id: "1234567", }, }, }); ``` ### Python ```python import requests response = requests.get( "https://trawle.com/v1/jobs/1234567", ) response.raise_for_status() data = response.json() ``` ### Response 200 ```json { "id": "1234567", "title": "string", "company": { "slug": "stripe", "name": "Stripe", "domain": "stripe.com", "openJobs": 0, "newLast30": 0, "trend": [0] }, "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, "closedAt": "2026-10-31T06:12:00.000Z", "applyUrl": "string", "url": "string", "department": "string", "summary": "string", "skills": ["string"], "experienceYears": { "min": 0, "max": 0 }, "visaSponsorship": "yes", "education": "none", "managesPeople": true, "relocation": true, "clearanceRequired": true, "equityOffered": true, "languages": ["string"], "postingLanguage": "en", "source": "greenhouse" } ``` ### Response 404 ```json { "error": { "code": "not_found", "message": "Job not found", "hint": "Job ids come from search results (`jobs[].id`)." } } ``` --- # Index size and freshness `GET https://trawle.com/v1/stats` ## Responses ### 200: Totals. Schema: SiteStats - `openJobs` (integer, required) - `companies` (integer, required): Companies with at least one open job. - `newToday` (integer, required): Open jobs first seen in the last 24 hours. - `dataDate` (string, required): Example: `2026-10-31T06:12:00.000Z`. - `historyStart` (string, required): First day of Trawle's own history; earlier dates come from employers. Example: `2026-10-31T06:12:00.000Z`. ## Examples ### cURL ```bash curl "https://trawle.com/v1/stats" ``` ### 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({ baseUrl: "https://trawle.com" }); const { data, error } = await trawle.GET("/v1/stats"); ``` ### Python ```python import requests response = requests.get( "https://trawle.com/v1/stats", ) response.raise_for_status() data = response.json() ``` ### Response 200 ```json { "openJobs": 0, "companies": 0, "newToday": 0, "dataDate": "2026-10-31T06:12:00.000Z", "historyStart": "2026-10-31T06:12:00.000Z" } ``` --- # Get a company and its hiring activity `GET https://trawle.com/v1/companies/{slug}` Open jobs, new and closed jobs, median days open, repost share and a 90-day trend. ## Path parameters - `slug` (string, required): 1 to 80 characters. Example: `stripe`. ## Responses ### 200: The company. Schema: Company - `slug` (string, required): Example: `stripe`. - `name` (string, required): Example: `Stripe`. - `domain` (string | null, required): Example: `stripe.com`. - `description` (string | null, required) - `industry` (string | null, required) - `hq` (object | null, required, PlaceRef) - `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`. - `careersUrl` (string | null, required) - `url` (string, required): This company on Trawle. - `stats` (object, required) - `openJobs` (integer, required) - `newLast7` (integer, required) - `closedLast30` (integer, required) - `medianDaysOpen` (integer | null, required) - `repostShare` (number, required): Share of open jobs that are reposts (0–1). - `trend` (array, required): Open jobs per day, oldest first, the last 90 days. - `families` (array, required) - `slug` (string, required) - `name` (string, required) - `count` (integer, required) - `places` (array, required) - `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`. - `count` (integer, required) ### 404: Not found. 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, optional): Validation problems, one per invalid field. - `path` (string, required) - `message` (string, required) ## Examples ### cURL ```bash curl "https://trawle.com/v1/companies/stripe" ``` ### 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({ baseUrl: "https://trawle.com" }); const { data, error } = await trawle.GET("/v1/companies/{slug}", { params: { path: { slug: "stripe", }, }, }); ``` ### Python ```python import requests response = requests.get( "https://trawle.com/v1/companies/stripe", ) response.raise_for_status() data = response.json() ``` ### Response 200 ```json { "slug": "stripe", "name": "Stripe", "domain": "stripe.com", "description": "string", "industry": "string", "hq": { "id": 0, "name": "Copenhagen", "country": "DK" }, "careersUrl": "string", "url": "string", "stats": { "openJobs": 0, "newLast7": 0, "closedLast30": 0, "medianDaysOpen": 0, "repostShare": 0, "trend": [0] }, "families": [ { "slug": "string", "name": "string", "count": 0 } ], "places": [ { "id": 0, "name": "Copenhagen", "country": "DK", "count": 0 } ] } ``` ### Response 404 ```json { "error": { "code": "not_found", "message": "Company not found", "hint": "Find company slugs with GET /v1/companies?q=" } } ``` --- # Find companies by name or domain `GET https://trawle.com/v1/companies` ## Query parameters - `q` (string, required): Company name or domain. 1 to 100 characters. Example: `stripe`. - `limit` (integer, optional): Default `8`. 1 to 20. Example: `8`. ## Responses ### 200: Best matches first. - `items` (array, required, CompanyMatch) - `slug` (string, required): Example: `stripe`. - `name` (string, required): Example: `Stripe`. - `domain` (string | null, required): Example: `stripe.com`. - `industry` (string | null, required) - `openJobs` (integer, required) ### 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, optional): Validation problems, one per invalid field. - `path` (string, required) - `message` (string, required) ## Examples ### cURL ```bash curl "https://trawle.com/v1/companies?q=stripe" ``` ### 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({ baseUrl: "https://trawle.com" }); const { data, error } = await trawle.GET("/v1/companies", { params: { query: { q: "stripe", }, }, }); ``` ### Python ```python import requests response = requests.get( "https://trawle.com/v1/companies", params={ "q": "stripe", }, ) response.raise_for_status() data = response.json() ``` ### Response 200 ```json { "items": [ { "slug": "stripe", "name": "Stripe", "domain": "stripe.com", "industry": "string", "openJobs": 0 } ] } ``` ### 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" } ] } } ``` --- # Find places (GeoNames) for the `places` filter `GET https://trawle.com/v1/places` Cities, metro areas, states, countries and multi-country regions ("EMEA", "Europe", "Anywhere") whose name starts with `q`. Use `items[].id` in `JobQuery.places`; `context` tells homonyms apart. ## Query parameters - `q` (string, required): Place name. 1 to 100 characters. Example: `copenhagen`. - `limit` (integer, optional): Default `8`. 1 to 20. Example: `8`. ## Responses ### 200: Best matches first. - `items` (array, required, PlaceMatch) - `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`. - `kind` (string, required): One of: `city`, `metro`, `admin1`, `country`, `region`. - `context` (string | null, required): Where it is, for telling homonyms apart ("Washington, United States"). ### 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, optional): Validation problems, one per invalid field. - `path` (string, required) - `message` (string, required) ## Examples ### cURL ```bash curl "https://trawle.com/v1/places?q=copenhagen" ``` ### 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({ baseUrl: "https://trawle.com" }); const { data, error } = await trawle.GET("/v1/places", { params: { query: { q: "copenhagen", }, }, }); ``` ### Python ```python import requests response = requests.get( "https://trawle.com/v1/places", params={ "q": "copenhagen", }, ) response.raise_for_status() data = response.json() ``` ### Response 200 ```json { "items": [ { "id": 2618425, "name": "Copenhagen", "country": "DK", "kind": "city", "context": "Capital Region, Denmark" } ] } ``` ### 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" } ] } } ``` --- # Find roles for the `roles` filter `GET https://trawle.com/v1/roles` Roles whose name or common job titles match `q` ("react" finds Frontend Engineer). Use `items[].slug` in `JobQuery.roles`. ## Query parameters - `q` (string, required): Role or job title. 1 to 100 characters. Example: `frontend`. - `limit` (integer, optional): Default `8`. 1 to 20. Example: `8`. ## Responses ### 200: Best matches first. - `items` (array, required, RoleMatch) - `slug` (string, required) - `name` (string, required) - `family` (any, required, Ref) - `openJobs` (integer, required) ### 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, optional): Validation problems, one per invalid field. - `path` (string, required) - `message` (string, required) ## Examples ### cURL ```bash curl "https://trawle.com/v1/roles?q=frontend" ``` ### 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({ baseUrl: "https://trawle.com" }); const { data, error } = await trawle.GET("/v1/roles", { params: { query: { q: "frontend", }, }, }); ``` ### Python ```python import requests response = requests.get( "https://trawle.com/v1/roles", params={ "q": "frontend", }, ) response.raise_for_status() data = response.json() ``` ### Response 200 ```json { "items": [ { "slug": "frontend-engineer", "name": "Frontend Engineer", "family": { "slug": "software-engineering", "name": "Software Engineering" }, "openJobs": 0 } ] } ``` ### 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" } ] } } ``` --- # List job families for the `families` filter `GET https://trawle.com/v1/families` ## Query parameters - `q` (string, optional): Optional name filter. 1 to 100 characters. Example: `engineering`. ## Responses ### 200: All families, in browsing order. - `items` (array, required, FamilyItem) - `slug` (string, required) - `name` (string, required) - `openJobs` (integer, required) ### 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, optional): Validation problems, one per invalid field. - `path` (string, required) - `message` (string, required) ## Examples ### cURL ```bash curl "https://trawle.com/v1/families" ``` ### 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({ baseUrl: "https://trawle.com" }); const { data, error } = await trawle.GET("/v1/families"); ``` ### Python ```python import requests response = requests.get( "https://trawle.com/v1/families", ) response.raise_for_status() data = response.json() ``` ### Response 200 ```json { "items": [ { "slug": "string", "name": "string", "openJobs": 0 } ] } ``` ### 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" } ] } } ```