{"openapi":"3.1.0","info":{"title":"TechnoJobs Public API","version":"1.0.0","summary":"Live UK tech vacancies and pay data, provenance-labelled.","description":"Read-only, unauthenticated, CORS-open access to the vacancies on TechnoJobs — a UK tech job board carrying predominantly permanent, salaried roles — and the pay data computed from them.\n\nFour promises this API keeps:\n\n1. **Provenance on every response.** `meta.supply` states which advert provenances the response was computed from. Only adverts from a verified employer source are included.\n2. **Sample size and provenance on every pay figure.** No median travels without its `n`, and pay the employer declared is never merged with pay we read out of their advert text.\n3. **Additive schemas.** Fields may be added to v1; none is ever removed, renamed or retyped. Breaking changes get a v2.\n4. **No people.** Candidate profiles, CVs, applications and contact details are not reachable from this API — not with a parameter, not with a key, not at any depth. There is no authenticated tier that sees more.\n\nAttribution: please credit TechnoJobs and link the specific advert. When you quote a pay figure, carry its `n` and its `provenance` with it. Human documentation: https://technojobs.co.uk/developers","contact":{"name":"TechnoJobs","url":"https://technojobs.co.uk/developers"}},"servers":[{"url":"https://technojobs.co.uk/api/public/v1","description":"TechnoJobs public API, v1"}],"paths":{"/":{"get":{"summary":"Service index","description":"Endpoint list, live counts, and links to this document and the documentation.","operationId":"getServiceIndex","responses":{"200":{"description":"Service index.","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/jobs":{"get":{"summary":"List live vacancies","description":"Newest first, cursor-paginated. Deliberately unranked: the website ranks (relevance, recency, a paid featured boost), and none of that belongs in a paginated dataset where page stability matters more than ordering taste.","operationId":"listJobs","parameters":[{"name":"limit","in":"query","schema":{"type":"integer","default":25,"maximum":100,"minimum":1},"description":"Records per page. Clamped to 100."},{"name":"cursor","in":"query","schema":{"type":"string"},"description":"Opaque cursor from `page.nextCursor`. Malformed cursors are a 400, never a silent page one."},{"name":"employer","in":"query","schema":{"type":"string"},"description":"Exact employer name, case-insensitive."},{"name":"jobType","in":"query","schema":{"type":"string","enum":["permanent","full_time","part_time","contract","temporary"]}},{"name":"workMode","in":"query","schema":{"type":"string","enum":["remote","hybrid","onsite"]}},{"name":"q","in":"query","schema":{"type":"string"},"description":"Full-text keyword filter over the advert."}],"responses":{"200":{"description":"A page of vacancies.","content":{"application/json":{"schema":{"type":"object","required":["meta","data","page"],"properties":{"meta":{"$ref":"#/components/schemas/Meta"},"data":{"type":"array","items":{"$ref":"#/components/schemas/Job"}},"page":{"$ref":"#/components/schemas/Page"}}}}}},"400":{"description":"Malformed cursor or parameter.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"The read failed. We answer 503 rather than an empty list.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/jobs/{idOrSlug}":{"get":{"summary":"One vacancy, with full advert text","operationId":"getJob","parameters":[{"name":"idOrSlug","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"The advert.","content":{"application/json":{"schema":{"type":"object","required":["meta","data"],"properties":{"meta":{"$ref":"#/components/schemas/Meta"},"data":{"$ref":"#/components/schemas/JobDetail"}}}}}},"404":{"description":"No such live advert. Excluded adverts are indistinguishable from unknown ones — this endpoint is not an oracle.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"The read failed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/employers":{"get":{"summary":"Employers with live vacancies","description":"Keyed on the employer name their own applicant-tracking system publishes, not on our internal organisation records.","operationId":"listEmployers","responses":{"200":{"description":"Employers, busiest first.","content":{"application/json":{"schema":{"type":"object","required":["meta","data","page"],"properties":{"meta":{"$ref":"#/components/schemas/Meta"},"data":{"type":"array","items":{"$ref":"#/components/schemas/Employer"}},"page":{"$ref":"#/components/schemas/Page"}}}}}},"429":{"description":"Rate limited.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"The read failed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/salaries":{"get":{"summary":"Pay benchmarks","description":"Board-wide, per role and per work mode. Two independent gates stand in front of every figure: provenance (real supply only) and sample size. A row under the minimum publishes its `n` and null figures.","operationId":"listBenchmarks","responses":{"200":{"description":"Benchmarks.","content":{"application/json":{"schema":{"type":"object","required":["meta","data","page"],"properties":{"meta":{"$ref":"#/components/schemas/Meta"},"data":{"type":"array","items":{"$ref":"#/components/schemas/Benchmark"}},"page":{"$ref":"#/components/schemas/Page"}}}}}},"429":{"description":"Rate limited.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"The read failed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/openapi.json":{"get":{"summary":"This document","operationId":"getOpenApi","responses":{"200":{"description":"OpenAPI 3.1 description.","content":{"application/json":{"schema":{"type":"object"}}}}}}}},"components":{"schemas":{"Supply":{"type":"object","description":"Which advert provenances this response was computed from. Stated on every response so a consumer never has to infer what the numbers are made of.","required":["included","excluded","note"],"properties":{"included":{"type":"array","items":{"type":"string"},"description":"Provenances included."},"excluded":{"type":"array","items":{"type":"string"},"description":"Provenances excluded."},"note":{"type":"string","description":"Plain-English statement of the supply gate."}}},"Meta":{"type":"object","required":["apiVersion","generatedAt","lastUpdated","supply","documentation"],"properties":{"apiVersion":{"type":"string","description":"Always \"v1\" for this document."},"generatedAt":{"type":"string","format":"date-time","description":"When this response was computed."},"lastUpdated":{"type":["string","null"],"format":"date-time","description":"Newest update timestamp in the underlying adverts. Null when the set is empty."},"supply":{"$ref":"#/components/schemas/Supply"},"documentation":{"type":"string","description":"Absolute URL of the human documentation."}}},"Page":{"type":"object","required":["count","hasMore","nextCursor"],"properties":{"count":{"type":"integer","description":"Records in this response."},"hasMore":{"type":"boolean","description":"Whether a further page exists."},"nextCursor":{"type":["string","null"],"description":"Opaque cursor for the next page. Do not parse it."}}},"Pay":{"type":"object","description":"An advertised pay band. `provenance` is load-bearing: \"employer_declared\" is the employer's own structured pay field, \"extracted_from_description\" is OUR reading of their prose, with the sentence it came from. Never present one as the other.","required":["currency","period","min","max","provenance","sourceText","confidence"],"properties":{"currency":{"type":"string","description":"ISO 4217, e.g. \"GBP\"."},"period":{"type":"string","description":"\"year\", \"day\", \"hour\", \"month\" or \"week\" — the unit the employer quoted."},"min":{"type":"integer","description":"Lower bound. Equals `max` when the advert quoted one figure."},"max":{"type":"integer","description":"Upper bound."},"provenance":{"type":"string","enum":["employer_declared","extracted_from_description"],"description":"Where the figure came from."},"sourceText":{"type":["string","null"],"description":"For an extracted figure: the exact sentence from the advert it was read out of. Null when the employer declared the pay."},"confidence":{"type":["number","null"],"description":"For an extracted figure: 0–1, never below our publish floor. Null when declared."}}},"Job":{"type":"object","required":["id","slug","url","title","employer","location","workMode","jobType","ir35Status","skills","pay","postedAt","updatedAt","applyUrl","applyMethod","source"],"properties":{"id":{"type":"string","description":"Stable UUID."},"slug":{"type":"string","description":"URL segment on technojobs."},"url":{"type":"string","description":"The advert on TechnoJobs — the correct link to cite."},"title":{"type":"string","description":"Advert title as published."},"employer":{"type":"object","required":["name"],"properties":{"name":{"type":"string","description":"The employer name as their own applicant-tracking system publishes it."}}},"location":{"type":"object","required":["label","region"],"properties":{"label":{"type":"string","description":"Location exactly as the employer wrote it. UNNORMALISED — \"London\", \"London (GB)\" and \"London, UK\" are distinct strings, because merging them would mean guessing."},"region":{"type":["string","null"],"description":"UK region, when the employer stated one. Usually null on ingested adverts."}}},"workMode":{"type":"string","description":"\"remote\", \"hybrid\" or \"onsite\"."},"jobType":{"type":"string","description":"\"permanent\", \"full_time\", \"contract\", \"temporary\" or \"part_time\"."},"ir35Status":{"type":["string","null"],"enum":["inside","outside",null],"description":"Only when the employer stated a determination. Never inferred."},"skills":{"type":"array","items":{"type":"string"}},"pay":{"oneOf":[{"$ref":"#/components/schemas/Pay"},{"type":"null"}]},"postedAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"},"applyUrl":{"type":"string","description":"Where a human applies — the employer's own form for external adverts."},"applyMethod":{"type":"string","enum":["internal","external"]},"source":{"type":"object","required":["provenance","description"],"properties":{"provenance":{"type":"string","enum":["ats_feed","employer_posted"]},"description":{"type":"string","description":"Plain-English account of how this advert reached us."}}}}},"JobDetail":{"type":"object","required":["id","slug","url","title","employer","location","workMode","jobType","ir35Status","skills","pay","postedAt","updatedAt","applyUrl","applyMethod","source","benefits","description"],"properties":{"id":{"type":"string","description":"Stable UUID."},"slug":{"type":"string","description":"URL segment on technojobs."},"url":{"type":"string","description":"The advert on TechnoJobs — the correct link to cite."},"title":{"type":"string","description":"Advert title as published."},"employer":{"type":"object","required":["name"],"properties":{"name":{"type":"string","description":"The employer name as their own applicant-tracking system publishes it."}}},"location":{"type":"object","required":["label","region"],"properties":{"label":{"type":"string","description":"Location exactly as the employer wrote it. UNNORMALISED — \"London\", \"London (GB)\" and \"London, UK\" are distinct strings, because merging them would mean guessing."},"region":{"type":["string","null"],"description":"UK region, when the employer stated one. Usually null on ingested adverts."}}},"workMode":{"type":"string","description":"\"remote\", \"hybrid\" or \"onsite\"."},"jobType":{"type":"string","description":"\"permanent\", \"full_time\", \"contract\", \"temporary\" or \"part_time\"."},"ir35Status":{"type":["string","null"],"enum":["inside","outside",null],"description":"Only when the employer stated a determination. Never inferred."},"skills":{"type":"array","items":{"type":"string"}},"pay":{"oneOf":[{"$ref":"#/components/schemas/Pay"},{"type":"null"}]},"postedAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"},"applyUrl":{"type":"string","description":"Where a human applies — the employer's own form for external adverts."},"applyMethod":{"type":"string","enum":["internal","external"]},"source":{"type":"object","required":["provenance","description"],"properties":{"provenance":{"type":"string","enum":["ats_feed","employer_posted"]},"description":{"type":"string","description":"Plain-English account of how this advert reached us."}}},"benefits":{"type":"array","items":{"type":"string"}},"description":{"type":"object","required":["text","paragraphs","responsibilities","requirements"],"properties":{"text":{"type":"string","description":"Full advert body as plain text."},"paragraphs":{"type":"array","items":{"type":"string"}},"responsibilities":{"type":"array","items":{"type":"string"}},"requirements":{"type":"array","items":{"type":"string"}}}}}},"Employer":{"type":"object","required":["name","liveJobs","jobsUrl"],"properties":{"name":{"type":"string","description":"Employer name as their applicant-tracking system publishes it."},"liveJobs":{"type":"integer","description":"Live vacancies from this employer right now."},"jobsUrl":{"type":"string","description":"This API, filtered to that employer."}}},"Benchmark":{"type":"object","description":"One published pay benchmark. `n` is ALWAYS present, including when no figure is published: \"we have 3 adverts and will not summarise them\" is a useful, honest answer, and omitting the row would read as \"no data\".","required":["scope","label","period","currency","n","provenance","median","p25","p75","low","high"],"properties":{"scope":{"type":"string","enum":["board","role","work_mode"]},"label":{"type":"string","description":"What this benchmark covers."},"period":{"type":"string","enum":["year","day"]},"currency":{"type":"string","enum":["GBP"]},"n":{"type":"integer","description":"Sample size. Adverts, after the provenance gate."},"provenance":{"type":["string","null"],"enum":["employer_declared","extracted_from_description","mixed",null],"description":"Where the sample's pay figures came from. Null when the sample is empty."},"median":{"type":["integer","null"],"description":"Null when the sample is below our minimum. The `n` still tells you why."},"p25":{"type":["integer","null"],"description":"Lower quartile of advert midpoints."},"p75":{"type":["integer","null"],"description":"Upper quartile of advert midpoints."},"low":{"type":["integer","null"],"description":"Lowest advert midpoint in the sample."},"high":{"type":["integer","null"],"description":"Highest advert midpoint in the sample."}}},"Error":{"type":"object","required":["meta","error"],"properties":{"meta":{"type":"object","required":["apiVersion","generatedAt","documentation"],"properties":{"apiVersion":{"type":"string"},"generatedAt":{"type":"string","format":"date-time"},"documentation":{"type":"string"}}},"error":{"type":"object","required":["code","message"],"properties":{"code":{"type":"string","enum":["not_found","invalid_request","invalid_cursor","rate_limited","unavailable"]},"message":{"type":"string"}}}}}}}}