# AI Passport for developers URL: /docs Sign users in with their AI Passport, and build passports into your own product. *** title: AI Passport for developers description: Sign users in with their AI Passport, and build passports into your own product. --------------------------------------------------------------------------------------------- AI Passport is a person's portable memory with an owner-controlled trust loop. These docs cover the two ways your product can work with it. Standard OpenID Connect. Your app signs users in the way it would with Google or Apple, and can read their approved memory with the same token. A managed, server-to-server API. Create passports inside your own signup flow, link data sources from your UI, contribute memories as an attributed source, and read them back under the owner's passes. ## Which one do you need Use **sign-in** when your users already have a passport and are present in a browser. Any OIDC client library works. Registration is open. Use a **deep integration** when you want to create the passport yourself, during your own signup. Onboarding is managed: contact us first. The two compose. When a deep integration meets an email that already has a passport, it routes that person through sign-in, and the owner grants the same delegation on our consent screen. ## For agents Every page here is available as plain markdown. * [`/llms.txt`](/llms.txt) is the index, with a markdown address per page (`/llms.mdx/`). * [`/llms-full.txt`](/llms-full.txt) is the whole corpus in one fetch. * Each page has a **Copy page as Markdown** button. ## Contact Questions, redirect hosts to admit, or a partnership conversation: [support@ego.ist](mailto:support@ego.ist). --- # Deep integrations URL: /docs/partners Create passports inside your signup flow, link sources from your UI, contribute memories as an attributed source, and read them back under the owner's passes. *** title: Deep integrations description: Create passports inside your signup flow, link sources from your UI, contribute memories as an attributed source, and read them back under the owner's passes. --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- Build AI Passport into your own product. A deep integration partner creates passports inside its own signup flow, links data sources from its own UI, contributes its records as an attributed memory source, and reads memory back under the owner's passes. Your user never leaves your flow to get a passport. Provider consent happens on the provider's origin. Every AI Passport permission decision happens on ours. | Fact | Value | | ------------- | ------------------------------------------ | | Base URL | `https://passport.ego.ist` | | Server plane | Partner API key (`Bearer pp_...`) | | User plane | Delegated OAuth token pair, per user | | Access token | 1 hour, refresh rotates | | Onboarding | Managed. One partner record, one key | | Batch ceiling | 20 items per call, daily quota per partner | ## What it is The partner API is the server-to-server counterpart of [Sign in with AI Passport](/docs/sign-in). Sign-in serves users who already have a passport and are present in a browser. Deep integration serves the moment before that: your signup form, your backend, a user who has never heard of us. You attest to the signup facts. We create the passport. You hold a delegated token that acts for that one user inside an explicit, owner-visible boundary. The boundary is the point. Everything your integration does is attributed to it. The owner can see the integration and can sever it in one tap. Suspension turns off partner access everywhere on the next request. Your writes land as reference data from a named source, never as instructions. Your reads follow the same passes as every other app. ## Onboarding Partner onboarding is managed. There is no self-serve signup for the partner record itself. Contact us through the [developer form](/developer#request-access) or at [support@ego.ist](mailto:support@ego.ist). You receive a partner id, an API key shown once at mint, and an OAuth client that the delegated tokens bind to. Key rotation overlaps: we mint the new key first, and we revoke the old prefix when you confirm the cutover. Two agreement points are settled during onboarding. First, the creation-notice email, which tells a person that a passport was created for their address. The notice is on by default. Only a written agreement can suppress it. Second, your source slug, the stable name your memories carry. The slug is fixed once anything has been attributed under it. ## Two credentials, two planes The **API key** authenticates your backend to the server plane. It can do exactly one thing: provision. It is never valid from a browser. The **delegated token pair** comes back from provisioning and authenticates the user plane, one pair per user. The stored pair holds standing, not capability. The partner scope is injected when the token is verified, and only while three things are true: the integration is enabled, your partner record is active, and the owner has not severed the delegation. Suspension and severing therefore take effect on the next request, with no token sweep. | Endpoint | Purpose | | ----------------------------------------- | ------------------------------------------------------------------------------------------------ | | `POST /partner/v1/users` | Server plane. Provision a passport for a net-new email and receive the delegated token pair. | | `GET /partner/v1/connectors` | User plane. Connectable sources for this user, connection state, and the health disclosure. | | `POST /partner/v1/connectors/connect` | User plane. Start a provider OAuth link; returns the provider URL and the pass interstitial URL. | | `POST /partner/v1/connectors/connect-key` | User plane. Submit an API-key connector's key on the user's behalf. | | `POST /partner/v1/source/items` | User plane. Push your records as source-attributed memories, up to 20 per call. | | `POST /partner/v1/recall` | User plane. Read memory: your own rows freely, cross-source rows under the owner's passes. | | `POST /token` | Standard OAuth refresh grant for the delegated pair. Rotation is one-shot. | ## Create a passport at signup Call this from your backend the moment your own account is created. The email shall be net-new to AI Passport. Attestation is never accepted against an existing account. A 409 means the person already has a passport. In that case, offer [Sign in with AI Passport](/docs/sign-in) instead. That grant creates the same delegation, with the owner deciding on our consent screen. ```bash curl -X POST https://passport.ego.ist/partner/v1/users \ -H 'authorization: Bearer pp_YOUR_KEY' \ -H 'content-type: application/json' \ -d '{ "email": "new-user@example.com", "external_user_id": "user-1042", "attestation": { "email_verified_at": "2026-08-13T12:00:00Z", "terms_ack": true, "terms_version": "your-terms-2026-01", "age_attested": true } }' ``` | Field | Presence | Meaning | | ------------------------------- | -------- | ---------------------------------------------------------------------------------------- | | `email` | Required | The address your user verified with you. Net-new only; an existing passport answers 409. | | `external_user_id` | Required | Your stable id for this user. Provisioning is idempotent on it. | | `attestation.email_verified_at` | Required | When your system verified the address. | | `attestation.terms_ack` | Required | You presented our terms notice in your flow. | | `attestation.terms_version` | Required | Which version you presented. | | `attestation.age_attested` | Required | The user meets the age requirement. | A 201 carries `created: true`, the passport user id, the delegation id, and the token pair. A replay with the same `external_user_id` answers 200 with a fresh pair and `created: false`. You never get a duplicate account. Store the pair server-side, next to your own user record. Unless your agreement suppresses it, the address receives a notice that you created a passport for it, with a claim link and a delete link. The delete link opens a confirmation page. A person who confirms there ends the delegation with the account. ## Link sources from your UI With the delegated token, you can offer connector linking inside your own product. `GET /partner/v1/connectors` lists what this user can connect, what each source reads, and the current connection state. Sources that read health-category data carry `special_category: true`. The response carries the disclosure text and its version for them. To start a link, post the connector name. For a health-category source, you shall first show the user the disclosure and echo its version in `disclosure_version`. Without the echo, the call refuses with `health_disclosure_required` and records nothing. Connectors that use an API key instead of provider OAuth take the user's key at `POST /partner/v1/connectors/connect-key`. ```bash curl -X POST https://passport.ego.ist/partner/v1/connectors/connect \ -H 'authorization: Bearer DELEGATED_ACCESS_TOKEN' \ -H 'content-type: application/json' \ -d '{ "connector": "google-calendar" }' ``` ```json { "authUrl": "https://.../oauth/connect/google-calendar?...", "grant_url": "https://passport.ego.ist/partner-connect/grant?ticket=..." } ``` Open `authUrl` in your popup. It is a hosted connect page that hands the user to the provider's own consent screen, where they authorize the source. Then send the same popup to `grant_url`. Neither consent happens in your UI. That separation is what makes the embedded flow trustworthy. A connector whose output needs no pass answers without a `grant_url`. In that case the link is complete after `authUrl`. ## The pass interstitial `grant_url` is a hosted page on our origin, armed by a single-use ticket that expires after 30 minutes. It asks the user one exact question: may your app read the categories this connector produces. Yes mints a bounded, 24-hour category pass for your OAuth client. No closes the flow, and the connector stays linked for the owner's own use. Passes never widen silently. The owner manages them from their passes page afterwards. This page is where cross-source recall access comes from. Without a granted pass, recall still works, but it serves only your own rows for the ungranted categories. ## Contribute memories as a source Your records land as normal, owner-visible memories attributed to `partner:your-slug`. They have the same lifecycle as any connector-derived memory: searchable, exportable, individually deletable, and revocable in bulk by source. They do not pass through the owner's review inbox. Honest attribution plus revocation is the contract, and no review notification is sent. ```bash curl -X POST https://passport.ego.ist/partner/v1/source/items \ -H 'authorization: Bearer DELEGATED_ACCESS_TOKEN' \ -H 'content-type: application/json' \ -d '{ "items": [ { "external_id": "res-8841", "content": "Booked a window table at Lilia for Friday 19:30, party of 2.", "category": "event", "occurred_at": "2026-08-13T19:30:00Z" } ] }' ``` | Field | Presence | Meaning | | ------------- | -------- | ------------------------------------------------------------------------------------------------ | | `external_id` | Required | Your stable id for the record, up to 128 characters, unique within the batch. The dedup handle. | | `content` | Required | The record text, up to 8,000 UTF-8 bytes. It is stored inside a quoted untrusted-data envelope. | | `category` | Optional | One of the writable memory categories. Unknown values and `instruction` are remapped to `other`. | | `occurred_at` | Optional | When the record happened (ISO 8601). Anchors the memory in time. | The whole batch is admitted or refused up front against your daily quota. Only new identities count. A retry of already-delivered ids is always safe and is never quota-blocked. Each item then answers independently: | Status | Meaning | | ----------- | ------------------------------------------------------------------------------------------------ | | `stored` | The item landed as an approved, source-attributed memory. | | `duplicate` | This `external_id` already landed. The original memory id is returned. | | `rejected` | Permanently refused: screened content, or content that cannot be stored. Do not retry unchanged. | | `locked` | The owner's memory store is sealed right now. Retry later. | | `retry` | A transient failure. Redeliver the same `external_id`; recovery is idempotent. | Content is screened before anything is recorded. Payment card numbers, credentials, and identifying personal data are rejected per item. Stored text is wrapped in a quoted envelope that marks it as untrusted reference data from your source. A model that reads it later treats it as a record, not as instructions. ## Read memory back Recall is a read-only call with two tiers in one answer. Rows your integration pushed are readable without a pass whenever the store is available: you already have that data, and a pass that protects it from you would be theater. A sealed store reports `locked` for every requested category, your own rows included. Everything else follows the category passes the owner granted your app, exactly as on every other read surface. ```bash curl -X POST https://passport.ego.ist/partner/v1/recall \ -H 'authorization: Bearer DELEGATED_ACCESS_TOKEN' \ -H 'content-type: application/json' \ -d '{ "categories": ["event", "preference"], "query": "dinner", "limit": 20 }' ``` ```json { "rows": [ { "memory_id": "...", "content": "...", "source": "partner:your-slug" } ], "skipped_categories": [ { "category": "preference", "reason": "no_pass" } ], "approval_url": "https://passport.ego.ist/passes" } ``` | Skip reason | Meaning | | ----------- | --------------------------------------------------------------------------------------------------------------- | | `no_pass` | The owner has not granted your app a pass for this category. Your own rows for it still arrive. | | `once_only` | The only live pass is single-use. This endpoint never spends it. Use the approval link to ask for a wider pass. | | `locked` | The owner's store is sealed. Nothing is readable right now, your own rows included. | The call is free of side effects. It never opens approval requests, never spends single-use passes, and never notifies the owner. When a category is skipped, surface `approval_url` to the user. The grant decision belongs on our origin. ## Rotation, severing, deletion Access tokens live one hour. Refresh at the standard `/token` endpoint with `grant_type=refresh_token`. Rotation is one-shot: persist the new pair before you discard the old one. A refresh that answers `invalid_grant` means the delegation is gone. The owner severed it, the account was deleted, or the pair was revoked. Treat that as the end of the relationship for this user until they reconnect through Sign in with AI Passport. Severing is immediate and complete. Tokens stop verifying. In-flight reads are refused before they answer. New writes cannot claim. The memories your integration contributed remain the owner's, attributed and manageable like any others. ## What the owner sees Your integration is a first-class object in the owner's passport. The owner sees the delegation on their connected-apps surface, under your name. A partner-created account shows the owner a review moment when they first arrive, and keeps showing it until they acknowledge it. Every memory you contribute carries your source label. The passes you hold are listed with everything else on their passes page. The same surface carries one control that forgets every memory your integration contributed, and it stays available after the owner disconnects you. Design your integration expecting the owner to look. ## Errors | Status | Error | Meaning | | ------ | -------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ | | `401` | `invalid_token` | User plane. The token is missing or expired, or the standing behind it is gone: a suspended partner, a severed delegation, or a deleted account. | | `401` | `invalid_key` / `key_revoked` | Server plane. The API key is malformed, unknown, or revoked. | | `403` | `forbidden` / `partner_suspended` / `delegation_revoked` | The credential stands but access does not. Provisioning answers `delegation_revoked` when the owner already severed this `external_user_id`. | | `409` | `account_exists` / `account_pending_deletion` | Provisioning only. The email already has a passport, or one waiting for deletion. Route the user through Sign in with AI Passport. | | `429` | `rate_limited` | Request throttle. Back off and retry. | | `429` | `quota_exceeded` | Source plane only. The daily item quota is spent; the answer carries `daily_item_quota`. | | `400` | `invalid_items` / `invalid_categories` / `invalid_query` / `invalid_limit` | The request shape is wrong. The body says which field. | | `503` | `busy` / `unavailable` / `partner_not_ready` | Transient. Retry with backoff; nothing was stored or claimed. | On the user plane, refusals are deliberately content-free. A severed delegation, a suspended partner, and a deleted account all surface as the same `invalid_token`, and a refresh in that state answers `invalid_grant`. Treat either as the end of the delegation for this user. Build your retry logic on the status code and the error string. A 503 is safe to retry: nothing was stored or claimed. ## A working example The repository ships a runnable partner, `examples/partner-demo`. It is a pretend reservations service that provisions a passport at its own signup, links a calendar from its UI, pushes reservations as source items, and reads them back on the two-tier recall contract. It is a single dependency-free Node file. The README walks through minting a local partner record to run it against your own backend. Questions, redirect hosts, or a partnership conversation: [support@ego.ist](mailto:support@ego.ist). --- # Sign in with AI Passport URL: /docs/sign-in OpenID Connect sign-in that can bring the user's approved memory along. *** title: Sign in with AI Passport description: OpenID Connect sign-in that can bring the user's approved memory along. ------------------------------------------------------------------------------------ Let people sign in to your app with their AI Passport, the way they sign in with Google or Apple. It is standard OpenID Connect, so any OIDC client library works without a custom integration. The difference is what arrives with the identity: if the user allows it, the same token reads the memory they have approved. Your app knows who they are and what they care about on the first screen. | Fact | Value | | ------------ | -------------------------------------- | | Issuer | `https://passport.ego.ist` | | Protocol | OpenID Connect on OAuth 2.1 | | Client type | Public, PKCE S256 required | | ID token | RS256, valid 1 hour | | Scopes | `openid`, `profile`, `email`, `memory` | | Access token | 1 hour, refresh rotates | ## What it is AI Passport runs an OAuth 2.1 authorization server with an OpenID Connect layer on top. Your app is a relying party. It sends the user to AI Passport, the user authenticates and approves the request, and your app receives an access token plus a signed ID token that names the user. An AI Passport identity is not separable from the memory behind it, so every identity assertion also carries a `passport` claim. The claim names the memory endpoint and says whether this token may read it. Signing a user in and reading their memory remain two decisions. The `memory` scope is what connects them, and the user grants it on the same consent screen. ## Before you build Registration is open, but authorization is not. Any client on the internet can register a name through Dynamic Client Registration, so a self-chosen name proves nothing. What cannot be faked is the redirect target: authorization codes are only ever delivered to a registered redirect URI. AI Passport therefore refuses to authorize a client whose redirect hosts it does not recognize, and the user never sees a screen for it. Send us the redirect hosts your app will use before you go live, through the [developer contact form](/developer#request-access) or at [support@ego.ist](mailto:support@ego.ist). Until a host is admitted, `/authorize` redirects back with `error=unauthorized_client`. You can still build the whole flow against a local backend, where the gate is off. Two more things to know before your first test. Users need an AI Passport account already: the sign-in gate authenticates people, it does not create accounts. And the identity your app receives is the account, not any marketing profile around it. ## Endpoints All paths are relative to the issuer, `https://passport.ego.ist`. Discovery is the only URL worth hardcoding. | Endpoint | Purpose | | --------------------------------------- | -------------------------------------------------------------------------------------------------------------- | | `GET /.well-known/openid-configuration` | Discovery document. Read it at startup and take every other URL from it rather than hardcoding paths. | | `GET /.well-known/jwks.json` | RS256 public signing keys for ID token verification, keyed by `kid`. | | `POST /register` | Dynamic Client Registration (RFC 7591). Returns a `client_id`. No client secret. | | `GET /authorize` | Authorization request. Sends the user through the sign-in gate and the consent screen. | | `POST /token` | Authorization code and refresh token grants. Returns an `id_token` when `openid` was granted. | | `GET\|POST /userinfo` | Identity claims for an access token that carries the `openid` scope. | | `POST /revoke` | Token revocation (RFC 7009). Use it when a user signs out of your app. | | `/mcp` | The memory resource, over streamable HTTP MCP, reachable with the same access token when `memory` was granted. | Discovery advertises `response_types_supported: ["code"]`, `subject_types_supported: ["public"]`, `id_token_signing_alg_values_supported: ["RS256"]`, `token_endpoint_auth_methods_supported: ["none"]`, and `code_challenge_methods_supported: ["S256"]`. There is no implicit flow, no hybrid flow, and no client secret. ## The flow 1. Your app reads the discovery document and registers once, keeping the `client_id`. 2. Your app sends the user to `/authorize` with PKCE, `state`, and `nonce`. 3. The user signs in at the AI Passport gate. If they are already signed in to their Passport, the gate hands off to a one-click confirmation instead of asking for a credential again. 4. The consent screen names your app, the account being signed in, and each thing you asked for in plain language. The user allows or denies. 5. On approval, the browser returns to your `redirect_uri` with a code and your `state`. The code is single use and expires in 5 minutes. 6. Your server exchanges the code at `/token` for an access token, a refresh token, and an ID token. You verify the ID token, and the user is signed in. The token exchange, ID token verification, and any memory read belong on your server. The browser should only ever carry the redirect. ## Register your app Register once and store the `client_id`. Every registration is normalized to a public PKCE client, so a `client_secret` you send is ignored and none is returned. Registering again mints a different id rather than updating the first. Treat the id as durable configuration and register again only when your redirect URIs change. ```bash curl -X POST https://passport.ego.ist/register \ -H 'content-type: application/json' \ -d '{ "client_name": "Acme Notes", "redirect_uris": ["https://acme.example/callback"], "grant_types": ["authorization_code", "refresh_token"], "response_types": ["code"], "token_endpoint_auth_method": "none" }' ``` The response contains your `client_id` and the registration echoed back. Redirect URIs are matched exactly at authorization time, with one exception from RFC 8252: a loopback redirect may change its port between registration and use. Register every environment you use, including local development. ## Send the user to authorize | Parameter | Presence | Notes | | ----------------------- | ------------------------- | ------------------------------------------------------------------------------------------------------- | | `response_type` | Required | `code` | | `client_id` | Required | The id returned by registration. | | `redirect_uri` | Required | Must exactly match a URI you registered. | | `scope` | Required | Space separated. Include `openid`, or you get a plain OAuth grant with no identity assertion. | | `state` | Required in practice | Your CSRF value. It is echoed back on both success and failure. | | `code_challenge` | Required | Base64url SHA-256 of your PKCE verifier. | | `code_challenge_method` | Required | `S256`. The plain method is not offered. | | `nonce` | Recommended | Echoed into the ID token so you can bind the token to this request. Send it and check it. | | `resource` | Recommended with `memory` | `https://passport.ego.ist/mcp`. Binds the token to the memory resource, which some deployments enforce. | ```text https://passport.ego.ist/authorize ?response_type=code &client_id=YOUR_CLIENT_ID &redirect_uri=https%3A%2F%2Facme.example%2Fcallback &scope=openid%20profile%20email%20memory &state=RANDOM_STATE &nonce=RANDOM_NONCE &code_challenge=BASE64URL_SHA256_OF_VERIFIER &code_challenge_method=S256 ``` A request without `openid` is not a sign-in. It is treated as a plain OAuth grant, it does not reach the consent screen, and no ID token is issued. A request with no scope at all falls back to `memory` alone, never to identity, so identity is something you ask for explicitly. The user has 10 minutes to finish at the gate before the request expires. If they take longer, start again from `/authorize`. ## Exchange the code ```bash curl -X POST https://passport.ego.ist/token \ -H 'content-type: application/x-www-form-urlencoded' \ -d grant_type=authorization_code \ -d code=THE_CODE \ -d client_id=YOUR_CLIENT_ID \ -d redirect_uri=https://acme.example/callback \ -d code_verifier=YOUR_PKCE_VERIFIER ``` ```json { "access_token": "...", "token_type": "Bearer", "expires_in": 3600, "refresh_token": "...", "scope": "openid profile email memory", "id_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6Ii4uLiJ9..." } ``` Read the returned `scope` rather than assuming you got what you asked for. The user can be signed in to your app while having declined the memory scope, and your app has to work in that state. ## Verify the ID token The ID token is an RS256 JWT. Verify it before you trust a single claim in it. Any OIDC library does this for you. If you verify by hand, the checks are: * Fetch `/.well-known/jwks.json` and pick the key whose `kid` matches the token header. During a signing key rotation the JWKS carries the retired public key alongside the current one, so select by `kid` instead of taking the first key, and refetch when a `kid` is unknown. * Verify the signature, then check `iss` equals the issuer, `aud` contains your `client_id`, `exp` is in the future, and `nonce` matches the one you sent. ```json { "iss": "https://passport.ego.ist", "aud": "YOUR_CLIENT_ID", "iat": 1786000000, "exp": 1786003600, "nonce": "RANDOM_NONCE", "sub": "9f1c2e6a-...", "email": "you@example.com", "email_verified": true, "name": "Ada Lovelace", "picture": "https://...", "passport": { "issuer": "https://passport.ego.ist", "mcp_url": "https://passport.ego.ist/mcp", "memory_access": true } } ``` `sub` is the stable account id and the only safe key for your user records. The subject type is public, so every app sees the same `sub` for a given person. Email addresses change. Do not key on them. Identity claims appear only when their scope was granted, and only when the account has them. Treat `name`, `picture`, and `email` as optional in your data model. ## Scopes and consent Four scopes exist, and there is no wildcard. The right column is the sentence the user reads on the consent screen, which is worth knowing when you decide what to ask for. | Scope | Releases | Shown to the user as | | --------- | ---------------------------------------------------------------------------------- | ------------------------------------- | | `openid` | Turns the request into a sign-in. Releases `sub`, and makes `/userinfo` available. | Confirm your identity | | `profile` | Releases `name` and `picture`, when the account has them set. | Your name and profile picture | | `email` | Releases `email` and `email_verified`. | Your email address | | `memory` | Lets the same access token read memory over MCP. | Read your AI Passport portable memory | One nuance: the `openid` sentence appears only when it is the whole request. When other scopes come along, the consent screen conveys the sign-in by naming the account being signed in instead. Ask for what your first screen actually needs. A sign-in that requests `openid profile email` is a smaller decision for the user than one that adds `memory`. You can send them through `/authorize` again later with the wider scope when the feature that needs it appears. ## UserInfo `/userinfo` returns the same claim set as the ID token for a valid access token that carries `openid`. Use it to refresh a profile later, rather than as a substitute for verifying the ID token at sign-in. ```bash curl https://passport.ego.ist/userinfo \ -H 'authorization: Bearer YOUR_ACCESS_TOKEN' ``` ## Bring the memory along When the user grants `memory`, the access token you already hold reads their Passport over MCP at the `mcp_url` in the `passport` claim. There is no second handshake and no second credential. Read `passport.memory_access` to know whether this token can do it. ```js // Streamable HTTP MCP client, same bearer token const transport = new StreamableHTTPClientTransport(new URL(claims.passport.mcp_url), { requestInit: { headers: { Authorization: `Bearer ${accessToken}` } }, }); await client.connect(transport); await client.callTool({ name: "recall", arguments: { query: "", categories: ["preference", "project"], purpose: "personalize onboarding" }, }); ``` The scope is permission to ask, not permission to read. Every recall names the memory categories and the purpose it needs, and the owner governs those with passes that are exact to one app, one category, and one duration. Without a matching pass the call comes back with an approval link for the owner rather than content, so handle that outcome as a normal state and show the link. An empty result and an unavailable engine are also different answers: an outage is retryable and must not be presented to the user as an empty Passport. A sign-in without the memory scope cannot read anything at `/mcp`. Anything your app writes back is a proposal that lands in the owner inbox, not a memory other apps can see, and it stays a proposal until the owner approves it. ## Refresh, expiry, revocation * Access tokens last 1 hour. ID tokens carry the same 1 hour lifetime. * Refresh tokens rotate: every exchange returns a new refresh token and retires the one you sent, so store the new one atomically. * A refresh returns no new ID token. The identity assertion is made once, at sign-in. Keep your own session after that, or call `/userinfo` when you need current claims. * A refresh can narrow scope but never widen it. Asking for a scope the grant does not carry fails with `invalid_scope`. * Revoke on sign-out: `POST /revoke` with `token` and an optional `token_type_hint`. Per RFC 7009 it answers success for unknown tokens too, so it is never an oracle for whether a token was valid. ```bash # Refresh (rotation is one-shot: store the new pair before you drop the old) curl -X POST https://passport.ego.ist/token \ -H 'content-type: application/x-www-form-urlencoded' \ -d grant_type=refresh_token \ -d refresh_token=YOUR_REFRESH_TOKEN \ -d client_id=YOUR_CLIENT_ID # Revoke on sign-out curl -X POST https://passport.ego.ist/revoke \ -H 'content-type: application/x-www-form-urlencoded' \ -d token=YOUR_REFRESH_TOKEN \ -d token_type_hint=refresh_token ``` Users can revoke your app at any time from their Passport. An account pending deletion stops authorizing immediately. Both surface to you as an ordinary invalid grant or invalid token. Treat those as a signal to start a new sign-in, not as an error to retry. ## Errors Authorization errors come back on your `redirect_uri` with your `state`. Token and resource errors are JSON, in the shape RFC 6749 defines. | Error | Where | What it means | | --------------------- | --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- | | `unauthorized_client` | Redirect to your app | Your client is not on the verified list. Nothing was shown to the user. See Before you build. | | `access_denied` | Redirect to your app | The user pressed Deny on the consent screen. The ticket is consumed, so send them through `/authorize` again to retry. | | `invalid_scope` | Redirect to your app | You asked for a scope outside the four supported ones. | | `invalid_grant` | 400 at `/token` | The code expired (5 minutes), was already used, belongs to another client, or the `redirect_uri` does not match the one from `/authorize`. | | `invalid_token` | 401 with `WWW-Authenticate` | The access token expired or was revoked. Refresh it, or start a new sign-in. | | `insufficient_scope` | 403 with `WWW-Authenticate` | The token is valid but lacks the scope the endpoint needs: `openid` for `/userinfo`, `memory` for `/mcp`. Scopes are never widened for you. | ## A working example The repository ships a complete relying party in `examples/signin-demo`: discovery, registration, PKCE, the token exchange, ID token verification against the JWKS, `/userinfo`, and a memory recall over MCP, in one dependency-light file you can read end to end. ```bash AI_PASSPORT_ISSUER=https://passport.ego.ist node examples/signin-demo/server.mjs # then open http://localhost:4000 ``` Questions, or a redirect host to admit: [support@ego.ist](mailto:support@ego.ist). For what AI Passport does with the memory behind the identity, see [how your memory is protected](/trust). ## Beyond sign-in: deep integrations Sign-in serves users who already have a passport. If you want to create passports inside your own signup flow, link data sources from your own UI, contribute your records as an attributed memory source, or read them back under the owner's passes, that is the deep integration program. It is a managed, server-to-server API with its own documentation at [Deep integrations](/docs/partners). The two compose: a person who already has a passport connects to a partner through this sign-in flow, and the same delegation is created with the owner deciding on our consent screen.