Credentials
Space credentials are short-lived JWTs for cross-service access to space data. A member requests a delegation token to prove their membership, exchanges the token for a credential JWT, then passes it to an external service that needs to read the space's records.
How credentials work
Credential issuance is a two-step process. The delegation token is a short-lived proof of membership (60-second TTL), and the credential is the bearer token used for cross-service access (2-hour TTL).
Credentials are ES256 JWTs signed with a P-256 keypair unique to each space. The keypair is generated on first credential request and stored encrypted (AES-256-GCM).
Step 1: Get a delegation token
The caller must be an authenticated member of the space. The delegation token is a short-lived proof of membership (60-second TTL).
Note: this endpoint is a GET request (not POST). The previous getMemberGrant endpoint (POST) is available as a legacy alias via dev.happyview.space.getMemberGrant.
const params = new URLSearchParams({
space: "at://did:plc:abc123/space/com.example.forum/main",
});
const response = await fetch(`https://happyview.example.com/xrpc/com.atproto.space.getDelegationToken?${params}`, {
headers: {
"X-Client-Key": CLIENT_KEY,
"Authorization": `DPoP ${ACCESS_TOKEN}`,
"DPoP": DPOP_PROOF,
},
});
interface DelegationTokenResponse {
delegationToken: string;
expiresAt: string;
}
const data: DelegationTokenResponse = await response.json();Response:
{
"delegationToken": "eyJhbGciOiJFUzI1NktFWSJ9...",
"expiresAt": "2026-05-09T12:01:00Z"
}Step 2: Get a space credential
Exchange the delegation token for a space credential JWT. The credential is signed by the space's keypair and has a 2-hour TTL.
const response = await fetch("https://happyview.example.com/xrpc/com.atproto.space.getSpaceCredential", {
method: "POST",
headers: {
"X-Client-Key": CLIENT_KEY,
"Authorization": `DPoP ${ACCESS_TOKEN}`,
"DPoP": DPOP_PROOF,
"Content-Type": "application/json",
},
body: JSON.stringify({
grant: "eyJhbGciOiJFUzI1NktFWSJ9...",
}),
});
interface CredentialResponse {
credential: string;
expiresAt: string;
}
const data: CredentialResponse = await response.json();Response:
{
"credential": "eyJhbGciOiJFUzI1NiJ9...",
"expiresAt": "2026-05-09T14:00:00Z"
}Credential claims
The JWT payload contains:
| Claim | Description |
|---|---|
iss | The space authority's DID (who signed it) |
sub | The full at:// space URI |
iat | Issued at (Unix timestamp) |
exp | Expiry (Unix timestamp) |
jti | Random nonce making each issued credential unique |
The credential is a bearer token: it is reused for the full 2-hour TTL and is not bound to a specific client or audience, so any service holding it can read the space's records. Containment relies on the short TTL and on revocation.
Revocation
A credential is invalidated before its TTL expires when its holder is removed
from the space (com.atproto.simplespace.removeMember): HappyView records the
revocation and rejects the credential on subsequent reads. Because a credential
is an opaque bearer token, revocation is scoped to a member — removing (and, if
needed, re-adding) a member revokes all of that member's outstanding credentials.
Using a credential
Pass the credential as a standard Bearer token in the Authorization header. HappyView distinguishes space credentials from other tokens by checking the JWT header's typ field (atproto-space-credential+jwt).
const response = await fetch(
"https://happyview.example.com/xrpc/com.atproto.space.getRecord?space=...&collection=...&rkey=...",
{
headers: {
"Authorization": `Bearer ${SPACE_CREDENTIAL}`,
},
},
);
const data = await response.json();No DPoP auth or client key is needed when authenticating via space credential — the credential itself is sufficient. The sub claim identifies the space being accessed.
HappyView verifies the credential by resolving the issuer's DID document, extracting the #atproto_space signing key, and validating the JWT signature and expiry. If valid, the request is granted read access to the space identified by sub.
App access control
Before issuing a credential, HappyView checks whether the calling app (identified by its DPoP client key) is allowed to access the space:
open(default): any app can get credentialsallowList: only apps whose client metadata URL appears in theallowedarray can get credentials
For open spaces, requests without a client key are allowed. For allowList spaces, a client key is required — requests without one are rejected.
External credential verification
HappyView can also verify credentials issued by other HappyView instances or space-aware services. When a Bearer space credential is presented, HappyView:
- Decodes the JWT without verification to extract the
iss(issuer DID) - Resolves the issuer's DID document
- Extracts the
#atproto_spacesigning key from the DID doc - Verifies the JWT signature and expiry
- Checks that the
subclaim matches the requested space
A credential issued by one instance can be used to read from another instance that hosts the same space's data.