Look a job up by the URL of its posting: paste the link a candidate applied through and get back the SAME payload as Get a job by id — full company record, both description renderings, and the resolved locations. Built for application trackers, which hold a URL and need the catalog record behind it.
The URL is parsed server-side into the posting identity the applicant tracking system assigned it, and that identity is looked up. Pass the URL as the candidate sees it: tracking parameters (?gh_src=, utm_*), locale segments (/en-US/), a trailing /apply step and grnh.se short links are all handled. Only the url parameter is read; nothing else varies the result.
Closed jobs resolve. A posting taken down years ago answers exactly like a live one — the response is the ordinary job payload with closed_at set. That is the point of the endpoint for a tracker: an application made months ago still resolves. The only lookback limit is when we first indexed the board (a job closed before that never existed for us).
Provisional (live-fetched) results. A URL we can parse but have not indexed yet may be fetched live from the applicant tracking system and returned on the spot. A provisional result is the SAME 200 JobDetail shape, with one difference: id, source_id and company_id are null. The posting is not (yet) a catalog record, so it has no stable id to fetch later by Get a job by id — but everything else (title, company name, both description renderings, resolved locations, salary, level) is populated just like a catalog hit. Treat a null id as "resolved live, not yet catalogued"; render it, but do not store it as a catalog id or expect /jobs/{id} to return it.
The two 404s. A miss is never an error on your side, so both cases return 404 with a stable detail. Match on the detail PREFIX (the sentences below are stable; treat anything after them as free text):
detail | Meaning | What to do |
|---|---|---|
Unsupported job board URL. | The URL is not a job posting URL we can parse — an aggregator link, a bespoke careers site, or a board page rather than a posting. | Stop retrying it. This will not start working. |
No job matched the provided URL. | The URL parsed fine, but nothing in the catalog matches it. | Treat as "not indexed yet" and retry later if it matters. |
The second answer is deliberately ONE answer: "we do not index that employer" and "we index that employer but not that posting" are byte-identical, and no field distinguishes them. Do not build logic that tries to. It is worth retrying because coverage grows: a posting less than a day old, or an employer we have just started indexing, can resolve on a later call for the same URL — every miss also feeds our board-discovery queue.
A malformed request — a missing url, a non-http(s) URL, or one over 2048 characters — is a 400 with the usual validation problem body, not a 404.
Rate limit. This endpoint draws on the same shared per-account budget as every other one (see Rate limits); it has no separate allowance. One lookup per tracked application, resolved once and cached on your side, sits comfortably inside it — resolving a whole backlog in a tight loop does not.
Authorization
bearerAuth Provide your secret API key (prefixed sk_) as a bearer token: Authorization: Bearer sk_....
In: header
Query Parameters
The job posting URL to resolve, exactly as it appears in the browser. Must be an absolute http/https URL of at most 2048 characters. Tracking parameters, locale segments and /apply suffixes are tolerated; grnh.se short links are followed.
1 <= length <= 2048Response Body
application/json
application/problem+json
application/problem+json
application/problem+json
application/problem+json
application/problem+json
curl -X GET "https://example.com/api/v1/jobs/resolve?url=string"{ "id": 0, "source_id": 0, "company_id": 0, "external_job_id": "string", "url": "string", "apply_url": "string", "title": "string", "location": "string", "locations": [ { "kind": "country", "code": "string", "name": "string", "admin1_name": "string", "country_code": "string" } ], "workplace": "remote", "employment_type": "full_time", "level": "intern", "experience_min_years": 0, "experience_max_years": 0, "sponsors_visa": true, "language": "string", "salary_raw": "string", "salary_min": 0, "salary_max": 0, "salary_currency": "string", "salary_period": "year", "posted_at": "string", "expires_at": "string", "content_hash": "string", "first_seen_at": "string", "closed_at": "string", "created_at": "string", "updated_at": "string", "description_md": "string", "description_html": "string", "company": { "id": 0, "name": "string", "website_url": "string", "logo_url": "string", "industry": "string", "size_range": "string", "founded_year": 0, "is_agency": true, "linkedin_id": "string", "created_at": "string", "updated_at": "string" }}{ "type": "string", "title": "string", "status": 0, "detail": "string", "errors": [ { "field": "string", "message": "string" } ]}{ "type": "string", "title": "string", "status": 0, "detail": "string", "errors": [ { "field": "string", "message": "string" } ]}{ "type": "string", "title": "string", "status": 0, "detail": "string", "errors": [ { "field": "string", "message": "string" } ]}{ "type": "string", "title": "string", "status": 0, "detail": "string", "errors": [ { "field": "string", "message": "string" } ]}{ "type": "string", "title": "string", "status": 0, "detail": "string", "errors": [ { "field": "string", "message": "string" } ]}