# Conventions and errors

Pagination, dates, headers, idempotency and the refusal vocabulary shared by every surface.

These rules hold on every surface. Each surface's own page covers only what differs.

## Dates, ids and versions

- Every timestamp is ISO 8601 in UTC, and every date is `YYYY-MM-DD`.
- Ids come in two flavours. A Prospex company id is an integer and stable forever. Prospect ids are opaque strings such as `prospect_4812`, and the API takes them exactly as it gave them.
- The API is versioned in its path, at `/api/v1/prospect/`. Feeds carry a `version` or `schema_version` in the body instead, bumped when a field changes meaning or leaves.
- A new field does not bump a version. Unknown response fields should be ignored.

## Pagination

Each paginated feed hands you a `next_url` that is absolute and carries every parameter you sent. Following it until it comes back empty is the whole of pagination. Cursors are opaque.

**A market** is a built ranking, and its order changes only when a build refreshes it. Its cursor carries the build it was issued under. A cursor from an older build answers **400 `invalid_parameter`** and asks you to start again.

**A Watch** is ordered by detection time. Its keyset cursor keeps page turns stable when newer signals arrive during a pull.

**A project** grows continuously, so its feed uses a keyset in one of two modes. Without `updated_since` it walks primary keys, which never move, so a page turn cannot skip a row that changed underneath you. With `updated_since` it walks oldest change first, and a row updated mid-walk is served again. Re-reading a row costs you a comparison. Skipping one costs you a company.

Page sizes: up to 500 rows on a market page, up to 200 on a project page.

## Parameters

The Watch feed, project feed and API refuse a parameter they do not know, with a 400 that names it and lists what would have worked.

The market feed reads three parameters — `tab`, `limit` and `cursor` — and ignores anything else.

## Headers

| Header | Where | Why |
| --- | --- | --- |
| Retry-After | On a 429 | Seconds until the budget resets, which is midnight. Sleep on this rather than on a guess. |
| X-Request-Id | Every API response | Also in the body as `request_id`, and recorded in the project's audit history. Quote it when something looks wrong. |
| X-RateLimit-* | Every API response | `Limit`, `Remaining` and `Reset` for the day's request allowance. |
| Idempotency-Key | Sent by you, on every API mutation | Required. |

## Idempotency

Every mutation carries an idempotency key you choose.

- The same key with the same request returns the original response byte for byte, with `Idempotency-Replayed: true`, and spends nothing a second time.
- The same key with a different request answers **422 `idempotency_conflict`**.
- Keys are scoped to the credential that used them, so two services in one account can both send `nightly-2026-08-21` without colliding.
