# TeamWork Online Sports Jobs Scraper (`piquno/teamworkonline-sports-jobs-scraper`) Actor

Scrape every job on TeamWork Online, the sports industry's main board. Salary, categories, career level, expiry date and the full posting. HTTP-only, no browser, no proxy.

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

## Pricing

from $2.00 / 1,000 jobs

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

## TeamWork Online Sports Jobs Scraper

Scrape the whole of **TeamWork Online** — the job board the sports industry actually uses. Teams, leagues, arenas, agencies, college athletics, motorsports. **~8,900 live postings.**

HTTP-only, no browser, no proxy. 26 flat fields per job, ready for CSV or Excel.

**$2.00 per 1,000 jobs.** No charge for empty rows.

***

### Why this one

**Nothing else on the Store covers this site.** Four separate searches for TeamWork Online returned zero actors. The general job-board scrapers don't reach it, and the one nominally sports-labelled actor is Brazilian gym data.

That matters because sports hiring barely overlaps with the mainstream boards. A ticket-sales role at an NHL club or a partnerships job at a college athletics department is posted here and often nowhere else.

#### What you get that the listing page doesn't show

- **Disclosed salary ranges**, parsed into `salaryMin` / `salaryMax` / `salaryPeriod`. These are not in the site's structured data — they only exist in the page HTML.
- **Job categories** — "Corporate Partnerships | Corporate Sponsorship Sales | Premium/Suite Sales"
- **`validThrough`** — an actual expiry date, which most job boards never publish. You can tell a live posting from a stale one.
- **The full posting text**, including department, reports-to, FLSA status and qualifications.
- **`applyUrl`** — the direct application link.

#### Two modes

Set `fetchDetails: false` for a fast, cheap sweep — one request per 100 jobs instead of one per job. Good for monitoring which organisations are hiring. Leave it on for salary, categories and full descriptions.

***

### Input

```json
{
  "vertical": "hockey-jobs",
  "searchQuery": "ticket sales",
  "maxJobs": 500,
  "fetchDetails": true,
  "withSalaryOnly": true
}
```

**Defaults:** `maxJobs` is **100** with `fetchDetails` on — a fast sample that finishes in roughly 90 seconds. Raise `maxJobs` for a bulk extract; the whole board is around 8,900 jobs.

Or crawl the whole board by leaving `vertical` at its default. `startUrls` accepts TeamWork Online listing URLs directly, including ones with filters already applied on the site.

#### Monitoring the board

Set `incremental: true` with its own `stateKey`. Only postings not seen before are emitted, tagged `changeType: "new"` — so a daily watch on 8,900 jobs bills for the handful that are actually new.

***

### Output

```json
{
  "jobId": "2186711",
  "jobTitle": "Manager, Partnerships & Premium",
  "organizationName": "NorCal - Major League Volleyball",
  "vertical": "other-sports-jobs",
  "careerLevel": "Manager",
  "jobCategories": "Corporate Partnerships | Corporate Sponsorship Sales | Premium/Suite Sales",
  "locationCity": "San Ramon",
  "locationState": "CA",
  "locationCountry": "US",
  "postalCode": "94583",
  "workArrangement": "Hybrid",
  "salaryMin": 62000,
  "salaryMax": 72000,
  "salaryPeriod": "year",
  "datePosted": "2026-08-21T00:00:00.000Z",
  "validThrough": "2026-09-30T00:00:00.000Z",
  "applyUrl": "https://www.teamworkonline.com/employment_opportunities/2186711/applications/new",
  "fieldsPopulated": 24
}
```

#### Field fill rates

Measured on live pages. Published so you can judge completeness before you buy, not after.

| Field | Fill |
|---|---|
| `jobId`, `jobTitle`, `organizationName`, `jobUrl` | 100% |
| `locationCity`, `locationState`, `careerLevel` | 100% |
| `datePosted`, `validThrough`, `description` | 100% with `fetchDetails` |
| `salaryMin` / `salaryMax` | only where the employer disclosed pay — a minority of postings |
| `workArrangement` | present when the employer stated it |

Nothing is inferred. A withheld salary stays `null` rather than being estimated.

***

### Pricing

| Event | Price |
|---|---|
| Job | **$0.002** ($2.00 / 1,000) |
| Actor start | $0.00005 |

