# Table APIs

> Every table has a URL that programs can read its rows from.

Every table has its own API: a URL, by an opaque key, that answers its rows as JSON, CSV, NDJSON or Parquet. Find it in the panel that the table's **API** button opens in the app.

- `https://api.rho.store/v/<key>`: the latest version, a page of rows as JSON objects.
- `https://api.rho.store/v/<key>@3`: version 3, for good. This always needs an API token.
- `https://api.rho.store/v/<key>.csv`: every row of the version, as a file. Also `.ndjson` and `.parquet`.

## Who can read it

Any table answers an [API token](/docs/guides/api-tokens/) of its workspace:

```bash
curl -H "Authorization: Bearer $RHO_API_KEY" https://api.rho.store/v/<key>
```

Once a table is [public](/docs/concepts/sharing/), the same URL also answers anyone who has it, with no token. They get the latest version only: `@3` still needs a token of the workspace, as an old version may hold what you removed before making the table public.

Replacing a table's links, in its **Share** panel, makes a new key. The old URL stops working for good, also for callers with a token.

## Rows

A JSON page answers the rows as objects, typed as the table is: numbers are numbers, dates and times are ISO text, and an empty cell is `null`. Cells are never cut short.

```json
{
  "table": "research/competitors",
  "version": 7,
  "columns": [{ "name": "company", "type": "VARCHAR" }],
  "rows": [{ "company": "Neon" }],
  "offset": 0,
  "total": 214,
  "next": "https://api.rho.store/v/<key>?offset=100"
}
```

`total` counts the rows that match, all pages together. `next` is the URL of the next page, and `null` on the last.

## Filter, sort, pick, page

| Parameter | What it does |
|---|---|
| `?country=SE` | Rows whose `country` is `SE` |
| `?price__gt=10` | Compare a column: `__eq`, `__ne`, `__contains`, `__gt`, `__lt`, `__gte`, `__lte`, `__empty`, `__not_empty` |
| `?sort=price`, `?sort=-price` | Order by a column, `-` for descending |
| `?select=company,price` | Only these columns, in this order |
| `?q=vector` | Rows with this text in any column |
| `?limit=`, `?offset=` | The page's size (100 rows unless you say, 500 at most) and start |

A column named like one of these parameters is filtered with `__eq`, as in `?sort__eq=x`. These are for JSON pages: a `.csv`, `.ndjson` or `.parquet` is every row of the version, and turns down a query.

## Caching

Every answer has an `X-Rho-Version` header and an ETag. Send the ETag back as `If-None-Match`, and you get `304 Not Modified` until the table has a new version. A pinned version (`@3`) never changes, so it can be cached.

The key follows the table through a rename. CORS is open on `/v/`, so a public table can be read from a browser.