# PeoplePerHour Scraper · Freelance Jobs (`bibian/peopleperhour-scraper`) Actor

Extract freelance jobs posted by clients on PeoplePerHour: budget, skills, experience level, country restriction and full description, ready to pipe into your own job board or lead list.

- **URL**: https://apify.com/bibian/peopleperhour-scraper.md
- **Developed by:** [Vivien MORVAN](https://apify.com/bibian) (community)
- **Categories:** Jobs
- **Security:** Limited permissions
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $0.96 / 1,000 project postings

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

## PeoplePerHour Scraper · Freelance Jobs

Extracts freelance jobs posted by clients on [PeoplePerHour](https://www.peopleperhour.com), a UK-based freelance marketplace used worldwide. Each job comes with its budget, skills, experience level and full description, output in the same unified field names (`datePosted`, `salaryMin`, `employmentType`...) shared by every job and freelance-mission Actor from this author, so it drops straight into a pipeline already built for one of the others.

| | |
|---|---|
| **What you get** | Open freelance jobs from PeoplePerHour: title, budget, skills, experience level, posting and expiry dates, and full description. |
| **Price** | $0.0012 per job (about $1.20 per 1,000): a 50-job test run costs about $0.06, start fee included. |
| **Speed** | No login, proxy or browser needed for a default run. |
| **Try it** | Click **Try for free**: the default input already returns 50 of the most recent jobs across every category. |
| **Keep it fresh** | Schedule it daily and keep only the new jobs: turn on **Only new results**. |

### What you can do with it

- **Find freelance work**: filter by keyword or category and check new jobs every morning.
- **Monitor demand for a skill**: how many jobs mention "wordpress" or "logo design" this week, and at what budget.
- **Feed a lead list**: a client posting a job for a skill you sell is a lead; filter by category.
- **Study the freelance market**: budgets, in-demand skills and experience levels requested over time.

### Input

| Field | What it does |
|---|---|
| Keywords | Job title or skill, using PeoplePerHour's own search (e.g. `wordpress`). Empty means every open job. |
| Category slug | Optional, PeoplePerHour's own category slug (e.g. `technology-programming`, `design`, `writing-translation`), copied from a filtered `peopleperhour.com/freelance-jobs/` URL. |
| Published within (days) | Keep only jobs posted in the last N days, computed from the job's own exact posting date. Use `1` for daily monitoring. |
| Maximum number of jobs | The run stops once this many jobs are saved. |
| Only new results | For scheduled runs: skip the jobs an earlier run of this same search already delivered. |
| Include full descriptions | Adds the full job description as plain text, with any e-mail address or phone number removed. |

### Output

One record per job, with the unified field names shared by every job and freelance-mission Actor from this author, plus a few fields specific to PeoplePerHour kept alongside them:

| Field | What it is |
|---|---|
| `id`, `url`, `title` | `peopleperhour.com:<job id>`, the public job page, and its title. |
| `companyName`, `companyUrl`, `confidentialCompany` | Always `null` / `true`. PeoplePerHour always truncates the client's display name to a first name and a last-name initial ("Social S."), for an individual and a registered business alike - the site gives no reliable signal to tell the two apart, so no client name is ever read into this record (see "Good to know"). |
| `locationLabel`, `city`, `region`, `postalCode`, `country` | The country the client restricted applicants to, when the job has that restriction (most jobs are open to "Any" country, so these stay `null`). PeoplePerHour never publishes a city or region for a job. |
| `remote` | `"remote"` when PeoplePerHour's own data marks the job as telecommute (the vast majority), `"unknown"` otherwise. |
| `employmentType` | Always `"freelance"`. |
| `salaryMin`, `salaryMax`, `salaryCurrency`, `salaryPeriod` | The client's own budget figure and currency (not PeoplePerHour's approximate GBP conversion when the client priced in another currency), e.g. `850 GBP` fixed or `15 GBP` per `"hour"`. PeoplePerHour shows a single figure, never a range, so `salaryMin` and `salaryMax` are equal. |
| `datePosted`, `validThrough` | The job's own posting and expiry timestamps. |
| `experienceLevel` | The level the client selected ("Entry", "Intermediate", "Expert"), when set. |
| `industry`, `subcategory` | PeoplePerHour's own category and subcategory, read from the job's URL (e.g. `"Design"` / `"Web Design"`). |
| `skills` | Skills listed by the client, as PeoplePerHour's own tags. |
| `source`, `sourceId`, `scrapedAt` | `"peopleperhour.com"`, the job's numeric id, and when this run read it. |
| `budgetType` | `"fixed"` or `"hourly"`, PeoplePerHour's own label for the budget above. |
| `description` | Full job description as plain text (only when **Include full descriptions** is on). Any e-mail address or phone number is removed before saving. |

One full record from a real run:

```json
{
  "id": "peopleperhour.com:4529239",
  "url": "https://www.peopleperhour.com/freelance-jobs/design/web-design/creative-graphical-web-design-social-media-concept-ui-4529239",
  "title": "Creative Graphical Web Design,  Social Media Concept, UI",
  "companyName": null,
  "companyUrl": null,
  "confidentialCompany": true,
  "locationLabel": null,
  "city": null,
  "region": null,
  "postalCode": null,
  "country": null,
  "remote": "remote",
  "employmentType": "freelance",
  "salaryMin": 40,
  "salaryMax": 40,
  "salaryCurrency": "USD",
  "salaryPeriod": "hour",
  "datePosted": "2026-11-02T01:08:21.000Z",
  "validThrough": "2026-12-02T01:08:21.000Z",
  "experienceLevel": "Expert",
  "industry": "Design",
  "skills": ["Typography", "Logo", "Brand identity", "Concept design", "User interface (UI)", "Website design"],
  "source": "peopleperhour.com",
  "sourceId": "4529239",
  "scrapedAt": "2026-11-02T01:10:59.620Z",
  "subcategory": "Web Design",
  "budgetType": "hourly",
  "description": "I'm spearheading a new social-media-meets-service hub... (full text, redacted of any contact details)"
}
```

Export the dataset as JSON, CSV or Excel, or read it through the Apify API.

### Good to know

- No login, proxy or browser needed for a default run: the Actor reads PeoplePerHour's own public job pages directly, respecting `robots.txt` (pagination always requests `?page=` first, the only query form the site's own `robots.txt` allows).
- `datePosted` and `validThrough` come from the job page's own timestamp, which carries no timezone marker; treated as UTC, the same documented best-effort approximation used by this author's other date-only sources.
- No personal data is collected. PeoplePerHour always truncates a client's name to a first name and a last-name initial, whether the account behind it is an individual or a registered business, and the page gives no way to tell the two apart (its own structured data names the employer "PeoplePerHour" itself, never the real client) - so the client's name, avatar and profile link are never read into this dataset at all, for any job. The list page also shows a second name and price next to each job card; that pair is a different freelancer PeoplePerHour suggests for the job, not its client, and is likewise never read.
- Any e-mail address or phone number a client typed into the description is removed before saving.
- A job with no explicit country restriction (the large majority) keeps `country` and `locationLabel` as `null`: PeoplePerHour does not require one, by the nature of a freelance marketplace.

