Breezy HR Jobs API
Query jobs from Breezy HR boards through one API — and why Breezy's posting pages 403 your client no matter which IP you come from.
Breezy HR is a visual, pipeline-first hiring tool aimed at small companies. Boards
live at <tenant>.breezy.hr, and there's a public per-tenant JSON feed that
returns the whole board in one call. It also returns no descriptions, and the
place they live is behind a WAF that doesn't care what IP you use. Breezy is the
only platform in this pool besides one other where a browser-fingerprint fetch tier
is mandatory rather than a fallback.
Try it#
curl -G https://api.hyperjobs.io/v1/jobs \
-H "Authorization: Bearer $HYPERJOBS_KEY" \
--data-urlencode "source=breezy" \
--data-urlencode "time_frame=7d" \
--data-urlencode "limit=2"{
"data": [
{
"id": "breezy:brightpath_customer-success-manager",
"title": "Customer Success Manager",
"company": {
"slug": "brightpath",
"name": "BrightPath",
"domain": "brightpath.co.za",
"website": "https://brightpath.co.za",
"logo": "https://media.licdn.com/dms/image/…",
"industry": "Financial Services",
"employee_count": 45
},
"location": {
"remote": false,
"countries": ["South Africa"],
"cities": ["Cape Town"],
"regions": ["Western Cape"],
"raw": ["Cape Town, Western Cape, South Africa"]
},
"employment_type": ["FULL_TIME"],
"work_arrangement": "Hybrid",
"salary": {
"min": 8000,
"max": 10000,
"currency": "ZAR",
"unit": "MONTH",
"annual_min": 96000,
"annual_max": 120000,
"summary": "R8,000 – R10,000 / month"
},
"skills": ["account management", "onboarding"],
"taxonomies": ["Customer Service"],
"apply_url": "https://brightpath.breezy.hr/p/a1b2c3d4e5f6-customer-success-manager",
"url": "https://brightpath.breezy.hr/p/a1b2c3d4e5f6-customer-success-manager",
"source": "breezy",
"posted_at": "2026-07-08T00:00:00.000Z",
"expires_at": null,
"updated_at": "2026-07-13T14:20:07.000Z"
}
],
"total": 2,
"limit": 2,
"offset": 0
}How Breezy boards actually work#
One GET for the list, no auth, no pagination:
curl "https://<tenant>.breezy.hr/json"<tenant> is the subdomain. The response is a bare JSON array. Per job:
friendly_id, name, url, published_date, type, location, locations[],
department, salary, and company.logo_url.
And no description. The body exists only on the HTML posting page — one fetch per posting, which is the entire cost of this platform.
The posting page is where it gets interesting#
curl "https://<tenant>.breezy.hr/p/<friendly_id>"Most posting pages carry a JobPosting JSON-LD block, and that's the clean path.
Some tenants ship none — there the body exists only as a <div class="description">
in the page HTML, and you have to walk the div nesting to find the matching close
tag, because the body is rich HTML full of nested divs and a lazy
.*?</div> truncates it at the first one. Two templates, two parsers, no way to
know which you'll get until you fetch.
Breezy's WAF fingerprints your client, not your IP
The posting pages 403 a plain HTTP client on both direct and residential proxy egress. Rotating IPs does nothing, because the IP was never the problem — it's the TLS and HTTP/2 fingerprint. This is the one wall in this pool that a proxy budget cannot buy its way past.
The fix is a fetch tier that reproduces a real browser's TLS and h2 fingerprint
(we use impit). Without it, every posting page fails, the catch swallows it, and
you get a full board of rows with description: null — a 200-shaped success
containing nothing worth having.
That last sentence is the whole reason Breezy is worth writing about. The failure isn't loud. It's a complete-looking board of description-less jobs, which under a no-description-no-job rule means a board of nothing.
What bites you#
is_remote is true for hybrid roles. Same trap as Ashby, same
consequence. Verified live on a tenant whose hybrid jobs all carried
is_remote: true. The bool alone mislabels every hybrid job as fully remote.
remote_details.value is the authoritative field — remote or hybrid — and it
can express the difference. Hybrid has to win first; only then does an explicit
remote mean remote, and only an explicit is_remote: false means on-site.
friendly_id repeats across tenants. They're human-readable — accountant,
sales-manager — so a key of breezy:<friendly_id> collides across boards and
one company's accountant overwrites another's. Ours is scoped:
breezy:<tenant>_<friendly_id>.
The display location drops the country. location.name gives Cape Town, ZA;
the structured city / state.name / country.name gives
Cape Town, Western Cape, South Africa. Prefer the structured fields.
salary is free text. R8,000 – R10,000 / month — a currency symbol you have
to recognise, an en dash, and a period suffix. It parses, but it's a string, not a
number, and it's often absent.
There's no expiry field. published_date is the only date. A pulled job is
only detectable by re-fetching the board.
Or use ours#
Breezy is a request per posting, through a browser-fingerprint tier, against two different page templates, keyed on ids that collide across tenants.
We pay that N+1 on every new posting — a description-less row is worthless, so there's no cheap mode worth having — and skip it for postings already in the pool, so a re-scrape of an unchanged board costs one call. When a posting page 403s, we climb to the impersonation tier rather than record an empty row. When the JSON-LD is missing, we walk the div nesting. We track 1,385 Breezy boards.
Then the usual: remote_details read instead of the hybrid-inclusive bool,
tenant-scoped ids, structured locations preferred over the display string, the
free-text salary parsed and normalised, dedup, and the company
join.
Breezy is in the fast refresh tier, attempted every cycle, despite the N+1 — see coverage.
Every source above normalises onto the same schema, with the hiring company already joined onto each posting.
Get an API key