Skip to main content

W3C VC Status List v1.0

The rule

  • Endpoint: GET as.oid4pay.com/oauth/status-list/{list_id} returns a VC Status List Credential per W3C VC Status List v1.0.
  • Single list (list_id=1) for v1; multiple lists once mandate count exceeds 100k.
  • Bitstring is gzip-compressed; one bit per mandate; bit position is mandate.credentialStatus.statusListIndex.
  • Response headers: Cache-Control: max-age=300, must-revalidate, public; ETag for cheap re-fetch.
  • The AS regenerates the published list every 60s.
  • Merchants and agents fetch the whole list once per 5 minutes and look up the index locally; no per-mandate round-trip.

Endpoint shape

GET /oauth/status-list/1 HTTP/1.1
Host: as.oid4pay.com

200 OK
Content-Type: application/vc+ld+json
Cache-Control: max-age=300, must-revalidate, public
ETag: "v123"

{
  "@context": [
    "https://www.w3.org/2018/credentials/v1",
    "https://w3id.org/vc/status-list/2021/v1"
  ],
  "id": "https://as.oid4pay.com/oauth/status-list/1",
  "type": ["VerifiableCredential", "StatusList2021Credential"],
  "issuer": "https://as.oid4pay.com",
  "issuanceDate": "2026-05-14T10:00:00Z",
  "credentialSubject": {
    "id": "https://as.oid4pay.com/oauth/status-list/1#list",
    "type": "StatusList2021",
    "statusPurpose": "revocation",
    "encodedList": "<base64url(gzip(bitstring))>"
  },
  "proof": { /* Ed25519 JWS proof */ }
}

Mandate revocation linkage

Every mandate VC carries a credentialStatus claim:

"credentialStatus": {
  "id": "https://as.oid4pay.com/oauth/status-list/1#42",
  "type": "StatusList2021Entry",
  "statusPurpose": "revocation",
  "statusListIndex": "42",
  "statusListCredential": "https://as.oid4pay.com/oauth/status-list/1"
}

A merchant verifier decodes the gzipped bitstring once per fetch and checks bit 42. Bit set => revoked => reject the mandate.

Worked example

// Node: fetch once per 5 minutes and cache.
import { fetchStatusList } from "@oid4pay/oid4ac-merchant";

const list = await fetchStatusList("https://as.oid4pay.com", "1");
// list.bitmap is a Uint8Array

function isRevoked(statusListIndex) {
  const byteIndex = Math.floor(statusListIndex / 8);
  const bitIndex  = statusListIndex % 8;
  return (list.bitmap[byteIndex] >> bitIndex) & 1;
}

if (isRevoked(mandate.credentialStatus.statusListIndex)) {
  throw new Error("mandate_status_revoked");
}

Performance

100k mandates fit in 12.5 KB uncompressed and roughly 1 KB after gzip. A single fetch per merchant per 5 minutes is negligible; the If-None-Match revalidation makes the steady-state cost a 304 response.