# Kula Jobs API

> How Kula's job boards actually work, why its board page is the wrong thing to scrape, and what ?source=kula returns in the HyperJobs API.

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

Kula is a sourcing and recruiting CRM suite that also hosts real public job
boards, mostly for engineering-heavy companies in India, the US and Southeast
Asia. Boards live at `careers.kula.ai/<account>`, and the underlying feed is one
of the cheapest in this pool: an unauthenticated JSON call returns up to 100
postings with every description already inline. No detail pass, no auth, no
cookies.

## What the board looks like

The board page at `careers.kula.ai/<account>` is a Next.js app-router app, and
it renders into an RSC flight payload. That payload is the tempting thing to
scrape, and it is the wrong one. The right target is the plain JSON endpoint the
board's own filter dropdown calls:

```bash
curl "https://careers.kula.ai/api/internal/ats_job_posts\
?accountName=<account>&type=ats_job_post.index&items=100&page=1"
```

`type` is mandatory — without it the API returns a `400` with
`err_invalid_type`. `items` is clamped to 100 server-side (ask for 1,000 and
you get 100, honestly echoed back in the response's `meta`), and `meta.pages`
tells you when to stop.

<Warning title="Some Kula boards render client-side, so the flight payload is empty">
  This is the failure that looks like success. `sarvam-ai`'s board page ships a
  layout-only payload with **zero postings in it** — while the JSON endpoint
  returns its 34 jobs, all with descriptions. A scraper built on the flight
  payload reads that board as empty at HTTP 200 and never errors.

  The flight payload is worse in three quieter ways too: it is coupled to the
  Next.js build id and re-escapes between releases; its descriptions are lazy
  `$60`-style references into byte-length-delimited chunks that chain with no
  newline between them, so a line-anchored parse finds exactly one of 48; and
  that declared length is in bytes rather than characters, so character-slicing
  resolves none of them. All of that work, to get less data than the documented
  route hands over for free.
</Warning>

## What bites you

**`ats_department` is an object, not a string.** It's `{id, name, depth}`. This
is the exact shape that, on another platform in this pool, wrote an object into
a scalar column and zeroed a whole board while a truthiness probe cheerfully
reported the department field at 100% fill. Take `.name`.

**`office.name` is a label, not a location.** An office literally *named*
"Remote" has `location: "Toronto, Ontario, Canada"`. Others read "Kula Chennai",
"New york", "San Francisco ,USA". The `location` field beside it is
Google-Places-normalised and was filled on 149 of 149 office rows, so `name` is
only ever a last resort for a board that leaves the structured parts blank.

**The job-level `workplace` field contradicts the office-level one, and the job
is right.** `workplace` is a clean three-way `office | remote | hybrid`. An
office named "Kula Chennai" carries `workplace: "remote"` alongside
`remote: false`. Read the job's field, not the office's boolean.

**There is no posting date. At all.** Not on the list, not on the job page, no
JSON-LD anywhere. The only `created_at` in the payload belongs to the board's
**font theme**. Any date you see on a Kula posting was inferred by whoever
indexed it, not published by Kula.

**A churned customer returns 403, not 404.** The body says
`err_account_terminated`. That is a permanently dead board rather than a block —
Kula fronts no WAF — so treating the 403 as a bot wall means retrying a
cancelled account forever.

**Salary has a real stated period, which is rare and worth using.** `interval`
is an enum running from `hourly` to `yearly`, plus `quarterly` and `one_time`.
The amounts arrive as decimal strings. The last two are the interesting ones: a
quarterly figure annualised naively lands 4x too low, and a one-off bonus isn't
a salary at all, so both keep their text and no structured number.

## Where this stands

Query it with `?source=kula`. Measured across live tenants: 238 jobs and 238
descriptions, averaging around 6,000 characters each. On the boards measured end
to end, descriptions, country and department all come back at 100%, and Kula is
one of the stronger platforms for the [company join](/docs/objects/company).

That is a measurement rather than a ceiling, and it moves as boards are added —
read the live numbers off [`/v1/meta`](/docs/api-reference/meta) → `sources`,
which is the only vocabulary worth coding against.

See [all platforms](/ats) · [coverage](/docs/reference/coverage)
