Use this repository to pull vacancies from Teamtailor career sites into Python through the Teamtailor Jobs Scraper on Apify. Teamtailor hosts the careers pages of many European employers, each at an address of the form company.teamtailor.com. You list the handles you care about, oatly and storytel for instance, and receive one row per open role with its title, link, publish and expiry dates, city and country, and the posting text with the HTML stripped out. Apify does the fetching; your script writes results.json and results.csv, which a notebook can turn into hiring dashboards, weekly diffs or a training set for a role classifier. It is a tidy way to watch a list of employers without opening their careers pages one by one.
Actor: Teamtailor Jobs Scraper Β· Publisher: Devil Scrapes Β· Also in JavaScript: teamtailor-jobs-scraper-javascript
- Handles or links, your choice: each entry in
companiescan be a bare handle likeoatlyor a fullhttps://oatly.teamtailor.com/jobslink; the Actor reduces both to the handle. - Every page of the feed: when an employer's job list spans several pages, the Actor follows them, up to ten pages per company, before applying your cap.
- Dates that sort correctly:
date_publishedandvalid_throughare ISO 8601 with the employer's UTC offset kept, and the publish date falls back to the posting's own date when the feed omits it. - Location split into parts: city, region and country arrive as three columns taken from the posting's first listed location, with the country as a two-letter code.
- Readable text by default:
description_textis the posting with tags removed and whitespace collapsed; setincludeHtmlto also receive the original markup indescription_html. - Two names per row:
companyechoes the handle you sent, which stays stable for joins, whilecompany_namecarries the display name the career site publishes. - A dead handle costs nothing extra: a company that returns 404 simply contributes zero rows, and one that keeps failing after retries is logged and skipped while the rest of the list finishes.
- Transient errors retried: timeouts, rate limits and server errors get up to five attempts with growing pauses, and a
Retry-Afterheader is respected.
Keep a list of fifty Teamtailor employers in a text file, run the script weekly and count new job_id values per company. A spike in openings is a clear signal for investors, sales teams and journalists covering a sector.
Group a large employer's rows by location_country and location_locality. In a few lines you see whether new roles cluster at headquarters or in new markets, which often precedes an official expansion announcement.
description_text is already free of markup, so it drops straight into scikit-learn, spaCy or an embedding model. Researchers can compare how employers describe the same role, or which tools appear most often in data engineering ads.
Many postings carry valid_through. Filter for deadlines in the next fourteen days and send the result to a recruiter or a candidate who would otherwise miss the window.
You need an Apify account and an API token from Console β Settings β Integrations. Apify's free plan includes monthly usage credit; this Actor's event prices are listed under Pricing below.
git clone https://github.com/devilscrapes/teamtailor-jobs-scraper-python.git
cd teamtailor-jobs-scraper-python
python -m venv .venv && source .venv/bin/activate
pip install -r requirements.txt
export APIFY_TOKEN=your_token_here
python main.py # uses input.json
python main.py my-input.json --out data --max-charge 2The script starts the Teamtailor Jobs Scraper, waits for it to finish, prints a link to the run in Apify Console and saves every result to results.json and results.csv. It exits with code 1 and the run's status message if the run fails.
Reduced to essentials, a run is the code below and nothing else. call() returns once the Actor has finished, and iterate_items() pages through the dataset lazily.
from apify_client import ApifyClient
client = ApifyClient("YOUR_APIFY_TOKEN")
run = client.actor("DevilScrapes/teamtailor-jobs-scraper").call(run_input={'companies': ['oatly', 'storytel'], 'maxJobsPerCompany': 5, 'includeHtml': False, 'proxyConfiguration': {'useApifyProxy': True}})
for item in client.dataset(run.default_dataset_id).iterate_items():
print(item)main.py sends input.json to the Actor without changes. The sample asks two employers, Oatly and Storytel, for five postings each, so at most ten rows come back and a test costs about $0.215. To find a handle, open the company's careers page and look for the teamtailor.com subdomain; a branded custom domain is rejected, so use the subdomain itself. Up to 1,000 handles fit in one run, and maxJobsPerCompany accepts values up to 5,000.
{
"companies": [
"oatly",
"storytel"
],
"maxJobsPerCompany": 5,
"includeHtml": false,
"proxyConfiguration": {
"useApifyProxy": true
}
}| Field | Type | Required | Default | Allowed values |
|---|---|---|---|---|
companies |
array | yes | ["oatly", "storytel"] |
β |
maxJobsPerCompany |
integer | no | 100 |
β |
includeHtml |
boolean | no | false |
β |
proxyConfiguration |
object | no | {"useApifyProxy": true} |
β |
Each row is one open posting. Seven fields are always filled: the handle, the numeric job_id, title, link, publish date, the plain-text description and the scrape timestamp. Location parts, expiry date and the hiring organisation can be null, and description_html stays null unless includeHtml is true. Closed postings vanish from the feed, so history has to come from your own saved snapshots.
| Field | Type | Always present |
|---|---|---|
company |
string | yes |
company_name |
string or null | no |
job_id |
integer | yes |
job_title |
string | yes |
job_url |
string | yes |
date_published |
string | yes |
valid_through |
string or null | no |
location_locality |
string or null | no |
location_region |
string or null | no |
location_country |
string or null | no |
hiring_organization_name |
string or null | no |
description_html |
string or null | no |
description_text |
string | yes |
scraped_at |
string | yes |
| Event | Price (USD) |
|---|---|
| Actor start | $0.2 |
| Result emitted | $0.0015 |
Prices as configured on 2026-08-20. The Actor page always shows the current price. Runs are billed to the Apify account that owns the token. Use --max-charge to cap what a single run can cost.
A weekly hiring summary with pandas (pip install pandas): per company and country, count the open roles, take the median age of the postings in days and count those whose deadline falls within the next 14 days. The table is printed and saved as hiring_summary.csv.
import json
import pandas as pd
jobs = pd.DataFrame(json.load(open("results.json", encoding="utf-8")))
now = pd.Timestamp.now(tz="UTC")
published = pd.to_datetime(jobs["date_published"], utc=True, format="ISO8601")
deadline = pd.to_datetime(jobs["valid_through"], utc=True, format="ISO8601")
jobs["age_days"] = (now - published).dt.days
jobs["closing_soon"] = (deadline - now).dt.days.between(0, 14)
jobs["country"] = jobs["location_country"].fillna("unknown")
summary = (
jobs.groupby(["company", "country"])
.agg(
open_roles=("job_id", "count"),
median_age_days=("age_days", "median"),
closing_soon=("closing_soon", "sum"),
)
.sort_values("open_roles", ascending=False)
)
summary.to_csv("hiring_summary.csv")
print(summary.to_string())No. Teamtailor has no shared job index to query; every employer runs a separate career site. You supply the handles, then filter job_title or description_text in pandas afterwards.
Either it has no open postings, the handle is misspelt, or the employer no longer uses Teamtailor. All three look the same from the outside: the company ends with zero rows and the run continues. The run's status message reports how many companies returned data.
The plain-text version collapses all whitespace into single spaces, which suits search indexes and language models. If you need headings and lists, set includeHtml to true and parse description_html yourself, for example with BeautifulSoup.
A malformed entry, such as a custom-domain URL or a value with spaces, is rejected during input validation and the run stops before fetching anything. A well-formed handle that simply does not exist only yields zero rows.
Set APIFY_TOKEN first: export the token in the same shell, or put it in a local.envfile your shell loads. Never commit it.- Run finished with
FAILEDorABORTED: open the printed Console link; the run log says why. Input validation errors name the field. - Fewer rows than expected: a
--max-chargecap also limits how many results can be charged. Raise the cap or lower the limits in your input. - Something looks wrong in the data: open an issue on the Actor page so the maintainers see it.
- Teamtailor Jobs Scraper on Apify Store
- JavaScript version of this example
- All Devil Scrapes Actors
- Apify web scraping cookbook
Related Actors:
- Personio Jobs Scraper β vacancies from Personio career pages, common among DACH employers.
- Recruitee Jobs Scraper β open roles from Recruitee-hosted career sites.
- Workable Jobs Scraper β job listings from companies that hire through Workable.
MIT licensed example code. The Actor itself runs on Apify and is maintained by Devil Scrapes. Not affiliated with Teamtailor.