---
title: "List Public Roles"
method: GET
path: "/api/public/roles"
tags: ["roles"]
---

# List Public Roles

`GET /api/public/roles`

## Request

- Base URL: `https://api.parlel.com` (the document declares no server; this is the origin it was published from)
- URL: `https://api.parlel.com/api/public/roles`
- Auth: none declared

THE PUBLIC JOB BOARD. The most important open-data surface in this product.

Everything else in this file exists to fill this endpoint, and everything
about it is chosen on the assumption that **its primary consumer is not a
browser**. It is what external agents read: an agent watching for senior Rust
roles above a salary floor is the canonical use of this product, and its
experience of Parlel is this JSON. Treat a change here the way you would treat
a change to a published API, because that is what it is.

Three consequences follow, and they are the reason this docstring is long.

**It is anonymous, and it stays anonymous.** No ``OptionalUserId``, no
per-viewer field, nothing that varies by who is asking -- which is what lets
the whole response be stored by a shared cache and served from it. An agent
polling every five minutes should be absorbed by a CDN, not by this process.
The day somebody adds a "saved" flag to these cards, every one of those polls
starts hitting Postgres.

**It is keyset paged, not offset paged.** ``(published_at DESC, id DESC)``,
over-fetched by one. An agent walking the whole board while companies publish
into it is the exact workload offset paging gets wrong -- rows shift under the
reader and the page after an ``OFFSET 24`` silently skips whatever landed
above it. A cursor cannot skip.

**Its filters are narrow on purpose.** Title text, remote mode, seniority,
location substring, and a compensation floor. Location uses the same
``LIKE`` pattern as the company directory (acceptable at current board
size; revisit with an index if the table grows large). Full-text search
over descriptions and skill filters still wait on dedicated indexes.

## What is on the board, and what is not

``status == "open"`` AND the company is public. Both halves, joined in one
statement rather than filtered in Python, because "the row never leaves the
database" is a stronger guarantee than "the serialiser remembered".

A draft is a role its owner has not decided to show anybody. A role belonging
to a private company is unreachable even when open -- the company page it links
to 404s -- so listing it would be advertising a dead end. A closed role stays
readable at its own URL and never appears here; that split is the whole point
of having three statuses.

## The compensation filter, and its honest sharp edge

``comp_min=130000`` returns roles whose ``comp_max`` clears the floor OR whose
``comp_min`` does. The first is the main clause: a role paying 120k-150k
clears a 130k floor and a filter that compared only against the bottom of the
band would hide it. The second catches a role that published only a floor.

The sharp edge: a role that published NO compensation at all is excluded, and
that is a real exclusion -- possibly a large one, since many companies post no
salary. It is still the right answer. Including them would mean answering
"show me roles above 130k" with a list that mostly cannot be checked, and a
filter that silently returns things it did not verify is worse than one that
returns fewer things. Currency is deliberately NOT converted: comparing a
number against a floor in another currency would be a made-up exchange rate,
so the caller filters on the amount and reads the currency off each card.

## Query budget

Two statements, constant in the page size: the page (roles joined to their
companies) and one batched skill read. There is no third for the company
details because the join already carries them -- a per-card company lookup is
the N+1 this endpoint would otherwise ship on the one page designed to be
crawled.

## Query parameters

- `limit` integer
- `cursor` string, nullable
- `q` string, nullable
- `remote` 'onsite' | 'hybrid' | 'remote', nullable
- `seniority` 'intern' | 'junior' | 'mid' | 'senior' | 'staff' | 'principal' | 'exec', nullable
- `location` string, nullable
- `comp_min` integer, nullable

## Response `200`

Successful Response

- object

## Other responses

- `422`: Validation Error

---

[API](https://skmtc.dev/parlel/apis/parlel-api.md) · [All operations](https://skmtc.dev/parlel/apis/parlel-api/llms.txt) · [OpenAPI document](https://skmtc.dev/parlel/apis/parlel-api/revisions/4d41a8f5cfc9?raw)
