# Phenom Jobs API

> Query jobs from Phenom career sites through one API — and how the Phenom search widget, its CSRF handshake and tenant-dependent descriptions work.

Source: https://hyperjobs.io/ats/phenom

Phenom People powers the career sites of large enterprises — the kind of employer
that runs its own branded jobs domain rather than a subdomain of its ATS. There's
no `phenom.com/<company>` pattern to look for: a Phenom board looks like
`jobs.bell.ca` or an enterprise's own careers host, and the only reliable tell is
the shape of the URLs underneath it. Our queue tracks 85 Phenom boards, the
smallest of the enterprise providers here, and every one of them was identified by
its endpoints rather than its domain.

## Try it

```bash
curl -G https://api.hyperjobs.io/v1/jobs \
  -H "Authorization: Bearer $HYPERJOBS_KEY" \
  --data-urlencode "source=phenom" \
  --data-urlencode "time_frame=7d" \
  --data-urlencode "limit=2"
```

```json title="Illustrative — the shape, not a live response"
{
  "data": [
    {
      "id": "phenom:R-441209",
      "title": "Network Operations Specialist",
      "company": {
        "slug": "atlas-telecom",
        "name": "Atlas Telecom",
        "domain": "atlastelecom.ca",
        "website": "https://atlastelecom.ca",
        "logo": "https://media.licdn.com/dms/image/…",
        "industry": "Telecommunications",
        "employee_count": 41000
      },
      "location": {
        "remote": false,
        "countries": ["Canada"],
        "cities": ["Montreal"],
        "regions": ["Quebec"],
        "raw": ["Montreal, Quebec, Canada"]
      },
      "employment_type": ["FULL_TIME"],
      "work_arrangement": "Hybrid",
      "seniority": "Mid-Senior level",
      "salary": null,
      "skills": ["bgp", "mpls", "network troubleshooting"],
      "taxonomies": ["IT & Infrastructure"],
      "apply_url": "https://jobs.atlastelecom.ca/us/en/job/R-441209",
      "url": "https://jobs.atlastelecom.ca/us/en/job/R-441209",
      "source": "phenom",
      "posted_at": "2026-07-11T00:00:00.000Z",
      "expires_at": null,
      "updated_at": "2026-07-14T03:52:18.000Z"
    }
  ],
  "total": 2,
  "limit": 2,
  "offset": 0
}
```

## How Phenom boards actually work

Phenom's search is a widget, and reaching it takes a handshake:

```bash
# 1. Seed the CSRF cookie and token
curl -c jar "https://<site>/us/en/search-results"

# 2. POST the widget endpoint with the token
curl -b jar -X POST "https://<site>/widgets" \
  -H "content-type: application/json" \
  -H "x-csrf-token: <token>" \
  -d '{"ddoKey":"refineSearch","from":0,"size":100,"pageName":"search-results", …}'
```

The token appears in two places — a cookie whose name contains `csrf`, and a
`"csrfToken": "…"` embedded in the search page's HTML. Either works; you need one.
Skip the seed request and the POST fails.

The response nests further down than you'd guess:
`refineSearch.data.jobs[]` with `refineSearch.data.totalHits` next to it. Page
with `from`, 100 at a time.

### Identifying a Phenom board is the hard part

Phenom has two URL signatures, and both only mean Phenom on the employer's own
careers host: the `/{country}/{lang}/search-results` page, and the `/widgets`
endpoint itself.

<Warning title="Match on the path, not the URL string">
  Testing whether the whole URL *contains* `/widgets` is the obvious rule and it
  is wrong. It claims JazzHR's own `app.jazz.co/widgets/bootstrap.js`. It claims
  any careers page with a CDN chat widget on it. We hit exactly that: a detector
  that matched substrings against the full URL happily returned the raw URL as a
  board token for boards that were not Phenom at all.

  Match `/widgets` as the **entire path**, match `search-results` as a path
  pattern, and normalise to the origin — which is what every real Phenom board
  token looks like anyway.
</Warning>

### Structured fields Phenom gives you

`latitude` and `longitude` — exact coordinates, no geocoding. `ml_skills`, a
structured skills array present on close to every posting on Phenom tenants.
`multi_location[]` for multi-site roles. `department`, `category`, and `unit` (the
business unit, finer than department).

Seniority arrives through whichever of three fields a tenant uses: `jobLevel`
(`Professional`, `Entry Level`, `Management`, `Student`), or — on tenants that
omit it — an `experienceLevel` career ladder (`Early Career`, `Mid-Career`,
`Senior Level`, `Co-op/Intern`), or `type`. Non-level values like `Non-Salaried`
and `Leadership Programs` map to nothing and should stay null rather than become a
seniority.

## What bites you

<Warning title="Whether descriptions ship inline is tenant-dependent">
  This is the defining property of Phenom and there's no flag for it. Some tenants
  return a full `description` in the search payload — one live board returns
  roughly 14,000 characters per job. Others omit the field entirely, and the full
  text exists only on the job-detail page.

  There's a `descriptionTeaser` as a fallback, which is a teaser: enough to look
  populated in a spot check, not enough to be the job ad. Location, coordinates
  and dates are always complete, so those rows are worth keeping — but a Phenom
  posting's description quality depends on which employer's site it came from, and
  nothing in the response tells you which case you're in.
</Warning>

**The location field isn't the one you'd guess.** Phenom's real field is
`location` — `Montreal, Quebec`. There is no `cityState`, no `city`. Reaching for
those returns undefined and the city is silently dropped. The `country` is
separate, so a city-only string needs it appended.

**`type` means two different things on two different tenants.** On most it's the
employment type — `Full-Time`. On career-ladder tenants it holds the *level*
instead — `Senior Level`. Same key, different meaning, no discriminator.

**Salary is detail-only and carries no currency.** Pay-transparency tenants embed
`minimumSalary` / `maximumSalary` / `salaryType` in the job-detail page's
`phApp.ddo` blob. There's **no currency key at all** — pay transparency is US
driven, so USD is a reasonable inference only when the job is confirmed US by its
country field. Otherwise the numbers are real and the currency is unknown, and we
leave it null rather than assume. That's why `salary` is null on most Phenom
postings here: it's an opt-in N+1 against a field the platform barely publishes.

## Or use ours

Phenom is a CSRF handshake, a POST with a fifteen-key body, a response nested
three levels down, a detector that's easy to get dangerously wrong, and a
description that may or may not be there depending on which enterprise you're
looking at.

You get it flattened: the handshake done, the pagination walked, coordinates and
`ml_skills` carried through, seniority resolved from whichever of three fields the
tenant uses, `type` disambiguated, and the description taken inline where the
tenant ships it. Rows are kept rather than dropped when only a teaser is
available — with the honest consequence that Phenom's description fill is uneven
in a way we can't fix from outside.

Phenom is in the fast refresh tier, attempted every cycle — see
[coverage](/docs/reference/coverage) — and it's one of the [17
sources](/docs/reference/sources) you can actually filter on, which several
larger enterprise adapters are not.

[Quickstart](/docs/quickstart) · [Pricing](/pricing)
