This page covers the two public parts of the Credentials worker: the status-list registry, which serves issuer-signed revocation lists, and the catalog, which holds what each issuer says it issues and what each verifier says it accepts, with their names, logos and card looks. Issuers use both; verifiers read status lists on every check and publish a catalog entry; wallets read the catalog to show who sends an offer or a request.
CREDENTIALS_WORKER_URL is the Credentials worker of your network: https://dev.credentials.ixo.earth, https://test.credentials.ixo.earth or https://credentials.ixo.earth. Each network has its own registry and its own catalog. A live example of both is the devnet test issuer: its list https://dev.credentials.ixo.earth/status/did:web:test-issuer.dev.credentials.ixo.earth/test1 and its entry https://dev.credentials.ixo.earth/v1/catalog/issuers/did:web:test-issuer.dev.credentials.ixo.earth.
Status lists
Each credential carries status.status_list = { uri, idx }. The uri names a list at the registry; idx is the credential's position. The list is a compressed array of bits signed by the issuer as a statuslist+jwt token (Token Status List, IETF draft 21, approved for publication as an RFC). Bit 0 means valid; any other value is treated as revoked.
CREDENTIALS_WORKER_URL/status/ISSUER_DID/LIST_IDISSUER_DID goes in the path exactly as written, not percent-encoded. LIST_ID is 1 to 64 letters, digits, _ or -.
For issuers
- Open a list with
newStatusBits. Sign and publish it before you release the first credential that points to it. - Allocate a random free index per credential with
randomFreeIndex, and record it. - Sign with
createStatusListToken({ bits, uri, iss, signer, ttl, exp, iat }), using yourassertionMethodkey. The token'ssubis exactly theuriin your credentials. - Publish with
PUT /status/ISSUER_DID/LIST_ID,Content-Type: application/statuslist+jwt, body the raw token. The UCAN invocation is forcredentials/issueonixo:credentials, signed with a key on your own DID document, bound by the fact{ method: 'PUT', path: '/status/ISSUER_DID/LIST_ID' }. - Revoke with
setStatusBit, save, re-sign with a neweriatand publish. If publishing fails, retry from a job; your database holds the truth. - Refresh every list from a scheduled job so no token expires. An expired token makes every credential on the list fail. Self (not live yet) signs tokens valid for 24 hours with
ttl300 seconds and republishes every list within 12 hours of expiry; the devnet test issuer signs tokens valid for 7 days withttl60 seconds, publishes at once on each revocation and refreshes every list at least daily.
The registry accepts a token only if all of these hold. It cannot change a bit itself.
| Check | Rule |
|---|---|
| Caller | The principal is ISSUER_DID, acting as itself (403 otherwise). A controller acting through ixo:credentials/ENTITY_DID cannot publish. |
| Body | At most 64 KB, application/statuslist+jwt (415 otherwise) |
| Token | typ statuslist+jwt; iss = ISSUER_DID; sub = this exact URL; signed (EdDSA or Ed25519) by the assertionMethod key its kid names |
| Dates | Whole unix seconds; iat at most 60 seconds in the future; exp after iat, in the future and at most 400 days ahead |
| Order | iat strictly newer than the stored token's; equal or older is 409 stale |
| Count | At most 20 lists per issuer; a new list beyond that is 409 too_many_lists |
| Storage | Near the worker's storage limit a new list is 507 storage_full; a newer version of an existing list is still accepted |
A list whose exp passed more than 30 days ago is deleted.
For verifiers
GET the uri (public, rate limited per address, Access-Control-Allow-Origin: *, cacheable for at most the smaller of ttl and 60 seconds). Do not follow redirects. Verify the token with verifyStatusListToken(token, { uri, issuerDoc }) and read the bit with statusAt(bits, idx). Cache a verified token no longer than its ttl. An index outside the list is a hard failure (no_status). If the list cannot be fetched or does not verify, do not accept the presentation (VerificationUnavailable). The kit's verifyPresentation does all of this by default.
The uri is inside every credential for its whole life. Choose the host and list ids before you issue your first real credential; moving the registry later means issuing again.
The catalog
The catalog is a public directory. Each entry is a claim made by its own DID about itself. It is not a trust list: nobody curates it, the worker does not vouch for names or URLs, and listing does not make an issuer trustworthy or bind a verifier. Which issuers to accept is each verifier's own decision.
The catalog is also the only place wallets take a sender's name, logo and card look from. Offers and requests carry none (a request carries only its purpose line). So the look of a sender cannot change from one offer or request to the next. Because the entries are self-declared, a sender can copy another sender's name and look; see What protects a person.
Issuer entry
{
"did": "ISSUER_DID",
"name": "Self",
"description": "Identity verification with a document and a liveness check",
"url": "https://ISSUER_HOST",
"logo_uri": "https://ISSUER_HOST/mark.png",
"color": "#0b6b7e",
"credential_issuer": "ISSUER_URL/identity",
"types": [
{
"vct": "VCT",
"name": "Identity Level 1",
"description": "Your identity was checked against an ID document",
"claims": [{ "name": "age_over_18", "label": "Over 18", "sd": "always" }],
"display": {
"background_color": "#0b6b7e",
"text_color": "#ffffff",
"logo_uri": "https://ISSUER_HOST/card-mark.png",
"background_image_uri": "https://ISSUER_HOST/card.jpg"
}
}
]
}logo_uri and color are the issuer's mark. types[].display is the card look of that type's credentials. Every look member is optional. A type may also carry type_metadata_uri. Each claim's sd is always, allowed or never. Credential types and schemas are not hosted on the Credentials worker: new types follow the IXO pattern of a type document in the GitHub repository ixofoundation/ns plus a protocol entity, as KYCAMLLevel1 does. Self's IdentityLevel1 keeps its current vct for now.
Verifier entry
{
"did": "VERIFIER_DID",
"name": "Yo Rewards",
"description": "Rewards for young people",
"url": "https://VERIFIER_HOST",
"logo_uri": "https://VERIFIER_HOST/mark.png",
"color": "#7a3e9d",
"accepts": [
{
"purpose": "Claim a reward",
"vct": "VCT",
"claims": ["age_over_18"],
"issuers": ["ISSUER_DID"]
}
]
}logo_uri and color are the verifier's mark. A verifier entry has no card look. Keep issuers the same as the issuer filter in your requests. A verifier without an entry is shown with a neutral mark.
The purpose in accepts describes your entry. What the person reads when you ask is the purpose of each request (see Request proof).
Name, logo and card look
| Rule | Value |
|---|---|
| Colors | color, background_color, text_color: #rrggbb only. A wallet ignores text_color when its contrast with the background is below 4.5:1. |
| Images | logo_uri, background_image_uri: https on the default port, no query string or fragment, a public host, at most 512 characters; png, jpeg or webp |
| Fetching | The worker stores the addresses and never fetches the images. The Auth Hub fetches them through its own media proxy, never from the person's browser. |
| Web addresses | The Auth Hub never shows url, credential_issuer or any other address from an entry to the person, not as a link and not as text to copy. |
| Invalid members | Dropped, never a 400, and listed by path in the PUT answer's display_dropped (for example color or types.0.display.text_color) |
| Without a look | Wallets show a neutral mark and card |
Rules
| Rule | Value |
|---|---|
| Size | At most 32 KB per entry |
| URLs | url, credential_issuer and type_metadata_uri must be https on a public host |
| Text | Names, descriptions, labels and purposes are cleaned (control, bidi and zero-width characters removed, whitespace collapsed) and length-capped |
| Write | PUT and DELETE /v1/catalog/issuers/DID with credentials/issue; /v1/catalog/verifiers/DID with credentials/verify. The body's did must equal the path. PUT replaces the whole entry: 201 when new, 200 when replaced, answering { sender_key, entry, updated_at, display_dropped }; DELETE answers 204. Near the worker's storage limit a new or larger entry is 507 storage_full. |
| Read | Public: GET /v1/catalog/issuers?vct=VCT&limit=&cursor= answers { items, next_cursor } with each item { sender_key, entry, updated_at }; GET /v1/catalog/issuers/DID answers { sender_key, entry, updated_at } for exactly that DID; the same for verifiers. sender_key is the DID in its canonical form. Without a token, reads are limited per address and cacheable (max-age=60); with an optional credentials/hold token they are limited per principal instead. |
Get listed in the catalog
Listing is self-service. There is no application, review or approval: any issuer or verifier with a did:ixo or did:web DID publishes its own entry, and wallets show it from then on.
- Choose the DID you send as. The entry belongs to exactly that DID, written exactly as in your offers or requests. Every DID is its own sender: an entry for
did:web:example.comsays nothing aboutdid:web:a.example.com. - Write the entry as JSON (issuer entry or verifier entry). Add your mark (
logo_uri,color) and, for an issuer, each type's card look (types[].display). Host images as png, jpeg or webp onhttps; the Auth Hub's media proxy refuses SVG and files over 256 KB. - Publish it with
PUT /v1/catalog/issuers/YOUR_DID(abilitycredentials/issue) orPUT /v1/catalog/verifiers/YOUR_DID(abilitycredentials/verify), as your own DID, with a request-bound UCAN invocation. Do it on each network you work on: devnet, testnet and mainnet have separate catalogs. - Check the answer.
display_droppednames every member that was not stored, for example a color that is not#rrggbb. Then read the entry back with the publicGET. - Keep it current from a daily job, and update it whenever your types, claims or accepted issuers change. A verifier keeps its
acceptsthe same as its requests.
The Auth Hub reads the catalog through a short cache (about 5 minutes) and changes a stored card look at most once a day, so a new name shows within minutes and a new look within a day.
Who may write an entry
The caller must act as the entry's DID:
- The DID itself, signing with a key on its own DID document, resource
ixo:credentials. Whoever controls an entity's DID document can add a key there and act as the entity, for the catalog and for everything else. - For an entity, also a controller of the entity, or a member of a controlling DAO group with voting weight above zero, resource
ixo:credentials/ENTITY_DID. This path is for catalog writes only: it cannot deposit offers or requests or publish status lists. Any such member can rewrite the entity's entry, includingcredential_issuerandurl, without a proposal.