# Job Posting Parser — seniority, salary and remote regions (`keith-apis/job-posting-parser`) Actor

Turn job postings into structured data: seniority, job function, remote/hybrid/onsite with allowed countries, annualized salary, skills and visa sponsorship flags, with a reason for every label. Rules-based, no LLM. $0.002 per posting.

- **URL**: https://apify.com/keith-apis/job-posting-parser.md
- **Developed by:** [Lai Keith](https://apify.com/keith-apis) (community)
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $2.00 / 1,000 posting parseds

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

## Job Posting Parser: seniority, remote regions, salary and skills from job ads

**Job Posting Parser is an Apify Actor that turns a list of job postings into structured data, one dataset row per posting: seniority, job function, remote/hybrid/onsite with the countries and regions a remote candidate may live in, salary with an annualized range, employment type, years of experience, skills and flags such as visa sponsorship.** It is rules-based with no LLM, so the same posting always gives the same result, every label comes with a reason, and you pay only for postings parsed ($0.002 each, or $2 per 1,000).

### What does Job Posting Parser do?

You give it a JSON list of job postings (the ad text as plain text or HTML, plus an optional title, location and company). For each posting it returns:

- **normalized_title:** gender tags (m/w/d), requisition IDs, "Remote - US" suffixes and pay snippets removed; Sr., Mgr and SWE expanded.
- **seniority:** one of 10 levels: intern, junior, mid, senior, staff, principal, lead, manager, director, exec. People managers are kept apart from IC titles such as Product Manager, and II, III and L5 are understood.
- **function:** one of 12 functions (engineering, data, design, product, marketing, sales, support, ops, finance, hr, legal, other), plus a specialty for engineering and data roles (backend, frontend, ml_ai, devops_sre, …).
- **remote:** remote, hybrid, onsite or unknown, with `regions_allowed` (ISO country codes such as US or DE, and regions such as worldwide, emea, europe, latam) and timezone hints.
- **salary:** min, max, currency, pay period and an annualized min and max (2,080 hours, 260 days, 52 weeks or 12 months a year). It reads $, €, £, 30 ISO currency codes and European number formats, and ignores funding rounds, stipends and 401(k).
- **employment_type, experience_years, skills** (114 known technologies and tools) and **flags:** visa sponsorship (offered, explicitly not, or not mentioned), security clearance, equity, relocation, travel, degree, required languages, talent-pool postings and AI/ML roles.
- **reasons:** one human-readable reason per label, e.g. `seniority=senior: title contains 'Senior'`.

Results land in the run's dataset, ready to export as CSV, JSON or Excel.

### When should I use it?

- **Job boards and aggregators:** add consistent filters (seniority, remote region, salary) to scraped or imported postings.
- **Labour-market analysis:** compare salaries across hourly, monthly and yearly ads, or count remote roles open to a country.
- **Recruiting and HR-tech:** tag incoming postings before they reach your ATS or search index.
- **After a scraper:** chain it after any job-scraping Actor to turn raw ads into clean fields.

If you need one posting at a time from your own code, use the [Job Posting Parser API](https://job-parser-api.fly.dev/) instead. It runs the same rules behind a REST endpoint.

### Input

```json
{
  "postings": [
    { "id": "job-1", "title": "Sr. Backend Engineer (Python) - Remote, US", "location": "Remote - US", "company": "Acme",
      "text": "Fully remote, open to candidates based in the United States or Canada. Base salary $160,000 - $200,000 per year plus equity. 5+ years of experience with Python, Django and PostgreSQL. We are unable to sponsor visas. Full-time." },
    { "id": "job-2", "text": "Freelance Product Designer - Remote (EMEA)\n$60 - $80 per hour, 6-month contract." }
  ]
}
```

| Field | Required | Description |
|---|---|---|
| `text` | yes, unless `title` is given | The ad or job description, plain text or HTML. `description` is accepted as an alias |
| `title` | no | Job title as posted (300 characters). If missing, the first line of `text` is used |
| `location` | no | Location as posted, e.g. "Remote - US" or "Hybrid (London)" |
| `company` | no | Company name, stripped from the title and skills |
| `id` | no | Your own ID, copied to the output row so you can join results |

A plain string in the list is treated as `text`. You can send up to 1,000 postings per run.

### Output

One row per posting (abridged; this is the real output for the first posting above):

```json
{
  "index": 0, "id": "job-1", "title": "Sr. Backend Engineer (Python) - Remote, US", "location": "Remote - US", "company": "Acme",
  "normalized_title": "Senior Backend Engineer (Python)",
  "seniority": { "level": "senior", "confidence": 0.85 },
  "function": { "name": "engineering", "confidence": 0.9, "specialty": "backend" },
  "remote": { "type": "remote", "confidence": 0.9, "regions_allowed": ["US", "CA"], "timezones": [] },
  "salary": { "min": 160000, "max": 200000, "currency": "USD", "period": "year", "text": "$160,000 - $200,000", "annualized_min": 160000, "annualized_max": 200000 },
  "employment_type": "full_time",
  "experience_years": { "min": 5, "max": null },
  "skills": ["Python", "Django", "PostgreSQL"],
  "flags": { "visa_sponsorship": false, "equity_offered": true, "...": "..." },
  "reasons": ["title: removed 'Remote, US'", "seniority=senior: title contains 'Senior'", "..."],
  "meta": { "parser_version": "1.0.0", "description_chars": 226, "description_was_html": false, "description_truncated": false }
}
```

### How accurate is it?

On 44 real held-out postings (title and location only): **seniority 97.7%, job function 93.2%, remote type 100%, allowed regions 95.5%**, after one round of generic fixes. Before those fixes the same set scored 95.5%, 68.2%, 97.7% and 93.2%. Job function is the weakest field because many titles are genuinely ambiguous.

### How much does it cost?

It's **pay per result: $0.002 per posting** ($2 per 1,000 postings), plus a little Apify platform usage. Postings with no title and no text are skipped and **not charged**. If you set a maximum spend on the run, it stops when that limit is reached.

### FAQ

**How do I extract salary, seniority and remote location from job postings in bulk?**
Paste your postings as `postings` (or pass a scraper's output), run the Actor, and export the dataset. Each row has the seniority level, remote type with allowed countries, and the salary with an annualized range.

**Can it tell which countries a remote job is open to?**
Yes. `remote.regions_allowed` lists ISO country codes and regions taken from the title, location and eligibility sentences such as "must be based in…" or "open to candidates in…". Office lists in the description are ignored.

**Why not just ask ChatGPT or another LLM to parse the postings?**
An LLM can return different answers for the same posting and costs tokens per call. This Actor is deterministic, fast, and explains each label, so the same fields come out the same way on every posting.

**How does it compare with other job description parsers?**
The closest Apify Actor for similar fields charges about $2.57 per 1,000; this one is $2 per 1,000. Enterprise parsers such as Textkernel (from about $99 a month) and Affinda (about $800 for 6,000 credits) cost far more.

**Is my data stored?**
The results stay in your own Apify storage, under your account's retention settings. The postings are parsed inside the run and are not sent to any other service.

### Related

- [Job Posting Parser API](https://job-parser-api.fly.dev/): the same parser as a REST API, one posting or a batch of up to 20 per call.

# Actor input Schema

## `postings` (type: `array`):

One object per job posting: text (the ad or description, plain text or HTML), plus optional title, location, company and id (passed through to the output). If title is missing, the first line of text is used. A plain string is treated as text. Up to 1,000 per run; charged per posting parsed, empty postings are skipped and not charged.

## Actor input object example

```json
{
  "postings": [
    {
      "id": "job-1",
      "title": "Sr. Backend Engineer (Python) - Remote, US",
      "location": "Remote - US",
      "company": "Acme",
      "text": "Fully remote, open to candidates based in the United States or Canada. Base salary $160,000 - $200,000 per year plus equity. 5+ years of experience with Python, Django and PostgreSQL. We are unable to sponsor visas. Full-time."
    },
    {
      "id": "job-2",
      "title": "Data Analyst (m/w/d)",
      "location": "Berlin, Germany",
      "text": "Hybrid role, 3 days a week in our Berlin office. €55.000 - €65.000 per year. SQL, Python and Tableau. Fluent German required."
    },
    {
      "id": "job-3",
      "text": "Freelance Product Designer - Remote (EMEA)\n$60 - $80 per hour, 6-month contract. Figma and design systems experience."
    }
  ]
}
```

# Actor output Schema

## `postings` (type: `string`):

No description

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "postings": [
        {
            "id": "job-1",
            "title": "Sr. Backend Engineer (Python) - Remote, US",
            "location": "Remote - US",
            "company": "Acme",
            "text": "Fully remote, open to candidates based in the United States or Canada. Base salary $160,000 - $200,000 per year plus equity. 5+ years of experience with Python, Django and PostgreSQL. We are unable to sponsor visas. Full-time."
        },
        {
            "id": "job-2",
            "title": "Data Analyst (m/w/d)",
            "location": "Berlin, Germany",
            "text": "Hybrid role, 3 days a week in our Berlin office. €55.000 - €65.000 per year. SQL, Python and Tableau. Fluent German required."
        },
        {
            "id": "job-3",
            "text": "Freelance Product Designer - Remote (EMEA)\n$60 - $80 per hour, 6-month contract. Figma and design systems experience."
        }
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("keith-apis/job-posting-parser").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = { "postings": [
        {
            "id": "job-1",
            "title": "Sr. Backend Engineer (Python) - Remote, US",
            "location": "Remote - US",
            "company": "Acme",
            "text": "Fully remote, open to candidates based in the United States or Canada. Base salary $160,000 - $200,000 per year plus equity. 5+ years of experience with Python, Django and PostgreSQL. We are unable to sponsor visas. Full-time.",
        },
        {
            "id": "job-2",
            "title": "Data Analyst (m/w/d)",
            "location": "Berlin, Germany",
            "text": "Hybrid role, 3 days a week in our Berlin office. €55.000 - €65.000 per year. SQL, Python and Tableau. Fluent German required.",
        },
        {
            "id": "job-3",
            "text": """Freelance Product Designer - Remote (EMEA)
$60 - $80 per hour, 6-month contract. Figma and design systems experience.""",
        },
    ] }

# Run the Actor and wait for it to finish
run = client.actor("keith-apis/job-posting-parser").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "postings": [
    {
      "id": "job-1",
      "title": "Sr. Backend Engineer (Python) - Remote, US",
      "location": "Remote - US",
      "company": "Acme",
      "text": "Fully remote, open to candidates based in the United States or Canada. Base salary $160,000 - $200,000 per year plus equity. 5+ years of experience with Python, Django and PostgreSQL. We are unable to sponsor visas. Full-time."
    },
    {
      "id": "job-2",
      "title": "Data Analyst (m/w/d)",
      "location": "Berlin, Germany",
      "text": "Hybrid role, 3 days a week in our Berlin office. €55.000 - €65.000 per year. SQL, Python and Tableau. Fluent German required."
    },
    {
      "id": "job-3",
      "text": "Freelance Product Designer - Remote (EMEA)\\n$60 - $80 per hour, 6-month contract. Figma and design systems experience."
    }
  ]
}' |
apify call keith-apis/job-posting-parser --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,keith-apis/job-posting-parser"
        }
    }
}
```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/913TPrQIrhVDURak1/builds/wCn74u5a471Qs4c4I/openapi.json
