Tes API endpoint
Searches Tes's (tes.com) teaching-jobs board. Returns listing facts (title, employer, location, salary, contract terms/types) plus a short excerpt of the listing description, not the full long-form job-description copy, and the same live faceted-search breakdown (position/subject/workplace category trees with counts, plus contract type/term counts) the real search page renders as its filter sidebar. location is a free-text place name (a UK town/city, an international city, or a bare country name) resolved to coordinates; omit for Tes's own default market, "United Kingdom". radius_miles selects the search radius around location. contract_type and contract_term are validated against Tes's own small, closed label sets. position, subject, and workplace are comma-separated filters -- Tes's own category labels are numerous and can change, so read a prior response's own facets.positions[].value (and facets.positions[].children[].value)/facets.subjects[].value/facets.workplaces[].children[].value for the live, current set rather than guessing. salary_min filters to jobs with an advertised salary at or above that amount (in the searched market's local currency); Tes's own filter panel offers only a minimum, no maximum.
/tes/jobs/searchQuery parameters:
keywords (optional): job title or keyword, e.g. maths teacher.location (optional, default United Kingdom): free-text place name -- a UK town/city, an international city, or a bare country name (e.g. Manchester, Sydney, United Kingdom). Resolved to coordinates via Tes's own location autocomplete; an unresolvable location returns 400.radius_miles (optional, default 30): search radius around location, one of 3, 5, 10, 15, 20, 30, 50, 70, 100, 500.page (optional, default 1, 1-500): one-based page.page_size (optional, default 20, 1-50): results per page.sort (optional, default relevance): one of relevance, newest, distance.contract_type (optional): one of Full Time, Part Time.contract_term (optional): one of Permanent, Fixed Term, Casual, Maternity Cover, Temporary, Supply.position (optional): comma-separated position category label(s), e.g. Teaching and Lecturing. Both parent categories (e.g. Teaching and Lecturing, Non-Teaching/Support, Leadership) and their child categories (e.g. Cover Supervisor) are valid. Not a curated enum -- the set is large and can change; read a prior response's own facets.positions[].value/facets.positions[].children[].value for the live, current set. An unrecognized value narrows to zero results rather than erroring.subject (optional): comma-separated subject label(s), e.g. Mathematics. See a prior response's own facets.subjects[].value for the live set. Same passthrough posture as position.workplace (optional): comma-separated workplace/organisation-type label(s), e.g. Secondary. See a prior response's own facets.workplaces[].children[].value for the live set. Same passthrough posture as position.salary_min (optional): minimum advertised salary, in the searched market's local currency (GBP for the default UK market), e.g. 28000. Tes's own filter panel offers only a minimum -- there is no working maximum to filter on.Notable response behavior:
total is the upstream's true result count across all pages, not just the current page's length.location in the response is the resolved place name actually searched (Tes's own canonical name for the match, which may differ slightly from what was passed in).facets is the same live faceted-search breakdown Tes's own search page renders as its filter sidebar: positions, subjects, and workplaces are category trees (a children array nests subcategories), while contract_types and contract_terms are flat. Every facet entry carries its own live count under the current query and filters already applied -- use these to discover valid position/subject/workplace values rather than guessing.position/subject/workplace value narrows to zero results rather than erroring, matching Tes's own upstream behavior for an unknown filter.employer_logo_url is empty when the employer hasn't uploaded a logo.| Field | Filled in, of 40 checked | Values in the sample |
|---|---|---|
| id | 40 | |
| title | 40 | “Teacher (SEN)” and 21 other values |
| url | 40 | tes.com |
| employer | 40 | “Heather Bridge School” and 26 other values |
| employer_logo_url | 40 | l.imgt.es |
| location | 40 | “Devon” and 25 other values |
| salary | 39 | “£34,069 - £52,835 per year” and 21 other values |
| contract_terms | 40 | median 1 entry |
| contract_types | 40 | median 1 entry |
| short_description | 40 | median 244 characters |
| closing_date | 40 | 2026-10-15 to 2026-11-14 |
| Parameter | Type | Required | Default | Description | Example |
|---|---|---|---|---|---|
| keywords | string | No | Job title or keyword | maths teacher | |
| location | string | No | Free-text place name resolved via Tes's own location autocomplete | Manchester | |
| radius_miles | integer | No | Search radius around location, in miles Allowed values: 3, 5, 10, 15, 20, 30, 50, 70, 100, 500. | ||
| page | integer | No | One-based page Minimum: 1. Maximum: 500. | ||
| page_size | integer | No | Results per page Minimum: 1. Maximum: 50. | ||
| sort | string | No | Sort order Allowed values: relevance, newest, distance. | ||
| contract_type | string | No | Contract type Allowed values: Full Time, Part Time. | ||
| contract_term | string | No | Contract term Allowed values: Permanent, Fixed Term, Casual, Maternity Cover, Temporary, Supply. | ||
| position | string | No | Comma-separated position category label(s) -- see the endpoint markdown | Teaching and Lecturing | |
| subject | string | No | Comma-separated subject label(s) -- see the endpoint markdown | Mathematics | |
| workplace | string | No | Comma-separated workplace/organisation-type label(s) -- see the endpoint markdown | Secondary | |
| salary_min | integer | No | Minimum advertised salary, in the searched market's local currency Minimum: 0. | 28000 | |
| x-api-key (header) | string | Yes | API key required |
curl -X GET "https://api.crawlora.net/api/v1/tes/jobs/search?radius_miles=3&page=1&sort=relevance&contract_type=Full+Time&contract_term=Permanent" \ -H "x-api-key: $CRAWLORA_API_KEY"
{
"code": 200,
"msg": "OK",
"data": {
"keywords": "teacher",
"location": "United Kingdom",
"page": 1,
"page_size": 20,
"total": 1504,
"jobs": [
{
"id": "2340998",
"title": "Minibus Driver",
"url": "https://www.tes.com/jobs/vacancy/minibus-driver-epsom-2340998",
"employer": "Epsom College",
"employer_logo_url": "https://l.imgt.es/employer-assets/employers/1039424/brandimages/20181030_1126AM_desktop_1039424_logo.jpg",
"location": "Epsom",
"salary": "£11,956 per year",
"contract_terms": [
"Permanent"
],
"contract_types": [
"Part Time"
],
"short_description": "Epsom College is an independent co educational day and boarding school...",
"closing_date": "2026-09-29T23:00:00.000Z"
}
],
"facets": {
"positions": [
{
"value": "Teaching and Lecturing",
"count": 713,
"children": [
{
"value": "Cover Supervisor",
"count": 120
}
]
}
],
"subjects": [
{
"value": "Mathematics",
"count": 200
}
],
"workplaces": [
{
"value": "Workplaces",
"count": 1000,
"children": [
{
"value": "Secondary",
"count": 400
}
]
}
],
"contract_types": [
{
"value": "Full Time",
"count": 1036
},
{
"value": "Part Time",
"count": 577
}
],
"contract_terms": [
{
"value": "Permanent",
"count": 1192
},
{
"value": "Fixed Term",
"count": 161
}
]
}
}
}Request schema
No body schema
Response schema
#/definitions/tes.jobSearchResponseDoc
| Field | Type | Required | Enum | Bounds | Example | Description |
|---|---|---|---|---|---|---|
| code | integer | No | 200 | |||
| data | tes.JobSearchResponse | No | ||||
| data.facets | object | No | Facets is the same live faceted-search breakdown tes.com's own search page renders as its filter sidebar (position/subject/workplace category trees plus contract type/term counts), so a caller can discover exact Position/Subject/Workplace filter values without guessing. Counts reflect the query and other filters already applied. | |||
| data.facets.contract_terms | array | No | ||||
| data.facets.contract_terms[].children | array | No | ||||
| data.facets.contract_terms[].count | integer | No | ||||
| data.facets.contract_terms[].value | string | No | ||||
| data.facets.contract_types | array | No | ||||
| data.facets.contract_types[].children | array | No | ||||
| data.facets.contract_types[].count | integer | No | ||||
| data.facets.contract_types[].value | string | No | ||||
| data.facets.positions | array | No | ||||
| data.facets.positions[].children | array | No | ||||
| data.facets.positions[].count | integer | No | ||||
| data.facets.positions[].value | string | No | ||||
| data.facets.subjects | array | No | ||||
| data.facets.subjects[].children | array | No | ||||
| data.facets.subjects[].count | integer | No | ||||
| data.facets.subjects[].value | string | No | ||||
| data.facets.workplaces | array | No | ||||
| data.facets.workplaces[].children | array | No | ||||
| data.facets.workplaces[].count | integer | No | ||||
| data.facets.workplaces[].value | string | No | ||||
| data.jobs | array | No | ||||
| data.jobs[].closing_date | string | No | ||||
| data.jobs[].contract_terms | array | No | ||||
| data.jobs[].contract_types | array | No | ||||
| data.jobs[].employer | string | No | ||||
| data.jobs[].employer_logo_url | string | No | ||||
| data.jobs[].id | string | No | ||||
| data.jobs[].location | string | No | ||||
| data.jobs[].salary | string | No | ||||
| data.jobs[].short_description | string | No | ||||
| data.jobs[].title | string | No | ||||
| data.jobs[].url | string | No | ||||
| data.keywords | string | No | ||||
| data.location | string | No | ||||
| data.page | integer | No | ||||
| data.page_size | integer | No | ||||
| data.total | integer | No | ||||
| msg | string | No | OK |
| Status | Description | Schema |
|---|---|---|
| 400 | Bad Request | #/definitions/app.Response |
| 429 | Too Many Requests | #/definitions/app.Response |
| 503 | Service Unavailable | #/definitions/app.Response |
Use environment variables for secrets and keep Crawlora API keys server-side.
curl -X GET "https://api.crawlora.net/api/v1/tes/jobs/search?radius_miles=3&page=1&sort=relevance&contract_type=Full+Time&contract_term=Permanent" \
-H "x-api-key: $CRAWLORA_API_KEY"