Skip to main content

Profile Overview

The Patient Portal Profile API lets the authenticated patient read and update their own profile (the /me resource). All access is self-only: the JWT subject is the only patient that can be read or updated through these endpoints — there is no :userId parameter and no admin-on-behalf path.

Endpoints

#MethodPathPurpose
1GET/api/v1/users/meReturn the authenticated patient's own profile
2PATCH/api/v1/users/meUpdate fields on the authenticated patient's own profile

Authentication

Both endpoints require a successful /verify-otp exchange first. The resulting access JWT and the tenant API key are presented on every call.

HeaderRequiredDescription
cv-api-keyYesTenant API key. Resolves the calling organization. Missing → 400 VALIDATION_ERROR.
AuthorizationYesBearer <accessToken> from POST /api/v1/users/auth/verify-otp. Missing or malformed → 401.
Content-TypeYes (PATCH only)Must be application/json.

The patientPortalAuth() middleware enforces, in order:

  1. cv-api-key header is present.
  2. Authorization header starts with Bearer .
  3. JWT verifies.
  4. JWT claim type === 'patient-portal' (blocks staff/Firebase tokens).
  5. cv-api-key resolves to an organization, and the resolved organization.id matches the JWT's organizationId claim (blocks cross-tenant token replay).
  6. The user referenced by the JWT still exists.

Any thrown error in this chain is collapsed into 401 VALIDATION_ERROR "Invalid or expired token".

Common Response Envelope

Successful responses always wrap the profile in data.profile:

{
"status": 200,
"success": true,
"data": { "profile": { "...": "see Profile Object below" } }
}

Error responses follow:

{ "status": 400, "success": false, "error": "<message>", "code": "<CODE>" }

Profile Object

The profile object always contains exactly these 17 fields, in this shape. Any underlying value of undefined is normalized to null in the response.

FieldTypeNullableNotes
idstring (UUID)NoThe patient's User.id. Same value as patientId from verify-otp.
emailstringNoAlways present. Read-only via this API.
firstNamestring | nullYes
lastNamestring | nullYes
phoneNumberstring | nullYesE.164 if present.
dobISO-8601 datetime | nullYesStored as Date; serialized as YYYY-MM-DDTHH:mm:ss.sssZ.
gender"MALE" | "FEMALE" | "OTHER" | nullYesServer enum.
addressstring | nullYes
address2string | nullYes
citystring | nullYes
statestring | nullYes
countrystring | nullYes2-letter uppercase ISO 3166-1 alpha-2 (server uppercases on write).
postalCodestring | nullYes
allergiesstring | nullYesFree-text.
healthConditionsstring | nullYesFree-text.
currentMedicationsstring | nullYesFree-text.
createdAtISO-8601 datetimeNoRead-only. The user's account-creation timestamp.

Restricted Fields and the Active-Case Rule

The fields firstName, lastName, dob, gender are gated by an active-case check on PATCH /me. The intent is to preserve identity continuity on case records that are mid-flight.

A case is considered active if its status is one of:

  • Approved
  • Assigned
  • InProgress
  • NoDecision
  • Rejected

Statuses outside that list — notably Open — are not active.

The check is per-organization: a patient with active cases in a different org may still update these fields when authenticated against an org with no active cases.

When the rule fires, no fields are updated — including the non-restricted fields in the same body. Surface a clear "complete or close active cases first" message and let the user retry without those four fields.

Server-Side Coercions

These transformations happen on the server and are observable in the response:

  • country — uppercased: "us""US".
  • dob — accepted as YYYY-MM-DD (string), persisted as Date, returned as ISO-8601 datetime.
  • undefined fields — normalized to null in the response.
  • Unknown body keys — silently dropped.

Security Properties

  • Self-only access. GET /me and PATCH /me always operate on the JWT subject. There is no :userId parameter and no admin-on-behalf path.
  • Token type pinned. Only JWTs with type: 'patient-portal' reach the handler.
  • Cross-tenant defense. The JWT's organizationId is verified against the cv-api-key-resolved org on every call.
  • Identity continuity. firstName, lastName, dob, gender cannot be changed while the patient has active cases in the calling organization.
  • No email mutation surface. email is read-only via these endpoints.
  • Atomic update. Profile updates use a single prisma.user.update; partial failures are not possible.

Integrator Guidance

  • Always send Authorization: Bearer <accessToken> and cv-api-key together; either alone fails.
  • Refresh proactively before the 15-minute access-token expiry. Both endpoints surface a generic 401 VALIDATION_ERROR on any auth failure, so do not rely on the message to distinguish "expired" vs "wrong tenant".
  • Use null to clear nullable fields (address, address2, city, postalCode, allergies, healthConditions, currentMedications). Omitting a field leaves it unchanged.
  • For country, send any 2-letter ISO-3166-1 alpha-2 code in any case; expect uppercase back.
  • For dob, send YYYY-MM-DD; expect ISO-8601 back. Do not send MM/DD/YYYY.
  • For gender, only MALE, FEMALE, OTHER are accepted. Case-sensitive.
  • The PATCH response is the full updated profile, not a diff. Replace the local profile object on success.