Look a job up by the URL of its posting: paste the link a candidate applied through and get back the same payload Get a job by id returns — 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 one difference is nullability: a posting we have not indexed yet can be answered LIVE, and a live row leaves the catalog-owned fields null (see "What is guaranteed" below), so write your client against the schema on this page.
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 job shape, with every field the posting itself supplies (title, both description renderings, resolved locations, salary, level) populated just like a catalog hit — but the fields that belong to a CATALOG RECORD rather than to the posting are null: id, source_id, company_id, content_hash, the lifecycle timestamps (first_seen_at, created_at, updated_at, closed_at, expires_at), and any field of company we could not establish, company.name included. 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. closed_at: null on a live row means "unknown", not "still open".
What is guaranteed. On /jobs/resolve, only title, url, and identity-adjacent fields are guaranteed non-null; everything else on a live row is best-effort nullable; corpus hits carry the GET /jobs/{id} non-null guarantees. The identity-adjacent field is external_job_id, the posting identity the URL parsed to. Model the response on THIS operation's schema, not on Get a job by id's: the same URL can answer from the catalog today and live tomorrow, so a client that requires the catalog guarantees will reject a valid provisional answer. locations is the one exception that is never null — always an array, empty when no place could be resolved.
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.
Skills. skills means the same here as on Search jobs, and a live row is extracted the same way as a catalog one: skills: null means NOT EXTRACTED (the posting has no description, is not in English, or has not been processed yet); skills: [] means extracted, and no known skill was found. Every slug the current release returns, with the aliases it is recognized by, is listed in skills-vocabulary.json; a job extracted before a release may still carry a slug that release retired. Search jobs explains how to use the file.
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, "skills": [ { "slug": "string", "name": "string", "kind": "string", "requirement": "required" } ], "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", "source": { "type": "string", "external_id": "string", "alias_ids": [ "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" } ]}