# /v1 API reference

Every `/v1` response uses the same envelope wrapper. It contains the requested timestamp, the filtered timestamp field, the returned rows, and coverage metadata. Error responses also use this envelope and include an `error` object.

Updated 2026-08-26 against the corpus described on [coverage](https://pit.aqx.llc/docs/coverage).

## Hosts

- **API**: `https://api.pit.aqx.llc`
- **Spec**: [`/v1/openapi.json`](https://pit.aqx.llc/v1/openapi.json) provides an OpenAPI 3 specification generated from the server types.
- **Local**: `http://127.0.0.1:8080`

The `/v1` API is additive. New fields may appear without a version change. Existing field names, error codes, and parameter names maintain their meaning. Nullable fields remain nullable.

## Envelope

All JSON response bodies use this structure, including errors.

**response body**

```
{
  "request_id": "uuid",
  "corpus_version": "sha256:…",
  "as_of": "2023-03-10T23:59:59Z",
  "visible_by": "published_at",
  "status": "ok",
  "results": [],
  "next_cursor": null,
  "count": 0,
  "coverage": { "touched": [], "missing": [] },
  "error": null
}
```

- **status**: `ok`, `error`, or `coverage_missing`
- **count**: Number of rows in the returned page. Gaps, 400, 401, and 429 responses return JSON null instead of 0.
- **coverage**: `touched` lists the scope used for the response. `missing` lists data that suppressed the response. See [Coverage](https://pit.aqx.llc/docs/coverage) for key schemas.
- **corpus_version**: SHA-256 hash of the source corpus. Sample routes return `sha256:sample`.
- **next_cursor**: Opaque pagination token bound to the query. Pass this value as `cursor` to fetch the next page. It is null on the last page.

Timestamps must use RFC3339 UTC format with a literal `Z`. Values without `Z` or with local offsets are rejected.

## Pagination

Pagination uses cursors sorted by `(published_at, id)`. The `limit` parameter defaults to 100 and accepts values from 1 to 1000. Other values return HTTP 400 with `invalid_request`. The `count` field contains the number of rows in the current page, so sum `count` across all pages to get the total.

A cursor encodes the route, query filters, and the `corpus_version`. To fetch the next page, send the same query with the `cursor` parameter added. You can change `limit` between pages, but all other query parameters must match. Reusing a cursor with different parameters or routes returns `invalid_request` for `cursor`. If the corpus reloads during pagination, the cursor returns `cursor_expired` and you must restart from the first page.

## Headers

| Header | On | Carries |
| --- | --- | --- |
| `X-Request-Id` | every response | Same value as `request_id`. Quote it in a bug report. |
| `X-Corpus-Version` | corpus reads | Same value as `corpus_version`. |
| `X-RateLimit-Limit` | every response | Requests allowed per minute. |
| `X-RateLimit-Remaining` | every response | Requests left in the current window. |
| `X-RateLimit-Reset` | every response | When the window resets, as UTC unix seconds. |
| `Retry-After` | 429 | Seconds to wait. |
| `ETag` | corpus reads | Corpus version plus a hash of the query. Send it back as `If-None-Match` for a 304. |
| `Cache-Control` | corpus reads | `private, max-age=300`, with `Vary: Authorization`. |

## Sample routes

Sample routes do not require an `Authorization` header or an account. The `example` parameter accepts `svb`, `aapl`, or `meta`; other values return `invalid_request`. Responses include `sample: true` and are rate-limited to 30 requests per minute per IP.

| GET | Parameters |
| --- | --- |
| `/v1/sample/news` | `example`, optional `as_of` and `visible_by` |
| `/v1/sample/diff` | `example` |
| `/v1/sample/coverage` | `example` |
| `/v1/sample/mapping` | `example`, `as_of` |

`GET /health` and `GET /v1/meta` are also open. `/v1/meta` returns the corpus version, accepted clocks, plan limits, and row and certificate counts per source. A certificate is the record for one source on one day.

## Paid routes

Send `Authorization: Bearer pit_live_…`. The `as_of` timestamp parameter is required on historical reads. Each query must specify either `ticker` or `cik`.

| GET | Returns |
| --- | --- |
| `/v1/news` | The cut at `as_of`. |
| `/v1/news/{id}` | One row, with `as_of` required so the read is still a point-in-time question. |
| `/v1/news/diff` | Rows that became visible between `as_of_start` and `as_of_end` on the same clock. |
| `/v1/coverage` | Certificates. Optional `source`, `date`, `as_of`. |
| `/v1/mapping/ticker/{ticker}` | Identity at `as_of`. |
| `/v1/mapping/cik/{cik}` | Identity at `as_of`. |
| `/v1/files` | Parquet partitions — one file per source per day — with a SHA-256 each and a signed GCS URL per file. Power plan. |
| `/v1/stream` | Server-sent events for new rows carrying `available_at`. |

**GET /v1/news — 200**

```
$ curl -s -H 'Authorization: Bearer pit_live_…' \
  "https://api.pit.aqx.llc/v1/news?ticker=SIVB&source=sec.edgar&as_of=2023-03-10T23:59:59Z"
```

Use `source` to filter by a specific source. If omitted, the query checks all seven sources. If any source lacks a certificate for that day, the request returns HTTP 409. See [Coverage](https://pit.aqx.llc/docs/coverage) for details.

Unknown query parameters return `invalid_request` and name the parameter. Misspelled filters fail the request instead of widening the result set.

## Errors

Error responses include an `error` object containing `{ code, message, param }` alongside the standard envelope. Branch your code on `code`, which is stable. `message` is human-readable and may change between releases. `param` names the invalid parameter or is null.

| `code` | HTTP | Cause |
| --- | --- | --- |
| `invalid_clock` | 400 | `as_of` missing or malformed, or `visible_by` naming a field that is not a query clock. |
| `invalid_request` | 400 | An unknown parameter, or a value outside its range. |
| `unauthorized` | 401 | The key is missing, malformed, or revoked. |
| `plan_required` | 403 | The account has no plan, or the plan does not include what was asked for — flat files, for instance. |
| `commercial_license_required` | 403 | The declared use needs a [commercial license](https://pit.aqx.llc/commercial). |
| `not_found` | 404 | No such row or route. |
| `coverage_missing` | 409 | A key in `coverage.missing`. `count` is JSON null. |
| `cursor_expired` | 409 | The corpus moved while a page walk was open, so the cursor points into a version we no longer serve. Restart the walk. |
| `rate_limited` | 429 | Over the per-minute limit. `Retry-After` says how long to wait. |
| `internal` | 500 | A server fault. `X-Request-Id` identifies it. |
| `billing_not_configured` | 503 | Checkout is unavailable in this deployment. |
| `auth_not_configured` | 503 | That login method is unavailable in this deployment. |

## Keys and limits

Log in using GitHub, Google, or email. Accounts support multiple API keys. Each key shows a name, prefix, created time, last-used time, and a revoke action. The secret is shown once upon creation and cannot be retrieved later. All plans can access the full ingested history.

|  | Sample | Researcher | Power | Desk |
| --- | --- | --- | --- | --- |
| Auth | none | Bearer key | Bearer key | Bearer key |
| Requests / minute | 30 per IP | 60 | 300 | 1,200 |
| Keys | — | 3 | 10 | 25 |
| as_of range | fixed examples | full history | full history | full history |
| Flat files | no | no | yes | yes |

Review plan details and rates on [pricing](https://pit.aqx.llc/pricing). An API key covers internal research and applications run for yourself or your employer. Redistributing data or serving it directly to external end users requires a [commercial license](https://pit.aqx.llc/commercial).
