> ## 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](https://developer.affinity.co/pages/external-api-v2/authentication.md) 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**.

![The Manage Apps settings page with the New App button](https://mintcdn.com/affinityco/C6ojaYDjJToaJjCs/images/oauth-apps-01-manage-apps-list.png?fit=max\&auto=format\&n=C6ojaYDjJToaJjCs\&q=85\&s=343a7425f0844476228476c016e2702d)

Manage Apps lists your API keys and your OAuth apps in one table.

![The New OAuth App form showing the name, description and client type fields](https://mintcdn.com/affinityco/C6ojaYDjJToaJjCs/images/oauth-apps-02-create-form.png?fit=max\&auto=format\&n=C6ojaYDjJToaJjCs\&q=85\&s=259fd712368b7cfc22d42096a15ed597)

The app creation form: name and client type.

![The New OAuth App form showing redirect URIs, allowed scopes and the IP allowlist](https://mintcdn.com/affinityco/C6ojaYDjJToaJjCs/images/oauth-apps-03-scopes-and-allowlist.png?fit=max\&auto=format\&n=C6ojaYDjJToaJjCs\&q=85\&s=928b6bfe6cb5495edc5560924e33d626)

Redirect URIs, allowed scopes and the IP allowlist, further down the same form.

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

> [!NOTE]
>
> **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.

![The Client Secret Created dialog with a copy button](https://mintcdn.com/affinityco/C6ojaYDjJToaJjCs/images/oauth-apps-04-client-secret.png?fit=max\&auto=format\&n=C6ojaYDjJToaJjCs\&q=85\&s=717a7a1d387e2a047fc31780cf53f78b)

Affinity shows the client secret once.

## 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](https://developer.affinity.co/pages/mcp/supported-clients.md).

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

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.

```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>
```

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.

![The Affinity OAuth consent screen listing the requested scopes](https://mintcdn.com/affinityco/C6ojaYDjJToaJjCs/images/oauth-apps-07-consent-screen.png?fit=max\&auto=format\&n=C6ojaYDjJToaJjCs\&q=85\&s=f1a7cf5ca6586e6926f1c602365d92b0)

The consent screen. Your users can uncheck a scope before allowing access.

* `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.

```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
```

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

```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>"
```

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](https://developer.affinity.co/pages/external-api-v2/versioning.md) 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](https://developer.affinity.co/pages/external-api-v2/rate-limits.md) 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.

![The OAuth app detail page](https://mintcdn.com/affinityco/C6ojaYDjJToaJjCs/images/oauth-apps-05-app-detail.png?fit=max\&auto=format\&n=C6ojaYDjJToaJjCs\&q=85\&s=dfccbbd7fcea8aef7ad241667ecaa33c)

The app detail page: client ID, redirect URIs, scopes, allowed IPs and the pinned API version.

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.

![The revoke confirmation dialog](https://mintcdn.com/affinityco/C6ojaYDjJToaJjCs/images/oauth-apps-06-revoke-confirmation.png?fit=max\&auto=format\&n=C6ojaYDjJToaJjCs\&q=85\&s=402a9fb9ca5555ba18564293be262729)

Revoking disconnects every user.

## 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](https://developer.affinity.co/pages/mcp/claude.md)
- [ChatGPT](https://developer.affinity.co/pages/mcp/chatgpt.md)
- [Copilot](https://developer.affinity.co/pages/mcp/copilot.md)
- [Security](https://developer.affinity.co/pages/mcp/security.md)
- [Troubleshooting](https://developer.affinity.co/pages/mcp/troubleshooting.md)


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