companies

List Public Companies

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.

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

Query parameters

limitinteger
cursorstring nullable
qstring nullable
industrystring nullable
size'1-10' | '11-50' | '51-200' | '201-500' | '501-1000' | '1001-5000' | '5000+' nullable
locationstring nullable
hiringboolean nullable

Response

Successful Response

object required

Changes

No recorded changes to this operation across all 1 revision of this API.