API reference
This page describes the API itself, independent of any one deployment. Three placeholders stand in for the three logical endpoints below — see an environment page (test, production) for the concrete base URLs to substitute:
| Placeholder | Role |
|---|---|
<auth-endpoint> | Authorization Server — token issuance |
<endpoint> | Issuer API — allocate, status, accounting |
<verifier-endpoint> | Verifier — published status lists |
Download OpenAPI 3.0 specification
Hand-authored, not generated from code annotations — but cross-checked against the real registered routes in CI (docs/openapi_coverage_test.go), so it can't silently drift from what the service actually implements. Source of truth: siros-status-service/docs/openapi.yaml — the copy here is synced from there, not maintained independently.
Authentication
Issuer requests carry a bearer access token, obtained via the OAuth 2.0 client-credentials grant with a JWT-bearer client assertion (RFC 7523) — there is no separate registered client secret and no pre-registration step. The assertion itself, self-signed, is the whole proof of possession:
| Method | Path |
|---|---|
POST | <auth-endpoint>/token |
Request — form-encoded (application/x-www-form-urlencoded):
| Field | Value |
|---|---|
grant_type | client_credentials |
client_assertion_type | urn:ietf:params:oauth:client-assertion-type:jwt-bearer |
client_assertion | the JWT described below |
The client_assertion JWT — present your
existing signing certificate chain as x5c if you have one (a real
issuer's signing key almost always already does, and it's the form this
service would rather you use); a bare jwk header works too, for a
key with no chain to present:
| Field | Where | Value |
|---|---|---|
alg | header | ES256 (only algorithm accepted) |
x5c | header | certificate chain, leaf first (RFC 7515 §4.1.6) — omit if using jwk |
jwk | header | bare public key (RFC 7515 §4.1.3) — omit if using x5c; exactly one of the two must be present |
iss | claim | your chosen issuer identifier |
sub | claim | same value as iss |
aud | claim | <auth-endpoint>/token — exact match required |
iat, exp | claim | now, and no more than 5 minutes later |
Sign with the EC (P-256) private key matching your chain's leaf certificate
(or matching the bare jwk) — that key is your identity;
there's nothing else to register. Generate a fresh assertion for every call: it
is a proof of possession for one /token request, not a credential
to save and reuse.
Response (200):
| Field | Value |
|---|---|
access_token | bearer token for the issuer API below |
token_type | Bearer |
expires_in | seconds until access_token expires |
A rejected assertion — expired, wrong aud, malformed, or an
untrusted key/chain — returns 401 with
{"error":"invalid_client", ...}, the same shape regardless of
which of those it was.
Issuer API
Every request below requires Authorization: Bearer <access_token>.
A missing, malformed, invalid, or expired token returns 401 with an
RFC 6750 §3 WWW-Authenticate challenge header — e.g.
Bearer error="invalid_token", error_description="the access token
expired" — rather than a JSON body.
Allocate an index
| Method | Path |
|---|---|
POST | <endpoint>/allocate |
Request body (JSON, optional):
| Field | Value |
|---|---|
exp | optional; the credential's own expiration (RFC 3339). Omit it to get this deployment's configured maximum. A value further out than that maximum is rejected with 400, not silently clamped. |
Response (201):
| Field | Value |
|---|---|
list_url | the status list this index lives in — fetch it from the verifier endpoint below |
index | this credential's index within that list |
exp | the expiration actually recorded (echoes the request, or the deployment's maximum if omitted) |
Set status
| Method | Path |
|---|---|
PATCH | <endpoint>/status/<list-id>/<index> |
Request body (JSON):
| Field | Value |
|---|---|
status | one of VALID, INVALID (revoked), SUSPENDED |
Response: 204 on success (no body).
403 if the calling issuer did not allocate this index — ownership
is enforced server-side, not just by knowing a valid-looking index.
404 if the list doesn't exist. 410 if the list has
been archived (every credential in it has already expired — there's never a
legitimate revocation left to make at that point).
Accounting
| Method | Path |
|---|---|
GET | <endpoint>/accounting/me |
No request body. Response (200):
| Field | Value |
|---|---|
issuer_id | the caller's own issuer identity, from their access token |
usage | array of {"shard_id": "...", "index_count": N} — one entry per shard this issuer has ever allocated in |
Authorization Server JWKS
| Method | Path |
|---|---|
GET | <auth-endpoint>/.well-known/jwks.json |
The Authorization Server's own public signing key, RFC 7517 JSON Web Key Set format. What this deployment's own ingress and ingestion services fetch and cache to verify access tokens offline — not something a typical issuer/verifier integration calls directly.
Verifier API
| Method | Path |
|---|---|
GET | <verifier-endpoint>/lists/<list-id> |
No authentication. Returns a signed Status List Token (RFC-shaped JWT,
Content-Type: application/statuslist+jwt, per
IETF
draft-ietf-oauth-status-list-21) — decompress and bit-unpack its
status_list claim to read any index in the list, not just one
you ask for by name. Standard HTTP caching applies (ETag,
Cache-Control) — poll as often as you like; a
If-None-Match match returns 304 with no body.