Developers
Free Jobs API
Every live working student job and internship in Germany on workingstudentjobs.de, as JSON. Free, no API key, no sign-up, updated hourly. Built for job apps, research and AI agents that help students find work.
Beta until October 31, 2026: field names and values may still be corrected until then. After that, v1 only gets additive changes.
Machine-readable:OpenAPI 3.1 specllms.txt
Quick start
One GET request returns the newest 100 jobs. Add ref=yourapp to every request so we can reach you before a breaking change.
curl "https://www.workingstudentjobs.de/api/v1/jobs?city=berlin&type=working_student&limit=20&ref=myapp"// Walk every live job, pausing when rate limited.
let url = "https://www.workingstudentjobs.de/api/v1/jobs?limit=300&ref=myapp";
const jobs = [];
while (url) {
const res = await fetch(url);
if (res.status === 429) {
const wait = Number(res.headers.get("Retry-After") ?? 60);
await new Promise((resolve) => setTimeout(resolve, wait * 1000));
continue;
}
if (!res.ok) throw new Error((await res.json()).error.message);
const page = await res.json();
jobs.push(...page.data);
url = page.links.next;
}import requests
res = requests.get(
"https://www.workingstudentjobs.de/api/v1/jobs",
params={"city": "munich", "language": "en", "ref": "myapp"},
timeout=30,
)
res.raise_for_status()
for job in res.json()["data"]:
company = job["company"]["name"] if job["company"] else ""
print(job["title"], company, job["apply_url"]){
"data": [
{
"id": "3f6c2a1e-8b4d-4e0a-9c7f-2d5e1b6a9f80",
"slug": "example-gmbh-working-student-data-analytics-3f6c2a1e",
"title": "Working Student Data Analytics (m/f/d)",
"url": "https://www.workingstudentjobs.de/jobs/example-gmbh-working-student-data-analytics-3f6c2a1e",
"apply_url": "https://www.workingstudentjobs.de/apply/3f6c2a1e-8b4d-4e0a-9c7f-2d5e1b6a9f80?ref=myapp",
"company": {
"name": "Example GmbH",
"slug": "example-gmbh",
"url": "https://www.workingstudentjobs.de/jobs/at/example-gmbh",
"logo_url": null,
"website_domain": "example.com"
},
"location": {
"city": "Berlin",
"city_slug": "berlin",
"state": "Berlin",
"state_code": "BE",
"country_code": "DE",
"postal_code": "10115",
"latitude": 52.52,
"longitude": 13.405
},
"types": [
"working_student"
],
"field": "tech",
"sub_field": "data",
"working_mode": "hybrid",
"salary": {
"min": 16,
"max": 18,
"currency": "EUR",
"period": "hour"
},
"skills": [
"SQL",
"Python",
"Tableau"
],
"languages": {
"german": "optional",
"english": "required"
},
"sponsored": false,
"details": {
"study_fields": [
"computer science",
"statistics"
],
"study_fields_or_comparable": true,
"study_levels": [
"bachelor",
"master"
],
"min_semester": 3,
"enrolment": "required",
"german_level": null,
"english_level": {
"level": "fluent",
"cefr": "C1"
},
"other_languages": [],
"hours_per_week": {
"min": 16,
"max": 20
},
"start": {
"asap": true,
"date": null
},
"duration_months": {
"min": 12,
"max": null
},
"internship_type": null,
"remote_days_max": 2,
"benefits": [
"flexible hours",
"public transport ticket"
],
"cover_letter": "not_needed",
"transcripts": null,
"prior_experience": "preferred"
},
"posted_at": "2026-10-16T07:12:40.000Z",
"first_seen_at": "2026-10-16T09:03:11.482Z",
"expires_at": null
}
],
"links": {
"self": "https://www.workingstudentjobs.de/api/v1/jobs?city=berlin&limit=1&ref=myapp",
"next": "https://www.workingstudentjobs.de/api/v1/jobs?city=berlin&limit=1&ref=myapp&cursor=..."
},
"meta": {
"limit": 1,
"count": 1,
"api_version": "1.0-beta"
}
}Endpoints
| Endpoint | Description |
|---|---|
| GET /api/v1/jobs | Live jobs, newest first, with filters and cursor pages. |
| GET /api/v1/jobs/{id} | One job by id. 301 when it was merged into another live listing, 410 when it has closed, 404 when no public job has this id. |
| GET /api/v1/cities | Every city with at least one live job and its count. Use these slugs for the city filter. |
| GET /openapi.json | The OpenAPI 3.1 description of all of the above. |
Parameters for /api/v1/jobs
All optional. Filters combine with AND; values inside one filter, comma separated, combine with OR. Unknown parameters are ignored, invalid values return 400.
| Parameter | Description |
|---|---|
| limit | Jobs per page, 1 to 300. Default 100. |
| cursor | Opaque page token. Follow links.next instead of building it yourself. |
| city | City slugs, up to 20, e.g. berlin,munich. German spellings like muenchen or koeln work too. Unknown slugs return 400 unknown_city. |
| field | Fields: tech, engineering, marketing, finance, operations, sales, hr, consulting, design, research, legal, logistics, education, healthcare, gastronomy-hospitality. |
| type | working_student, internship, or both. A job can be both. |
| language | en: English is enough and German is not required. de: German is required. |
| remote | Working modes: remote, hybrid, onsite. |
| since | Only jobs we first saw at or after this ISO 8601 date or date-time, e.g. 2026-10-15 or 2026-10-15T08:00:00Z. |
| ref | Your app's name, 1 to 64 characters of letters, digits, dot, dash or underscore. Kept on every apply_url. |
Job fields
Each job in data has these fields. Null means the posting does not say, never "no".
| Field | Description |
|---|---|
| id, slug, title | Stable id (UUID), URL slug and job title. |
| url | The job page on workingstudentjobs.de. |
| apply_url | Send applicants here. It redirects to the employer's application page, or to our job page when the employer takes applications by email or the job has closed. |
| company | Name, slug, company page, logo and website domain. Null when unknown. |
| location | City, slug, state, postal code and coordinates. Null for postings that name no city. |
| types | working_student and/or internship. |
| field, sub_field | field is one of the field values above, sub_field a finer label such as controlling. |
| working_mode | remote, hybrid or onsite. Onsite when the posting does not say. |
| salary | min, max, currency and period (hour, day, week, month, year). Null when the posting states no pay. |
| skills | Skills named in the posting. |
| languages | german: required, optional or not_required. english: required or not_required. |
| sponsored | true for a paid listing. A flag only, the order is never changed. |
| details | Facts our extractor read from the posting: study fields and levels, minimum semester, enrolment, language levels, hours per week, start, duration, internship type, remote days, benefits, cover letter, transcripts, prior experience. Null until the current posting text has been read. |
| posted_at, first_seen_at, expires_at | posted_at (when the employer published it, else first_seen_at), first_seen_at (when we first saw it) and expires_at. |
Not included: the full job description and the original posting link. Show the job with url, and send applicants to apply_url.
Pagination and sync
Jobs are ordered by first_seen_at, newest first. Each page has links.next, an absolute URL for the next page, or null on the last one. There is no total count. To keep a copy in sync, poll with since set to your last poll minus 72 hours and dedupe by id: jobs we hold for review publish later with an earlier first_seen_at. Also walk the full list once a day (about 70 requests at limit=300): it catches the rare job held for review longer than 72 hours and shows which jobs have closed. For a single job, /api/v1/jobs/{id} returns 410 once it has closed.
Rate limits
60 requests per minute per IP. Responses usually carry X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset (Unix seconds). Over the limit you get 429 with Retry-After: wait that many seconds, then go on. Replies served from our edge cache may repeat the headers of an earlier reply.
Caching
Responses are cached for up to an hour, which matches how often new jobs arrive. Send the ETag back as If-None-Match to get an empty 304 when nothing changed. CORS is open (Access-Control-Allow-Origin: *), so browsers can call the API directly.
Errors
Errors are JSON with a stable code and a readable message:
{ "error": { "code": "unknown_city", "message": "Unknown city slug: atlantis. GET /api/v1/cities lists the valid slugs." } }| Code | Status | Description |
|---|---|---|
| invalid_parameter | 400 | A parameter has a wrong format or value. |
| invalid_cursor | 400 | The cursor was changed or truncated. Use links.next as is. |
| unknown_city | 400 | A city slug does not exist. See /api/v1/cities. |
| not_found | 404 | No public job with this id. |
| method_not_allowed | 405 | Only GET, HEAD and OPTIONS are supported. |
| gone | 410 | The job has closed. |
| rate_limited | 429 | Over 60 requests per minute. Wait for Retry-After. |
| internal_error | 500 | Something failed on our side. Retry later. |
Attribution and ref
Link every job you show with its url or apply_url. That keeps the click counts employers see accurate, which keeps the listings coming. Add ref=yourapp to your requests: it is carried onto apply_url, it tells us which apps send students, and it lets us contact you before breaking changes. No key, no sign-up.
Versioning and changelog
The version is in the path (/api/v1) and in meta.api_version. After the beta, v1 only gets additive changes: new fields, new parameters, new values. Removals and renames ship as v2, and v1 then stays up for 6 more months with Deprecation and Sunset headers. 2026-10-17: v1 beta released.
Terms
Free to use, including in commercial apps. Do not re-host the listings as a competing job board and do not get around the rate limit. The full conditions are in section 6 of our Terms of Service.
Contact
Building something with the API, found a wrong field, or need a filter we do not have? Write to contact@workingstudentjobs.de.