Identity
Identity lets a website or API check that Link vouches for an agent, or that the customer’s details are verified, before it allows access. Agents get credentials from Link with Link CLI. Services check them with the Agent Identity SDK (@stripe/agent-identity). For step-by-step instructions, see Prove and verify identity.
Credentials
Agents use two types of credential from Link:
| Credential | What it proves | Agent command | HTTP header | Service method |
|---|---|---|---|---|
| Agent Attestation Token (AAT) | Link issued it, without personal details or the identity of the agent | identity attestations pop | Authorization | verifyAttestation |
| Identity presentation | Selected claims, such as a verified email, signed by Link and the credential holder | identity credentials present | Identity-Presentation | verifyClaims |
How it works
A service asks for a credential with an HTTP 401 challenge, and the agent retries with the credential. For example, an event site can use both checks:
- The agent requests the event list. The site returns HTTP
401with aPrivateTokenchallenge. - The agent retries with an AAT. The site verifies it and grants an application session.
- The agent registers for an event. The site returns HTTP
401with anIdentity-Presentationchallenge foremailandemail_verified. - The agent presents those claims. The site verifies them and completes the registration.
The attestation challenge is the same for every request, so an agent can answer it from tokens it requested earlier. The claims challenge includes the service’s audience and a nonce, and the agent signs a new presentation for each one.
Local storage
Link CLI keeps identity files in the home directory:
| File | Contents |
|---|---|
~/.link-cli/identity/attestations/pool.json | Unused AATs. request adds tokens, and pop removes one. |
~/.link-cli/identity/credentials/current.json | The signed identity credential. A new request replaces it. |
~/.link-cli/identity/holder-key.jwk | The holder key that signs presentations. Link CLI creates it on first use and reuses it. |
List saved credentials and attestations without logging in or calling Link:
link-cli identity credentials list --format json
link-cli identity attestations list --format jsonCredential output reports the path, issuer, expiry, an expired flag, holder-key path and thumbprint, and claim names. Attestation output reports the path, issuer and key IDs, stored_token_count, and storage (pool or export) of each batch, plus total_token_count across all batches. It covers JSON files in ~/.link-cli/identity/attestations only, so files you export with --output-file don’t appear. Only pool batches are available to pop.
Both commands report per-file errors next to valid entries. Inspect them instead of treating storage as empty. The commands never print tokens, claim values, or private keys, and they don’t verify signatures.
Identity files belong to the home directory. Logging in as a different customer or passing --auth doesn’t switch them. Use a separate home directory for each customer.
Responsibilities
The SDK verifies credentials. Your service owns these decisions:
- AAT reuse: The SDK doesn’t enforce single use. Anyone with a valid bearer token can present it again, including to a different service, while its signing key is trusted. If you need single use, keep your own spent-token store.
- Presentation replay: Verification doesn’t consume the nonce, so the same presentation verifies again with the same nonce. If you need single use, atomically mark the interaction complete before you authorize the operation.
- Interaction expiry: The SDK doesn’t check
expiresAt. Reject expired interactions yourself, and check again after verification completes. - Authorization: A valid credential doesn’t decide what the caller can do, and successful verification doesn’t create a session. It also doesn’t prove that an AAT and an identity presentation come from the same holder.
MCP clients
The PrivateToken and Identity-Presentation challenges are a custom exchange over the MCP HTTP transport, separate from the MCP OAuth flow. The MCP TypeScript client doesn’t answer them and can start OAuth discovery on a 401. Handle the challenges at the HTTP transport layer. See the MCP integration guide for server and client requirements.
On an MCP server, check credentials before JSON-RPC dispatch, not inside a successful tool response. Link CLI doesn’t expose its identity commands as MCP tools.
Handle errors
Use the error to choose the next action.
Agent errors
These errors come from Link CLI:
ATTESTATION_POOL_EMPTY: Request a batch, then pop a token.- A requested claim is unavailable: Check
claim_namesin the output ofidentity credentials list. Don’t disclose other claims or the raw credential. - The holder key is missing or doesn’t match: Keep the files. Restore the correct key, or request a new credential.
- The service rejects the audience or nonce: Confirm the intended service and get a new challenge before you present again.
pool.json.lockremains after a crash: Make sure no attestation commands are running, then remove the lock directory. If the crash happened afterpopremoved a token, that token is lost.
Verification errors
Each SDK failure includes one of these codes:
| Code | Meaning | What to do |
|---|---|---|
incomplete_protocol_request | The credential is missing or empty. | Read the correct header and send a challenge. |
malformed_protocol_input | The credential can’t be parsed. | Pass a single header value, including the attestation scheme. Reject duplicates. |
invalid_private_token | The AAT authenticator is invalid. | Confirm the token is intact and Link issued it. |
challenge_mismatch | The token doesn’t match the supported bearer challenge. | Use a bearer AAT. Key-bound AATs aren’t supported. |
unknown_issuer | Link doesn’t accept the token key. | Check the issuer configuration and key retirement. |
invalid_claims_presentation | The claims presentation failed verification. | Check the audience, expected nonce, required disclosures, signatures, and expiry. |
issuer_unavailable | The SDK couldn’t read the issuer metadata or keys. | Check connectivity and timeouts. Return 503. |
isRejection(failure) is true for credential rejections, which get a 401 and a new challenge, and false for issuer_unavailable, which gets a 503. FAILURE_CODES exports the full list.
SDK reference
All LinkVerifier methods are asynchronous:
| Method | Input | Result |
|---|---|---|
attestationChallenge(options?) | Optional maxAgeSeconds, default 300. This doesn’t expire tokens. | wwwAuthenticate array and challengeDigest |
verifyAttestationOrThrow(authorization) | Header value, null, or undefined | issuer, tokenKeyId, and bindingMode, or throws VerificationError |
claimsChallenge(options) | Nonempty claims, optional purpose, and optional nonceTtlSeconds, default 300 | wwwAuthenticate, problem body, nonce, and expiresAt |
verifyClaimsOrThrow(presentation, options) | Header value, expected nonce, and requiredClaims | issuer, claims, holderKeyThumbprint, and vct, or throws VerificationError |
claimsSupported() | None | Claim names that Link advertises |
warm() | None | Optionally fills the issuer key cache |
verifyAttestation and verifyClaims take the same input and return a result with valid and failures instead of throwing. Check valid, because the result object is truthy on failure. A VerificationError has the same code and failures as a failed result. Challenge builders and warm() throw on failure.
These limits apply to every verifier:
Authorizationvalues can contain up to 8,192 characters, and identity presentations up to 65,536 characters. Larger values fail before parsing.- The verifier revalidates token keys every 900 seconds by default. Set
issuerOptions.maxKeyAgeSecondsto change this. After Link retires a key, tokens signed with it stop verifying unless you setissuerOptions.retiredKeyGraceSeconds. - Credential signing keys are cached for one hour per process.
- The SDK has no metrics or logging hooks. Record results at the call site, and don’t log tokens, presentations, or disclosed claims.