# Handshake Jobs Scraper (`memo23/handshake-jobs-scraper`) Actor

Scrape public Handshake jobs and internships: employer, salary range, location, dates, role taxonomy and the full description. Search by city and role, or paste any listing or job URL. Every row carries the employer's own careers domain. No login and no student account needed.

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

## Pricing

from $1.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

## Handshake Jobs Scraper

Scrape public job and internship postings from **Handshake** — the platform US colleges run their campus recruiting on. Search by city and role, or paste a listing or job URL. No login, no cookies, no student account.

Every posting comes back with the employer, salary range, location, dates, classification and the full job description — plus the employer's own careers URL, which most Handshake scrapers throw away.

![How Handshake Jobs Scraper works](https://raw.githubusercontent.com/muhamed-didovic/muhamed-didovic.github.io/main/assets/how-it-works-handshake.png)

### Why use this scraper

- **No account needed.** This reads Handshake's public `find-jobs` pages and public job pages. It never touches a student login, a university SSO, or private student data.
- **The employer's real website, on every row.** Handshake publishes each employer's own careers URL inside the posting. That gives you `employerWebsite` and `employerDomain` for free, and makes the optional email enrichment accurate instead of a domain guess.
- **Multi-location postings kept intact.** A campus posting listed in Tampa, Dallas and Jersey City returns all three in `allLocations`, not just the first.
- **Salary that is actually there.** Handshake employers fill in structured pay far more often than general job boards do — roughly 95% of rows in testing carried `salaryMin`/`salaryMax` with a currency and period.
- **Monitoring mode.** Run it on a schedule and get only postings you have not seen before, with no charge for the ones that get skipped.
- **Expired postings are reported honestly.** Handshake's listing pages are cached, so some ids point at jobs that have closed. Those are counted as `expired`, not silently dropped and not billed.

### Overview

Handshake is where roughly 18 million US students and recent graduates look for entry-level jobs and internships, and where employers post to reach them. The postings this actor reads are the ones Handshake publishes openly for search engines, so no credentials are involved at any point.

Coverage is drawn from Handshake's own public `find-jobs` grid: **94 US cities**, a **remote** listing, **62 nationwide role categories**, and **762 city-plus-role pages**. Each of those pages hands back up to 200 job ids in one request.

### Supported inputs

| Input | Example | What it does |
|---|---|---|
| Cities | `austin-tx`, `Austin, TX` | Every published role in that city |
| Roles | `nursing`, `Software Engineering` | That role nationwide |
| Cities + Roles | `akron-oh` + `nursing` | The city/role page when Handshake publishes one |
| Remote | `remoteOnly: true` | Handshake's remote listing |
| Listing URL | `https://joinhandshake.com/find-jobs/austin-tx/sales/` | Scrapes that page directly |
| Job URL | `https://app.joinhandshake.com/public/jobs/11320393` | Scrapes that single posting |

Cities accept plain text (`Austin, TX`) or Handshake's slug (`austin-tx`). Bare city names work when unambiguous, so use `portland-or` or `portland-me` rather than `Portland`.

### Use cases

- **Campus recruiting intelligence.** See which employers are hiring new graduates in a city, at what salary, in which role categories.
- **Entry-level salary benchmarking.** `salaryMin`, `salaryMax`, `salaryCurrency` and `salaryPeriod` across a whole role or metro.
- **Recruiting-agency lead lists.** Every row carries the employer's own domain, so an employer hiring 12 nurses in Akron is a qualified prospect with a website you can act on.
- **Job-board aggregation.** Pull Handshake's early-career supply into your own board or newsletter.
- **New-posting alerts.** Monitoring mode plus Apify's scheduler gives a "posted since last run" feed.
- **Labour-market research.** Role-group taxonomy, industry, employment type and posting dates for early-career hiring.

### How it works

1. Your cities and roles resolve to Handshake `find-jobs` listing URLs. Handshake only publishes some city/role pairs, so a pair it does not publish widens to that city's unfiltered listing instead of returning nothing.
2. Each listing page is fetched once. The job ids live in the page's embedded Next.js payload, up to 200 per page, not in the visible 15 cards. These pages do not paginate, so breadth comes from adding cities and roles rather than walking page numbers.
3. Every job id is fetched in parallel from its public job page and parsed from the `JobPosting` structured data block.
4. The 15 server-rendered cards carry three things the structured data does not — role groups, the remote flag and pay schedule — so those get merged in where available.
5. Optionally, each employer's own careers site is read for a contact email.

### Input configuration

| Field | Type | Default | Description |
|---|---|---|---|
| `locations` | array | `["new-york-ny"]` | Cities to search, as slugs or plain text |
| `roles` | array | `[]` | Role categories; empty means every role in the chosen cities |
| `remoteOnly` | boolean | `false` | Add Handshake's remote listing |
| `startUrls` | array | `[]` | Listing or job URLs; replaces the city/role search when set |
| `enrichEmails` | boolean | `false` | Look up a contact email on the employer's own site |
| `qualifyByPayment` | boolean | `false` | Flag employers that take payments online; needs `enrichEmails` |
| `monitoringMode` | boolean | `false` | Return only postings not seen on previous runs |
| `resetMonitoringState` | boolean | `false` | One-shot: clear the "already seen" record |
| `maxItems` | integer | `1000` | Hard cap on jobs collected |
| `maxConcurrency` | integer | `10` | Job pages fetched in parallel |
| `proxy` | object | disabled | Optional; Handshake's public pages serve fine direct |

#### Example input

```json
{
  "locations": ["austin-tx", "new-york-ny"],
  "roles": ["sales", "software-engineering"],
  "maxItems": 500,
  "enrichEmails": false
}
```

### Output overview

One row per job posting. No padding rows, no duplicate rows for the same posting inside a run.

### Output sample

```json
{
  "jobId": "11320393",
  "jobUrl": "https://app.joinhandshake.com/public/jobs/11320393",
  "title": "Sales Account Manager",
  "employerName": "Ford Audio-Video, LLC",
  "employerWebsite": "http://www.fordav.com",
  "employerDomain": "fordav.com",
  "employerLogoUrl": "https://joinhandshake-production.imgix.net/app/public/assets/institutions/15545/original/hs-emp-logo-data.",
  "description": "As a Sales Account Manager, you will:\n\n• Prospect, qualify, and close new business opportunities.\n\n• Present AV solutions to senior executives.",
  "descriptionHtml": "<p></p><p class=\"MsoNormal\">As a Sales Account Manager, you will:</p><ul>...",
  "employmentType": "FULL_TIME",
  "jobType": "Job",
  "industry": "Construction",
  "roleGroups": [
    "Sales Managers",
    "Advertising Sales Agents",
    "Customer Success Managers and Sales Representatives of Services"
  ],
  "location": "Austin, Texas",
  "addressLocality": "Austin",
  "addressRegion": "Texas",
  "addressCountry": "United States",
  "allLocations": [
    { "city": "Austin", "state": "Texas", "country": "United States" }
  ],
  "isRemote": false,
  "salaryMin": 82500,
  "salaryMax": 440000,
  "salaryCurrency": "USD",
  "salaryPeriod": "YEAR",
  "paySchedule": "Annual Salary",
  "datePosted": "2026-08-18T19:35:46Z",
  "validThrough": "2027-08-18T19:35:45Z",
  "scrapedAt": "2026-08-25T10:10:00.868Z"
}
```

### Key output fields

| Field | Type | Notes |
|---|---|---|
| `jobId` | string | Handshake's numeric posting id |
| `jobUrl` | string | Public job page |
| `title` | string | Job title, with Handshake's `\| employer \| Handshake` suffix removed |
| `employerName` | string | Hiring organisation |
| `employerWebsite` | string | The employer's own careers URL, from the posting |
| `employerDomain` | string | Bare domain from that URL |
| `employerLogoUrl` | string | Employer logo |
| `description` | string | Full description as plain text |
| `descriptionHtml` | string | Full description with original markup |
| `employmentType` | string | `FULL_TIME`, `PART_TIME`, `INTERN`, `CONTRACTOR` |
| `jobType` | string | Handshake's own Job vs Internship split; see the FAQ |
| `industry` | string | Employer industry |
| `roleGroups` | array | Handshake role taxonomy; see the FAQ |
| `location` | string | Primary location, `City, State` |
| `allLocations` | array | Every site on a multi-location posting |
| `isRemote` | boolean | See the FAQ |
| `salaryMin` / `salaryMax` | number | Pay range |
| `salaryCurrency` | string | Usually `USD` |
| `salaryPeriod` | string | `YEAR`, `HOUR`, `MONTH` |
| `paySchedule` | string | Handshake's own label, e.g. `Annual Salary` |
| `datePosted` | string | ISO 8601 |
| `validThrough` | string | ISO 8601 expiry |
| `contactEmail` | string | Only when `enrichEmails` is on |
| `contactWebsite` | string | Only when `enrichEmails` is on |
| `emailEnrichment` | object | Emails, phones, socials, confidence; only when `enrichEmails` is on |

### FAQ

**Do I need a Handshake account or student login?**
No. This reads only the pages Handshake publishes openly for search engines. It never authenticates, and it never touches student profiles or applications.

**Why are `jobType`, `roleGroups`, `isRemote` and `paySchedule` empty on some rows?**
Handshake renders 15 rich job cards per listing page but hands out up to 200 ids. Those four fields exist only on the 15 cards. Rows built from the other ids carry every other field in full. To maximise how many rows get them, use more city and role combinations with a smaller `maxItems` each, rather than one page with a large cap.

**Why did I get fewer rows than `maxItems`?**
Handshake's listing pages are cached and can lag by several days, so some ids point at postings that have since closed. Those return a 404, are counted as `expired` in the run log, and produce no dataset row and no charge.

**Why did my city plus role combination return the whole city instead?**
Handshake only publishes certain city/role pages. `akron-oh/nursing` exists, `austin-tx/nursing` does not. When a pair has no page, the run widens to that city's unfiltered listing and says so in the log.

**How many jobs can I get from one city?**
Up to 200 ids per listing page, and the page reports Handshake's own `totalCount` so you can see what it is capping. Add role pages for the same city to reach more.

**Do I need a proxy?**
No. Handshake's public pages answer fine over a direct connection. The proxy input is there for high-volume runs or blocked networks.

**How accurate is the email enrichment?**
Handshake hands over the employer's own careers URL, so enrichment reads that exact site rather than guessing a domain from the company name. Hit rate still depends on whether the employer publishes an address at all; large employers usually route through forms instead. You are billed only when an email is returned.

### Support

Found a bug or need a field that is missing? Open an issue on the actor's Issues tab and it will be looked at.

### Additional services

Need this data on a schedule, pushed into your own database, or combined with other job boards? Custom scraping and data-delivery work is available on request via the Issues tab.

### Explore more scrapers

Other job-board scrapers in this collection: Indeed, Dice, ZipRecruiter, Glassdoor, Google Jobs, Greenhouse, Ashby, BambooHR, CharityJob, CWjobs, Bayt, and a combined Greenhouse/Lever/Workday/iCIMS ATS scraper covering 52 applicant tracking systems.

### 🤖 For AI agents & LLM apps

This actor is a clean data source for agent pipelines. Output is flat JSON with stable field names, one object per posting.

- **Deterministic input.** Pass `locations` and `roles` as arrays of slugs; no natural-language parsing needed on your side.
- **Stable ids.** `jobId` is Handshake's own posting id, so it is safe as a primary key across runs.
- **Deduplication built in.** `monitoringMode` keeps a per-user record of delivered ids, so an agent polling on a schedule sees each posting exactly once.
- **Both description formats.** `description` is plain text ready for embedding or summarisation; `descriptionHtml` keeps the markup when you need structure.
- **Cost control.** `maxItems` is a hard cap, and expired postings are never billed.

Call it from the Apify API, the MCP server, or any LangChain / LlamaIndex Apify integration.

### ⚠️ Disclaimer

This actor collects only publicly accessible job postings that Handshake publishes for search engine indexing. It does not log in, does not bypass authentication, does not access student profiles, applications or any private data, and does not solve CAPTCHAs.

You are responsible for how you use the data, including compliance with Handshake's terms of service, applicable data-protection law (GDPR, CCPA and equivalents), and any restrictions that apply to contact data in your jurisdiction. This actor is not affiliated with, endorsed by, or connected to Handshake or Stryder Corp.

### SEO Keywords

handshake scraper, handshake jobs scraper, joinhandshake scraper, handshake job scraper api, scrape handshake jobs, handshake internships scraper, college jobs scraper, campus recruiting data, entry level jobs scraper, new grad jobs scraper, student jobs api, university job board scraper, handshake job listings export, handshake data extraction, early career jobs dataset, internship listings scraper, handshake employer data, graduate jobs scraper usa, handshake salary data, campus hiring intelligence

# Actor input Schema

## `locations` (type: `array`):

Cities to search. Accepts Handshake slugs (`austin-tx`) or plain text (`Austin, TX`). Bare city names work when they are unambiguous, so use `portland-or` or `portland-me` rather than `Portland`. Combined with Roles as a cross-product: 3 cities x 2 roles = 6 listing pages.

## `roles` (type: `array`):

Handshake role categories to search. Accepts slugs (`software-engineering`) or plain text (`Software Engineering`). Handshake publishes 62 of them, including nursing, registered-nurse, software-engineering, sales, marketing, finance, teaching, social-work, data-analytics and mechanical-engineering. Leave empty to get every role in the chosen cities.

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

Adds Handshake's remote listing to whatever the Cities field already covers. Turn this on with no cities set to scrape remote postings only.

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

Listing URLs (`https://joinhandshake.com/find-jobs/austin-tx/nursing/`) and direct job URLs (`https://app.joinhandshake.com/public/jobs/11325998`) are both supported and auto-classified. Mix them freely in one run.

## `enrichEmails` (type: `boolean`):

Every Handshake posting carries the employer's own careers URL, so enrichment reads that site directly for a contact email instead of guessing the domain. Adds contactEmail and contactWebsite columns plus a detailed emailEnrichment object. Best-effort, billed only when an email is returned, never for misses.

## `qualifyByPayment` (type: `boolean`):

Requires "Enrich with employer contact emails". Scans each employer's website, reusing the pages already fetched for email discovery so there is no extra cost or time, for payment processors and e-commerce platforms (Stripe, Shopify, PayPal, Paddle, Lemon Squeezy, WooCommerce, Square, Chargebee and more). Adds takesPayments, paymentProcessors, stripeLiveKey and paymentConfidence.

## `monitoringMode` (type: `boolean`):

When enabled, jobs whose Handshake id has already been delivered to you are silently skipped: no detail page is fetched and no charge applies for them. The first run after enabling monitoring returns every match, which builds your baseline. Every run after that returns only new postings. Combine with Apify's scheduler for a 'posted since last run' feed.

## `resetMonitoringState` (type: `boolean`):

One-shot toggle: clear the saved 'jobs already seen' record at the start of this run. Use it when you change cities or roles, want to rebuild the baseline, or need to recover from a missed delivery. Has no effect when monitoring mode is off.

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

Hard cap on the number of jobs collected. Each Handshake listing page yields up to 200 job ids and does not paginate, so raise this and add more city or role combinations to go wider.

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

Maximum number of job pages fetched in parallel.

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

Minimum number of job pages fetched in parallel.

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

Number of retries before a failed request is given up.

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

Handshake's public pages serve fine without a proxy, so this is optional. Enable it if you are running at high volume or from a blocked network.

## Actor input object example

```json
{
  "locations": [
    "new-york-ny"
  ],
  "roles": [],
  "remoteOnly": false,
  "startUrls": [],
  "enrichEmails": false,
  "qualifyByPayment": false,
  "monitoringMode": false,
  "resetMonitoringState": false,
  "maxItems": 1000,
  "maxConcurrency": 10,
  "minConcurrency": 1,
  "maxRequestRetries": 5,
  "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 = {
    "locations": [
        "new-york-ny"
    ],
    "roles": [],
    "startUrls": []
};

// Run the Actor and wait for it to finish
const run = await client.actor("memo23/handshake-jobs-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 = {
    "locations": ["new-york-ny"],
    "roles": [],
    "startUrls": [],
}

# Run the Actor and wait for it to finish
run = client.actor("memo23/handshake-jobs-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 '{
  "locations": [
    "new-york-ny"
  ],
  "roles": [],
  "startUrls": []
}' |
apify call memo23/handshake-jobs-scraper --silent --output-dataset

```

## MCP server setup

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