# Freshteam Jobs API

> Query jobs from Freshteam boards through one API with ?source=freshteam — plus how Freshteam's job pages really behave, and why the platform is going away.

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

Freshteam is Freshworks' HR and applicant tracking product for small businesses,
with boards at `<tenant>.freshteam.com/jobs`. Its tenancy is India-heavy, then
US, UK and APAC.

Before anything else on this page: **Freshworks is sunsetting Freshteam.**

## The platform is being shut down

Renewals stopped on **2026-03-07**. Existing customers keep access only to the
end of their current term, which means the tenant pool doesn't drop off a cliff —
it **bleeds out through 2026 and 2027**, one expiring contract at a time.

A page selling you a jobs API for a dying platform would be a bad page. So, the
measurement rather than the pitch. As of **2026-07-15**, of 1,605 known tenants:

| | Tenants |
| --- | --- |
| Serving at least one live job | **710** (~14,384 jobs) |
| Dead | 567 |

<Warning title="What that means if you're deciding whether to build this">
  Freshteam is **still alive today** and it is **not alive long-term**. Both
  halves are true and neither is the interesting one on its own.

  If you need Freshteam data now, it's there and it's free to fetch. If you're
  costing a scraper as an asset you'll amortise over three years, don't: the
  asset expires. We built the adapter because it's cheap reuse of a JSON-LD path
  we already had, not because we think this platform has a future. Every number
  on this page needs re-measuring before anyone spends anything more on it, and
  when the live count collapses the right move is to drop the boards, not the
  code.
</Warning>

That framing also corrects our own earlier call. Freshteam was written off
entirely on the sunset announcement, on the reasoning that a dying platform isn't
worth an adapter. That was wrong in a specific way: "announced EOL" and "not
serving jobs" are different facts, and only one of them had been checked. 710
live tenants is not nothing.

## What the adapter reads

```bash
# List — 1,000 postings per page
curl -L "https://<tenant>.freshteam.com/jobs?page=1"

# Detail — one JobPosting JSON-LD per posting
curl -L "https://<tenant>.freshteam.com/jobs/<id>/<slug>"
```

The official `/api/job_postings` and `/api/v1/job_postings` both **401**. The
public HTML is the only way in, and the body is detail-only, so it's an N+1.

Pagination is real here, unlike several platforms in this pool that ignore the
param and hand you page one forever: `?page=2` returns 1,000 ids with **zero**
overlap with page one. Crawled `abnhire` to exhaustion — 1,000/page × 5 pages =
5,000 distinct, page 6 adds nothing. But `?offset=` and `?per_page=` **are**
ignored (both return page one at 200), so `page` is the only lever, and the
termination condition is "a page contributed nothing new" rather than a total,
because no total is exposed.

## What bites you

### A dead tenant answers 200, not 404

Freshworks serves an ~889-byte *"We couldn't find `{domain}`"* stub for any
expired or unclaimed subdomain. At HTTP 200.

With the sunset actively killing tenants this is the **common** case, not an edge
case — 567 of 1,605. And the failure is nasty in a specific way: treated as an
empty board, a dead tenant expires every job you had for that company, and burns
your whole detail budget on nothing. "Gone" and "hiring nothing" have to be told
apart explicitly, and the only way to do it is to recognize the stub.

### `addressLocality` and `addressRegion` are inverted platform-wide

Freshteam puts the **city in `addressRegion`** and the **state in
`addressLocality`** — the exact opposite of the schema.org spec it's otherwise
following. Verified across 371 postings on ~200 tenants:

| Posting | `addressLocality` | `addressRegion` |
| --- | --- | --- |
| Noida | `Uttar Pradesh` | `Noida` |
| San Diego | `California` | `San Diego` |
| Stuttgart | `Baden-Württemberg` | `Stuttgart` |

<Note title="Why this one is worse than a normal bug">
  A correct schema.org reader gets this **wrong**, and gets it wrong silently: it
  emits `"State, City, Country"`, a downstream parser takes the first part as the
  city, and every Freshteam posting in your database says its city is
  `California` or `Uttar Pradesh`.

  There's no error, no null, no anomaly — just a plausible city name in the city
  field on 100% of rows. A shared, correct, well-tested helper is the wrong tool
  here precisely *because* it's correct. Freshteam needs its own reader.
</Note>

### JSON-LD string values are entity-encoded, including the location

`&#39;` in a city name is not cosmetic. `"Thompson&#39;s Station"` contains a
semicolon, a location parser reads `;` as a **separator**, and one city becomes
two junk cities: `["Thompson&#39", "s Station"]`. Address parts have to be
entity-decoded before they're joined, not after.

### About 8% of live tenants ship no JSON-LD at all

5 of 60 tenants sampled (`avomind`, 219 jobs; `oml`; `10times`; `paxcom`;
`openinapp`) render the body in JavaScript and carry **no description anywhere in
the HTML**. There's no fallback to reach for. Those postings are skipped rather
than stored empty — a bodyless job is not a job.

### Two smaller ones

**Job ids are base64url**, not numeric (`JLX_yOg0d7bW`). The obvious
`/jobs/(\d+)` regex matches nothing and the board reads as empty at HTTP 200.

**`remote="false"` is an unchecked default, not an assertion.** Freshteam's form
has no hybrid concept at all, so `false` lumps on-site and hybrid together — and
it's demonstrably stale on real postings (one is `remote="false"` with "Mode of
Work - Work from Home" in its body). Only `"true"` is trusted. Reading `false` as
On-site would mislabel every hybrid job on the platform.

### What Freshteam simply doesn't have

Verified across 371 postings, so you don't go looking: `baseSalary` **0/371**.
`validThrough` **0/371**. `hiringOrganization.sameAs` and `.logo` **0/371** —
only `name`. No comp, no expiry, and no org-domain signal, which makes attributing
a Freshteam posting to a real company genuinely hard rather than merely fiddly.

## Where this stands

`?source=freshteam` is served today. And the unusual thing to say at the bottom
of a page like this: **it may not stay that way, and that would be the right
outcome.** Freshteam is a shrinking pool with a known end date. When the live
tenant count collapses far enough, covering it stops being worth the request
budget, and the right move is the one in the warning above — drop the boards,
keep the code. We'd rather tell you that here than have you plan around a source
that quietly empties out.

Which is the argument for coding against the keys of
[`/v1/meta`](/docs/api-reference/meta)`.sources` rather than against this page:
they're the values `?source=` actually accepts, with counts, so a source that
thins out shows you the count before it shows you an outage. [Sources &
coverage](/docs/reference/sources) says the same in prose.

See [all platforms](/ats) · [quickstart](/docs/quickstart).
