Phenom People's SPA Job-Posting Architecture, and How to Monitor It for Hiring Signals Without a Paid Contract
Why this is in the vault
Kwik Trip — a phData enterprise client — runs Phenom People for talent management, and the 2026-07-13 meeting prep flagged a parked "Phenom scraper" ticket because JS-rendered career pages appeared to block standard scraping. This brief resolves that assumption (the SPA is backed by a plain JSON POST endpoint, no headless browser needed) and provides the concrete recipe for building a hiring-signal monitor as a Cloudflare Worker cron against the phData client portfolio — a direct BD signal feed for RDCO's phData engagement.
The question
How does Phenom People's SPA architecture serve enterprise job postings, and what are reliable ways to monitor it for hiring-signal changes without a paid contract? (Context: Kwik Trip and other phData enterprise clients run Phenom for talent management; the 7/13 Kwik Trip prep flags "Phenom scraper is queued" because JS-rendered pages with tenantRefNum params block standard scraping. The point is detecting a client's org/hiring shifts as a BD signal for phData / RDCO.)
What we already know (from the vault)
- [[2026-07-13-kwik-trip-meeting-prep]]: jobs.kwiktrip.com is a Phenom SPA — descriptions, requirements, salaries, posting dates, and search results are JS-rendered; a plain fetch returns only page shell, meta tags, and sitemap URL slugs. The ATS was verified via cdn.phenompeople.com assets and the embedded
phAppconfig; the Kwik Trip tenant refNum isKTQKTRUS, and phenomtrack.js appears in robots.txt. - Same doc: the "Phenom scraper is queued" and the brigade jobs-snapshot ticket is parked pending it. Titles in the prep were reconstructed from sitemap slugs only (1,790 live postings) — nothing deeper (no posting dates, no descriptions), which is exactly the gap this brief closes.
- [[2026-05-29-fde-search-share-baseline]]: RDCO already treats job-posting counts over time as a strategic signal (Indeed FDE postings +729% YoY as a demand indicator) — the same delta-tracking method applies to per-client Phenom feeds.
- The phData client portfolio is the target surface: detecting an org/hiring shift at a client (new data-eng reqs, a security build-out, a warehouse migration hire) is a business-development trigger for phData.
What the web says
- Two access paths exist, and only one is free. Phenom's official Jobs API is
GET https://api.phenom.com/jobs-api/v1/jobs, but it requires an OAuth 2.0 token requested via api-management@phenom.com — i.e., a contract/credential. It also can't search on title or description, only locale/category/siteType (developer.phenom.com/card/Jobs, fantastic.jobs/ats/phenompeople). - The free path is the career site's own JSON widget endpoint. Every Phenom career site is a SPA that hydrates itself by POSTing to
https://{tenant-domain}/widgetswith a JSON body whereddoKey: "refineSearch". This is the same XHR the browser SPA fires — so the "JS-rendered" barrier is illusory: the data is a plain JSON POST, no headless browser required (jobo.world/ats/phenompeople). - The tenantRefNum is the key, and it's already in the page. Each site has a unique
refNum(Kwik Trip =KTQKTRUS) embedded in the page HTML /phAppconfig. Extract it once per tenant, then pass it in the POST payload as"refNum": "<code>"(jobo.world). - Payload shape (verbatim fields observed):
lang(e.g.en_global),deviceType,country,pageName(search-results),size(results per page, e.g. 20),from(pagination offset),jobs,counts,all_fields,clearAll,jdsource,isSliderEnable,pageId,siteType,keywords,global,selected_fields,sort,locationData,refNum, andddoKey. The response nests under arefineSearchkey containingjobs[]andtotalHits— sototalHitsalone is a one-number hiring-volume gauge, andjobs[]carries the per-posting metadata (jobo.world, WebSearch corroboration). - Rate / anti-bot posture: No auth required on the widget endpoint. Unofficial throughput ~100–120 req/min; some tenants sit behind Cloudflare bot-management that blocks naive automated requests, so realistic browser-like headers, a session cookie, and inter-request delays may be needed. Jobs typically update daily, so caching + low-frequency polling is both sufficient and lower-risk (jobo.world).
- Paid aggregators exist as a fallback (jobo.world's connect.jobo.world, fantastic.jobs at ~$1000/mo) — they resell exactly this feed, which confirms the endpoint is stable enough to build a business on, but we don't need them for a handful of tracked tenants.
Convergences and contradictions
- Convergence: Official docs and third-party scrapers agree there is no free authenticated public API — but they also agree the career site is backed by a queryable JSON layer. The reliable free route is the site's own
/widgets+refineSearchcall, not the OAuth API. - Resolves a vault assumption: the Kwik Trip prep treated "JS-rendered → blocked" as a hard wall. It isn't — the SPA's data source is a documented JSON POST. Headless rendering is not required; a plain HTTP POST + JSON diff suffices. That directly unblocks the parked brigade jobs-snapshot ticket.
- Contradiction / caveat: the official API "can't search titles"; the widget endpoint does accept
keywords/all_fields, so the free path is actually richer for our use case. Counter-caveat: Cloudflare coverage is tenant-specific and unverified for jobs.kwiktrip.com — must be tested live before declaring the recipe production-ready.
Synthesis for RDCO
There is a concrete, no-contract monitoring recipe, and it's cheap. For any Phenom tenant, do a one-time capture of the refNum from the career-site HTML (phApp config; Kwik Trip = KTQKTRUS), then poll POST https://{tenant-domain}/widgets with a JSON body {"ddoKey":"refineSearch","refNum":"<code>","pageName":"search-results","from":0,"size":100,"lang":"en_global", ...}, paginating via from/size until you've walked totalHits. The response refineSearch.jobs[] gives you the structured postings (title, location, category, requisition/job id, and — the fields the sitemap-slug method couldn't reach — posting/opening metadata). This is exactly the request the browser SPA already makes, so no headless Chromium, no Playwright, no paid feed — just an HTTP POST and a JSON parse.
Turn that into a hiring-signal monitor by snapshotting each tenant weekly and diffing against the prior snapshot: track (a) totalHits trend, (b) net-new requisition ids, (c) removed/filled reqs, and (d) category/title/location shifts. For phData BD, the load-bearing deltas are role-family movements — e.g., a burst of Kwik Trip data-engineer/BI reqs (currently zero in the sitemap snapshot) would flip the "12-person team not visibly scaling" read into an active-hiring signal worth a partner conversation, and a spike in security/infra reqs corroborates a platform build-out. Run it as a Cloudflare Worker cron (we already have the rails) writing snapshots to R2/D1, one row per tenant per poll, with a simple diff emitting a Notion task or channel ping when a threshold trips.
Two guardrails before productionizing across the client portfolio. First, verify Cloudflare posture per tenant — hit jobs.kwiktrip.com/widgets once with browser-like headers; if it 403s, add a session-cookie warm-up step (navigate the site root first) or fall back to a single Playwright session to mint cookies, then reuse them for cheap POSTs. Second, do a ToS/robots pass: the vault notes phenomtrack.js in Kwik Trip's robots.txt; confirm /widgets isn't disallowed and keep volume conservative (daily-cache cadence, single low-rate poller) so this reads as courteous monitoring, not aggression — this is a client's site, and the BD upside is not worth an IP block or a relationship ding. Net: this is a reversible, low-cost build that unblocks the parked jobs-snapshot ticket and generalizes to every phData client on Phenom by swapping one refNum.
Open follow-ups
- Does jobs.kwiktrip.com/widgets actually respond to a server-side POST, or is it behind Cloudflare bot-management? (Live probe with realistic headers — one request.)
- What exact date/requisition fields does
refineSearch.jobs[]return, and do they let us compute reliable "new vs filled" deltas (posting date, req-open date, last-modified)? - ToS/robots review: is
/widgetsdisallowed in the Phenom/tenant robots.txt, and what's the acceptable-use line for polling a client's own public career site? - Portfolio sweep: which other phData clients run Phenom (vs Workday/Greenhouse/iCIMS)? Build the ATS-vendor-per-client map so the monitor's reach is known.
- Rate-limit / IP strategy: is a single poller at daily cadence safe across N tenants, or do we need IP rotation / staggering to stay under bot-detection thresholds?
Related
- [[2026-07-13-kwik-trip-meeting-prep]]
- [[2026-05-29-fde-search-share-baseline]]
Sources
- Vault: rdco-vault/01-projects/phdata/2026-07-13-kwik-trip-meeting-prep.md — [[2026-07-13-kwik-trip-meeting-prep]]
- Vault: rdco-vault/06-reference/research/2026-05-29-fde-search-share-baseline.md — [[2026-05-29-fde-search-share-baseline]]
- Web: https://jobo.world/ats/phenompeople (unofficial /widgets + refineSearch mechanism, refNum, payload fields, rate/Cloudflare notes)
- Web: https://developer.phenom.com/card/Jobs (official OAuth-gated Jobs API — the paid path)
- Web: https://fantastic.jobs/ats/phenompeople (official API access model + paid aggregator fallback)
- Web: https://developer.phenom.com/apiDetail (Phenom API base docs)