Developer docs

Price history API

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.

shell
curl -s "https://oimo.com/v1/prices/history?providers=openai,anthropic" \
  -H "Authorization: Bearer $OIMO_API_KEY"

Query parameters

ParameterDescription
providersComma-separated provider slugs from GET /v1/providers. Default: every active provider whose prices are all in USD. An unknown slug is 400 invalid_query.
asOfRFC 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.
aliasAn alias group's id or name (from GET /v1/comparison/aliases). Only rows of the group's members. See Alias groups.
limitRows per page, 1 to 10,000. Default 5,000.
cursornextCursor 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.

Response

200 OK
{
  "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…"
}
FieldDescription
asOfThe instant the history was read at. Send it back to get the same rows.
providersThe providers served, sorted.
aliasOnly with alias=: the group, see Alias groups.
pricesPrice rows, ordered by contentHash.
nextCursorThe next page's cursor, or null on the last page.

Price rows

FieldDescription
providerProvider slug.
sourceThe provider page or API the price was read from.
serviceCategoryKind of service, for example llm_tokens, or null.
resourceThe provider's name for the resource.
canonicalResourceCanonical id (provider/resource), or null.
dimensionWhat is billed, for example input or output tokens.
unitBilling unit, for example usd_per_token or usd_per_hour.
pricingTierTier or price axes: standard, batch, a region, or key=value;… pairs.
priceDecimal string, exactly as published. Never a float.
currencyUSD, or a credit type for credit-priced providers.
approvalStatusAt asOf: auto (published after automatic checks) or approved (approved by a person).
recordedAtWhen the price was first recorded.
effectiveDateWhen the price takes effect, if the provider stated one; otherwise null.
approvedAtFor approved rows, when the approval was published; otherwise null.
contentHashStable id of the row.
renamedToPresent only on rows of a renamed resource: the name it is listed under at asOf.
promotionPresent 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).

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. Wait retry-after seconds and send the same request again; the cursor stays valid.
Node.js 18 or later (built-in fetch)
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);
shell
pip install requests
Python
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:

{
  "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).
  • 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

StatusCodeWhen
400invalid_queryA 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).
400as_of_too_recentasOf is later than 15 minutes ago (error.field is asOf), or a cursor's pinned asOf is (error.field is cursor).
401unauthorizedNo API key was sent.
401invalid_api_keyThe key is wrong or was deleted.
404alias_not_foundNo published alias group has this id or name.
409alias_changedThe alias group changed or was deleted between pages: start again from the first page (Paging).
429rate_limitedToo many requests; wait retry-after seconds. See Rate limits.
500internal_errorOur failure. Retry with backoff.

GET /v1/providers

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

{
  "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.

200 OK
{
  "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 responseIn price history and webhook eventsMeaning
groups[].idalias.idThe stable id. Store this one: it never changes.
groups[].resourcealias.nameThe display name, which can change when the group is renamed.
groups[].revisionalias.revisionThe 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).

200 OK
{
  "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.