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

# OAuth Apps and MCP Clients (CIMD)

An OAuth app lets your users connect a tool to Affinity without sharing an API key. You create the
app in your own organization, and only your users can authorize it. Affinity calls this a second-party
app.

Use an OAuth app when a tool acts on behalf of a person and each person should sign in as
themselves. If a script acts as a single service account, an
[API key](/pages/external-api-v2/authentication) is simpler.

MCP clients that publish a Client ID Metadata Document (CIMD) don't need an app. You add them by their
URL instead. See [MCP clients with a metadata URL (CIMD)](#mcp-clients-with-a-metadata-url-cimd).

## Prerequisites

* **Plan**: Scale, Advanced, or Enterprise.
* **Access**: OAuth apps are in early access. If you don't see **OAuth App** under **New App** in
  Manage Apps, ask your Affinity contact to turn it on for your organization.
* **Permission**: `Manage all OAuth apps`. By default, only Administrators have it.
* **IP allowlist**: to set one, you may also need the `Manage IP allowlist` permission.

## Create the app

1. In Affinity, go to **Settings** → **Manage Apps**.
2. Click **New App**, then **OAuth App**. If you can't create API keys, the button is called
   **New OAuth App** instead.
3. Enter a **Name** and a **Description**. Your users see the name on the consent screen.
4. Choose the **Client type** (the default is **Public**):
   * **Public** is for apps whose code runs on the user's device, such as mobile, desktop or
     single-page apps, or anywhere else the code ships to the user. A
     public client has no secret and must use PKCE.
   * **Confidential** is for a server you control, where a secret can stay private. You get a client
     secret, shown once.
5. Enter one **Redirect URI** per line, as a full URL such as `https://example.com/callback`.
6. Choose the **Allowed scopes**, which decide what the app can do for your users. Pick at least one.
7. Optionally, set an **API version**, an **IP allowlist**, an icon and a website URL.
8. Click **Create**.

<Frame caption="Manage Apps lists your API keys and your OAuth apps in one table.">
  <img src="https://mintcdn.com/affinityco/C6ojaYDjJToaJjCs/images/oauth-apps-01-manage-apps-list.png?fit=max&auto=format&n=C6ojaYDjJToaJjCs&q=85&s=343a7425f0844476228476c016e2702d" alt="The Manage Apps settings page with the New App button" width="1240" height="545" data-path="images/oauth-apps-01-manage-apps-list.png" />
</Frame>

<Frame caption="The app creation form: name and client type.">
  <img src="https://mintcdn.com/affinityco/C6ojaYDjJToaJjCs/images/oauth-apps-02-create-form.png?fit=max&auto=format&n=C6ojaYDjJToaJjCs&q=85&s=259fd712368b7cfc22d42096a15ed597" alt="The New OAuth App form showing the name, description and client type fields" width="1100" height="720" data-path="images/oauth-apps-02-create-form.png" />
</Frame>

<Frame caption="Redirect URIs, allowed scopes and the IP allowlist, further down the same form.">
  <img src="https://mintcdn.com/affinityco/C6ojaYDjJToaJjCs/images/oauth-apps-03-scopes-and-allowlist.png?fit=max&auto=format&n=C6ojaYDjJToaJjCs&q=85&s=928b6bfe6cb5495edc5560924e33d626" alt="The New OAuth App form showing redirect URIs, allowed scopes and the IP allowlist" width="1100" height="600" data-path="images/oauth-apps-03-scopes-and-allowlist.png" />
</Frame>

After you click **Create**, Affinity opens the app's detail page, where you'll find the
**Client ID**. For a confidential app, this happens after you close the client secret dialog. A
client ID looks like `1f0c2b7a-3d4e-4a1b-9c8d-5e6f70819aa2`.

<Info>
  **A confidential app shows its client secret only once.** Copy it and store it like a password.
  Affinity keeps only a hash, so you can't view it again or rotate it. If you lose it, you'll need
  to delete the app and create a new one, which disconnects every user.
</Info>

<Frame caption="Affinity shows the client secret once.">
  <img src="https://mintcdn.com/affinityco/C6ojaYDjJToaJjCs/images/oauth-apps-04-client-secret.png?fit=max&auto=format&n=C6ojaYDjJToaJjCs&q=85&s=717a7a1d387e2a047fc31780cf53f78b" alt="The Client Secret Created dialog with a copy button" width="620" height="500" data-path="images/oauth-apps-04-client-secret.png" />
</Frame>

## MCP clients with a metadata URL (CIMD)

Some MCP clients don't need you to create an app. Instead, their `client_id` is a URL that points to a
JSON file about the client, called a Client ID Metadata Document (CIMD). For example, Claude uses
`https://claude.ai/oauth/mcp-oauth-client-metadata`.

### Built-in MCP clients

Clients such as Claude, ChatGPT and Notion are already set up. You can turn each one on or off in
**Settings** → **Affinity MCP**. For setup steps, see [Supported clients](/pages/mcp/supported-clients).

### Add an MCP client by its Client URL (CIMD)

To use another MCP client with a metadata URL, add it in Manage Apps.

<Note>
  Adding a client by its Client URL (CIMD) is in early access. If you don't see **Client URL (CIMD)**
  under **New App** in Manage Apps, ask your Affinity contact to turn it on.
</Note>

1. Go to **Settings** → **Manage Apps**, click **New App**, then **Client URL (CIMD)**. If you can't
   create API keys, click **Add by Client URL (CIMD)** instead.
2. Paste the client's metadata URL, such as `https://acme.example.com/oauth/client.json`, and click
   **Preview**.
3. Review the preview. Check the hostname: the name and logo come from the vendor, so anyone can set
   them. You'll also see a warning if every redirect URI is on localhost.
4. Optionally, narrow the **Allowed Scopes** or add an **IP Allowlist**.
5. Click **Add App**. Your users can now sign in with the client.

Nothing is saved until you click **Add App**. If Affinity can't read the URL, the form tells you why.

Affinity saves a copy of the document when you add the client. If the vendor updates it, open the
client and click **Refresh Metadata**. To change the allowed scopes or the IP allowlist, click
**Edit Details**. The name, website and redirect URIs always come from the document.

To remove the client, open it and click **Revoke App**. This disconnects everyone in your organization
who uses it. To bring it back, add it again with the same URL. Your users will need to connect again.

### CIMD metadata document requirements

If you build an MCP client, your metadata document must:

* Be served over `https` at the exact URL you use as `client_id`, with no redirects, and respond
  within 5 seconds.
* Set `client_id` to that same URL.
* Include `client_name` and at least one `redirect_uris` entry.
* Use redirect URIs on the same registrable domain as the `client_id`, or on localhost. For example, a
  `client_id` on `app.acme.example.com` can redirect to `https://auth.acme.example.com/callback` or
  `http://127.0.0.1/callback`.
* Include `code` in `response_types`, if you set it.
* Set `token_endpoint_auth_method` to `none` or `private_key_jwt`. Either way, Affinity treats the
  client as public: it uses PKCE and has no client secret.

## Scopes

| Scope | What it grants |
| - | - |
| `api` | Read and write through the Affinity APIs, as the user. |
| `api.read` | Read-only access through the Affinity APIs. |
| `mcp` | Read and write through the Affinity MCP server. It also satisfies `api`. |
| `mcp.read` | Read-only access through the MCP server. It also satisfies `api.read`. |
| `offline_access` | Keeps the token working after the user signs out of Affinity. Use it if your app runs in the background. |

`api` doesn't grant `mcp`, so ask for `mcp` if your app talks to the MCP server.

If you leave out the `scope` parameter when authorizing, Affinity grants every scope the app is
approved for. Ask only for the scopes you need.

## Authorize a user

Send the user to the authorize endpoint. A public client must include the PKCE parameters, and a
confidential client can leave them out.

<CodeGroup>
  ```http Public client theme={null}
  GET https://login.affinity.co/auth/oauth2/authorize
    ?client_id=<your client id>
    &redirect_uri=https://example.com/callback
    &response_type=code
    &scope=api%20offline_access
    &state=<random string you check later>
    &code_challenge=<base64url of the SHA-256 of your verifier>
    &code_challenge_method=S256
  ```

  ```http Confidential client theme={null}
  GET https://login.affinity.co/auth/oauth2/authorize
    ?client_id=<your client id>
    &redirect_uri=https://example.com/callback
    &response_type=code
    &scope=api%20offline_access
    &state=<random string you check later>
  ```
</CodeGroup>

The user signs in, reviews the scopes and clicks **Allow Access**. Affinity then redirects to your
redirect URI with `code` and `state`. Users can uncheck scopes before allowing, so check the `scope`
field in the token response instead of assuming you got everything you asked for.

<Frame caption="The consent screen. Your users can uncheck a scope before allowing access.">
  <img src="https://mintcdn.com/affinityco/C6ojaYDjJToaJjCs/images/oauth-apps-07-consent-screen.png?fit=max&auto=format&n=C6ojaYDjJToaJjCs&q=85&s=f1a7cf5ca6586e6926f1c602365d92b0" alt="The Affinity OAuth consent screen listing the requested scopes" width="640" height="670" data-path="images/oauth-apps-07-consent-screen.png" />
</Frame>

* `response_type` must be `code`, since it's the only supported flow.
* `redirect_uri` must exactly match a registered URI. Only `localhost`, `127.0.0.1` and `[::1]` can
  use a different port.
* PKCE supports `S256` only, not `plain`.
* **A public app must send PKCE.** A confidential app can send it too, but then it must also send
  the verifier in the next step.
* The code expires after **5 minutes** and can only be used once.

## Exchange the code for tokens

A public client sends `client_id` and `code_verifier` in the request body. A confidential client
uses HTTP Basic authentication, with the client ID as the username and the client secret as the
password.

<CodeGroup>
  ```bash Public client theme={null}
  curl -X POST https://login.affinity.co/auth/oauth2/token \
    -H "Content-Type: application/x-www-form-urlencoded" \
    -d grant_type=authorization_code \
    -d "code=<the code>" \
    -d redirect_uri=https://example.com/callback \
    -d "client_id=<your client id>" \
    -d "code_verifier=<your verifier>"
  ```

  ```bash Confidential client theme={null}
  curl -X POST https://login.affinity.co/auth/oauth2/token \
    -u "<your client id>:<your client secret>" \
    -H "Content-Type: application/x-www-form-urlencoded" \
    -d grant_type=authorization_code \
    -d "code=<the code>" \
    -d redirect_uri=https://example.com/callback
  ```
</CodeGroup>

A confidential client can also send `client_id` and `client_secret` as form fields, but if the
Basic header is present, Affinity ignores them. If the client sent a PKCE challenge when authorizing,
it must also send `code_verifier` here.

The response looks like this:

```json theme={null}
{
  "access_token": "...",
  "refresh_token": "...",
  "token_type": "bearer",
  "expires_in": 86400,
  "refresh_token_expires_in": 7776000,
  "scope": "api offline_access"
}
```

Access tokens last **1 day** and refresh tokens last **90 days**.

## Refresh a token

<CodeGroup>
  ```bash Public client theme={null}
  curl -X POST https://login.affinity.co/auth/oauth2/token \
    -H "Content-Type: application/x-www-form-urlencoded" \
    -d grant_type=refresh_token \
    -d "refresh_token=<the refresh token>" \
    -d "client_id=<your client id>"
  ```

  ```bash Confidential client theme={null}
  curl -X POST https://login.affinity.co/auth/oauth2/token \
    -u "<your client id>:<your client secret>" \
    -H "Content-Type: application/x-www-form-urlencoded" \
    -d grant_type=refresh_token \
    -d "refresh_token=<the refresh token>"
  ```
</CodeGroup>

A public client gets a new refresh token each time and the old one stops working, so always store
the new one. A confidential client can keep using the same refresh token until it expires, even
though the response includes a new one.

## Call the API

```bash theme={null}
curl https://api.affinity.co/v2/persons \
  -H "Authorization: Bearer <access_token>" \
  -H "X-Affinity-Api-Version: 2026-09-17"
```

Read-only endpoints need `api.read`, and endpoints that change data need `api`. Some read-only
endpoints use `POST`, such as `POST /v2/notes/search`.

## API version

For each request, Affinity picks the API version in this order:

1. The `X-Affinity-Api-Version` header.
2. The app's pinned version.
3. `2024-01-01`, the oldest published version.

New apps are pinned to the current version when you create them. To be sure which version you get,
send the header. An unsupported version returns `400` with a validation error on the header. See
[Versioning](/pages/external-api-v2/versioning) for the published versions.

## Discovery

Affinity publishes its OAuth metadata at
`https://login.affinity.co/.well-known/oauth-authorization-server`, and its signing keys at
`https://login.affinity.co/.well-known/jwks.json`. Clients that read RFC 8414 metadata don't need
any endpoint configuration.

## Connect the app to the Affinity MCP server

Your OAuth app can also call the hosted Affinity MCP server at `https://mcp.affinity.co/mcp`.

1. Give the app the `mcp` scope, or `mcp.read` for read-only access. Add `offline_access` if the
   client runs in the background.
2. Point your MCP client at `https://mcp.affinity.co/mcp`.
3. The client reads `https://mcp.affinity.co/.well-known/oauth-protected-resource/mcp`, finds
   `https://login.affinity.co` and runs the flow above.

The MCP server passes your token to the Affinity API unchanged, so the same scope rules and rate limits
apply as for a direct API call.

## Restrict the app to known IP addresses

With an IP allowlist, the app accepts calls only from the addresses you list.

* Add up to 100 entries, each an IPv4 address, IPv6 address or CIDR range.
* Leave the list empty to allow any address.
* Calls from other addresses return `403` with `Access denied`.
* The allowlist applies only to Affinity API v2, not to the older v1 API.

## Rate limits

OAuth apps share the same per-user, per-organization and concurrent limits as API keys, and get the
same rate limit headers. When you go over a limit, the API returns `429`. See
[Rate limits](/pages/external-api-v2/rate-limits) for the current values.

## Errors

| What happened | Where | Response |
| - | - | - |
| Unknown client, or a redirect URI that is not registered | Authorize | An error page. Affinity does not redirect, because the URI is not trusted. |
| The user pressed Cancel | Authorize | Redirect with `error=access_denied` |
| Scope asked for is not approved for the app | Authorize | Redirect with `error=invalid_scope` |
| A public app sent no PKCE challenge | Authorize | Redirect with `error=invalid_request` |
| Wrong or missing client secret | Token | `400` with `error=invalid_client` |
| Code expired, or already used | Token | `400` with `error=invalid_grant` |
| `redirect_uri` differs from the authorize call | Token | `400` with `error=invalid_request` |
| Token lacks the scope the endpoint needs | API | `403` with `Insufficient scope` |
| Caller IP is not on the allowlist | API | `403` with `Access denied` |
| Access token expired | API | `401`. Use the refresh token to get a new access token. |
| The app was revoked | API | `401`. Revoking deletes the app, so create a new one and ask your users to authorize it. |
| Too many requests | API | `429` with `Rate limit exceeded` |

## Manage the app

Go to **Settings** → **Manage Apps** and click the app. The detail page shows the client ID,
redirect URIs, scopes, allowed IPs, authentication method, creator and pinned version.

<Frame caption="The app detail page: client ID, redirect URIs, scopes, allowed IPs and the pinned API version.">
  <img src="https://mintcdn.com/affinityco/C6ojaYDjJToaJjCs/images/oauth-apps-05-app-detail.png?fit=max&auto=format&n=C6ojaYDjJToaJjCs&q=85&s=dfccbbd7fcea8aef7ad241667ecaa33c" alt="The OAuth app detail page" width="1200" height="640" data-path="images/oauth-apps-05-app-detail.png" />
</Frame>

From there, you can edit the name, description, redirect URIs, scopes, IP allowlist and version.

<Warning>
  **Revoking an app you created deletes it, and you can't undo it.** The app loses API access, every user in
  your organization who connected it is disconnected, and its client ID stops working, so it can't
  be authorized again. To restore access, create a new app and ask your users to authorize it.
</Warning>

<Frame caption="Revoking disconnects every user.">
  <img src="https://mintcdn.com/affinityco/C6ojaYDjJToaJjCs/images/oauth-apps-06-revoke-confirmation.png?fit=max&auto=format&n=C6ojaYDjJToaJjCs&q=85&s=402a9fb9ca5555ba18564293be262729" alt="The revoke confirmation dialog" width="1200" height="700" data-path="images/oauth-apps-06-revoke-confirmation.png" />
</Frame>

## Current limits

* There's no token revocation endpoint. A token stops working when it expires, the user disconnects
  the app, or the app is revoked. Without `offline_access`, it also stops when the user signs out of
  Affinity.


## Related topics

- [Claude](/pages/mcp/claude.md)
- [ChatGPT](/pages/mcp/chatgpt.md)
- [Copilot](/pages/mcp/copilot.md)
- [Security](/pages/mcp/security.md)
- [Troubleshooting](/pages/mcp/troubleshooting.md)


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