API reference
Everything is one REST call away.
One base URL, one header, JSON everywhere. The catalog and the coverage ledger are open without a key; data access signs short-lived URLs over object storage so bulk downloads never pass through the API.
Quickstart
Get a key, list the catalog, sign a URL, read the file. The free plan covers the sample surface across every market, so nothing below needs a paid plan.
import pmdbook = pmd.books("btc-updown-5m-1762598700-up",start="2026-11-08T12:05:00Z",end="2026-11-08T12:10:00Z")print(book.head(3))
Authentication
A key looks like pmd_a1b2c3d4_... and travels in the Authorization header. We store only a sha256 hash and a clear-text prefix, so a key is displayed once at creation and can never be read back - revoke and mint a new one instead.
Endpoints
| Method | Path | What it does | Access |
|---|---|---|---|
| Catalog - open, no key | |||
| GET | /v1/catalog/series | List series | free |
| GET | /v1/catalog/markets | List markets, filtered by series | free |
| GET | /v1/catalog/markets/{slug} | One market with its tokens | free |
| GET | /v1/catalog/coverage | Coverage ledger per market-hour | free |
| Data - key required | |||
| POST | /v1/data/urls | Sign short-lived download URLs | plan |
| GET | /v1/data/usage | Quota used today, entitlements, reset time | key |
| Account | |||
| POST | /v1/auth/register | Create an account and mint the first key | open |
| POST | /v1/auth/login | Sign in and mint a key | open |
| GET | /v1/me | The current account | key |
| GET | /v1/keys | List keys by prefix | key |
| POST | /v1/keys | Mint an additional key | key |
| DELETE | /v1/keys/{prefix} | Revoke a key | key |
| Billing | |||
| GET | /v1/plans | Price list, entitlements and licence per plan | open |
| POST | /v1/orders | Order a plan; we invoice, nothing is charged | key |
| GET | /v1/orders | Your orders and their status | key |
| POST | /v1/orders/{id}/cancel | Cancel a pending order | key |
Admin endpoints exist for the back office and are not part of the public contract.
Schemas
Four surfaces, one naming convention. Timestamps are milliseconds since the epoch, and the venue clock and our receive clock are separate columns.
One row per change of the ladder. lp2 (lossless, about 7x smaller than parquet) or parquet.
| ts_ms | int64 | venue timestamp, milliseconds |
| local_ts_ms | int64 | our receive timestamp, kept apart from the venue clock |
| token_id | string | outcome token the book belongs to |
| side | enum | bid | ask |
| price | float64 | level price, 0..1 |
| size | float64 | resting size after the change |
| action | enum | add | change | remove | snapshot |
| seq | int64 | monotonic per token; gaps are flagged, not hidden |
Delivery and formats
Signed URLs
POST a list of object paths, get back short-lived URLs. The bucket is never exposed and the API never proxies bytes.
lp2
Our lossless columnar format for book events: about 7x smaller than parquet over the same tape. The reader is open source.
parquet
Every surface is also available as parquet, partitioned hour x market, for people who would rather not install anything.
Paths are normalised before signing: absolute paths, backslashes and any component of .. are rejected rather than cleaned up.
Quotas and limits
Four things are kept apart on purpose: what a plan may access, how much it may take, how fast it may ask, and what it actually consumed. Consumption is an idempotent ledger keyed per account, path and minute, so a retried request is never counted twice.
| Plan | Datasets | Signed URLs / day | Requests / min |
|---|---|---|---|
| free | samples, catalog, coverage | 50 | 60 |
| research | + full tape | 20,000 | 600 |
| pro | + full tape | 200,000 | 3,000 |
| academic | + full tape | 20,000 | 600 |
Errors
Errors are a JSON object with a detail field: a string for our own refusals, a list of field errors for schema validation.
| Status | detail | When |
|---|---|---|
| 400 | bad request | A malformed object path: absolute, with .. or a backslash. |
| 401 | missing or invalid api key | No Authorization header, or the key was revoked. |
| 402 | plan does not cover this path | The path is on the tape and the plan is free. |
| 404 | not found | Unknown market, series or order. |
| 409 | conflict | You already hold that plan, or an order is already open. |
| 422 | validation error | A field is missing or has the wrong type; the body lists them. |
| 429 | quota exhausted | The daily signing quota for the plan is used up. |
Versioning
Paths carry /v1. Additive changes - new endpoints, new optional fields - ship inside v1. Anything that removes or renames a field gets /v2 and an overlap period. The OpenAPI document is the contract, and the SDK is generated from it.