A row is billed only if it has a job id, and it is billed **before** it is pushed — so you are never charged for a row you did not receive. Rows removed by your filters are never billed. The actor honours the maximum charge per run set in run options and stops fetching once that budget is spent.

***

### Limitations — read before you buy

- **Salary is often absent.** Sports employers frequently withhold pay. Use `withSalaryOnly` if you need it, but expect the count to drop substantially.
- **Full details cost one request per job.** 500 jobs with `fetchDetails: true` is ~505 requests, and the actor sustains about one job per second. Turn it off for broad sweeps — listing-only is one request per 100 jobs.
- **Filters that need detail data** (`withSalaryOnly`, `postedWithinDays`) require `fetchDetails: true`.
- **The board is US-centric.** Some international roles appear, but most postings are US-based.
- **Format risk.** Listings are parsed from server-rendered HTML and details from the page's structured data. If either changes, the actor logs it and stops rather than silently returning partial rows — and a failed detail fetch degrades to the listing row rather than dropping the job.

***

### Running locally

```bash
npm install
npm test
```

Tests run against captured live pages in `test/fixtures/` — no network required. They assert the things that actually break this scraper: 100 cards per page, page 2 disjoint from page 1, the pagination container being a `<nav>` rather than a `<div>`, and a posting with no salary not shifting every other field.

# Actor input Schema

## `searchQuery` (type: `string`):

Free-text search, e.g. "ticket sales" or "analytics". Leave empty to take the whole board.

## `location` (type: `string`):

Place name, e.g. "Chicago, IL". Leave empty for all locations.

## `vertical` (type: `string`):

Which listing to crawl. The default covers the whole board; the sport-specific sections are narrower and faster.

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

Optional. Paste TeamWork Online listing URLs directly, including ones with filters already applied. Overrides the fields above.

## `maxJobs` (type: `integer`):

Hard limit. The default of 100 is a quick sample that completes in about 90 seconds even with full details on. The whole board is around 8,900 jobs at 100 per listing page — raise this for a bulk extract.

## `fetchDetails` (type: `boolean`):

Visit each posting for salary, categories, full description, and the expiry date. Turn off for a fast, cheap listing-only crawl — one request per 100 jobs instead of one per job.

## `incremental` (type: `boolean`):

Remember job ids between runs and emit only postings not seen before. Turns a scheduled watch into a cheap delta instead of re-billing the whole board.

## `stateKey` (type: `string`):

Namespace for incremental state. Use a different key per saved search so separate schedules do not share history.

## `withSalaryOnly` (type: `boolean`):

Many employers withhold pay. Requires full details to be fetched.

## `workArrangements` (type: `array`):

Keep only these arrangements.

## `careerLevels` (type: `array`):

Keep only these levels, e.g. Intern, Manager, Director.

## `postedWithinDays` (type: `integer`):

Keep only postings published in the last N days. Requires full details to be fetched.

## `proxyConfiguration` (type: `object`):

Optional and off by default. Every probe returned full content with no challenge, so the actor runs unproxied. Only enable this if the log shows repeated blocks.

## Actor input object example

```json
{
  "vertical": "jobs-in-sports",
  "maxJobs": 100,
  "fetchDetails": true,
  "incremental": false,
  "stateKey": "default",
  "withSalaryOnly": false,
  "proxyConfiguration": {
    "useApifyProxy": false
  }
}
```

# Actor output Schema

## `jobs` (type: `string`):

One row per job: title, organisation, location, salary, categories, career level, posted and expiry dates, and the full description.

## `runSummary` (type: `string`):

Rows pushed, jobs billed, rows filtered out, jobs skipped as already seen, and detail-fetch failures.

# 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 = {
    "maxJobs": 100
};

// Run the Actor and wait for it to finish
const run = await client.actor("piquno/teamworkonline-sports-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 = { "maxJobs": 100 }

# Run the Actor and wait for it to finish
run = client.actor("piquno/teamworkonline-sports-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 '{
  "maxJobs": 100
}' |
apify call piquno/teamworkonline-sports-jobs-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,piquno/teamworkonline-sports-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/Nx9enqrX4mMudi3ZF/builds/mtSKDsliO4HBAeEny/openapi.json
