> For the complete documentation index, see [llms.txt](https://gitbook-docs.coinmetrics.io/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://gitbook-docs.coinmetrics.io/api-reference/api-conventions.md).

# API Conventions

The rules on this page hold across every endpoint in this reference. Each endpoint's own page lists the parameters it accepts and the fields it returns, and this page describes the behavior those parameters share: how timestamps are parsed, how numbers and nulls are represented, how paging works, and what an error looks like.

{% hint style="info" %}
Rate limits, authentication, and diagnostic response headers have their own pages in this reference: [Rate limits](https://docs.coinmetrics.io/api-reference/rate-limits), [Authentication](https://docs.coinmetrics.io/api-reference/authentication), and [Response headers](https://docs.coinmetrics.io/api-reference/response-headers).
{% endhint %}

## Endpoint families

Endpoints are grouped by what they answer rather than by product.

<table data-full-width="true"><thead><tr><th>Family</th><th>Answers</th><th>Notes</th></tr></thead><tbody><tr><td><code>/reference-data/*</code></td><td>What entities and metrics exist, and how their identifiers are formed</td><td>Descriptions, units, and data types. Independent of your entitlements.</td></tr><tr><td><code>/catalog-v2/*</code></td><td>What your API key can query, and over what time range</td><td>Returns <code>min_time</code> and <code>max_time</code> per frequency.</td></tr><tr><td><code>/catalog-all-v2/*</code></td><td>What Coin Metrics supports in full, regardless of entitlement</td><td>Use to compare your coverage against the whole dataset.</td></tr><tr><td><code>/timeseries/*</code></td><td>Historical and current observations over HTTP</td><td>Computed, sampled, or event series. See <a href="#frequency-and-granularity">Frequency and granularity</a>.</td></tr><tr><td><code>/timeseries-stream/*</code></td><td>The same data in real time over WebSocket</td><td>Named to match the HTTP endpoint, with a different parameter set.</td></tr></tbody></table>

Endpoints that do not fit this shape follow the same conventions: ATLAS blockchain data, taxonomy, constituents, profiles, and blockchain metadata.

{% hint style="warning" %}
The unversioned `/catalog/*` and `/catalog-all/*` endpoints are deprecated and are not part of this reference. Use their `-v2` equivalents, whose response shape differs.
{% endhint %}

## The discovery loop

Those families compose into one pattern that holds across the entire API, whatever the data type: find out what exists, check what is available to you and over what range, then read it. The three layers are separate because the answers change on different schedules. What exists changes when Coin Metrics adds coverage, what is available to you changes with your subscription, and the data itself changes continuously.

**What exists.** The 13 `/reference-data/*` endpoints describe the universe independently of any entitlement. Six return entities and their attributes (`assets`, `exchanges`, `markets`, `pairs`, `indexes`, `protocols`), and seven return the metrics defined at each entity level, such as `/reference-data/asset-metrics`, with each metric's full name, description, unit, and data type, and for most of them a `docs_url` pointing at its documentation page. This layer is where identifier formats are established, so it is the right place to learn that a spot market is written `coinbase-btc-usd-spot` before passing one anywhere else.

**What is available, and when.** The 31 `/catalog-v2/*` endpoints report `min_time` and `max_time` per entity and frequency, so they answer whether a series covers the window you need. Each has an exact `/catalog-all-v2/*` twin: the first reflects your API key's entitlements, the second the whole Coin Metrics dataset. Comparing the two is how you tell an absence of data from an absence of access.

**Read it.** The 28 `/timeseries/*` endpoints serve history and current values over HTTP, and 11 `/timeseries-stream/*` endpoints serve real-time messages over WebSocket.

Reading the Bitcoin reference rate runs the loop like this, and the same three calls work for any metric by substituting the entity and metric names:

| Step | Call                                                                         | What it returns                                                          |
| ---- | ---------------------------------------------------------------------------- | ------------------------------------------------------------------------ |
| 1    | `/reference-data/asset-metrics?metrics=ReferenceRateUSD`                     | That the metric is quoted in USD, is a `decimal`, and what it means      |
| 2    | `/catalog-v2/asset-metrics?assets=btc&metrics=ReferenceRateUSD`              | The frequencies carrying it, each with its own `min_time` and `max_time` |
| 3    | `/timeseries/asset-metrics?assets=btc&metrics=ReferenceRateUSD&frequency=1d` | The values                                                               |

{% hint style="info" %}
Step 2 is the step most often skipped and the one that explains most empty responses. The same metric at the same frequency can report a much shorter history on `/catalog-v2` than on `/catalog-all-v2`, because the first is bounded by what your key is entitled to. A query outside that window returns no rows rather than an error, so an empty response is worth checking against the catalog before treating it as missing data.
{% endhint %}

[Coverage](https://coverage.coinmetrics.io/) presents the first two layers as browsable tables, which is usually faster than steps 1 and 2 when you are exploring rather than automating.

## Frequency and granularity

Two different parameters control the time resolution of a timeseries response, and confusing them is easy because both take values that look like intervals. `frequency` **selects** the rows, and `granularity` **downsamples** them.

`frequency` sets the interval between the rows you get back. What each row *means* is a property of the metric rather than of `frequency`, and Coin Metrics uses two timestamp conventions for that. Under the point-in-time convention a row is the value at its own timestamp, so changing `frequency` changes only how often you sample and the values at timestamps common to both are identical. Under the beginning-of-interval convention a row is a summary statistic over the interval starting at its timestamp. A metric's name is the signal: an interval suffix such as `_1d` means it is an aggregation, and no suffix means it is point-in-time. [What timestamp conventions does Coin Metrics use?](https://docs.coinmetrics.io/service-and-support/faqs#what-timestamp-conventions-does-coin-metrics-use) lists which endpoints and metrics follow which.

`granularity` downsamples a stream of observations that already exists at its own natural cadence. It defaults to `raw`, which returns every observation, and `1m`, `1h`, or `1d` return a subset instead. Every row is still one real observation rather than an average of the interval, so changing `granularity` changes how many rows come back and never what any row says.

<table data-full-width="true"><thead><tr><th>Shape</th><th>Parameter</th><th>What a row is</th><th>Examples</th></tr></thead><tbody><tr><td>Frequency series</td><td><code>frequency</code></td><td>Either the value at its timestamp or a summary of the interval starting there, depending on the metric</td><td><code>/timeseries/asset-metrics</code>, <code>/timeseries/market-candles</code>, <code>/timeseries/pair-metrics</code></td></tr><tr><td>Sampled series</td><td><code>granularity</code></td><td>One observation, as recorded</td><td><code>/timeseries/market-quotes</code>, <code>/timeseries/market-orderbooks</code>, <code>/timeseries/market-openinterest</code></td></tr><tr><td>Event series</td><td>neither</td><td>One event, at the time it happened</td><td><code>/timeseries/market-trades</code>, <code>/timeseries/market-liquidations</code>, <code>/timeseries/market-funding-rates</code></td></tr></tbody></table>

{% hint style="warning" %}
Which `frequency` values an endpoint supports varies by endpoint **and by entity**, so a frequency valid for one asset or index can be rejected for another on the same endpoint. Read the supported set from `/catalog-v2/*`, where frequencies are listed per entity, rather than assuming a family-wide list. An unsupported value returns `bad_parameter`.
{% endhint %}

## Timestamps

Timestamps follow ISO 8601. Requests accept ten forms, and fractional seconds must carry exactly 3, 6, or 9 digits.

| Precision    | Accepted forms                                                 |
| ------------ | -------------------------------------------------------------- |
| Date only    | `2020-03-13`, `20200313`                                       |
| Seconds      | `2020-03-13T15:25:15`, `2020-03-13T152515`                     |
| Milliseconds | `2020-03-13T15:25:15.123`, `2020-03-13T152515.123`             |
| Microseconds | `2020-03-13T15:25:15.123456`, `2020-03-13T152515.123456`       |
| Nanoseconds  | `2020-03-13T15:25:15.123456789`, `2020-03-13T152515.123456789` |

A trailing `Z` is optional and its case is not significant. A compact date combined with a time, such as `20200313T152515`, is not accepted.

Query timestamps are interpreted as UTC unless `timezone` names another zone. Responses are always UTC with nanosecond precision, regardless of the request.

`start_time` defaults to the Unix epoch and `end_time` to the current time. Both bounds are inclusive by default. Setting `start_inclusive` or `end_inclusive` to `false` excludes that boundary observation, so a `1d` range of `2020-01-05` to `2020-01-08` returns four rows by default and three with either flag set to `false`.

{% hint style="warning" %}
A fraction of any other length is rejected with `bad_parameter`, so `2020-03-13T15:25:15.12345789` fails on its eight digits. The error message lists every accepted form.
{% endhint %}

## Numbers

Numbers in responses are JSON strings rather than JSON numbers, because many of these values do not survive a round trip through a 64-bit float. The decimal separator is always a dot, and the number of decimal places is whatever the source blockchain or exchange reported. The API does not round or pad.

Amounts are denominated in an asset's whole unit rather than its smallest indivisible subunit. Bitcoin values are in BTC, not satoshis, and Ether values are in ETH, not wei. This is the unit an exchange or a price feed quotes in, not the integer representation a blockchain stores internally, so a value is directly comparable to a quoted price and needs no rescaling by the asset's decimal precision.

Choosing the whole unit costs no precision, since subunit precision is carried in the decimal places instead. An Ether amount can arrive as `467.472695979897181983`, which is the same information wei would carry as an integer, and also a good illustration of why the value is a string.

## Nulls

A field with no value is normally omitted from the response rather than returned as `null`.

An explicit `null` means the value is genuinely undefined rather than missing: a metric that cannot be computed for that timestamp, for example because it would divide by zero, or a metric requested alongside others that has no value at a timestamp where they do.

`/timeseries/asset-metrics` accepts `null_as_zero=true` to convert those nulls to zero. No other endpoint takes the parameter, and sending it elsewhere returns `unsupported_parameter`, so treat it as a feature of that one endpoint rather than a convention.

## Paging

Responses are paged. `page_size` defaults to 100 and accepts up to 10,000, and a few endpoints set their own bounds.

When more results exist, the response carries `next_page_token` and `next_page_url` at the top level. Fetch `next_page_url` unmodified to get the next page, and treat the absence of the field as the end of the result set.

`paging_from` controls which end of the time interval the first page comes from, and its default is not uniform:

| Default               | Endpoints                                                                                                      |
| --------------------- | -------------------------------------------------------------------------------------------------------------- |
| `end`, newest first   | `/timeseries/*`, `/blockchain/*`, `/blockchain-v2/*`, `/constituent-snapshots/*`, `/constituent-timeframes/*`  |
| `start`, oldest first | `/catalog-v2/*`, `/catalog-all-v2/*`, `/reference-data/*`, `/taxonomy/*`, `/taxonomy-metadata/*`, `/profile/*` |

{% hint style="warning" %}
Because most timeseries endpoints page from `end`, a request whose page size is smaller than the interval returns the *newest* rows in that interval, not the oldest. Pass `paging_from=start` when you intend to read a range forward. `/timeseries/protocol-metrics` is the one timeseries endpoint that already defaults to `start`.
{% endhint %}

### Limiting rows per entity

`limit_per_<entity>` caps how many rows each requested entity contributes, which is how you read the latest value for many entities in a single request rather than one request each. The name matches the entity, and one of `limit_per_asset`, `limit_per_market`, `limit_per_exchange`, `limit_per_exchange_asset`, `limit_per_exchange_pair`, `limit_per_pair`, `limit_per_index`, or `limit_per_institution` is available on 21 of the 28 `/timeseries/*` endpoints.

Which rows you get follows `paging_from`. On an endpoint defaulting to `end`, `limit_per_asset=1` returns each asset's most recent value, and adding `paging_from=start` returns its earliest instead. `page_size` still applies to the response as a whole, so a request whose per-entity limits add up to more than one page still pages normally.

{% hint style="warning" %}
Because `json_stream` ignores `paging_from`, it also changes which rows `limit_per_<entity>` selects. One otherwise identical query, `assets=btc&metrics=PriceUSD&frequency=1d&limit_per_asset=1`, returns Bitcoin's most recent daily price as `json` and its earliest, dated 2010-07-18, as `json_stream`.
{% endhint %}

## Response formats

The `format` parameter selects the response encoding. `json` is the default and is accepted everywhere, and the other two are not universal.

| Value         | Behavior                                                                         | Accepted on                                                                                        |
| ------------- | -------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------- |
| `json`        | A single object with a `data` array plus paging fields.                          | Every endpoint                                                                                     |
| `json_stream` | Newline-delimited objects in one unbounded response, with no paging round trips. | 17 of 28 `/timeseries/*`, plus every `/catalog-v2/*`, `/catalog-all-v2/*`, and `/reference-data/*` |
| `csv`         | Flat rows with a header.                                                         | 25 of 28 `/timeseries/*`. Not accepted on catalog or reference data                                |

{% hint style="warning" %}
`json_stream` ignores `page_size` and `paging_from` rather than rejecting them. It returns the whole requested range in one response, oldest first, even where the same endpoint would have returned the newest rows first as `json`. A query whose row order or row count changes when you switch format is almost always this.
{% endhint %}

## Errors

Errors return a JSON body with a stable machine-readable `type` and a human-readable `message`.

```json
{
  "error": {
    "type": "bad_parameter",
    "message": "Bad parameter 'page_size'. Must be at most 10000."
  }
}
```

<table data-full-width="true"><thead><tr><th>Status</th><th>Type</th><th>Cause</th></tr></thead><tbody><tr><td>400</td><td><code>bad_parameter</code></td><td>A parameter is present but its value is malformed or unsupported. The message names the parameter and, where the set is bounded, the accepted values.</td></tr><tr><td>400</td><td><code>missing_parameter</code></td><td>A required parameter was not supplied.</td></tr><tr><td>400</td><td><code>unsupported_parameter</code></td><td>The parameter is not valid for this endpoint, even though another endpoint accepts it.</td></tr><tr><td>400</td><td><code>bad_parameters</code></td><td>Each parameter is individually valid but the combination is not.</td></tr><tr><td>401</td><td><code>unauthorized</code></td><td>No API key was supplied to an endpoint that requires one.</td></tr><tr><td>401</td><td><code>wrong_credentials</code></td><td>The API key is not valid or has expired.</td></tr><tr><td>403</td><td><code>forbidden</code></td><td>The key is valid but is not entitled to at least one requested resource, or to the requested time range.</td></tr><tr><td>404</td><td><code>not_found</code></td><td>No such endpoint or path.</td></tr><tr><td>413</td><td><code>request_too_large</code></td><td>An ATLAS job request body exceeds the accepted size.</td></tr><tr><td>414</td><td>none</td><td>The request URI exceeded roughly 16 KB, which a long comma-separated <code>markets</code> or <code>assets</code> list will reach. Split the list across requests. Returned by the edge with no JSON body, so handle it separately from the errors above.</td></tr><tr><td>429</td><td><code>too_many_requests</code></td><td>A rate limit was exceeded, or too many WebSocket connections are open for the key.</td></tr><tr><td>500</td><td><code>operation_failed</code></td><td>A server-side failure.</td></tr></tbody></table>

`type` is the field to write code against. It is a fixed identifier covered by the [Backward Compatibility Policy](/api-reference/backward-compatibility.md), whereas `message` is written for a human reading a log and may be reworded in any release, which that policy explicitly permits. Matching on message substrings is the most common way an integration breaks on an otherwise compatible change.

Neither `type` nor the status code alone is a complete signal, so pair them. A single status can cover several types: `401` is `unauthorized` when no key was supplied and `wrong_credentials` when the key is invalid, and only the second is worth alerting on. A single type also covers several causes: `bad_parameter` is returned both for a `page_size` above the maximum and for a `format` an endpoint does not support. Use `type` to decide what to do, and read `message` when you need to know which parameter was at fault, since it names the parameter and, where the set is bounded, lists the accepted values.

## Streaming

`/timeseries-stream/*` endpoints deliver each observation as a JSON message as soon as it is known.

`backfill` sets what arrives on connect. The default `latest` sends the most recent value for each subscribed entity before real-time messages begin, and `none` starts from live data.

Every message carries `cm_sequence_id`, a counter that increases by one per message and **resets to zero on every reconnect**. A gap in the sequence means messages were dropped within that connection. It is not a cursor and cannot be used to resume where a previous connection stopped.

The server sends a WebSocket PING frame to any connection idle for 10 seconds and expects the PONG that standard WebSocket libraries answer with automatically. Connections are also recycled periodically by the infrastructure in front of the API, so treat a close as routine and reconnect rather than as an error.

## Related

* [Rate limits](https://docs.coinmetrics.io/api-reference/rate-limits): request budgets, parallel-request caps, and the `X-RateLimit-*` headers.
* [Backward Compatibility Policy](/api-reference/backward-compatibility.md): which of these conventions may change, and in what kind of release.
* [How To Use the Coin Metrics API Efficiently](https://docs.coinmetrics.io/tutorials-and-examples/user-guides/how-to-use-the-coin-metrics-api-efficiently-http): applying paging and formats to large reads.
* [How to Troubleshoot Common Errors](https://docs.coinmetrics.io/tutorials-and-examples/user-guides/how-to-troubleshoot-common-errors): what to do about a specific status code.
* [Python API Client](https://docs.coinmetrics.io/python-api-client): a client that handles paging and formats for you.


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://gitbook-docs.coinmetrics.io/api-reference/api-conventions.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
