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.
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. |
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. |
Response
{
"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. |
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).
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, thealiasgroup at the revision the first page read, and the position. Every page of one walk reads the sameasOf. SendingasOf,providersoraliasbeside a cursor is400 invalid_query(error.fieldiscursor). - What it does not pin:
limit. Each request uses thelimitsent with it, or 5,000 when none is sent. Send the samelimiton every page. - Lifetime: a cursor does not expire. Its
asOfis in the past, so the rows behind it never change and a walk can continue later. A cursor withaliasstops 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'sasOfback with the samealiasandproviders; withoutasOf, the new walk reads a newer instant. - After
429 rate_limited: the endpoint has its own rate limit. Waitretry-afterseconds and send the same request again; the cursor stays valid.
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);pip install requestsimport 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.idis the group's stable id;alias.nameis its display name, which can change.alias=accepts either.membersis the whole group. Rows are served for the members whose provider is inproviders; withoutproviders, 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 sameasOfcan return other rows after the group changes. Storealias.revisionbesideasOfto tell whether the group changed since you last read it. A different revision means the group was saved again: its members may have changed (thealias.member_addedandalias.member_removedwebhook events say how), or only its name, or nothing visible. Comparemembersto 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
| 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). |
| 429 | rate_limited | Too many requests; wait retry-after seconds. See 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.
{
"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.
{
"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).
{
"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.