> ## Documentation Index
> Fetch the complete documentation index at: https://docs.apostra.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Sign in a self-built agent

> How an agent you build signs in to Apostra with OAuth: discovery, client registration, redirect URIs, tokens, browser use, and the errors you can hit.

If you build your own agent and a person is there to approve it, you do not ask
Apostra for access first. Point an MCP client at the endpoint, let it sign in,
and the person approves it on a consent screen. No account admin approves your
agent, and no allowlist needs your domain or origin on it.

If nobody is there to click, skip to [Agents with no person present](/v3/agent-sign-in#agents-with-no-person-present).

## The short path

Point an MCP SDK client at `https://api.apostra.com/mcp/v3`. A client that
supports MCP authorization does the rest:

1. It calls the endpoint without a token and gets `401`. The
   `WWW-Authenticate` header carries a `resource_metadata` URL.
2. It reads that metadata to find Apostra's authorization server, then reads
   `/.well-known/oauth-authorization-server` for the endpoints, including
   `registration_endpoint` and `client_id_metadata_document_supported: true`.
3. It identifies itself, with a [Client ID Metadata Document](/v3/agent-sign-in#client-id-metadata-documents)
   or by [registering](/v3/agent-sign-in#dynamic-client-registration).
4. It opens a browser. The person signs in to Apostra, then approves or denies
   your agent on the consent screen.
5. The browser returns to your agent's redirect URI with an authorization
   code. The client exchanges it, with a PKCE verifier, for an access token and
   a refresh token.

Then your agent calls the endpoint with `Authorization: Bearer <access token>`.
Call `get_status` first to confirm which account the person signed in as.

An MCP SDK client does all of this when you give it a redirect URI and a place to
store tokens. Read the OAuth section of the SDK you use. The rest of this page
is for when you write the flow yourself, or when something fails.

## What the person sees

Apostra asks every time an agent authorizes, including when the person is
already signed in. The consent screen shows:

| Shown | What it is | Why it matters |
| - | - | - |
| The agent's name | The `client_name` your agent registered, or the name in its metadata document | Your agent chooses this name, so it is not proof of anything |
| Returns you to | The address the browser goes to after the decision: the redirect URI's origin, or its scheme and host for a custom scheme | This is the part the person can check. A code only ever goes to a redirect URI your client declared |
| App published by | The domain of your metadata document URL. Shown only for [Client ID Metadata Document](/v3/agent-sign-in#client-id-metadata-documents) clients | Apostra fetched your document from that domain, so the domain vouches for the name and redirect URIs |
| Access to | The MCP endpoint the agent is asking for, such as `https://api.apostra.com/mcp/v3` | The grant works on that one endpoint only |
| Signed in as | The Apostra account email | The agent acts as this person, with their permissions |

That screen is the trust anchor. The person, not an admin, decides whether this
agent may act as them. **Approve** issues the code. **Deny** sends the browser to
your redirect URI with `error=access_denied` and issues nothing. The decision is
tied to a one-time session that lasts 10 minutes.

## Register your agent

Your agent identifies itself to Apostra in one of two ways. Both create a public
client: no client secret, with PKCE doing the proof. Use one or the other.

### Client ID Metadata Documents

A Client ID Metadata Document is a JSON file on your own domain. Your `client_id`
is the URL of that file. Nothing is registered ahead of time, and Apostra fetches
the file when a person starts authorizing. The MCP specification (2026-07-28)
prefers this method, and Apostra advertises support with
`client_id_metadata_document_supported: true`, so clients that prefer it use it
without being told.

```json theme={null}
{
  "client_id": "https://agent.example.com/oauth/client.json",
  "client_name": "Example Agent",
  "client_uri": "https://agent.example.com",
  "redirect_uris": ["https://agent.example.com/oauth/callback"],
  "token_endpoint_auth_method": "none"
}
```

Send the file's URL as `client_id` when you start OAuth. Requirements:

* The URL uses `https`, has a path beyond `/`, and has no fragment, username,
  password, or `.` or `..` path segments. It must already be in canonical form:
  lowercase host, no default port.
* `client_id` in the file equals the URL exactly.
* `redirect_uris` is required and each one follows the
  [redirect URI rules](/v3/agent-sign-in#redirect-uri-rules). The `redirect_uri` you send to
  `/authorize` must match one of them exactly, character for character. A
  metadata document gets no loopback subpath allowance.
* The file is public. Leave `token_endpoint_auth_method` out or set it to
  `none`. A `client_secret` makes the document invalid.
* Serve it with status `200`, a JSON content type, and a body of at most 5 KB.
  Apostra does not follow redirects, connects only to public internet
  addresses, and gives up after a few seconds.
* Apostra caches a valid file for as long as your `Cache-Control: max-age` says,
  between 1 minute and 24 hours, and 5 minutes when you set none. `no-store` and
  `no-cache` count as the 1-minute minimum. It remembers a failed lookup for
  about a minute. A change to `redirect_uris` takes effect when the cached copy
  expires.

### Dynamic client registration

Dynamic client registration is a `POST` to `/auth/register`. Use it when you
cannot host a public file, for example a desktop tool or a local development
agent.

```bash theme={null}
curl -X POST https://api.apostra.com/auth/register \
  -H "Content-Type: application/json" \
  -d '{
    "client_name": "Example Agent",
    "redirect_uris": ["https://agent.example.com/oauth/callback"]
  }'
```

The `201` response carries a generated `client_id` that starts with
`mcp_client_`, plus the redirect URIs you sent. Do not treat it as a secret.
Notes:

* Always send `redirect_uris`. A request without them returns a shared,
  preconfigured client that is not yours.
* Send up to 25 redirect URIs of at most 2048 characters each. `client_name` is
  at most 120 characters. `client_uri` and `logo_uri`, if you send them, must be
  `https` URLs.
* A registration you never use to start an authorization is deleted after 24
  hours. A registration unused for 90 days is deleted too. Register again when
  you see [`redirect_uri is not allowed`](/v3/agent-sign-in#troubleshooting) for a client that
  used to work.

## Redirect URI rules

Both registration methods use the same rules for each redirect URI:

* **Web agents:** `https` on any host.
* **Local tools:** `http://localhost`, `http://127.0.0.1` or `http://[::1]`,
  with or without a port. No other `http` address is accepted.
* **Desktop and mobile apps:** a private-use scheme, such as
  `cursor://anysphere.cursor-mcp/oauth/callback` or
  `com.example.agent:/oauth/callback`. The scheme is a letter followed by
  letters, digits, `+`, `-` or `.`, at least two characters long, and something
  must follow it (a host or a path beyond `/`). These schemes are refused:
  `http` (other than loopback), `javascript`, `data`, `file`, `blob`,
  `vbscript`, `about`, `filesystem`, `ws`, `wss` and `ftp`.
* **Never:** a fragment (`#...`) or embedded credentials (`user:pass@`). A URI is
  at most 2048 characters.

The `redirect_uri` you send to `/authorize` and to `/auth/token` must match a
registered URI exactly. The one exception is a client registered with dynamic
registration and a `localhost` or `127.0.0.1` redirect: the request may use the
same host and port with a deeper path (registered `http://localhost:8123/cb`
accepts `http://localhost:8123/cb/done`), as long as neither has a query string.
The port must still match. `http://[::1]` and custom schemes match exactly.

## Tokens and PKCE

PKCE is required, with the `S256` method only. Send `code_challenge` (43 to 128
base64url characters) and `code_challenge_method=S256` to `/authorize`, then the
matching `code_verifier` to `/auth/token`. `plain` is refused.

```text theme={null}
GET https://api.apostra.com/authorize
  ?response_type=code
  &client_id=<your client_id>
  &redirect_uri=<exact redirect URI>
  &code_challenge=<S256 challenge>
  &code_challenge_method=S256
  &resource=https://api.apostra.com/mcp/v3
  &state=<opaque value>
```

Always send `resource`. It names the MCP endpoint the token is for, and it must
be the endpoint's exact URL, with no query or fragment. If you leave it out, the
grant is for the bare `/mcp` endpoint. Apostra accepts a `/mcp` grant at
`/mcp/v3` too, but at no other endpoint, so send the URL you will call.

The browser comes back to your redirect URI with `code`, `state` and `iss` (the
issuer, `https://api.apostra.com`). Check `state`, and check `iss` if your OAuth
library supports it: Apostra advertises
`authorization_response_iss_parameter_supported`. The code works once and expires
after 10 minutes. Exchange it. `/auth/token` accepts form-encoded or JSON bodies.

```bash theme={null}
curl -X POST https://api.apostra.com/auth/token \
  -d grant_type=authorization_code \
  -d code=<code> \
  -d code_verifier=<verifier> \
  -d client_id=<your client_id> \
  -d redirect_uri=<exact redirect URI> \
  -d resource=https://api.apostra.com/mcp/v3
```

The response is a standard token response with `token_type: Bearer`, scope
`mcp:access`, and:

| Token | Lifetime | Notes |
| - | - | - |
| Access token (`mcp_at_...`) | 1 hour (`expires_in: 3600`) | Opaque, not a JWT. Send it as a bearer token |
| Refresh token (`mcp_rt_...`) | 30 days | Rotates on every use |

Both tokens are bound to the person, your client, and the exact `resource`.
Refresh before the access token expires:

```bash theme={null}
curl -X POST https://api.apostra.com/auth/token \
  -d grant_type=refresh_token \
  -d refresh_token=<refresh token> \
  -d client_id=<your client_id> \
  -d resource=https://api.apostra.com/mcp/v3
```

Each refresh returns a new access token and a new refresh token, with a fresh 30
days. Save the new refresh token and discard the old one. Apostra tolerates
concurrent callers that present the same refresh token within 90 seconds of its
rotation (they get the same new pair), so two parallel workers do not break the
grant. Presenting an already-used refresh token after that is treated as theft,
and Apostra ends the whole grant. Use a lock around refresh, or keep one refresher.

### What ends a grant

A grant lasts until one of these happens:

* **Your agent calls the revocation endpoint.** `POST /auth/revoke` with `token`
  set to the access or refresh token (form-encoded or JSON) ends the whole grant.
  Send `client_id` too. A `client_id` that does not match the token's client is
  silently ignored, and the grant stays live. The endpoint returns `200` for
  tokens it does not recognize, and `400` only when `token` is missing.
* **The person disconnects your agent** from **Connected apps** in their
  account menu. Both tokens stop working immediately.
* **The refresh token goes 30 days without use.**
* **Apostra refuses a refresh** because the account no longer has active access,
  or because of a platform-wide security sign-out. The refresh returns
  `invalid_grant` and your agent must send the person through sign-in again.

A person signing out of Apostra in their browser does **not** end the grant. The
grant belongs to your agent, not to their browser session, so your agent keeps
working until one of the events above.

## Agents that run in a browser

A browser-based agent can run on any origin. These paths answer a request from
any origin with CORS headers and no approval step:

`/authorize`, `/auth/register`, `/auth/token`, `/auth/revoke`,
`/auth/mcp/authorize`, `/.well-known/*`, `/mcp`, `/mcp/*` (including
`/mcp/v3`), and the per-Seller `/seller/{id}/mcp` endpoints.

Some rules follow from that:

* **Bearer token only.** Apostra returns `Access-Control-Allow-Origin` but not
  `Access-Control-Allow-Credentials`. Send `Authorization: Bearer <access token>`
  and do not set `credentials: "include"`. Do not rely on Apostra cookies.
  A page with no token gets nothing, which is why no origin needs approval.
* **Everything else stays closed.** These paths do not open the rest of the REST
  API, such as `/api/v3/*`, to your origin. An MCP access token is rejected on
  the HTTP API anyway.
* **`null` origins are refused.** A sandboxed `srcdoc` frame or a page opened from
  a `file:` URL sends `Origin: null`, and Apostra does not answer it with CORS
  headers. Serve the agent from a real `https` origin.
* **Do not put an API key in browser code.** Use the OAuth flow above.
* **Redirect URIs still matter.** An agent on another origin can call these
  paths, but it only receives an authorization code at a redirect URI its client
  declared, and only after the person approves it.

## Agents with no person present

A background agent, a scheduled job, or a deployed backend has nobody to approve
a consent screen. Do not use this flow. Choose one of these:

* **An agent that has no credential yet** starts with
  [`https://api.apostra.com/auth.md`](https://api.apostra.com/auth.md) and the
  machine-readable contract at `/.well-known/buyer-activation.json`. See
  [Agent registration](/v2/authentication#agent-registration).
* **A user API key** for simple automation. A person creates it in
  **Settings → API Access**. See [Authentication](/v3/authentication).
* **An M2M application** when your backend needs the OAuth `client_credentials`
  flow. See [Authentication](/v3/authentication).
* **An identity-only buyer-agent credential:** see
  [buyer-agent credentials](/v2/setup/buyer-agent-credentials).

## Troubleshooting

These are the messages your agent can receive. Errors from `/authorize`,
`/auth/token`, `/auth/revoke` and `/auth/register` are OAuth JSON bodies with
`error` and `error_description`.

| Error | Where | What it means | What to do |
| - | - | - | - |
| `invalid_redirect_uri` | `/auth/register` | A redirect URI breaks the [rules](/v3/agent-sign-in#redirect-uri-rules), or `redirect_uris` is missing, is not an array, is empty, or has more than 25 entries | Fix the URI. The `error_description` says which: `Invalid redirect URI`, `At least one redirect_uri is required`, `redirect_uris must be an array`, or `At most 25 redirect URIs are allowed` |
| `invalid_client_metadata` | `/auth/register` | `client_name` is not a string or is over 120 characters, or `client_uri` or `logo_uri` is not a valid `https` URL | Fix the field named in `error_description` |
| `invalid_request`, `redirect_uri is not allowed` | `/authorize` | The `client_id` and `redirect_uri` do not pair up. For a registered client: the URI is not an exact registered URI, or the registration expired. For a metadata document URL: Apostra could not fetch or validate the document, or it does not list this exact URI | Check the URI character for character. For a document, confirm the [requirements](/v3/agent-sign-in#client-id-metadata-documents) and wait about a minute, because a failed lookup is remembered that long. For a registration, register again |
| `invalid_request`, `PKCE is required...` | `/authorize` | `code_challenge` or `code_challenge_method` is missing | Send both |
| `invalid_request`, `Only S256 code_challenge_method is supported` | `/authorize` | You sent `plain` or another method | Use `S256` |
| `invalid_request`, `code_challenge must be a valid base64url-encoded string (43-128 chars)` | `/authorize` | The challenge is not base64url or is the wrong length | Use the base64url, unpadded SHA-256 of your verifier |
| `unsupported_response_type` | `/authorize` | `response_type` is not `code` | Send `response_type=code` |
| `invalid_target` | `/authorize`, `/auth/token` | The `resource` is not an exact, supported MCP endpoint URL on the host you called, or it differs from the one the grant was issued for. Descriptions include `resource does not identify a supported MCP endpoint`, `resource must exactly match the authorization request`, `resource must exactly match the original authorization grant`, and `authorization server hostname is not verified` (you called a host Apostra does not serve as an OAuth server) | Send `https://api.apostra.com/mcp/v3` every time, with no query or fragment |
| `invalid_grant`, `Invalid code_verifier` | `/auth/token` | The verifier does not hash to the challenge you sent to `/authorize` | Send the verifier that made that challenge, for this authorization |
| `invalid_grant`, `The authorization code binding does not match` | `/auth/token` | `client_id` or `redirect_uri` differs from the one used at `/authorize` | Send the same values |
| `invalid_grant`, `The authorization code is invalid, expired, or already used` | `/auth/token` | Codes work once and last 10 minutes | Start sign-in again |
| `invalid_grant`, `The refresh token is invalid, expired, or already used` | `/auth/token` | The refresh token was spent, revoked, or past 30 days | Start sign-in again. Next time, save each new refresh token |
| `invalid_grant`, `...Reauthorization is required.` | `/auth/token` | The account lost access, or a platform-wide sign-out ended the grant | Start sign-in again |
| `invalid_request`, `client_id is required...` or `redirect_uri is required...` or `code_verifier is required for PKCE` | `/auth/token` | A required parameter is missing from the exchange or refresh | Send `client_id` with every exchange and refresh, plus `redirect_uri` and `code_verifier` on the exchange |
| `temporarily_unavailable` (`503`) | `/auth/token`, `/auth/revoke` | Apostra's sign-in services are briefly unavailable | Retry with a short backoff |
| `error=access_denied` on your redirect URI | Redirect | The person chose **Deny** | Do not retry on a loop. Tell the person the agent is not connected |
| `Invalid or expired MCP session` | Consent screen | The person took more than 10 minutes, or the redirect URI stopped being valid | Start sign-in again |
| `401` with `WWW-Authenticate` | `/mcp/v3` | No token, an expired token, or a token issued for another endpoint | Refresh, or run the flow again for `/mcp/v3` |
| `421 Unrecognized OAuth hostname` | `/.well-known/*` | You called a host Apostra does not serve as an OAuth server | Use `https://api.apostra.com` |
| Browser says the request is blocked by CORS | Browser | The path is not one of the [browser paths](/v3/agent-sign-in#agents-that-run-in-a-browser), the request used `credentials: "include"`, or the origin is `null` | Call only those paths, send the bearer token in a header, and serve from `https` |

If sign-in still fails after you check this table, include the exact
`error` and `error_description`, the `client_id`, and the time of the failure
when you [contact support](/v3/support).

## Related

* [Authentication](/v3/authentication) covers credentials, resource binding, and
  how a person approves an MCP client.
* [MCP client setup](/v3/client-setup) has commands for Codex, Claude, and other
  hosts.
* [Connect to Microsoft Copilot Studio](/v2/reference/connect-copilot-studio) shows a
  manual client registration for a host whose wizard cannot register itself.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.