---
title: "List Public Companies"
method: GET
path: "/api/public/companies"
tags: ["companies"]
---

# List Public Companies

`GET /api/public/companies`

## 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/companies`
- Auth: none declared

Every company that has published a page, newest first.

## It is also the customer search

An account looking for who to sell to uses this endpoint with filters on --
the ICP query, in the language of the people who will run it. There is
deliberately no second endpoint for that: a customer search IS this directory
with a WHERE clause, and the symmetry with ``profiles.list_public_profiles``
is the point. Both sides of the network are searched the same way, by the same
kind of caller, with the same cache policy and the same allowlist.

Every filter is AND-ed with the others and skipped entirely when absent, so an
unfiltered call is byte-for-byte the response it always was.

  ``q``         free text over ``name``, ``tagline`` and ``industry``, OR-ed
                among themselves. Those three are what a company says it is.
                NOT over ``about``: that column is up to 6,000 characters of
                prose, and an unindexed ``LIKE`` across a page of them is the
                scan that makes a directory feel broken.
  ``industry``  exact and case-insensitive, for the caller who already knows
                the label and wants the whole bucket rather than a substring
                of it. Both are offered because they are different questions:
                ``q=fin`` is exploration, ``industry=Fintech`` is a segment.
  ``size``      exact, on the closed ``COMPANY_SIZES`` vocabulary. A typo is a
                422 rather than an empty page, which is what makes "no
                companies that size" believable when it comes back.
  ``location``  case-insensitive contains, because company locations are typed
                ("Bengaluru, India") and an equality test would match almost
                nothing.
  ``hiring``    the highest-signal filter here. See below.

**Filtering does not widen the serialiser.** ``_directory_company`` is
unchanged, so ``contact_email`` is still absent from every row no matter which
filter matched -- which matters more here than it looks, because this is
precisely the endpoint somebody would point a lead-generation script at. A
filter is a question about the row; the allowlist is the answer the row agreed
to give, and the two are not connected.

## ``hiring``, and why it is an EXISTS

A company with an open backend role is a company that is spending money on
building software, which is the single most useful thing this directory knows
about who is worth talking to. So it is a first-class filter rather than
something a caller derives by reading ``open_role_count`` off a page they
already paid for.

It is a correlated EXISTS and deliberately not a join. A join onto ``roles``
returns one row per open role, so a company with three openings appears three
times: three duplicates in the page, three duplicates fed into
``_open_role_counts``, and -- worst -- a page of ``limit + 1`` ROWS that is not
``limit + 1`` COMPANIES, which silently breaks the keyset over-fetch that
decides ``has_more``. A DISTINCT would paper over the duplicates and then
fight the ORDER BY. EXISTS also stops at the first matching role rather than
counting them all, which is the whole question being asked.

``hiring=false`` is not a filter for "companies that are not hiring". Like
``open_to_work`` on the people directory, this parameter is a switch: on
narrows, off and absent are the same. An unticked box in a filter bar must not
send a request that hides everybody the box was about.

## No viewer

Like the people directory and unlike every other public read here, this takes
no ``OptionalUserId``. Nothing in the response depends on who is asking, so
resolving a viewer would buy nothing and would cost the shared cache -- which
is the entire reason this response is cacheable at all. That survives the
filters: a filtered response is still identical for every caller who sends the
same query string, and a query string is part of a cache key where a viewer is
not.

## Newest first, not by size or by open roles

Recency is honest with one company listed and with ten thousand. Sorting by
open-role count sounds more useful and cannot be keyset-paginated: the count
is an aggregate over another table that changes under the reader, so a cursor
built on it would skip and repeat rows as companies publish and close
openings. Sorting by ``size`` would be sorting a page of strangers by a
self-reported bucket, which is an advert rather than an ordering.

So: keyset on ``(created_at DESC, handle DESC)``, a total order that pages
correctly even when a hundred companies share a timestamp -- which they will,
the day a backfill creates them.

None of the filters touch the sort key, which is what makes paging a filtered
search work: every predicate narrows the set, the cursor still names a
position in a total order over what is left, and a deeper page costs no more
statements than the first.

## Query budget

Two statements, constant in the page size and constant in the number of
filters: the page, and one batched open-role tally. Every filter lives inside
statement one, including ``hiring`` -- a correlated EXISTS is part of the same
statement, where a "does this company have roles?" lookup per row would be the
N+1 this endpoint exists to avoid on a page designed to be crawled.

## Query parameters

- `limit` integer
- `cursor` string, nullable
- `q` string, nullable
- `industry` string, nullable
- `size` '1-10' | '11-50' | '51-200' | '201-500' | '501-1000' | '1001-5000' | '5000+', nullable
- `location` string, nullable
- `hiring` boolean, 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)
