API & webhooks
The roster is available as JSON, and changes can be pushed to an endpoint you control. No account, no API key, no signup. The data is the same data behind the tables on this site, refreshed once a day.
Fetch the roster
curl https://llmroster.dev/api/v1/roster.jsonAnonymous, no key, no parameters. Conditional requests work: send If-None-Match with the ETag from the previous response and a changed roster costs you a 304 instead of the full body.
Subscribe to changes
curl -X POST https://llmroster.dev/api/v1/subscriptions \
-H 'Content-Type: application/json' \
-d '{"url": "https://example.com/hooks/llmroster", "events": ["model.became_paid"]}'Registration is open — there is no API key. What stands in for one is proof that you control the endpoint: we POST a challenge and require your endpoint to return it. A URL that answers everything with 200 is rejected, because that is what a relay target looks like.
What the roster returns
The payload is derived from the same snapshot as this site, so it cannot drift from
the tables. generated_at tells you how fresh it is.
| Field | Type | Meaning |
|---|---|---|
schema_version | integer | Currently 2. Check this before parsing. |
generated_at | string | ISO 8601 UTC. Refreshed once a day by the pipeline. |
counts | object | models and sources. |
sources | object | Per-source health for the run: ok, count, error. A failing source degrades the roster rather than failing the build. |
models | object[] | The roster. See below. |
| Field | Type | Meaning |
|---|---|---|
id | string | Normalized model id: lowercase, no trailing :free / -free. Stable across runs and the join key for everything else. |
display_id | string | The id as one provider reports it, so it is directly callable on at least one endpoint. |
name | string | Display name. |
description | string | Provider-supplied summary. May be empty on Zen, which has no descriptions of its own. |
free_basis | string | price-0, zen-free or zen-micro. Micro-priced Zen models are labelled, never presented as $0. |
sources | string[] | Which sources confirm the model free. |
context_length | integer | null | Maximum context in tokens, or null when no source publishes it. |
modalities | string | Input/output modalities, e.g. text+image+video->text. |
tools | boolean | Whether tool/function calling is advertised. |
endpoints | object[] | Callable endpoints. See below. |
benchmarks | object | Artificial Analysis scores. See below. |
endpoints
A model can be callable on more than one provider, and the id differs between them.
Use endpoints[].model_id with endpoints[].base_url rather than
assuming the normalized id works everywhere.
| Field | Type | Meaning |
|---|---|---|
provider | string | One of the sources above. |
base_url | string | OpenAI-compatible base URL. |
model_id | string | The id to send on that provider. Not the normalized id — providers prefix and suffix differently, so the normalized form is often not callable. |
Known base URLs:
nous—https://inference-api.nousresearch.com/v1opencode-zen—https://opencode.ai/zen/v1openrouter—https://openrouter.ai/api/v1
benchmarks
| Field | Type | Meaning |
|---|---|---|
source | string | Always artificial-analysis. |
matched | boolean | Whether an Artificial Analysis record was matched to this model. false means no score at all, not a zero. |
aa_name | string | Name of the matched Artificial Analysis record, when any. |
paid_proxy | boolean | The matched AA record is the paid variant of the same model, so the score is indicative rather than a free-tier measurement. |
excluded | boolean | The model is in BENCH_EXCLUDE: matched but deliberately kept out of rankings because it is known-broken. The score is still reported. |
intelligence_index | number | null | null when the dataset has no value for this field. |
engineering_index | number | null | null when the dataset has no value for this field. |
agentic_terminal | number | null | null when the dataset has no value for this field. |
gpqa | number | null | null when the dataset has no value for this field. |
output_tokens_per_second | number | null | null when the dataset has no value for this field. |
time_to_first_token_seconds | number | null | null when the dataset has no value for this field. |
Webhooks
A webhook is an HTTPS endpoint we POST to when something changes. You choose the event types; each event is delivered per model rather than as a batch.
| Type | When it fires |
|---|---|
model.became_paid | The model was free and now is not. Fires once per transition; a model that stays paid does not repeat. |
model.added | A model joined the roster, with the endpoints it is callable on. |
model.removed | A model left the roster. cause is no_longer_free. Removals caused by a source outage are suppressed, not reported. |
endpoint.added | An already-listed model became callable on another provider. |
What a delivery looks like
{"at": "2026-09-29T18:00:00Z", "attempt": 1, "data": {"last_seen_free": "2026-09-20T00:00:00Z", "model_id": "z-ai/glm-5.2", "name": "GLM 5.2", "pricing": {"completion": 1.1e-06, "prompt": 4e-07}, "source": "openrouter"}, "id": "3f9c1a7be2d04c85", "type": "model.became_paid"}data carries the fields relevant to the event type. model.became_paid includes pricing, which is the observed price that caused the transition.
Verifying a delivery
Every delivery is signed. Compute the signature over the timestamp and the body exactly as received — the timestamp is inside the signed material, so a captured delivery cannot be replayed with a fresh one.
expected = "sha256=" + hmac.new(
secret.encode(),
timestamp.encode() + b"." + body_bytes,
hashlib.sha256).hexdigest()Use hmac.compare_digest rather than == to compare. Reject anything whose timestamp is older than a few minutes.
X-Roster-Event: model.became_paid
X-Roster-Timestamp: 1790714400
X-Roster-Delivery: 42
X-Roster-Signature: sha256=...X-Roster-Delivery is the id of the delivery attempt and is useful for deduplicating on your side: one event can be delivered more than once if a retry happens.
Limits and rules
- Reads are anonymous and rate limited to 30 requests per minute per address, with a burst. Exceeding it returns
503, not429, so it is not confused with a rate-limit page from a proxy in front of the site. - Registration is limited to 10 requests per minute per address and to 5 active subscriptions per address, because each attempt costs us an outbound request to your endpoint.
- Endpoints must be
httpsand publicly routable. Loopback, private ranges, cloud metadata addresses and IPv4-mapped IPv6 forms of them are refused. - Retries are bounded. A delivery is retried with backoff, and a subscription that keeps failing is disabled rather than retried forever.
Verifying you control an endpoint
Before a subscription is created we POST a challenge containing a random token in the
X-Roster-Challenge header. Your endpoint must return 2xx and
include that token in the response body. This is what stops our service being used to
send unwanted traffic to an address you do not control.
{
"type": "endpoint.verification",
"challenge": "<token sent in X-Roster-Challenge>"
}
A response of 200 that does not echo the token is refused. If you need the
challenge and a real delivery to be distinguishable, the presence of the header tells
them apart.
The secret is returned exactly once, in the 201 response to
registration. It is not sent again and not recoverable afterwards. Store it before you
discard the response.
Questions or corrections: contact page.