# 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.