# PeoplePerHour Job Scraper (`memo23/peopleperhour-job-scraper`) Actor

PeoplePerHour Job Scraper extracts open freelance jobs by keyword, category slug, or listing URL. Each row is one project: title, full brief, budget, client name and city, proposal count, and urgency badges. No login. Hourlies, profiles, and job-detail URLs stay out of scope.

- **URL**: https://apify.com/memo23/peopleperhour-job-scraper.md
- **Developed by:** [Muhamed Didovic](https://apify.com/memo23) (community)
- **Categories:** Jobs
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.00 / 1,000 job founds

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

## PeoplePerHour Job Scraper

Turn any PeoplePerHour keyword, category, or `/freelance-jobs` listing into structured freelance-job rows.

Start from a search term, a category slug, or a listing URL. Each row is one open project: title, full brief, budget, client, proposals, and badges. No PeoplePerHour login.

![How the PeoplePerHour Job Scraper works](https://raw.githubusercontent.com/muhamed-didovic/muhamed-didovic.github.io/main/assets/how-it-works-peopleperhour.png)

### Why Use This Scraper?

- Multiple entry points — keywords, category slugs, or listing URLs in one run
- Full project brief on every row (it already lives in the listing JSON)
- Client name, city, country, and public profile URL
- Budget in native currency plus converted amount, fixed or hourly
- Proposal counts and urgency / featured / opportunity / pre-funded badges
- Accepts the common `queries` + `item_limit` input aliases
- One row per `projectId`, billed per job written

### Overview

Built for freelancers, agencies, and researchers who need a feed of **open PeoplePerHour projects** — not hourlies (fixed-price services) and not freelancer profiles.

Every start type resolves to the same row shape: one job. A category URL and a keyword search both emit project rows. Job **detail** URLs are skipped because PeoplePerHour only embeds the job JSON on listing pages.

### Supported Inputs

#### URL types

| URL type | Pattern | Example |
|---|---|---|
| Jobs board | `/freelance-jobs` | `https://www.peopleperhour.com/freelance-jobs` |
| Category listing | `/freelance-jobs/{slug}` | `https://www.peopleperhour.com/freelance-jobs/technology-programming` |
| Keyword search | `/freelance-jobs?keyword=` | `https://www.peopleperhour.com/freelance-jobs?keyword=wordpress` |

#### Copy-pasteable startUrls

```json
{
  "startUrls": [
    { "url": "https://www.peopleperhour.com/freelance-jobs" },
    { "url": "https://www.peopleperhour.com/freelance-jobs/design" }
  ]
}
```

#### Keyword / search mode

```json
{
  "searchQueries": ["wordpress", "logo design"],
  "maxItems": 50,
  "sortBy": "latest"
}
```

#### Unsupported inputs

- Job detail URLs (`/freelance-jobs/{cat}/{sub}/{slug}-{id}`) — no job JSON on that page
- Hourlie / service listing URLs (`/services`, `/hourlies`)
- Freelancer profile URLs (`/freelancer/...`)
- Hosts outside peopleperhour.com
- `?q=` search — that query param does not filter on this site; use `keyword`

### Use Cases

| Audience | Use case |
|---|---|
| **Freelancers** | Watch fresh projects that match a skill keyword and bid while proposal counts are still low |
| **Agencies** | Feed outbound / proposal workflows with client name, country, and budget |
| **Market researchers** | Track demand, budgets, and category mix on a UK-facing freelance board |
| **Recruiters** | See which skills clients ask for, and how they price the work |
| **Aggregators** | Normalize PeoplePerHour jobs into a shared jobs feed |

### How It Works

1. Build listing URLs from `searchQueries`, `categorySlugs`, and pasted `startUrls`.
2. Fetch each page over HTTP (impit, got-scraping, and impers race; first 200 wins).
3. Parse `window.PPHReact.initialState` — the Redux blob with `entities.projects`.
4. Walk `?page=N` (20 jobs per page) until `maxItems` or the last page.
5. Dedupe by `projectId`, write the row, charge `job-found`.

Default proxy is Apify RESIDENTIAL pinned to GB. Datacenter exits have returned HTTP 202 on this site in the cloud.

### Input Configuration

| Field | Type | Required | Notes |
|---|---|---|---|
| `searchQueries` | string\[] | no | Each value becomes `?keyword=`. Example: `wordpress`. Default: none. |
| `categorySlugs` | string\[] | no | Path slugs: `artificial-intelligence`, `technology-programming`, `writing-translation`, `design`, `digital-marketing`, `video-photography`, `business`, `music-audio`, `marketing-branding-sales`, `social-media`. |
| `startUrls` | url\[] | no | Listing / category / search URLs. Job detail URLs are skipped. |
| `sortBy` | enum | no | `latest` (default), `popularity`, `ending`, `budget`, `relevance`. Applied to keyword and category searches. |
| `priceType` | enum | no | `fixed_price` or `hourly`. Omit for both. |
| `experienceLevel` | enum | no | `entry`, `intermediate`, or `expert`. Omit for all. Applied to every listing, including pasted URLs. |
| `postedWithin` | enum | no | `24h`, `7d`, or `30d`. Omit for any age. On `sort=latest`, paging stops once a page is older than the window. |
| `remoteOnly` | boolean | no | Default `false`. Restricts keyword/category searches to remote projects. |
| `onlyUniqueProjects` | boolean | no | Default `true`. Dedupes by `projectId` across the run. |
| `includeDescription` | boolean | no | Default `true`. Set `false` for title/budget/client/URL-only rows. |
| `maxItems` | integer | no | Hard cap on jobs written and billed. Default `50`. |
| `maxConcurrency` | integer | no | Default `6`. Pagination stays sequential so the cap is exact. |
| `proxy` | object | no | Default Apify RESIDENTIAL + country `GB`. |

Aliases accepted in the JSON input (not shown as separate Console fields): `queries` → `searchQueries`, `item_limit` → `maxItems`.

#### Example — keyword

```json
{
  "searchQueries": ["wordpress"],
  "maxItems": 50,
  "sortBy": "latest"
}
```

#### Example — category listing

```json
{
  "startUrls": [
    { "url": "https://www.peopleperhour.com/freelance-jobs/digital-marketing" }
  ],
  "maxItems": 30
}
```

#### Example — hourly + remote

```json
{
  "searchQueries": ["data entry"],
  "priceType": "hourly",
  "remoteOnly": true,
  "maxItems": 20
}
```

#### Example — expert jobs from the last day

```json
{
  "searchQueries": ["wordpress"],
  "experienceLevel": "expert",
  "postedWithin": "24h",
  "sortBy": "latest",
  "maxItems": 20
}
```

### Output Overview

One flat row per project. The listing blob already includes the full brief and nested client, so there is no second hop and no separate client dataset.

`experienceLevel` is derived from PeoplePerHour's budget bracket (`1` entry, `2` intermediate, `3` expert). Skill tags are **not** on the listing JSON and stay absent.

### Output Samples

Keyword run (`searchQueries: ["wordpress"]`), trimmed from a live row:

```json
{
  "projectId": "4522678",
  "title": "Website & Payment Portal Design  - Wordpress",
  "url": "https://www.peopleperhour.com/freelance-jobs/design/web-design/website-payment-portal-design-wordpress-4522678",
  "description": "I am looking to build a relatively basic website for my refurbishment & maintenance business…",
  "budget": 2500,
  "budgetConverted": 3355.32,
  "budgetBracket": 3,
  "currency": "GBP",
  "projectType": "fixed_price",
  "experienceLevel": "expert",
  "locationType": "remote",
  "category": "Design",
  "subCategory": "Web Design",
  "postedDate": "2026-09-21T09:33:58.000Z",
  "proposalCount": 49,
  "isUrgent": false,
  "isFeatured": false,
  "isOpportunity": false,
  "isPreFunded": false,
  "clientName": "Calum N.",
  "clientCity": "Glasgow",
  "clientCountry": "United Kingdom",
  "clientUrl": "https://www.peopleperhour.com/freelancer/calum-zxmqnqay",
  "searchQuery": "wordpress"
}
```

### Key Output Fields

**Job**

- `projectId` — stable PeoplePerHour project id
- `title`, `url`, `description`
- `postedDate`, `expiryDate` — ISO-8601
- `searchQuery`, `sourceUrl` — which input produced the row

**Money**

- `budget`, `budgetConverted`, `currency`
- `projectType` — `fixed_price` or `hourly`
- `budgetBracket` / `experienceLevel` — entry / intermediate / expert

**Client**

- `clientName`, `clientCity`, `clientCountry`, `clientCountryCode`
- `clientUrl`, `clientId`

**Activity**

- `proposalCount`, `unreadProposalCount`
- `isUrgent`, `isFeatured`, `isOpportunity`, `isPreFunded`
- `locationType`, `whereCanBid`, `duration`

### FAQ

**Do I need a PeoplePerHour account?**
No. The actor reads public listing pages only.

**Why were my job URLs skipped?**
Detail pages hydrate empty. Paste `/freelance-jobs`, a category page, or use `searchQueries`.

**Does `q=wordpress` work?**
No. Use `keyword` (the `searchQueries` field). `q` returns the unfiltered board.

**Are skill tags included?**
No. They are not in the listing JSON. The full brief, budget, client, and badges are.

**Does it scrape hourlies or freelancer profiles?**
No. Jobs only.

**What does a run cost?**
`$0.005` per GB of memory at start (default after publish is 512 MB → one start event) plus `$0.001` per job written. Empty rows are not charged.

**What proxy should I use?**
Keep the default: Apify residential, country GB. Datacenter proxies have returned HTTP 202 on listing URLs in the cloud.

### Support

Open an issue on this actor's **Issues** tab in the Apify Console. Bugs and field requests are welcome.

### Additional Services

Need a scheduled lead feed, extra fields, or a push into a CRM / warehouse? Custom work is available via the Issues tab.

### Explore More Scrapers

- [Fiverr Scraper](https://apify.com/memo23/fiverr-scraper) — gigs on the other large freelance marketplace
- [LinkedIn Jobs Scraper](https://apify.com/memo23/linkedin-jobs-scraper) — hired-role postings, from $0.25/1k
- [LinkedIn People Search](https://apify.com/memo23/linkedin-people-search) — people, not projects

The rest of the portfolio is on the [memo23 Store page](https://apify.com/memo23).

### 🤖 For AI Agents & LLM Apps

Compact reference for AI agents calling this actor via the [Apify MCP server](https://mcp.apify.com) or the Apify API (actor: `memo23/peopleperhour-job-scraper`).

**Purpose:** Scrapes public PeoplePerHour freelance **jobs** (not hourlies, not profiles) into one dataset row per project.

**Minimal input:**

```json
{
  "searchQueries": ["wordpress"],
  "maxItems": 25
}
```

**Output:** one row per job — projectId, title, url, description, budget, budgetConverted, currency, projectType, experienceLevel, locationType, category, subCategory, postedDate, expiryDate, proposalCount, isUrgent, isFeatured, isOpportunity, isPreFunded, clientName, clientCity, clientCountry, clientUrl, searchQuery.

**Behaviors an agent should know:**

- Always set `maxItems` (default 50). It is the hard cap on rows and on `job-found` charges. Filtered-out jobs are not charged.
- `experienceLevel` is `entry`, `intermediate`, or `expert`. `postedWithin` is `24h`, `7d`, or `30d`. Omit either to skip that filter.
- `searchQueries`, `categorySlugs`, and `startUrls` can be mixed; each keyword and each category is its own listing crawl.
- Job detail URLs in `startUrls` are skipped. Use listing/search/category URLs only.
- `queries` and `item_limit` are accepted aliases for `searchQueries` and `maxItems`.
- Skills are not returned. No login, no cookies.
- Billed `job-found` per written row plus `apify-actor-start` per GB of memory. Empty runs do not charge per-job events.

***

### ⚠️ Disclaimer

This Actor is an independent tool and is not affiliated with, endorsed by, or sponsored by PeoplePerHour Ltd or any of its subsidiaries or affiliates. All trademarks mentioned are the property of their respective owners.

The scraper accesses only publicly available freelance-job listing pages — no authenticated endpoints, bidder inboxes, or content behind a PeoplePerHour login. Users are responsible for ensuring their use complies with PeoplePerHour's Terms of Service, applicable data-protection law (GDPR, CCPA, etc.), and any contractual obligations of their own organisation. Do not use results to spam clients.

***

### SEO Keywords

PeoplePerHour scraper, PeoplePerHour job scraper, PeoplePerHour jobs API, scrape PeoplePerHour, Apify PeoplePerHour, PPH job scraper, freelance jobs scraper, PeoplePerHour projects, PeoplePerHour budget data, gig economy scraper, freelance marketplace data, PeoplePerHour proposals, PeoplePerHour category scrape, Fiverr alternative jobs scraper, Upwork alternative jobs data, PeoplePerHour lead generation, scrape freelance jobs UK, PeoplePerHour JSON export

# Actor input Schema

## `searchQueries` (type: `array`):

Free-text searches. Each value becomes PeoplePerHour's own `keyword` query (not `q` — that param does not filter). Example: wordpress, logo design.

## `categorySlugs` (type: `array`):

PeoplePerHour category path slugs. Allowed values: artificial-intelligence, technology-programming, writing-translation, design, digital-marketing, video-photography, business, music-audio, marketing-branding-sales, social-media.

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

Full /freelance-jobs URLs, including category pages and URLs with your own filters already applied. Job detail URLs are skipped.

## `sortBy` (type: `string`):

Applied to keyword and category searches. Listing URLs keep whatever sort is already in the URL.

## `priceType` (type: `string`):

Limit keyword/category searches to fixed-price or hourly jobs. Leave unset for both.

## `experienceLevel` (type: `string`):

Keep only jobs whose budget bracket matches this level: entry, intermediate, or expert. Leave unset for all three. Applied after each listing page is parsed, including pasted URLs.

## `postedWithin` (type: `string`):

Keep only jobs posted in this window: 24h, 7d, or 30d, measured from the run start. Leave unset for any age. On sort=latest the crawl stops once a page is older than the window.

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

Only return openly remote projects on keyword/category searches.

## `onlyUniqueProjects` (type: `boolean`):

Deduplicate by project ID across keywords, categories, and pages.

## `includeDescription` (type: `boolean`):

The listing JSON already contains the full brief. Turn off for smaller rows (title, budget, client, URL only).

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

Hard cap on jobs written to the dataset (and billed). Leave empty to use 50, or to let item\_limit apply. Example: 25.

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

Reserved for parallel listing starts. Pagination is sequential so the item cap stays exact.

## `queries` (type: `array`):

Drop-in alias for searchQueries. Used when searchQueries is empty. Same values: free-text keywords such as wordpress. Default: unused.

## `item_limit` (type: `integer`):

Drop-in alias for maxItems. Used when maxItems is omitted. Hard cap on jobs written and billed. Example: 30. Default: unused.

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

PeoplePerHour has returned HTTP 202 on datacenter exits in the Apify cloud. Default is residential pinned to GB.

## Actor input object example

```json
{
  "searchQueries": [
    "wordpress"
  ],
  "categorySlugs": [],
  "startUrls": [],
  "sortBy": "latest",
  "remoteOnly": false,
  "onlyUniqueProjects": true,
  "includeDescription": true,
  "maxItems": 50,
  "maxConcurrency": 6,
  "proxy": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ],
    "apifyProxyCountry": "GB"
  }
}
```

# 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 = {
    "searchQueries": [
        "wordpress"
    ],
    "categorySlugs": [],
    "startUrls": [],
    "sortBy": "latest",
    "remoteOnly": false,
    "onlyUniqueProjects": true,
    "includeDescription": true,
    "maxItems": 50,
    "proxy": {
        "useApifyProxy": true,
        "apifyProxyGroups": [
            "RESIDENTIAL"
        ],
        "apifyProxyCountry": "GB"
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("memo23/peopleperhour-job-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 = {
    "searchQueries": ["wordpress"],
    "categorySlugs": [],
    "startUrls": [],
    "sortBy": "latest",
    "remoteOnly": False,
    "onlyUniqueProjects": True,
    "includeDescription": True,
    "maxItems": 50,
    "proxy": {
        "useApifyProxy": True,
        "apifyProxyGroups": ["RESIDENTIAL"],
        "apifyProxyCountry": "GB",
    },
}

# Run the Actor and wait for it to finish
run = client.actor("memo23/peopleperhour-job-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 '{
  "searchQueries": [
    "wordpress"
  ],
  "categorySlugs": [],
  "startUrls": [],
  "sortBy": "latest",
  "remoteOnly": false,
  "onlyUniqueProjects": true,
  "includeDescription": true,
  "maxItems": 50,
  "proxy": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ],
    "apifyProxyCountry": "GB"
  }
}' |
apify call memo23/peopleperhour-job-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,memo23/peopleperhour-job-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/eyQOQedAwLqmtcrwK/builds/ywzIeiH3naRuzpN7b/openapi.json