### Why this one

- **Reads PeoplePerHour's own structured job data**, not a brittle scrape of the visible page text: budget, skills, experience level and exact dates come from the same schema.org data the job page itself publishes for search engines, not guessed from formatting that can change.
- **One schema with this author's other job and freelance actors.** The same field names everywhere (`datePosted`, `salaryMin`, `employmentType`, `skills`...), so PeoplePerHour drops into a pipeline built for Workana, Freelancer.com or any other source from this author without remapping a single field.
- **No client is ever named**, individual or business. A generic scraper would copy whatever truncated name the page shows; this Actor treats that as personal data and keeps it out of the dataset entirely, for every job, with no guesswork about which accounts are "really" companies.

### Frequently asked questions

**Do I need an account or a proxy for PeoplePerHour?** No. The Actor reads publicly visible jobs only.

**Why is `companyName` always empty?** Because PeoplePerHour shows every client's name the same truncated way, whether they are an individual or a business, with no way to tell which is which. See "Good to know" above.

**Why is `country` almost always empty?** Most PeoplePerHour jobs are open to applicants from any country; `country` is only filled when the client explicitly restricted the job to one.

**Which formats can I export?** JSON, CSV, Excel, XML, HTML table or RSS, from the dataset page or through the API.

**Is it legal to scrape?** The Actor collects publicly visible jobs and respects the robots.txt rules of the site. How you use the data is your responsibility: check the site's terms and the laws that apply to you, for example the GDPR.

**Something does not work?** Open an issue in the Issues tab. Every public actor is test-run daily and the broken ones are repaired.

**Did this save you time?** A short review on the Store page helps other people find the Actor, and tells me what to improve next.

# Actor input Schema

## `keywords` (type: `string`):

Job title or skill, for example "wordpress" or "logo design". Uses PeoplePerHour's own search. Leave empty for every open job.

## `category` (type: `string`):

PeoplePerHour's own category slug, copied from the "/freelance-jobs/" part of a filtered peopleperhour.com URL (for example "technology-programming", "design", "writing-translation"). Leave empty for every category.

## `publishedWithinDays` (type: `integer`):

Keep only jobs posted in the last N days (computed from the job's own exact posting date). Use 1 for a daily monitoring run. Leave empty for no limit.

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

The run stops once this many jobs are saved.

## `onlyNew` (type: `boolean`):

For scheduled runs. Skip the results that an earlier run of this same search already delivered, so each run returns, and charges, only what is new. The memory is kept in a key-value store of your account named "only-new-memory-bibian-" followed by the ID of this Actor.

## `includeDescriptions` (type: `boolean`):

Add the full job description as plain text. Any e-mail address or phone number a client typed into the description is removed.

## Actor input object example

```json
{
  "keywords": "wordpress",
  "publishedWithinDays": 7,
  "maxItems": 50,
  "onlyNew": false,
  "includeDescriptions": true
}
```

# Actor output Schema

## `jobs` (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 = {
    "keywords": "wordpress",
    "publishedWithinDays": 7,
    "maxItems": 50
};

// Run the Actor and wait for it to finish
const run = await client.actor("bibian/peopleperhour-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 = {
    "keywords": "wordpress",
    "publishedWithinDays": 7,
    "maxItems": 50,
}

# Run the Actor and wait for it to finish
run = client.actor("bibian/peopleperhour-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 '{
  "keywords": "wordpress",
  "publishedWithinDays": 7,
  "maxItems": 50
}' |
apify call bibian/peopleperhour-scraper --silent --output-dataset

```

## MCP server setup

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