Skip to content

About

Teamtailor jobs scraper in Python: give it employer handles such as oatly and get every open vacancy with title, ISO publish and expiry dates, city, country code and plain-text description, ready for pandas and scheduled hiring reports. Python example for the Devil Scrapes Apify Actor.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

Β 

History

1 Commit

Folders and files

Repository files navigation

Teamtailor Jobs Scraper β€” Python example

Teamtailor Jobs Scraper in Python: employer career sites to pandas, JSON and CSV

Run on Apify Python MIT license CI

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

πŸ”₯ What you get

  • Handles or links, your choice: each entry in companies can be a bare handle like oatly or a full https://oatly.teamtailor.com/jobs link; 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_published and valid_through are 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_text is the posting with tags removed and whitespace collapsed; set includeHtml to also receive the original markup in description_html.
  • Two names per row: company echoes the handle you sent, which stays stable for joins, while company_name carries 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-After header is respected.

🎯 Use cases

Track hiring velocity across a watchlist

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.

Map where a company is hiring

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.

Study job ads as text

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.

Catch roles before they close

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.

⚑ Quick start

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 2

The 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.

🧩 The core call

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)

πŸ“₯ Input

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} β€”

πŸ“€ Output

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

πŸ’° Pricing

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.

πŸ› οΈ Working with the data in Python

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())

❓ FAQ

Can I search all Teamtailor jobs by keyword?

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.

Why did a company I listed return no rows?

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.

Why are paragraph breaks missing from description_text?

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.

Does a typo in one entry break the whole run?

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.

🧯 Troubleshooting

  • Set APIFY_TOKEN first: export the token in the same shell, or put it in a local .env file your shell loads. Never commit it.
  • Run finished with FAILED or ABORTED: open the printed Console link; the run log says why. Input validation errors name the field.
  • Fewer rows than expected: a --max-charge cap 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.

πŸ”— More from Devil Scrapes

Related Actors:


MIT licensed example code. The Actor itself runs on Apify and is maintained by Devil Scrapes. Not affiliated with Teamtailor.

About

Teamtailor jobs scraper in Python: give it employer handles such as oatly and get every open vacancy with title, ISO publish and expiry dates, city, country code and plain-text description, ready for pandas and scheduled hiring reports. Python example for the Devil Scrapes Apify Actor.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages