> ## Documentation Index
> Fetch the complete documentation index at: https://docs.oimo.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Price history API

> GET /v1/prices/history: every reviewed AI and cloud price, pinned to an instant, by provider or alias group, with cursor paging.

`GET /v1/prices/history` returns the reviewed price history: every published price, old rates included, as it stood at an instant you choose. Call it instead of keeping a copy of the prices; pin the instant to make a computation reproducible.

```bash theme={null}
curl -s "https://oimo.com/v1/prices/history?providers=openai,anthropic" \
  -H "Authorization: Bearer $OIMO_API_KEY"
```

## Query parameters

| Parameter | Description |
| - | - |
| `providers` | Comma-separated provider slugs from `GET /v1/providers`. Default: every active provider whose prices are all in USD. An unknown slug is `400 invalid_query`. |
| `asOf` | RFC 3339 timestamp, for example `2026-10-01T00:00:00Z`. The history as it was published at that instant. Default, and latest allowed: 15 minutes ago. Any fraction of a second and any UTC offset is accepted; the instant is truncated to milliseconds and answered in UTC with six fraction digits (`2026-10-01T00:00:00.123000Z`), so an `asOf` from a response can be sent back unchanged. |
| `alias` | An alias group's `id` or name (from `GET /v1/comparison/aliases`). Only rows of the group's members. See [Alias groups](#alias). |
| `limit` | Rows per page, 1 to 10,000. Default 5,000. |
| `cursor` | `nextCursor` of the previous page. It pins `asOf`, `providers` and `alias` but not `limit`: send `limit` again with it, or the page has the default size. Sending `asOf`, `providers` or `alias` with a cursor is `400 invalid_query`. See [Paging](#paging). |

## Response

```json 200 OK theme={null}
{
  "asOf": "2026-09-14T11:45:00.000000Z",
  "providers": [
    "anthropic",
    "openai"
  ],
  "prices": [
    {
      "provider": "openai",
      "source": "openai-docs",
      "serviceCategory": "llm_tokens",
      "resource": "gpt-5",
      "canonicalResource": "openai/gpt-5",
      "dimension": "input",
      "unit": "usd_per_token",
      "pricingTier": "standard",
      "price": "0.00000125",
      "currency": "USD",
      "approvalStatus": "auto",
      "recordedAt": "2026-08-19T23:03:24.000000Z",
      "effectiveDate": null,
      "approvedAt": null,
      "contentHash": "0a3f…"
    }
  ],
  "nextCursor": "eyJ2IjoxLC…"
}
```

| Field | Description |
| - | - |
| `asOf` | The instant the history was read at. Send it back to get the same rows. |
| `providers` | The providers served, sorted. |
| `alias` | Only with `alias=`: the group, see [Alias groups](#alias). |
| `prices` | Price rows, ordered by contentHash. |
| `nextCursor` | The next page's cursor, or `null` on the last page. |

### Price rows

| Field | Description |
| - | - |
| `provider` | Provider slug. |
| `source` | The provider page or API the price was read from. |
| `serviceCategory` | Kind of service, for example `llm_tokens`, or `null`. |
| `resource` | The provider's name for the resource. |
| `canonicalResource` | Canonical id (`provider/resource`), or `null`. |
| `dimension` | What is billed, for example `input` or `output` tokens. |
| `unit` | Billing unit, for example `usd_per_token` or `usd_per_hour`. |
| `pricingTier` | Tier or price axes: `standard`, `batch`, a region, or `key=value;…` pairs. |
| `price` | Decimal string, exactly as published. Never a float. |
| `currency` | `USD`, or a credit type for credit-priced providers. |
| `approvalStatus` | At `asOf`: `auto` (published after automatic checks) or `approved` (approved by a person). |
| `recordedAt` | When the price was first recorded. |
| `effectiveDate` | When the price takes effect, if the provider stated one; otherwise `null`. |
| `approvedAt` | For `approved` rows, when the approval was published; otherwise `null`. |
| `contentHash` | Stable id of the row. |
| `renamedTo` | Present only on rows of a renamed resource: the name it is listed under at asOf. |
| `promotion` | Present only on a promotional row (tier `promotional` or `promotional-…`) whose record says when the promotion ends: `lastDay` (the last promotional day, `YYYY-MM-DD`, US/Pacific), `standardFrom` (when the list price applies again), and `standardTier` and `standardPrice` (the list tier and price it stands beside). A field the record lacks is `null`. Informative only: rows are never filtered on it. |

A price line is one provider, source, resource, dimension, unit, currency and tier. Its current price at `asOf` is its row with the newest `recordedAt`; the older rows are its history.

New fields may be added to the response. Ignore fields you do not recognize.

## Pinning with asOf

A row is in the answer at `asOf` when it was published at that instant. A price published on day 1 and withdrawn on day 2 is in the answer at a day-1 `asOf` and not at a day-2 one. Nothing that happens later changes the answer at an earlier `asOf`, so **the same `asOf` always returns the same rows**, and `approvalStatus` is the status at `asOf`, not today's.

`asOf` cannot be later than 15 minutes ago: prices land shortly after the instant they carry, and a read closer to now could miss rows that a later read at the same instant would see. A later value is `400 as_of_too_recent`.

Store the response's `asOf` beside anything you compute from it; asking again with it reproduces the input exactly. With `alias=` that holds while `alias.revision` is unchanged ([Alias groups](#alias)).

## Paging

Rows are ordered by `contentHash`. While `nextCursor` is not `null`, request the next page with `cursor` and `limit` only. Ask only for the providers you need: one large provider can be tens of thousands of rows.

* **What a cursor pins:** `asOf`, `providers`, the `alias` group at the revision the first page read, and the position. Every page of one walk reads the same `asOf`. Sending `asOf`, `providers` or `alias` beside a cursor is `400 invalid_query` (`error.field` is `cursor`).
* **What it does not pin:** `limit`. Each request uses the `limit` sent with it, or 5,000 when none is sent. Send the same `limit` on every page.
* **Lifetime:** a cursor does not expire. Its `asOf` is in the past, so the rows behind it never change and a walk can continue later. A cursor with `alias` stops working once the group changes.
* **After `409 alias_changed`:** the group changed between pages (its members or its name, or it was deleted), so the pages already read may belong to the old group. Discard them and start again from the first page, without a cursor. To read the same instant, send the first page's `asOf` back with the same `alias` and `providers`; without `asOf`, the new walk reads a newer instant.
* **After `429 rate_limited`:** the endpoint has its own [rate limit](/limits#rate-limits). Wait `retry-after` seconds and send the same request again; the cursor stays valid.

```javascript Node.js 18 or later (built-in fetch) theme={null}
const base = "https://oimo.com/v1/prices/history";
const headers = { authorization: `Bearer ${process.env.OIMO_API_KEY}` };
const limit = 10000; // the cursor does not pin it: send it on every page

async function getPage(url) {
  for (;;) {
    const response = await fetch(url, { headers });
    if (response.status === 429) {
      const seconds = Number(response.headers.get("retry-after")) || 1;
      await new Promise((resolve) => setTimeout(resolve, (seconds + Math.random()) * 1000));
      continue;
    }
    if (!response.ok) throw new Error(`${response.status} ${await response.text()}`);
    return response.json();
  }
}

let url = `${base}?providers=openai,anthropic&limit=${limit}`;
const rows = [];
let asOf;
for (;;) {
  const page = await getPage(url);
  asOf = page.asOf;
  rows.push(...page.prices);
  if (page.nextCursor === null) break;
  url = `${base}?cursor=${encodeURIComponent(page.nextCursor)}&limit=${limit}`;
}
console.log(rows.length, "rows at", asOf);
```

```bash theme={null}
pip install requests
```

```python Python theme={null}
import os
import random
import time

import requests

BASE = "https://oimo.com/v1/prices/history"
LIMIT = 10000  # the cursor does not pin it: send it on every page
headers = {"Authorization": f"Bearer {os.environ['OIMO_API_KEY']}"}

def get_page(params):
    while True:
        response = requests.get(BASE, headers=headers, params=params, timeout=60)
        if response.status_code == 429:
            seconds = float(response.headers.get("retry-after", "1"))
            time.sleep(seconds + random.random())
            continue
        response.raise_for_status()
        return response.json()

params = {"providers": "openai,anthropic", "limit": LIMIT}
rows = []
while True:
    page = get_page(params)
    rows.extend(page["prices"])
    if page["nextCursor"] is None:
        break
    params = {"cursor": page["nextCursor"], "limit": LIMIT}
print(len(rows), "rows at", page["asOf"])
```

## Alias groups

`alias=` narrows the answer to one alias group: the same resource at every provider that sells it, each under its own name. The rows are the ordinary rows of the group's members; nothing is merged or converted. The answer adds the group:

```json theme={null}
{
  "alias": {
    "id": "8c4e7b1a-2f3d-4e5a-9b6c-0d1e2f3a4b5c",
    "name": "claude-opus-5.5",
    "revision": 212,
    "members": [
      {
        "provider": "anthropic",
        "resource": "claude-opus-5-5"
      },
      {
        "provider": "aws",
        "resource": "claude-opus-5.5"
      },
      {
        "provider": "azure",
        "resource": "claude-opus-5-5"
      }
    ]
  }
}
```

* `alias.id` is the group's stable id; `alias.name` is its display name, which can change. `alias=` accepts either.
* `members` is the whole group. Rows are served for the members whose provider is in `providers`; without `providers`, those are the active USD-priced providers that sell it.
* An unknown group is `404 alias_not_found`.
* **The rows are pinned to `asOf`; the group is not.** Membership is always the group as published when you read it: there is no parameter to ask for an earlier revision, so the same `asOf` can return other rows after the group changes. Store `alias.revision` beside `asOf` to tell whether the group changed since you last read it. A different revision means the group was saved again: its members may have changed (the `alias.member_added` and `alias.member_removed` webhook events say how), or only its name, or nothing visible. Compare `members` to know.
* If the group changes while you page through it (any new revision, or it is deleted), the next page is `409 alias_changed`: start again from the first page ([Paging](#paging)).
* A member is an exact resource name: a renamed resource's earlier rows are included only if the group lists the old name too.

## Prices that are not money

`price` is in `currency`, which is not always USD. `snowflake` prices in two steps: what a service consumes in credits (`unit: credit_per_token`, `currency: SNOWFLAKE_AI_CREDIT` or `SNOWFLAKE_PLATFORM_CREDIT`) and what a credit costs (`unit: usd_per_credit`, `currency: USD`, per cloud, region and edition in `pricingTier`). The cost in USD is the consumption rate times the `usd_per_credit` price of the same credit type.

These rows are served only when you name the provider (`providers=snowflake`), so code that sums `price` as money never receives them by default.

## Renamed resources

When a provider renames a resource, rows under the old name carry `renamedTo`: the name the resource is listed under at `asOf`. Join on `source` and `renamedTo` to read one history across the rename. Rows of a resource that was never renamed have no `renamedTo`.

## Errors

| Status | Code | When |
| - | - | - |
| 400 | `invalid_query` | A parameter is malformed: `error.field` names it (`asOf`, `providers`, `limit`, `alias`, `cursor`). An unknown provider slug is one (`providers`), and so is `asOf`, `providers` or `alias` sent beside a cursor (`cursor`). |
| 400 | `as_of_too_recent` | `asOf` is later than 15 minutes ago (`error.field` is `asOf`), or a cursor's pinned `asOf` is (`error.field` is `cursor`). |
| 401 | `unauthorized` | No API key was sent. |
| 401 | `invalid_api_key` | The key is wrong or was deleted. |
| 404 | `alias_not_found` | No published alias group has this id or name. |
| 409 | `alias_changed` | The alias group changed or was deleted between pages: start again from the first page ([Paging](#paging)). |
| 429 | `rate_limited` | Too many requests; wait `retry-after` seconds. See [Rate limits](/limits#rate-limits). |
| 500 | `internal_error` | Our failure. Retry with backoff. |

## GET /v1/providers

The provider directory: the slugs to use in `providers` and in webhooks.

```json theme={null}
{
  "providers": [
    {
      "slug": "openai",
      "displayName": "OpenAI",
      "aliases": [],
      "status": "active",
      "activatedAt": "2026-09-30T11:50:00.000Z",
      "sources": [
        {
          "key": "openai-docs",
          "kind": "prices",
          "freshness": "fresh"
        },
        {
          "key": "tos-openai-terms",
          "kind": "terms",
          "freshness": "fresh"
        }
      ]
    }
  ]
}
```

`status` is `active`, or `pending` while a provider is being added (a webhook can already subscribe to it). `activatedAt` is `null` for providers older than the directory. `freshness` is `fresh`, `stale` or `unknown`.

## GET /v1/comparison/aliases

The published alias groups.

```json 200 OK theme={null}
{
  "version": 212,
  "groups": [
    {
      "id": "8c4e7b1a-2f3d-4e5a-9b6c-0d1e2f3a4b5c",
      "resource": "claude-opus-5.5",
      "revision": 212,
      "updatedAt": "2026-10-13T19:07:41.000Z",
      "members": [
        {
          "provider": "anthropic",
          "resource": "claude-opus-5-5"
        },
        {
          "provider": "aws",
          "resource": "claude-opus-5.5"
        },
        {
          "provider": "azure",
          "resource": "claude-opus-5-5"
        }
      ]
    }
  ],
  "canReview": false,
  "unpriced": [],
  "resources": [
    {
      "provider": "azure",
      "displayName": "Azure",
      "resource": "claude-opus-5-5",
      "comparisonResource": "claude-opus-5.5"
    },
    {
      "provider": "openai",
      "displayName": "OpenAI",
      "resource": "gpt-5",
      "comparisonResource": "openai/gpt-5"
    }
  ]
}
```

Each entry of `groups` has an `id`, a `resource`, a `revision`, `updatedAt` and its `members` (`provider`, `resource`). The same group has other field names elsewhere:

| In this response | In price history and webhook events | Meaning |
| - | - | - |
| `groups[].id` | `alias.id` | The stable id. Store this one: it never changes. |
| `groups[].resource` | `alias.name` | The display name, which can change when the group is renamed. |
| `groups[].revision` | `alias.revision` | The revision: it goes up every time the group is saved, whether its members, its name or nothing changed. |

`alias=` and a webhook's alias groups take the `id` or the name; a webhook stores the `id`. A group whose members are all no longer listed (their provider or sources were retired) is listed with no members and `"retired": true`. `version` is the version of the whole catalog: it goes up with every change to any group, deletions included, so it can be higher than every listed group's revision. `unpriced` lists the members whose resource has not been seen priced for a week. `resources` lists every resource the catalog knows, with `comparisonResource`: the name of its group or, when it is in none, its canonical id (else its own name). `canReview` is always `false` for an API key.

## GET /v1/catalog

`GET /v1/catalog?provider=openai` lists what you can filter a webhook on for one provider: `resources` (each with `resource`, `canonicalResource` and `dimensions`) and `docs` (each with the document slug `doc` and its `url`).

```json 200 OK theme={null}
{
  "provider": "openai",
  "updatedAt": "2026-10-14T09:15:00.000Z",
  "resources": [
    {
      "resource": "gpt-5",
      "canonicalResource": "openai/gpt-5",
      "dimensions": [
        "cache_read",
        "input",
        "output"
      ]
    }
  ],
  "docs": [
    {
      "doc": "terms-of-use",
      "url": "https://openai.com/policies/terms-of-use"
    }
  ]
}
```

A provider that has no catalog yet answers `"updatedAt": null` with empty lists. An unknown or missing `provider` is `404 provider_not_found`; note that the price history answers an unknown slug in `providers` with `400 invalid_query` instead.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.