# Doing Good Leeds Scraper (`memo23/doinggoodleeds-scraper`) Actor

Scrape paid jobs, volunteering, events, and training from doinggoodleeds.org.uk via WP-JSON. Pick any subset of 4 entity types. ~178 entities total. Title, employer, location, salary, apply email/URL, full description HTML inline per row. JSON or CSV out, billed per result.

- **URL**: https://apify.com/memo23/doinggoodleeds-scraper.md
- **Developed by:** [Muhamed Didovic](https://apify.com/memo23) (community)
- **Categories:** Jobs, Automation, Agents
- **Stats:** 19 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: 5.00 out of 5 stars

## Pricing

from $2.00 / 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.

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

## What's an Apify Actor?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
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.
Actors are written with capital "A".

## 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.
The best way to integrate Actors is as follows.

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).

# README

## Doing Good Leeds Scraper

**Scrape paid jobs, volunteering opportunities, events, and training courses from doinggoodleeds.org.uk — Leeds' volunteer hub.** Four custom-post-type collections behind one WP-JSON REST API: pick one or many via the `entityTypes` input. Each row carries full structured data — title, employer, location, salary (when present), apply email/URL, and the complete description HTML. JSON or CSV out, no compute charge per run, just per result.

#### How it works

![How Doing Good Leeds Scraper works](https://raw.githubusercontent.com/muhamed-didovic/muhamed-didovic.github.io/main/assets/how-it-works-doinggoodleeds.png)

#### ✨ Why use this scraper?

Tracking Leeds' voluntary-sector hiring? Mapping volunteer opportunities for placement programmes? Building a community events calendar? Cataloguing the training courses charities are funding?

- 🎯 **Four entity types in one actor.** `job-listings` (paid jobs, ~31), `volunteers` (volunteering opportunities, ~23), `event` (events, ~34), `training-course` (training courses, ~90) — pick any subset.
- ⚡ **WP-JSON REST API as the data source.** Each entity is a WordPress custom post type with its own `/wp-json/wp/v2/<cpt>` endpoint.
- 🏷️ **Custom `location` taxonomy.** WP Job Manager's `_job_location` meta is usually empty on Doing Good Leeds — we fall back to their custom `location` taxonomy (Leeds, regional Yorkshire, etc.).
- 📧 **Apply email / URL captured.** `_application` meta is split into `applyEmail` vs `externalApplyUrl` automatically.
- 🌟 **Cloudflare-friendly.** Only the passive `__cf_bm` cookie is enforced — any sane UA passes without proxy.
- 📤 **Clean exports.** One row per entity, all fields inline. JSON + CSV exported automatically.

#### 🎯 Use cases

| Team | What they build |
|------|-----------------|
| **Voluntary sector recruiters** | Daily Leeds nonprofit hiring feeds |
| **Volunteer co-ordinators** | Opportunity mapping for student placement programmes |
| **Community events platforms** | Pull events into a unified Leeds calendar |
| **Training providers** | Track what courses other charities are running / funding |
| **Workforce strategy** | Leeds third-sector pay benchmarks |
| **Researchers** | Leeds civil-society datasets (jobs + volunteers + events + training) |

#### 📥 Supported inputs

| URL pattern | Behaviour |
|---|---|
| `https://doinggoodleeds.org.uk/jobs/` etc. | **Listings** for any CPT (the actor picks based on `entityTypes`) |
| `https://doinggoodleeds.org.uk/job/<slug>/` | **Single paid job** |
| `https://doinggoodleeds.org.uk/volunteer/<slug>/` | **Single volunteer opportunity** |
| `https://doinggoodleeds.org.uk/event/<slug>/` | **Single event** |
| `https://doinggoodleeds.org.uk/training-course/<slug>/` | **Single training course** |
| `https://doinggoodleeds.org.uk/wp-json/wp/v2/{job-listings\|volunteers\|event\|training_course}` | **WP-JSON endpoint** |

Leave `startUrls` empty + pick `entityTypes` to scrape every entity of those types.

**Not supported:** mixing entity types in a single dataset row (each row is one CPT); hosts outside `doinggoodleeds.org.uk`.

#### 🔄 How it works

1. **Resolve start URLs** — either from explicit `startUrls`, or from `entityTypes` (default `["job-listings"]`).
2. **Classify + translate** each URL into the canonical `/wp-json/wp/v2/<cpt>` shape — tagging it with which CPT it represents.
3. **Walk pagination** via `X-WP-TotalPages` from the response header.
4. **Parse each WP-JSON item** — title, content HTML, WP Job Manager meta (where present), `_embed` taxonomies (categories, types, location).
5. **Push one normalised row per entity** to the dataset, tagged with the source CPT via the `cpt` field.

#### ⚙️ Input parameters

| Parameter | Type | Default | Description |
|---|---|---|---|
| `startUrls` | array | `["https://doinggoodleeds.org.uk/wp-json/wp/v2/job-listings"]` | Browser URLs, single-detail URLs, or WP-JSON endpoints. |
| `entityTypes` | array | `["job-listings"]` | Used when `startUrls` is empty. Allowed values: `job-listings`, `volunteers`, `event`, `training-course`. |
| `enrichTaxonomies` | boolean | `true` | When `true`, embeds taxonomy term names + featured image via WP-JSON `_embed`. |
| `postedWithinHours` | integer | (none) | Only return rows posted in the last N hours (24 = last day, 72 = last 3 days). Empty/0 = all. Ideal for daily monitoring runs that only want fresh postings. |
| `maxItems` | integer | `1000` | Hard cap on rows pushed (~178 total across all CPTs). |
| `maxConcurrency` / `minConcurrency` | integer | `5` / `1` | Parallel WP-JSON page-fetch limits. |
| `maxRequestRetries` | integer | `5` | Retries before a failed request is given up. |
| `proxy` | object | No proxy | Cloudflare lets us through without a proxy. |

#### 📊 Output overview

Each entity is one **single dataset row**. The `type` field tells you what entity it is (`job`, `volunteer`, `event`, `training`), and the `cpt` field carries the raw CPT slug.

#### 📦 Output sample

```json
{
  "type": "job",
  "cpt": "job-listings",
  "source": "doinggoodleeds.org.uk",
  "jobId": "111130",
  "slug": "young-adults-worker-3",
  "jobUrl": "https://doinggoodleeds.org.uk/job/young-adults-worker-3/",
  "wpJsonUrl": "https://doinggoodleeds.org.uk/wp-json/wp/v2/job-listings/111130",
  "title": "Young Adults Worker",
  "description": "<p>Young Adults Worker role at Waythrough…</p>",
  "descriptionText": "Young Adults Worker role at Waythrough…",
  "companyName": null,
  "companyWebsite": null,
  "companyDomain": null,
  "location": "Leeds",
  "locations": ["Leeds"],
  "remote": false,
  "salary": null,
  "categories": [],
  "employmentTypes": ["Full Time"],
  "contractType": "Full Time",
  "featured": false,
  "filled": false,
  "status": "publish",
  "postedDate": "2026-04-23T10:01:46Z",
  "modifiedDate": "2026-04-23T10:01:46Z",
  "applyType": "email",
  "applyUrl": "https://doinggoodleeds.org.uk/job/young-adults-worker-3/",
  "applyEmail": "olivia.hodgson@waythrough.org.uk",
  "externalApplyUrl": null,
  "featuredImageUrl": null,
  "authorId": 1,
  "authorName": null,
  "scrapedAt": "2026-05-20T00:13:00.000Z"
}
```

#### 🗂 Key output fields

| Group | Fields |
|---|---|
| **Identifiers** | `type`, `cpt` (`job-listings` / `volunteers` / `event` / `training-course`), `source`, `jobId`, `slug`, `jobUrl`, `wpJsonUrl`, `scrapedAt` |
| **Content** | `title`, `description` (HTML), `descriptionText` (plain) |
| **Dates** | `postedDate` (ISO), `modifiedDate` (ISO) |
| **Employer** | `companyName` (often null), `companyWebsite`, `companyDomain`, `companyTagline`, `featuredImageUrl` |
| **Location** | `location` (primary), `locations[]` (all taxonomy terms), `remote` |
| **Compensation** | `salary.{currency, min, max, unit, raw}` (when present in WP Job Manager meta) |
| **Taxonomies** | `categories[]`, `employmentTypes[]`, `contractType` |
| **Flags** | `featured`, `filled`, `status` |
| **Apply flow** | `applyType`, `applyUrl`, `applyEmail`, `externalApplyUrl` |

#### ❓ FAQ

**Can I scrape all four entity types in one run?**
Yes. Set `entityTypes` to `["job-listings", "volunteers", "event", "training-course"]`. Each row will have a `cpt` field indicating which collection it came from.

**Why are some salaries empty?**
WP Job Manager's `_job_salary` meta isn't always populated for voluntary-sector roles. Look at the `description` HTML for compensation info when `salary` is null.

**Why is `companyName` often null?**
Doing Good Leeds doesn't enforce the `_company_name` meta field. Org name is usually in the description or apply email domain — pull it from there if needed.

**Can I scrape private pages or applicant data?**
No. Only the public WP-JSON REST API.

**How do I limit results?**
Set `maxItems`. Run with `["training-course"]` alone if you only want the 90 courses.

#### 💬 Support

- For issues or feature requests, please use the **Issues** tab on the actor's Apify Console page.
- Author's website: <https://muhamed-didovic.github.io/>
- Email: <muhamed.didovic@gmail.com>

#### 🛠 Additional services

- Custom output shape, additional fields, or one-off datasets: <muhamed.didovic@gmail.com>
- Similar scrapers for other regional CVS / volunteer hubs: drop an email.
- For API access (no Apify fee, just usage): <muhamed.didovic@gmail.com>

#### 🔎 Explore more scrapers

See other scrapers at [memo23's Apify profile](https://apify.com/memo23) — covering job boards, real estate, social media, and more.

***

### 🤖 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/doinggoodleeds-scraper`).

**Purpose:** Extract paid jobs, volunteering opportunities, events, and training courses from doinggoodleeds.org.uk (Leeds' volunteer hub) via its public WP-JSON REST API — one structured row per entity.

**Minimal input:**

```json
{ "entityTypes": ["job-listings"], "maxItems": 25 }
```

`entityTypes` accepts `job-listings`, `volunteers`, `event`, `training-course`. Instead you can pass specific URLs via `startUrls` (e.g. `["https://doinggoodleeds.org.uk/jobs/"]`), which overrides `entityTypes`.

**Output:** one dataset row per entity — type, cpt, source, jobId, slug, jobUrl, wpJsonUrl, title, description (HTML), descriptionText, companyName, companyWebsite, companyDomain, location, locations, remote, salary {currency, min, max, unit, raw}, categories, employmentTypes, contractType, featured, filled, status, postedDate, modifiedDate, applyType, applyUrl, applyEmail, externalApplyUrl, featuredImageUrl, authorId, authorName, scrapedAt.

**Behaviors an agent should know:**

- `entityTypes` selects which CPTs to scrape when no `startUrls` are given; `startUrls` (when present) override it.
- Always set `maxItems` (default 1000), though the whole site is only ~178 items across all CPTs, so runs stay small.
- `postedWithinHours` filters to fresh rows for daily monitoring (rows with no posted date are always kept).
- `enrichTaxonomies` (default true) embeds decoded term names + featured image via WP-JSON `_embed` with no extra HTTP requests.
- A built-in residential proxy is used at no extra cost; the `proxy` input is only an optional override.
- Billed per result, no compute charge per run.

***

### ⚠️ Disclaimer

This Actor is an independent tool and is not affiliated with, endorsed by, or sponsored by Doing Good Leeds, Voluntary Action Leeds (VAL), or any of their subsidiaries or affiliates. All trademarks mentioned are the property of their respective owners.

The scraper accesses only the publicly available WP-JSON REST endpoints and public detail pages on doinggoodleeds.org.uk — no authenticated endpoints, recruiter-only features, or content behind a login. Users are responsible for ensuring their use complies with doinggoodleeds.org.uk's Terms of Service, applicable data-protection law (GDPR, CCPA, etc.), and any contractual obligations of their own organisation.

***

### SEO Keywords

doing good leeds scraper, scrape doinggoodleeds.org.uk, doing good leeds api, leeds volunteer scraper, leeds charity jobs scraper, leeds nonprofit jobs api, leeds volunteer opportunities api, leeds community events scraper, leeds training courses scraper, voluntary action leeds scraper, Apify doing good leeds, leeds third sector jobs, leeds civil society data, yorkshire volunteer hub api, wp-json scraper, wp job manager scraper, charityjob alternative scraper, vassheffield alternative scraper, barnsleycvs alternative scraper, uk cvs jobs scraper, leeds nonprofit recruitment data

# Actor input Schema

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

Supported shapes: `/jobs/`, `/job/<slug>/`, `/volunteer/<slug>/`, `/event/<slug>/`, `/training-course/<slug>/`, and any `/wp-json/wp/v2/<cpt>` endpoint. Mixed types are fine in one run.

## `entityTypes` (type: `array`):

Select which custom post types to include when no `startUrls` are given. Allowed values: `job-listings` (paid jobs), `volunteers` (volunteering opportunities), `event` (events), `training-course` (training courses).

## `enrichTaxonomies` (type: `boolean`):

When enabled (recommended), each row carries decoded taxonomy term names (employment type, location term) plus featured image URL. Uses WP-JSON `_embed` — no extra HTTP requests.

## `postedWithinHours` (type: `integer`):

Filters on each row's posted date. Set 24 for the last day, 72 for the last 3 days, etc. Leave empty (or 0) to return everything regardless of age. Rows whose source has no posted date are always kept.

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

Hard cap on rows pushed. Doing Good Leeds has ~178 items total across all CPTs.

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

Maximum WP-JSON pages fetched in parallel.

## `minConcurrency` (type: `integer`):

Minimum WP-JSON pages fetched in parallel.

## `maxRequestRetries` (type: `integer`):

Number of retries before a failed WP-JSON request is given up.

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

Leave empty — the actor already routes all traffic through its own built-in residential proxy at no extra cost to you. Only set this if you want to use your own proxies.

## Actor input object example

```json
{
  "startUrls": [
    "https://doinggoodleeds.org.uk/search-jobs/?phrase=dev&sort=closing_asc&start_date=1&start_month=1&start_year=2022&end_date=1&end_month=12&end_year=2030"
  ],
  "entityTypes": [
    "job-listings"
  ],
  "enrichTaxonomies": true,
  "maxItems": 1000,
  "maxConcurrency": 5,
  "minConcurrency": 1,
  "maxRequestRetries": 5
}
```

# 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 = {
    "startUrls": [
        "https://doinggoodleeds.org.uk/search-jobs/?phrase=dev&sort=closing_asc&start_date=1&start_month=1&start_year=2022&end_date=1&end_month=12&end_year=2030"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("memo23/doinggoodleeds-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 = { "startUrls": ["https://doinggoodleeds.org.uk/search-jobs/?phrase=dev&sort=closing_asc&start_date=1&start_month=1&start_year=2022&end_date=1&end_month=12&end_year=2030"] }

# Run the Actor and wait for it to finish
run = client.actor("memo23/doinggoodleeds-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 '{
  "startUrls": [
    "https://doinggoodleeds.org.uk/search-jobs/?phrase=dev&sort=closing_asc&start_date=1&start_month=1&start_year=2022&end_date=1&end_month=12&end_year=2030"
  ]
}' |
apify call memo23/doinggoodleeds-scraper --silent --output-dataset

```

## MCP server setup

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