Skip to content

About

Teamtailor jobs scraper for JavaScript and Node.js: collect open roles from many Teamtailor career sites in one run and serve them from your API, sync them to a job board or post new ones to a webhook. JavaScript 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 β€” JavaScript example

Teamtailor Jobs Scraper for JavaScript and Node.js: career-site vacancies as JSON for apps

Run on Apify JavaScript MIT license CI

This repository connects Node.js to the Teamtailor Jobs Scraper with one ES module and the official apify-client package. Pass an array of Teamtailor handles, the part before .teamtailor.com in an employer's careers address, and the run resolves to plain objects: a numeric job_id, title, apply link, ISO dates, city, region and country code, and a description in text and, if requested, HTML. Apify performs the requests and retries, which keeps your Express route, BullMQ worker or scheduled cloud function small. Typical consumers are niche job boards, recruiting CRMs and sales tools that treat new openings as a buying signal.

Actor: Teamtailor Jobs Scraper Β· Publisher: Devil Scrapes Β· Also in Python: teamtailor-jobs-scraper-python

πŸ”₯ 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

Sync a niche job board

Run the module nightly for the employers your board covers, upsert rows by job_id and delete the ones that stopped appearing. Since closed roles leave the feed, the absence of a job_id is your expiry signal.

Send new openings to a webhook

Compare today's job_id set with the previous run and POST only the newcomers to Slack, Teams or a CRM endpoint. A sales team watching target accounts hears about a new head of data before the press release.

Serve vacancies from your own API

Store rows in Postgres or a document database and expose /jobs?company=storytel&country=SE. Your front end filters on location_country and date_published without calling Teamtailor at request time.

Render job pages with the original markup

With includeHtml on, description_html keeps the employer's headings and lists. Sanitise it on the server and render it in a Next.js or Astro page so the posting reads the way the company wrote it.

⚑ 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-javascript.git
cd teamtailor-jobs-scraper-javascript
npm install
export APIFY_TOKEN=your_token_here
node index.js                       # uses input.json
node index.js 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

Inside an existing service, these lines are all you need to copy. Awaiting call() waits for the run to end, and listItems() hands back the dataset's rows.

import { ApifyClient } from 'apify-client';

const client = new ApifyClient({ token: 'YOUR_APIFY_TOKEN' });
const run = await client.actor('DevilScrapes/teamtailor-jobs-scraper').call({"companies": ["oatly", "storytel"], "maxJobsPerCompany": 5, "includeHtml": false, "proxyConfiguration": {"useApifyProxy": true}});
const { items } = await client.dataset(run.defaultDatasetId).listItems();
console.log(items);

πŸ“₯ Input

index.js loads input.json and passes it on as it is. Two employers, Oatly and Storytel, are capped at five roles each, so a smoke test returns no more than ten objects for about 21.5 cents. In your own code, build companies from a database table of tracked employers; entries may be bare handles or https://<handle>.teamtailor.com links, but branded career domains are refused at validation time.

{
  "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

Every object is one vacancy. company, job_id, job_title, job_url, date_published, description_text and scraped_at are guaranteed; the remaining keys may be null. Dates carry their UTC offset, so new Date() parses them correctly, and location_country is a two-letter code you can map to a flag or a locale.

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 JavaScript

This script turns two saved runs into a webhook payload. It reads previous.json if present, keeps only postings whose job_id is new, and either prints the payload or POSTs it to WEBHOOK_URL with the global fetch in Node 20. No packages beyond Node.js itself.

import { readFile, writeFile } from 'node:fs/promises';

const current = JSON.parse(await readFile('results.json', 'utf8'));
const previous = JSON.parse(await readFile('previous.json', 'utf8').catch(() => '[]'));
const seen = new Set(previous.map((job) => job.job_id));

const fresh = current
    .filter((job) => !seen.has(job.job_id))
    .sort((a, b) => new Date(b.date_published) - new Date(a.date_published))
    .map((job) => ({
        company: job.company_name ?? job.company,
        title: job.job_title,
        where: [job.location_locality, job.location_country].filter(Boolean).join(', '),
        url: job.job_url,
    }));

const payload = { text: `${fresh.length} new Teamtailor roles`, jobs: fresh };
if (process.env.WEBHOOK_URL && fresh.length > 0) {
    const response = await fetch(process.env.WEBHOOK_URL, {
        method: 'POST',
        headers: { 'content-type': 'application/json' },
        body: JSON.stringify(payload),
    });
    console.log(`webhook answered ${response.status}`);
} else {
    console.log(JSON.stringify(payload, null, 2));
}
await writeFile('previous.json', JSON.stringify(current));

❓ FAQ

Does a run fail when every company fails?

No, the run still finishes as succeeded. Check the status message, which reads like Done β€” 0 of 3 companies returned data due to request failures in that case, or compare the row count with what you expected before you delete anything downstream.

Can I track hundreds of employers in one run?

Yes, companies accepts up to 1,000 entries and they are processed one after another. For very long lists, consider splitting them across several runs so each finishes within your platform's time limits.

How do I type the rows in TypeScript?

The client package includes declarations for runs and datasets. For the vacancies, write a TeamtailorJob interface from the output table, with job_id as number, the seven guaranteed keys required and the rest as string | null.

Do I need a Teamtailor login or customer key?

No. Every Teamtailor career site already publishes its open roles as a machine-readable job feed, and that feed is what the Actor reads. Your Apify token is the only secret the module needs, and it belongs in an environment variable, not in source control.

🧯 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 for JavaScript and Node.js: collect open roles from many Teamtailor career sites in one run and serve them from your API, sync them to a job board or post new ones to a webhook. JavaScript example for the Devil Scrapes Apify Actor.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages