# Authentication model

> How OAuth, API keys, and JWTs fit together across the platform.

<!-- Source: https://docs.actuallycare.com/concepts/authentication -->

ActuallyCare has one backend and three credential types. Which one you need depends entirely on who — or what — is making the call. This page explains the model; for copy-paste REST details see [API authentication](/api/authentication), and for the plain-English version written for agents see [permissions and safety](/agents/permissions-and-safety).

## Who uses what

| You are... | You use... | Why |
|---|---|---|
| An agent connecting Claude to your CRM | OAuth 2.1 | Claude handles the whole flow — you just log in and approve once |
| A developer writing a server-side integration or script | API key | One long-lived secret, no login flow, no token refresh logic |
| A developer building an app where your users sign in | JWT | Short-lived tokens tied to a real user session, with refresh rotation |

## OAuth 2.1 — for Claude users

When you add ActuallyCare as a connector in Claude, Claude starts an OAuth 2.1 flow with PKCE (Proof Key for Code Exchange — a standard protection against intercepted authorization codes). You never see a token.

What you do see is the consent page at `app.actuallycare.com/mcp/authorize`, titled "Grant Claude access to ActuallyCare". It lists exactly what you're approving:

1. Read your CRM data — escrows, listings, clients, leads, and calendar
2. Create and update records when you ask
3. Draft emails and texts for your approval — nothing sends without your okay
4. Archive or delete records, with confirmation

If you're not already logged in to ActuallyCare, you go through the normal login first.

After approval:

- **Access tokens last 24 hours.**
- **Refresh tokens rotate automatically**, so you stay connected without re-approving.

To disconnect, remove the connector in Claude's own settings (Settings, then Connectors). That's the revocation path — there is no separate ActuallyCare page for revoking Claude's access.

Custom MCP clients can also use OAuth 2.1 via dynamic client registration — see [building custom apps](/mcp/custom-apps).

## API keys — for servers and scripts

API keys are the simplest credential for code that runs unattended. Send the key in the `X-API-Key` header (the header `API-Key` also works):

```bash
curl "https://api.actuallycare.com/v1/listings" \
  -H "X-API-Key: YOUR_API_KEY"
```

Key facts:

- Keys are **64-character hex strings** — there's no prefix to look for.
- The full key is shown **once**, at creation. It's stored hashed; afterwards the UI only shows the first 8 and last 4 characters. If you lose a key, create a new one.
- A key acts as the user who created it, so requests inherit that user's role and visibility.
- Scopes **grant** a key its access — they don't narrow it. A key created without scopes has no access at all and gets `403` on every endpoint. Grant `{"all":["read","write"]}` for broad access, or per-resource grants like `{"clients":["read","write"]}`, and adjust later with `PATCH /v1/api-keys/:id/scopes`. (Scopes are an API-key concept — JWT users aren't scope-checked.)

Create keys in the app under Settings, in the API Keys tab (`app.actuallycare.com/settings?tab=api`). Every user can create their own keys — each key acts as you, with your role's visibility. You can also create keys via the API itself: `POST /v1/api-keys` with a `name`, a `scopes` object, and an optional `expiresInDays` between 1 and 365. If you omit `expiresInDays`, the key never expires — there's no implicit default, so set an expiry explicitly.

Standard hygiene applies: keep keys in environment variables, never commit them to source control, never ship them in client-side code, and rotate them periodically.

## JWTs — for user-session apps

If you're building an app where each user logs in with their own ActuallyCare account, use JWT auth. Log in:

```bash
curl -X POST "https://api.actuallycare.com/v1/auth/login" \
  -H "Content-Type: application/json" \
  -d '{"email": "you@example.com", "password": "YOUR_PASSWORD"}'
```

The response follows the standard envelope:

```json
{
  "success": true,
  "data": {
    "user": {},
    "accessToken": "...",
    "refreshToken": "...",
    "expiresIn": "15m",
    "tokenType": "Bearer"
  },
  "timestamp": "2026-06-11T17:32:08.123Z"
}
```

Then send the access token on every request:

```text
Authorization: Bearer YOUR_ACCESS_TOKEN
```

What to know:

- **Access-token lifetime is role-based**, ranging from 15 minutes to 8 hours. The `expiresIn` field is a **duration string** like `"15m"` or `"8h"` — not seconds — so parse the string or decode the JWT's `exp` claim rather than assuming a number.
- **Refresh with `POST /v1/auth/refresh`.** The refresh token rotates on every use, with a 30-day sliding window and a 90-day absolute cap. After that, the user logs in again.
- **The same JWT also works against the MCP server**, so a user-session app can call MCP tools on the user's behalf.

## One backend, three front doors

The credentials map onto the two surfaces like this:

| Surface | Accepts |
|---|---|
| REST API (`api.actuallycare.com/v1`) | API key, JWT |
| MCP server (`mcp.actuallycare.com/mcp`) | OAuth 2.1, API key, JWT |

Whichever door you come through, you're acting as a specific user with a specific role — authentication decides who you are, and the role decides what you can see and change. How roles shape data visibility is covered in [the data model](/concepts/data-model).
