Developer API

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.

RosterGETno key required
Schemav2check before parsing
Events4delivered per model

Fetch the roster

curl https://llmroster.dev/api/v1/roster.json

Anonymous, 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.

Top-level fields
FieldTypeMeaning
schema_versionintegerCurrently 2. Check this before parsing.
generated_atstringISO 8601 UTC. Refreshed once a day by the pipeline.
countsobjectmodels and sources.
sourcesobjectPer-source health for the run: ok, count, error. A failing source degrades the roster rather than failing the build.
modelsobject[]The roster. See below.
Each entry in models[]
FieldTypeMeaning
idstringNormalized model id: lowercase, no trailing :free / -free. Stable across runs and the join key for everything else.
display_idstringThe id as one provider reports it, so it is directly callable on at least one endpoint.
namestringDisplay name.
descriptionstringProvider-supplied summary. May be empty on Zen, which has no descriptions of its own.
free_basisstringprice-0, zen-free or zen-micro. Micro-priced Zen models are labelled, never presented as $0.
sourcesstring[]Which sources confirm the model free.
context_lengthinteger | nullMaximum context in tokens, or null when no source publishes it.
modalitiesstringInput/output modalities, e.g. text+image+video->text.
toolsbooleanWhether tool/function calling is advertised.
endpointsobject[]Callable endpoints. See below.
benchmarksobjectArtificial 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.

Each entry in endpoints[]
FieldTypeMeaning
providerstringOne of the sources above.
base_urlstringOpenAI-compatible base URL.
model_idstringThe 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:

benchmarks

benchmarks object, per model
FieldTypeMeaning
sourcestringAlways artificial-analysis.
matchedbooleanWhether an Artificial Analysis record was matched to this model. false means no score at all, not a zero.
aa_namestringName of the matched Artificial Analysis record, when any.
paid_proxybooleanThe matched AA record is the paid variant of the same model, so the score is indicative rather than a free-tier measurement.
excludedbooleanThe model is in BENCH_EXCLUDE: matched but deliberately kept out of rankings because it is known-broken. The score is still reported.
intelligence_indexnumber | nullnull when the dataset has no value for this field.
engineering_indexnumber | nullnull when the dataset has no value for this field.
agentic_terminalnumber | nullnull when the dataset has no value for this field.
gpqanumber | nullnull when the dataset has no value for this field.
output_tokens_per_secondnumber | nullnull when the dataset has no value for this field.
time_to_first_token_secondsnumber | nullnull 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.

Event types accepted in the events[] filter
TypeWhen it fires
model.became_paidThe model was free and now is not. Fires once per transition; a model that stays paid does not repeat.
model.addedA model joined the roster, with the endpoints it is callable on.
model.removedA model left the roster. cause is no_longer_free. Removals caused by a source outage are suppressed, not reported.
endpoint.addedAn 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

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.