Skip to content
Logo

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:

CredentialWhat it provesAgent commandHTTP headerService method
Agent Attestation Token (AAT)Link issued it, without personal details or the identity of the agentidentity attestations popAuthorizationverifyAttestation
Identity presentationSelected claims, such as a verified email, signed by Link and the credential holderidentity credentials presentIdentity-PresentationverifyClaims

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:

  1. The agent requests the event list. The site returns HTTP 401 with a PrivateToken challenge.
  2. The agent retries with an AAT. The site verifies it and grants an application session.
  3. The agent registers for an event. The site returns HTTP 401 with an Identity-Presentation challenge for email and email_verified.
  4. 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:

FileContents
~/.link-cli/identity/attestations/pool.jsonUnused AATs. request adds tokens, and pop removes one.
~/.link-cli/identity/credentials/current.jsonThe signed identity credential. A new request replaces it.
~/.link-cli/identity/holder-key.jwkThe 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 json

Credential 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_names in the output of identity 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.lock remains after a crash: Make sure no attestation commands are running, then remove the lock directory. If the crash happened after pop removed a token, that token is lost.

Verification errors

Each SDK failure includes one of these codes:

CodeMeaningWhat to do
incomplete_protocol_requestThe credential is missing or empty.Read the correct header and send a challenge.
malformed_protocol_inputThe credential can’t be parsed.Pass a single header value, including the attestation scheme. Reject duplicates.
invalid_private_tokenThe AAT authenticator is invalid.Confirm the token is intact and Link issued it.
challenge_mismatchThe token doesn’t match the supported bearer challenge.Use a bearer AAT. Key-bound AATs aren’t supported.
unknown_issuerLink doesn’t accept the token key.Check the issuer configuration and key retirement.
invalid_claims_presentationThe claims presentation failed verification.Check the audience, expected nonce, required disclosures, signatures, and expiry.
issuer_unavailableThe 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:

MethodInputResult
attestationChallenge(options?)Optional maxAgeSeconds, default 300. This doesn’t expire tokens.wwwAuthenticate array and challengeDigest
verifyAttestationOrThrow(authorization)Header value, null, or undefinedissuer, tokenKeyId, and bindingMode, or throws VerificationError
claimsChallenge(options)Nonempty claims, optional purpose, and optional nonceTtlSeconds, default 300wwwAuthenticate, problem body, nonce, and expiresAt
verifyClaimsOrThrow(presentation, options)Header value, expected nonce, and requiredClaimsissuer, claims, holderKeyThumbprint, and vct, or throws VerificationError
claimsSupported()NoneClaim names that Link advertises
warm()NoneOptionally 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:

  • Authorization values 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.maxKeyAgeSeconds to change this. After Link retires a key, tokens signed with it stop verifying unless you set issuerOptions.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.