Skip to content
For developers
View as Markdown

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 CRMOAuth 2.1Claude handles the whole flow — you just log in and approve once
A developer writing a server-side integration or scriptAPI keyOne long-lived secret, no login flow, no token refresh logic
A developer building an app where your users sign inJWTShort-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.

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

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_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:

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