Skip to main content

Credential

GET /v1/credential describes the credential presented on the request: who issued it, which application holds it, what it grants, how that application was resolved, and when it was issued and expires. It requires no scope — a credential may always describe itself — but a revoked or expired one is refused 403 like anywhere else, which makes this a liveness check.

curl https://api.dfos.com/v1/credential \
-H 'Authorization: DFOS <request-proof>' \
-H 'X-Credential: <credential>'
{
"subjectDid": "did:dfos:z8zt7ecn9h8n782kae3k796crva2c73",
"clientDid": "did:dfos:7ken2dr4ahc9vtfz3n8ce6ad2kr7h9t",
"scopes": ["read:profile", "read:email", "read:memberships", "read:posts", "write:posts"],
"attenuation": [
{ "resource": "api:api.dfos.com", "action": "read:profile,read:email,read:memberships" },
{
"resource": "api:api.dfos.com/spaces/9ctvrdn9vedda7efetrhcdakfh4cr2k",
"action": "read:posts,write:posts"
}
],
"tier": "jit",
"domain": "fieldnotes.example",
"issuedAt": "2026-09-01T16:20:00.000Z",
"expiresAt": "2026-11-30T16:20:00.000Z"
}
const res = await fetch('https://api.dfos.com/v1/credential', { headers });
if (res.status === 403) return promptUserToReconnect();
const { scopes } = await res.json();
if (!scopes.includes('read:memberships')) return promptUserForMoreAccess();

There is no path parameter: the subject is always the credential that authenticated the request.

attenuation is the grant itself​

scopes says what the grant carries; attenuation says what it carries where, echoed back exactly as the credential was signed. One entry per resource:

resourceWhat it covers
api:<host>The API itself. For a space-level action that means every space the user belongs to, including ones they join later.
api:<host>/spaces/<space DID body>One named space: its did without the did:dfos: prefix.

A grant narrowed to two spaces carries one entry per space and may carry no api:<host> entry at all.

Read the grant; do not re-derive it — a user can narrow whatever your app asked for at consent. Treat an unrecognized resource form as an opaque string rather than inferring coverage from its shape: which entry admits which request is specified in API-AUTH, and a route's 403 is the definitive answer.

A null domain means a local application​

domain is the bare hostname the grant was issued to, and it is null on the loopback tier.

tierWhat it meansdomain
jitResolved live from the application's /.well-known/dfos-app.json. Every hosted app is this.present
loopbackThe key-proven local tier: a client on someone's machine that proved it holds the audience key.null

A loopback client — a CLI, an agent, a script — proved a key, not an origin, so identify the holder by clientDid. tier is an open enum: treat an unrecognized value as an opaque string.

Fields​

FieldNotes
subjectDidProtocol DID of the user who issued the credential — the subject every gated route serves.
clientDidProtocol DID of the application the credential was issued to. Canonical when domain is null.
scopesThe actions this grant carries, in DFOS's canonical order. Authoritative — a route 403s when its action is absent.
attenuationThe grant as signed: one {resource, action} entry per resource. Says what the grant covers where.
tierHow the application was resolved at consent time.
domainHostname the grant was issued to, or null for a local application.
issuedAtWhen the user approved the grant (ISO 8601 UTC).
expiresAtWhen the grant lapses on its own (ISO 8601 UTC). A revocation ends it on your next request, long before this date.

Calling it​

The same two headers as every gated route — see Authentication and Scopes and credentials. 401 means your proof was wrong, so sign a fresh one and retry. 403 means the credential is revoked, expired, or not one DFOS issued; ask the user for a new grant. 413 means the request body exceeded what this endpoint will authenticate. 429 means the per-grant budget is exhausted; the body names the scope and a retryAfterMs, and Rate limits and caching has the budgets. 503 means verification could not complete; retry with backoff.

Cache-Control: no-store, never shared-cached: the response names a specific user and a specific grant.