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.
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#
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"{
"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:
# 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.
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.
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#
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.
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 — and it's one of the 17 sources you can actually filter on, which several larger enterprise adapters are not.
Every source above normalises onto the same schema, with the hiring company already joined onto each posting.
Get an API key