# Otta Scraper \[$2.50/1K💰] | Startup Job Data (`ahmed_jasarevic/otta-scraper`) Actor

Scrape Otta - now Welcome to the Jungle - startup and tech jobs with salary range, equity, tech stack, remote policy and company funding. Search the live job index or enumerate the 43k-URL sitemap.

- **URL**: https://apify.com/ahmed_jasarevic/otta-scraper.md
- **Developed by:** [Ahmed Jasarevic](https://apify.com/ahmed_jasarevic) (community)
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $2.40 / 1,000 results

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.
Since this Actor supports Apify Store discounts, the price gets lower the higher subscription plan you have.

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

## Otta Scraper

Extract startup and tech job listings from Otta — now part of Welcome to the Jungle — with salary range, equity, tech stack, remote policy, seniority and company funding, using either targeted keyword search or full sitemap enumeration.

- **Two collection modes.** Search the live Algolia job index for a specific query, or enumerate the entire ~43,000-URL job sitemap for complete coverage.
- **Compensation depth.** `salary.min`, `salary.max`, `salary.currency`, free-text `otherCompensation` (such as `+ Equity`) and an explicit `salary.hidden` flag for employers who deliberately withhold the range.
- **Derived seniority.** `seniorityLevel` is computed from required years of experience into `Junior`, `Mid`, `Senior` or `Staff/Lead/Principal` — you do not have to build that band yourself.
- **Built-in buyer filters.** Country, remote-only, minimum salary and currency filters are applied in both modes.

### Main Use Cases

- **Startup job database** — build a structured catalogue of startup and tech roles from a live job index
- **Job salary benchmarking** — map advertised pay ranges by role, seniority, company and location
- **Equity compensation data** — capture free-text equity and other-compensation notes via `otherCompensation`
- **Tech stack intelligence** — aggregate `techStack` per company to see what a startup actually builds with
- **Startup hiring trends analysis** — track posted volume, `postedAt` dates and `acceptingApplications` over time
- **Company funding enrichment** — join `totalFunding`, `sectors`, `companySize` and `companyHeadquarters` onto every role
- **Remote job data** — isolate remote and remote-only postings with `isRemote` and `remoteOnly`

### How It Works

1. **Mode selection.** If `query` is set, search mode runs. Otherwise sitemap mode runs, enumerating `https://app.welcometothejungle.com/sitemap1.xml` (~43,000 job URLs). `query` takes priority over `startUrls`.
2. **Search mode** reads Welcome to the Jungle's public Algolia credentials from `https://www.welcometothejungle.com/api/env` and queries the jobs index directly, paging until your limit or the 1,000-result Algolia pagination cap.
3. **Sitemap mode** walks the job URLs and parses the embedded Apollo response cache from each page.
4. Each job is flattened into one row with salary, equity, tech stack, locations, seniority, funding and `acceptingApplications` (derived from `live` plus the `validUntil` expiry).
5. Buyer-side filters (`country`, `remoteOnly`, `minSalary`, `currency`) are applied in both modes. Adaptive rate limiting keeps the crawl polite.

### Map the Startup Job Market by Salary, Tech Stack and Funding

Most Otta extractors return a title, a company and a URL. This one returns the fields you actually analyse: a full salary range with currency, free-text other-compensation notes, the technologies the role uses, a derived seniority band, whether the employer hid the salary, and the company's total funding and sectors — so a single run answers "who is paying for senior engineers in London, and how much have they raised?"

### Input

| Field | Type | Required | Default | Notes |
|---|---|---|---|---|
| `query` | string | no | `data engineer` | Search keywords. Setting it switches the Actor into search mode and takes priority over `startUrls`. |
| `startUrls` | array | no | — | Job URLs or an extra sitemap to enumerate. Objects of the form `{ "url": "..." }`. Only used in sitemap mode. |
| `maxItems` | integer | no | `100` | Maximum jobs to collect. Clamped to 10 on the FREE pricing tier. |
| `maxRequestsPerCrawl` | integer | no | `120` | Request budget for the run. |
| `maxConcurrency` | integer | no | `8` | Parallel requests, clamped to 1–50. |
| `requestsPerSecond` | number | no | `1.5` | Target request rate, clamped to 0.5–20. The Actor throttles adaptively under back-pressure. |
| `country` | string | no | — | Country filter. Accepts common aliases such as `UK` or `United Kingdom`. Passed to Algolia as a country code in search mode. |
| `remoteOnly` | boolean | no | `false` | Keep only remote roles. |
| `minSalary` | integer | no | — | Keep roles whose top of range is at least this value. |
| `currency` | string | no | — | Keep roles in this currency. Only filters roles that publish a salary currency. |
| `proxy` | object | no | — | Proxy configuration. Not needed on the default path. |

### Output

One row per job (41 fields):

`jobId`, `externalId`, `url`, `applyUrl`, `title`, `companyName`, `companyUrl`, `team`, `salary`, `salaryMin`, `salaryMax`, `salaryCurrency`, `techStack`, `locations`, `locationCodes`, `remoteOnly`, `isRemote`, `jobType`, `subFunction`, `postedAt`, `lastSeenAt`, `validUntil`, `description`, `requirements`, `mission`, `companyWebsite`, `companyDescription`, `companySize`, `companyHeadquarters`, `companyIndustry`, `seniorityLevel`, `sectors`, `totalFunding`, `fundingCurrency`, `live`, `acceptingApplications`, `isInternship`, `minYearsExperience`, `maxYearsExperience`, `originalPostingUrl`, `crawledAt`

`salary` is an object with `min`, `max`, `currency`, `otherCompensation` and `hidden`. `salaryMin` / `salaryMax` / `salaryCurrency` are flat duplicates, convenient for table views and filtering.

### Example Input

```json
{
  "query": "software engineer",
  "country": "UK",
  "remoteOnly": true,
  "minSalary": 60000,
  "currency": "GBP",
  "maxItems": 200,
  "requestsPerSecond": 1.5
}
```

### Example Output

```json
{
  "jobId": "1840233",
  "externalId": "wtj-1840233",
  "url": "https://www.welcometothejungle.com/en/jobs/1840233-senior-backend-engineer",
  "applyUrl": "https://boards.greenhouse.io/meridianlabs/jobs/4412087",
  "title": "Senior Backend Engineer",
  "companyName": "Meridian Labs",
  "companyUrl": "https://app.welcometothejungle.com/companies/meridian-labs",
  "team": "Platform",
  "salary": {
    "min": 75000,
    "max": 95000,
    "currency": "GBP",
    "otherCompensation": "+ Equity and 28 days holiday",
    "hidden": false
  },
  "salaryMin": 75000,
  "salaryMax": 95000,
  "salaryCurrency": "GBP",
  "techStack": ["Go", "PostgreSQL", "Kubernetes", "AWS", "gRPC"],
  "locations": ["Remote from UK"],
  "locationCodes": ["REMOTE_UK"],
  "remoteOnly": true,
  "isRemote": true,
  "jobType": "Engineering",
  "subFunction": "Backend",
  "postedAt": "2026-01-28T09:12:00Z",
  "lastSeenAt": "2026-02-10T21:04:00Z",
  "validUntil": "2026-04-30T23:59:00Z",
  "description": "You will own the ingestion pipeline that feeds our clinical trial platform.\n\nRequirements:\n- 5+ years building production backend services\n- Strong Go or Rust experience\n- Comfortable with event-driven architecture\n\nNice to have:\n- Experience in regulated or healthcare data",
  "requirements": ["5+ years building production backend services", "Strong Go or Rust experience", "Comfortable with event-driven architecture"],
  "mission": "Make clinical trial data usable in minutes, not months.",
  "companyWebsite": "https://meridianlabs.io",
  "companyDescription": "Series A healthtech building a clinical trial data platform used by 40 NHS trusts.",
  "companySize": "11-50",
  "companyHeadquarters": "London, United Kingdom",
  "companyIndustry": "Health, Data",
  "seniorityLevel": "Senior",
  "sectors": ["Health", "Data"],
  "totalFunding": 14500000,
  "fundingCurrency": "USD",
  "live": true,
  "acceptingApplications": true,
  "isInternship": false,
  "minYearsExperience": 5,
  "maxYearsExperience": null,
  "originalPostingUrl": "https://boards.greenhouse.io/meridianlabs/jobs/4412087",
  "crawledAt": "2026-02-11T10:02:41.880Z"
}
```

### Pricing

Pay-per-result at `$2.50` per 1,000 jobs on the FREE tier. On the FREE tier `maxItems` is clamped to **10 jobs**, so budget a paid tier for real market datasets.

This Actor is priced above the cheapest verified alternative (`$2.00` per 1,000), and deliberately so — the extra capability is the point: search mode, sitemap enumeration, equity capture, derived seniority, funding data and proxy-free access.

### Integrations & Automation

- **Apify API** — call from Python, Node or any HTTP client and read the dataset directly.
- **Schedules** — a daily run tracks new postings and salary movement for a chosen query; a weekly run is enough for hiring-trend analysis.
- **Webhooks** — push completed runs into a warehouse or alerting workflow.
- **MCP** — callable by AI agents through the Apify MCP server.

### Related Actors

- `ahmed_jasarevic/preply-scraper` — comparable structured-catalogue extraction for the language tutoring market, if your recruitment research spans both
- `ahmed_jasarevic/link-preview-api` — enrich company and job URLs with Open Graph and JSON-LD metadata

### FAQ

#### What happened to Otta?

Otta was absorbed by Welcome to the Jungle. Otta URLs redirect, and the live job data now lives on `welcometothejungle.com`. This Actor reads the current Welcome to the Jungle job index and sitemap, so it keeps working after the merger.

#### Does Otta have an API?

Not a public documented one. This Actor uses two internal surfaces: the site's public Algolia jobs index for search, and the embedded Apollo response cache on each job page for detail. Search mode reads the public Algolia credentials the site publishes at `https://www.welcometothejungle.com/api/env`.

#### When should I use search mode versus sitemap mode?

Use search mode when you want a targeted slice — a role, a country, a salary floor. Use sitemap mode when you want full coverage of the ~43,000-job sitemap. Setting `query` automatically selects search mode, and `query` takes priority over `startUrls` when both are present.

#### Why do I get fewer than 1,000 results in search mode?

Algolia serves at most 1,000 results for any single query, so `maxItems` is clamped to that cap in search mode regardless of what you request. To go beyond 1,000 jobs you need sitemap mode.

#### How is `salary.hidden` different from a missing salary?

`salary.hidden` is `true` when the employer actively concealed the range, which is a meaningful signal about company policy. A job where no salary is published at all has `salary: null`. Keeping those two cases distinct matters for pay-transparency analysis.

#### How is `seniorityLevel` calculated?

It is derived from `minYearsExperience`: up to 1 year is `Junior`, up to 3 is `Mid`, up to 6 is `Senior`, and above that is `Staff/Lead/Principal`. Jobs with no stated experience requirement return `null`.

#### What are alternatives to this actor?

The main alternative is a simpler Otta-focused extractor. Some job data is also available through third-party job APIs, but those return normalised postings rather than the company context — funding, sectors, size, mission — that this Actor includes. Otta itself, Wellfound and LinkedIn are alternative *sources* rather than equivalent scrapers.

#### Do I need a proxy?

No. The default configuration needs none. A `proxy` field is available if you want to add one.

### For AI Agents & LLM Apps

**Purpose:** returns one row per startup/tech job with salary range, equity, tech stack, seniority, remote policy and company funding.

**Minimal working input** — smallest input that produces a valid run:

```json
{ "query": "data engineer", "maxItems": 5 }
```

**Targeted search variant** with buyer filters:

```json
{
  "query": "software engineer",
  "country": "UK",
  "remoteOnly": true,
  "minSalary": 60000,
  "currency": "GBP",
  "maxItems": 100
}
```

**Full-coverage sitemap variant** — omit `query`, raise the request budget:

```json
{
  "maxItems": 5000,
  "maxRequestsPerCrawl": 5000,
  "requestsPerSecond": 1.5,
  "maxConcurrency": 8
}
```

**Output fields** — `jobId, externalId, url, applyUrl, title, companyName, companyUrl, team, salary, salaryMin, salaryMax, salaryCurrency, techStack, locations, locationCodes, remoteOnly, isRemote, jobType, subFunction, postedAt, lastSeenAt, validUntil, description, requirements, mission, companyWebsite, companyDescription, companySize, companyHeadquarters, companyIndustry, seniorityLevel, sectors, totalFunding, fundingCurrency, live, acceptingApplications, isInternship, minYearsExperience, maxYearsExperience, originalPostingUrl, crawledAt`

**Behaviors an agent should know**

- `query` **switches modes**. Set it and you get Algolia search; omit it and you get sitemap enumeration. `query` wins over `startUrls` when both are set.
- `startUrls` items are objects: `{ "url": "..." }`. Passing bare strings will not be read correctly.
- The input schema *prefills* `query` with `data engineer`, which is what the Apify console form sends. **Over the API, omitting `query` or sending an empty string selects sitemap mode**, because the Actor only enters search mode when `query` is non-empty after trimming. For full-coverage runs, send no `query` and raise `maxRequestsPerCrawl`.
- Search mode is hard-capped at **1,000 results** by Algolia regardless of `maxItems`.
- On the FREE pricing tier `maxItems` is clamped to **10**.
- `minSalary` compares against the **top of the advertised range** (`salary.max`, falling back to `salary.min`), not the bottom.
- `currency` only filters jobs that actually publish a salary currency; jobs with `salary: null` pass through.
- `country` becomes an Algolia `offices.country_code` filter in search mode, and a text match on headquarters plus locations in sitemap mode — so counts can differ between modes.
- `salary: null` means no salary published. `salary.hidden: true` means the employer concealed it. Do not conflate them.
- `acceptingApplications` is derived from `live` plus `validUntil` expiry; it is not a raw source field.
- No proxy is required on the default path.

**Billing:** pay-per-result — `$2.50` per 1,000 jobs on the FREE tier, where the FREE tier also limits you to 10 jobs per run.

### Legal & Compliance

This Actor is an independent tool. It is not affiliated with, endorsed by, or sponsored by Otta or Welcome to the Jungle.

It reads only publicly accessible job listings and the public sitemap, using the same public search index the website itself uses. It does not bypass authentication, does not attempt to defeat access controls, and rate-limits itself to avoid undue load on the source site.

You are responsible for your own compliance with the source site's Terms of Service and with applicable data-protection law. Job output includes company names, workplace locations and public job descriptions. Note that EU platform obligations (such as GDPR and the EU Platform Work Directive) place specific obligations on parties that build job databases or matching tools — assess your own position before publishing or redistributing this data. Aggregated salary analysis may also attract your jurisdiction's pay-transparency rules.

Please do not use extracted personal data for unsolicited commercial outreach in violation of applicable law, including CAN-SPAM, GDPR or TCPA.

### SEO Keywords

otta scraper, otta jobs, otta api, otta job data, startup jobs, startup job database, tech job listings data, job salary data, equity compensation data, remote job data, tech stack by company, company funding data, hiring trends analysis, startup hiring intelligence, junior developer jobs data, job market research, welcome to the jungle jobs, europe startup jobs, build a job board, job board api alternative, otta alternatives

# Actor input Schema

## `query` (type: `string`):

Position / job name to search for (e.g. "data engineer"). When set, the actor searches the Welcome to the Jungle job index instead of enumerating the sitemap, and this wins over startUrls. Results come straight from the index (one API call, no page fetches); Algolia search returns at most 1000 matching jobs.

## `startUrls` (type: `array`):

Sitemap URLs to enumerate job pages from (defaults to the Welcome to the Jungle job sitemap), or individual job URLs to scrape directly. Ignored when Search keywords is set.

## `maxItems` (type: `integer`):

Maximum number of jobs to scrape. Free users are capped at 10.

## `maxRequestsPerCrawl` (type: `integer`):

Safety cap on total HTTP requests made by the crawler.

## `maxConcurrency` (type: `integer`):

How many job pages may be fetched at the same time. Request starts are still paced by the rate limit below, so this only adds headroom for slow responses.

## `requestsPerSecond` (type: `number`):

Target request rate. The crawler paces requests at this rate and automatically slows down (and recovers) whenever the site answers with HTTP 429 or 403.

## `country` (type: `string`):

Optional case-insensitive filter matched against the job locations and company headquarters (e.g. "UK", "United Kingdom", "Germany", "France").

## `remoteOnly` (type: `boolean`):

When true, keep only jobs flagged remote-only or located entirely in a "Remote from ..." bucket.

## `minSalary` (type: `integer`):

Keep only jobs whose top-of-range (or only) salary is at least this value, in the job's own currency. Jobs without a published salary are dropped.

## `currency` (type: `string`):

Optional ISO currency code filter, matched against the published salary currency (e.g. "GBP", "EUR", "USD").

## `proxy` (type: `object`):

Select proxies to be used by your crawler.

## Actor input object example

```json
{
  "query": "data engineer",
  "startUrls": [
    {
      "url": "https://app.welcometothejungle.com/sitemap1.xml"
    }
  ],
  "maxItems": 100,
  "maxRequestsPerCrawl": 120,
  "maxConcurrency": 8,
  "requestsPerSecond": 1.5,
  "remoteOnly": false,
  "proxy": {
    "useApifyProxy": false
  }
}
```

# Actor output Schema

## `results` (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 = {
    "query": "data engineer",
    "startUrls": [
        {
            "url": "https://app.welcometothejungle.com/sitemap1.xml"
        }
    ],
    "proxy": {
        "useApifyProxy": false
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("ahmed_jasarevic/otta-scraper").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 = {
    "query": "data engineer",
    "startUrls": [{ "url": "https://app.welcometothejungle.com/sitemap1.xml" }],
    "proxy": { "useApifyProxy": False },
}

# Run the Actor and wait for it to finish
run = client.actor("ahmed_jasarevic/otta-scraper").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 '{
  "query": "data engineer",
  "startUrls": [
    {
      "url": "https://app.welcometothejungle.com/sitemap1.xml"
    }
  ],
  "proxy": {
    "useApifyProxy": false
  }
}' |
apify call ahmed_jasarevic/otta-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,ahmed_jasarevic/otta-scraper"
        }
    }
}
```

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/Heg2KLzTcEm5ItPA2/builds/GPpRrk8mtlGMAOVkz/openapi.json
