Authentication model
How OAuth, API keys, and JWTs fit together across the platform.
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, and for the plain-English version written for agents see 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:
- Read your CRM data — escrows, listings, clients, leads, and calendar
- Create and update records when you ask
- Draft emails and texts for your approval — nothing sends without your okay
- 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.
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):
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
403on every endpoint. Grant{"all":["read","write"]}for broad access, or per-resource grants like{"clients":["read","write"]}, and adjust later withPATCH /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:
curl -X POST "https://api.actuallycare.com/v1/auth/login" \
-H "Content-Type: application/json" \
-d '{"email": "[email protected]", "password": "YOUR_PASSWORD"}'The response follows the standard envelope:
{
"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:
Authorization: Bearer YOUR_ACCESS_TOKENWhat to know:
- Access-token lifetime is role-based, ranging from 15 minutes to 8 hours. The
expiresInfield is a duration string like"15m"or"8h"— not seconds — so parse the string or decode the JWT'sexpclaim 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.