Skip to content
For developers

Authentication API

REST endpoints for authentication — request and response reference with examples.

Session management for user-facing apps: register, log in, refresh tokens, and manage sessions. If you are building a server-side integration, you usually want an API key instead.

Base URL: https://api.actuallycare.com/v1 · Errors use the standard envelope — see Errors.

Register new user#

POST/v1/auth/register

Creates a new user account and always returns a signed JWT.

Waitlist gate: when the platform is accepting signups (ACCEPT_NEW_USERS=true, or the registration arrives via a brokerage invite) the account is created with status active and onboarding starts immediately. Otherwise the account is created with status waitlisted — the returned JWT can only reach /auth/* and /waitlist/* endpoints; every other endpoint returns 403 with error.code ACCOUNT_NOT_ACTIVE until an admin approves the account. Both outcomes return 201 with the same shape; check data.status and follow data.redirect.

Request body#

FieldTypeRequiredDescription
emailstringYesUser's email address (login identifier, stored lowercase) · Format: email
usernamestringYesUnique username — letters, numbers, and underscores only (stored lowercase)
passwordstringYesAccount password — min 8 chars, at least one uppercase letter and one number
firstNamestringYesUser's first name
lastNamestringYesUser's last name
roleenumNoOptional starting role for the account · One of: agent, broker, lender, vendor · Default: "agent"
emailKindenumNoOptional classification of the provided email address · One of: personal, work, offers
phonestringNoOptional phone number (required for SMS consent to be recorded)
smsConsentbooleanNoOptional explicit consent to receive SMS (recorded only when phone is provided)

Responses#

StatusMeaning
201User registered — data.status is active or waitlisted depending on the ACCEPT_NEW_USERS gate
400Invalid request data
409A user with this email or username already exists (case-insensitive check)
429Rate limit exceeded — too many requests in the current window. Wait for the window to reset (see Retry-After) before retrying.
500Internal server error

Example request#

cURL
curl -X POST "https://api.actuallycare.com/v1/auth/register" \
  -H "Content-Type: application/json" \
  -d '{
    "email": "[email protected]",
    "username": "sarahchen",
    "password": "your-password",
    "firstName": "Sarah",
    "lastName": "Chen",
    "phone": "(661) 555-0142"
  }'

Example response (201)#

{
  "success": true,
  "data": {
    "user": {
      "id": "b7e9d2c4-5f1a-4e8b-9c3d-2a6f8e0b4d17",
      "email": "[email protected]",
      "firstName": "Sarah",
      "lastName": "Chen",
      "role": [
        "agent"
      ],
      "isActive": true,
      "status": "active"
    },
    "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6ImI3ZTlkMmM0LTVmMWEtNGU4Yi05YzNkLTJhNmY4ZTBiNGQxNyJ9.Qm4x8pT2cVhLk9wYzR0aXNlY3VyZQ",
    "status": "active",
    "redirect": "/get-started",
    "onboarding": {
      "sampleDataGenerated": true,
      "tutorialAvailable": true,
      "nextStep": "/onboarding/welcome"
    }
  },
  "message": "User registered successfully"
}

Login user#

POST/v1/auth/login

Authenticate with email OR username plus password. On full success, returns a JWT access token whose lifetime is role-based (expiresIn duration string: 15m for system_admin and default, 4h for broker/team_lead, 8h for agent) and sets a refreshToken httpOnly cookie (default 30 days, JWT_REFRESH_TOKEN_EXPIRY_DAYS).

The 200 body has four possible data shapes: full login, TOTP challenge (requiresTOTP), SMS challenge (requiresSMS), or waitlisted account.

Request body#

FieldTypeRequiredDescription
emailstringNoUser's email address (provide email OR username; case-insensitive)
usernamestringNoUsername (provide email OR username; case-insensitive)
passwordstringYesAccount password

Responses#

StatusMeaning
200Password verified — data is one of four shapes (full login, TOTP required, SMS required, waitlisted)
401Invalid credentials (INVALID_CREDENTIALS) or disabled account (ACCOUNT_DISABLED)
403Account suspended (ACCOUNT_SUSPENDED)
423Account locked (ACCOUNT_LOCKED). The 5th consecutive failed attempt locks the account for 30 minutes; attempts made WHILE locked return this 423 with details.locked_until (the failure that triggers the lock itself returns 401).
429Rate limit exceeded — too many requests in the current window. Wait for the window to reset (see Retry-After) before retrying.
500Internal server error

Example request#

cURL
curl -X POST "https://api.actuallycare.com/v1/auth/login" \
  -H "Content-Type: application/json" \
  -d '{
    "email": "[email protected]",
    "username": "sarahchen",
    "password": "YourPassword123!"
  }'

Example response (200)#

{
  "success": true,
  "data": {
    "user": {
      "id": "b7e9d2c4-5f1a-4e8b-9c3d-2a6f8e0b4d17",
      "email": "[email protected]",
      "username": "sarahchen",
      "firstName": "Sarah",
      "lastName": "Chen",
      "role": [
        "agent"
      ],
      "isActive": true,
      "teamId": "3c1e8f5a-7d2b-4a9c-b6e4-0f8d2c5a7e91",
      "teamName": "Chen Home Team",
      "brokerageId": "9e4b2d7f-1a6c-4e8b-a3d5-6c0f9b2e4d78",
      "corporationName": "Golden Empire Realty",
      "scopeLevel": "team",
      "verticals": [
        "realtor"
      ],
      "managedTeams": []
    },
    "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6ImI3ZTlkMmM0LTVmMWEtNGU4Yi05YzNkLTJhNmY4ZTBiNGQxNyJ9.Qm4x8pT2cVhLk9wYzR0aXNlY3VyZQ",
    "accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6ImI3ZTlkMmM0LTVmMWEtNGU4Yi05YzNkLTJhNmY4ZTBiNGQxNyJ9.Qm4x8pT2cVhLk9wYzR0aXNlY3VyZQ",
    "refreshToken": "9c2f8e4a1d6b3c5e7f0a2b4d6e8f1a3c5b7d9e0f2a4c6e8b1d3f5a7c9e0b2d4f",
    "expiresIn": "8h",
    "tokenType": "Bearer",
    "requiresTOTPSetup": false
  },
  "message": "Login successful"
}

Refresh access token#

POST/v1/auth/refresh

Exchanges the refresh token (httpOnly refreshToken cookie, or refreshToken in the request body for cookie-less clients) for a new access token. The refresh token is rotated on every call — the previous one is invalidated and the new one is set on the same httpOnly cookie. A device-fingerprint mismatch (different IP + user agent than the token was issued to) revokes the whole token family and returns 401 FINGERPRINT_MISMATCH.

Request body#

FieldTypeRequiredDescription
refreshTokenstringNoRefresh token — only needed when the httpOnly cookie is unavailable (e.g. native clients)

Responses#

StatusMeaning
200Token refreshed (a rotated refreshToken cookie is also set)
401Missing, invalid, expired, or revoked refresh token (NO_REFRESH_TOKEN, INVALID_REFRESH_TOKEN, or FINGERPRINT_MISMATCH)
429Rate limit exceeded — too many requests in the current window. Wait for the window to reset (see Retry-After) before retrying.
500Internal server error

Example request#

cURL
curl -X POST "https://api.actuallycare.com/v1/auth/refresh" \
  -H "Content-Type: application/json" \
  -d '{
    "refreshToken": "your-refreshToken"
  }'

Example response (200)#

{
  "success": true,
  "data": {
    "accessToken": "example accessToken",
    "expiresIn": "8h",
    "refreshTokenExpiresIn": "30d"
  }
}

Logout user#

POST/v1/auth/logout

Revokes the current refresh token (if present) and clears the refreshToken cookie. Succeeds even without a token — the endpoint requires no authentication and always returns 200 for a well-formed request.

Responses#

StatusMeaning
200Logged out successfully (refreshToken cookie cleared)
429Rate limit exceeded — too many requests in the current window. Wait for the window to reset (see Retry-After) before retrying.
500Internal server error

Example request#

cURL
curl -X POST "https://api.actuallycare.com/v1/auth/logout"

Example response (200)#

{
  "success": true,
  "message": "Logged out successfully"
}

Verify token validity#

GET/v1/auth/verify

Checks if the JWT access token is valid. Returns the same full profile payload as GET /auth/profile (both are served by the same handler).

Responses#

StatusMeaning
200Token is valid — body matches GET /auth/profile
401Token missing, invalid, or expired
429Rate limit exceeded — too many requests in the current window. Wait for the window to reset (see Retry-After) before retrying.
500Internal server error

Example request#

cURL
curl "https://api.actuallycare.com/v1/auth/verify" \
  -H "Authorization: Bearer YOUR_JWT"

Example response (200)#

{
  "success": true,
  "data": {
    "user": {
      "id": "b7e9d2c4-5f1a-4e8b-9c3d-2a6f8e0b4d17",
      "email": "[email protected]",
      "role": [
        "agent"
      ]
    }
  }
}

Get user profile#

GET/v1/auth/profile

Returns authenticated user's profile information

Responses#

StatusMeaning
200Profile retrieved successfully (payload is nested under data.user)
401Unauthorized
429Rate limit exceeded — too many requests in the current window. Wait for the window to reset (see Retry-After) before retrying.
500Internal server error

Example request#

cURL
curl "https://api.actuallycare.com/v1/auth/profile" \
  -H "Authorization: Bearer YOUR_JWT"

Example response (200)#

{
  "success": true,
  "data": {
    "user": {
      "id": "b7e9d2c4-5f1a-4e8b-9c3d-2a6f8e0b4d17",
      "email": "[email protected]",
      "username": "sarahchen",
      "firstName": "Sarah",
      "lastName": "Chen",
      "role": [
        "agent"
      ],
      "isActive": true,
      "status": "active",
      "emailVerified": true,
      "licenseVerified": false,
      "lastLogin": "2026-07-15T14:32:10.000Z",
      "createdAt": "2026-07-15T14:32:10.000Z",
      "updatedAt": "2026-07-15T14:32:10.000Z",
      "timezone": "America/Los_Angeles",
      "teamId": "550e8400-e29b-41d4-a716-446655440000",
      "teamName": "example teamName",
      "brokerageId": "550e8400-e29b-41d4-a716-446655440000",
      "aiPlanTier": "free",
      "subscriptionTier": "free",
      "subscriptionStatus": "none",
      "hasStripeCustomer": false,
      "scopeLevel": "personal",
      "managedTeams": [
        {}
      ]
    }
  }
}

Update user profile#

PUT/v1/auth/profile

Updates authenticated user's profile information

Request body#

FieldTypeRequiredDescription
firstNamestringNo
lastNamestringNo
currentPasswordstringNoRequired when changing the password · Format: password
newPasswordstringNoNew password — changing it revokes ALL refresh tokens (every session is logged out) · Format: password
homeCitystringNo
homeStatestringNo
homeZipstringNo
homeLatnumberNo
homeLngnumberNo
licensedStatesarray of stringsNo
searchRadiusMilesintegerNo
timezonestringNo

Responses#

StatusMeaning
200Profile updated successfully
400Validation error (no fields to update, missing currentPassword, or incorrect current password)
401Unauthorized
429Rate limit exceeded — too many requests in the current window. Wait for the window to reset (see Retry-After) before retrying.
500Internal server error

Example request#

cURL
curl -X PUT "https://api.actuallycare.com/v1/auth/profile" \
  -H "Authorization: Bearer YOUR_JWT" \
  -H "Content-Type: application/json" \
  -d '{
    "firstName": "Sarah",
    "lastName": "Chen",
    "currentPassword": "your-currentPassword",
    "newPassword": "your-newPassword",
    "homeCity": "Bakersfield",
    "homeState": "CA",
    "homeZip": "93309",
    "homeLat": 35.3433,
    "homeLng": -119.0587,
    "licensedStates": [
      "CA"
    ],
    "searchRadiusMiles": 25,
    "timezone": "America/Los_Angeles"
  }'

Example response (200)#

{
  "success": true,
  "data": {
    "id": "b7e9d2c4-5f1a-4e8b-9c3d-2a6f8e0b4d17",
    "email": "[email protected]",
    "firstName": "Sarah",
    "lastName": "Chen",
    "role": [
      "agent"
    ],
    "isActive": true,
    "home_city": "Bakersfield",
    "home_state": "CA",
    "home_zip": "93309",
    "home_lat": 35.3433,
    "home_lng": -119.0587,
    "licensed_states": [
      "CA"
    ],
    "search_radius_miles": 25
  },
  "message": "Profile updated successfully"
}

Logout from all devices#

POST/v1/auth/logout-all

Invalidates all refresh tokens for the authenticated user, logging them out from all sessions

Responses#

StatusMeaning
200Logged out from all devices successfully
401Unauthorized
429Rate limit exceeded — too many requests in the current window. Wait for the window to reset (see Retry-After) before retrying.
500Internal server error

Example request#

cURL
curl -X POST "https://api.actuallycare.com/v1/auth/logout-all" \
  -H "Authorization: Bearer YOUR_JWT"

Example response (200)#

{
  "success": true,
  "message": "Logged out from all devices successfully"
}

List active sessions#

GET/v1/auth/sessions

Returns list of active refresh tokens/sessions for authenticated user

Responses#

StatusMeaning
200Sessions retrieved successfully (array is nested under data.sessions)
401Unauthorized
429Rate limit exceeded — too many requests in the current window. Wait for the window to reset (see Retry-After) before retrying.
500Internal server error

Example request#

cURL
curl "https://api.actuallycare.com/v1/auth/sessions" \
  -H "Authorization: Bearer YOUR_JWT"

Example response (200)#

{
  "success": true,
  "data": {
    "sessions": [
      {
        "id": "3f8c1d7a-9e2b-4c6f-a1d8-7b4e2c9f5a30",
        "createdAt": "2026-06-28T17:03:22.481Z",
        "expiresAt": "2026-07-28T17:03:22.481Z",
        "ipAddress": "203.0.113.42",
        "userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36",
        "deviceInfo": {},
        "isCurrent": true
      }
    ]
  }
}