User API

All endpoints require a Firebase ID token in the Authorization: Bearer <token> header. A missing or invalid token returns 401 UNAUTHORIZED.

All endpoints with :id enforce that the authenticated user's UID matches :id. A mismatch returns 403 FORBIDDEN.


Get User

GET /user/:id?lat=<number>&lng=<number>&notificationToken=<string|null>

Auth: Bearer token; caller must match :id

Returns the user's profile, client configuration, and sunrise/sunset times for the given location. Creates the user document if it does not exist.

Query Parameters

Parameter Type Required Description
lat number yes −90 to 90; used to compute sun times
lng number yes −180 to 180; used to compute sun times
notificationToken string \| null yes FCM push token; pass null to clear

Response 200

{
  "user": {
    "id": "uid_abc123",
    "authUser": {/* AuthUser object */},
    "name": "Arjun Mehta",
    "email": null,
    "phoneNumber": "+919876543210",
    "photoURL": "https://example.com/photo.jpg",
    "isAnonymous": false,
    "notificationToken": "fcm-token-xyz",
    "rides": ["ride_id1", "ride_id2"], // populated from rides subcollection
    "settings": {
      "homeLocation": { "lat": 12.9716, "lng": 77.5946 },
      "notifications": true,
      "shareLocation": true,
    },
    "type": "subscriber",
    "status": "active",
    "subscriptionExpiryAt": "2026-06-01T00:00:00.000Z",
    "createdAt": "2025-01-01T00:00:00.000Z",
    "updatedAt": "2025-06-01T08:00:00.000Z",
  },
  "config": {
    // Nested intentionally: the outer `config` groups location-derived data
    // (sunrise/sunset) with the inner `config` block from getClientConfig().
    "config": {
      "appSHA1": "string | null",
      "revenueCatAPIKey": "string",
      "sendCrashlyticsData": true,
      "urlAboutUs": "https://...",
      "urlPrivacy": "https://...",
      "urlTerms": "https://...",
    },
    "sunrise": "06:15",
    "sunset": "18:42",
  },
}

Errors

Status Code Condition
401 UNAUTHORIZED No valid Firebase ID token
403 FORBIDDEN Authenticated user UID ≠ :id

Save Settings

POST /user/:id/settings

Auth: Bearer token; caller must match :id

Request

{
  "homeLocation": { "lat": 12.9716, "lng": 77.5946 }, // null to clear
  "notifications": true,
  "shareLocation": false,
}

Response 200

{ "success": true }

Errors

Status Code Condition
401 UNAUTHORIZED No valid Firebase ID token
403 FORBIDDEN Authenticated user UID ≠ :id

Save Notification Token

POST /user/:id/notification-token

Auth: Bearer token; caller must match :id

Request

{ "token": "fcm-token-xyz" } // null to clear

Response 200

{ "success": true }

Errors

Status Code Condition
401 UNAUTHORIZED No valid Firebase ID token
403 FORBIDDEN Authenticated user UID ≠ :id

Save Alternative Details

POST /user/:id/alt

Auth: Bearer token; caller must match :id

Updates name, phone number, or photo URL. Only non-null fields are written.

Planned (email verification side-effect) — not yet live. Providing email when authUser.email is null saves the address to Firestore with isEmailVerified: false and enqueues SendVerificationEmailActivity on the worker to send a verification email via Resend. The email is not active until verified — see Verify Email Confirmation (Web).

Request

{
  "name": "Arjun Mehta", // null to skip
  "phoneNumber": "+919876543210", // null to skip; E.164 format
  "photoURL": "https://example.com/photo.jpg", // null to skip
  "email": "rider@example.com", // null to skip; only accepted when authUser.email is null
}

Response 200

{ "success": true }

Errors

Status Code Condition
400 INVALID_FIELD phoneNumber does not match E.164 pattern
401 UNAUTHORIZED No valid Firebase ID token
403 FORBIDDEN Authenticated user UID ≠ :id
409 EMAIL_ALREADY_SET email provided but authUser.email is already non-null
409 EMAIL_ALREADY_IN_USE email is already associated with another account

Resend Verification Email

Planned — not yet live.

POST /user/:id/verify-email

Auth: Bearer token; caller must match :id

Re-enqueues SendVerificationEmailActivity. If an unused, non-expired verificationTokens doc already exists for this user, the activity reuses it (same link is resent). A new token is generated only when the existing one has expired or been used.

Request — empty body

Response 200

{ "success": true }

Errors

Status Code Condition
400 NO_EMAIL_SET user.email is null; no email to verify
400 EMAIL_ALREADY_VERIFIED isEmailVerified is already true
401 UNAUTHORIZED No valid Firebase ID token
403 FORBIDDEN Authenticated user UID ≠ :id
429 TOO_MANY_REQUESTS Resend rate limit hit

Verify Email Confirmation (Web)

Planned — not yet live.

GET https://95octane.com/verify-email?token=<token>

Astro SSR route. Looks up verificationTokens/{token}; validates expiresAt > now() and usedAt == null; writes isEmailVerified: true on the user document and sets usedAt on the token. Renders a static confirmation page on success; error page for expired or already-used tokens. No Firebase ID token required — the token in the URL is the only credential.


Delete User

DELETE /user/:id

Auth: Bearer token; caller must match :id

Sets status: "deleted", records deletedAt, and revokes all active Firebase Auth sessions. Permanent data removal is handled by a background workflow after a 7-day grace period (see worker/workflows/user.md).

Request — empty body

Response 202

{ "success": true }

Errors

Status Code Condition
401 UNAUTHORIZED No valid Firebase ID token
403 FORBIDDEN Authenticated user UID ≠ :id

List Favorites

GET /user/:id/favorites

Auth: Bearer token; caller must match :id

Returns all saved favorite locations. If settings.homeLocation is set, it is injected as the first item with id: "home-location" and type: "home".

Response 200

{
  "favorites": [
    {
      "id": "home-location", // synthetic; only present when homeLocation is set
      "title": "Home",
      "type": "home",
      "latitude": 12.9716,
      "longitude": 77.5946,
      "placeId": null,
      "createdAt": "2025-06-01T08:00:00.000Z",
      "updatedAt": "2025-06-01T08:00:00.000Z",
    },
    {
      "id": "fav_abc123",
      "title": "Office",
      "type": "destination",
      "latitude": 12.9352,
      "longitude": 77.6245,
      "placeId": "ChIJbU60yXAWrjsR4E9-UejD3_g",
      "createdAt": "2025-06-01T08:00:00.000Z",
      "updatedAt": "2025-06-01T08:00:00.000Z",
    },
  ],
}

Errors

Status Code Condition
401 UNAUTHORIZED No valid Firebase ID token
403 FORBIDDEN Authenticated user UID ≠ :id

Add Favorite

POST /user/:id/favorite

Auth: Bearer token; caller must match :id

Reserved type value: type: "home" is server-managed. The "Home" favorite is synthesized on every GET /user/:id/favorites response from the user's settings.homeLocation (set via POST /user/:id/settings) — it is not stored in the favorites subcollection. Clients must not submit type: "home" here; use POST /user/:id/settings with homeLocation instead.

Request

{
  "title": "Office", // required; 3–100 chars
  "type": "destination", // required; one of origin | destination | meetingPoint | haltPoint | restaurant | fuelStation | other ("home" is reserved)
  "latitude": 12.9352, // required
  "longitude": 77.6245, // required
  "placeId": "ChIJbU60yXAWrjsR4E9-UejD3_g", // required; null if no Place ID
}

Response 201

{
  "id": "fav_abc123",
  "title": "Office",
  "type": "destination",
  "latitude": 12.9352,
  "longitude": 77.6245,
  "placeId": "ChIJbU60yXAWrjsR4E9-UejD3_g",
  "createdAt": "2025-06-01T08:00:00.000Z",
  "updatedAt": "2025-06-01T08:00:00.000Z",
}

Errors

Status Code Condition
400 MISSING_FIELD title, type, latitude, longitude, or placeId not provided
400 INVALID_FIELD title < 3 or > 100 chars; invalid type; placeId present but < 6 chars
401 UNAUTHORIZED No valid Firebase ID token
403 FORBIDDEN Authenticated user UID ≠ :id

Remove Favorite

DELETE /user/:id/favorite/:favoriteId

Auth: Bearer token; caller must match :id

Request — empty body

Response 200

{ "success": true }

Errors

Status Code Condition
401 UNAUTHORIZED No valid Firebase ID token
403 FORBIDDEN Authenticated user UID ≠ :id
404 NOT_FOUND Favorite with :favoriteId does not exist for this user

Add Authentication Provider

No server endpoint required.

Linking an additional sign-in provider (Google or Apple) is a client-side Firebase Auth SDK operation. The client calls linkWithCredential() on the FirebaseUser object directly.

The server reflects the result automatically: the next GET /user/:id call returns the updated authUser.provider array (e.g. ["google.com", "apple.com"]) from the Firebase Auth token metadata. No server-side write or endpoint is needed.


Buddy System

All buddy endpoints are Planned — not yet live.

All buddy endpoints require a Firebase ID token in Authorization: Bearer <token>. A missing or invalid token returns 401 UNAUTHORIZED. All endpoints enforce that the authenticated user's UID matches :id — mismatch returns 403 FORBIDDEN.


Send Buddy Request

POST /user/:id/buddy-request

Auth: Bearer token; caller must match :id

Creates mirrored buddyRequests docs on both users — type: "sent" on the sender, type: "received" on the recipient. expiresAt is set to 90 days from creation.

Planned stub: Check users/{:id}/blockedUsers/{recipientId} and users/{recipientId}/blockedUsers/{:id} before creating the request. If either doc exists, return 200 { "success": true } — a silent success that gives no indication a block is in place.

Request

{ "recipientId": "uid_xyz" }

Response 201

{
  "id": "req_abc123",
  "type": "sent",
  "otherUserId": "uid_xyz",
  "senderId": "uid_abc",
  "recipientId": "uid_xyz",
  "createdAt": "2026-05-24T10:00:00.000Z",
  "expiresAt": "2026-08-22T10:00:00.000Z",
}

Errors

Status Code Condition
400 MISSING_FIELD recipientId not provided
401 UNAUTHORIZED No valid Firebase ID token
403 FORBIDDEN Authenticated user UID ≠ :id
404 USER_NOT_FOUND recipientId does not exist or is not active
409 REQUEST_ALREADY_PENDING A pending request exists between these users in either direction
409 ALREADY_BUDDIES Users are already connected

Cancel or Decline Buddy Request

DELETE /user/:id/buddy-request/:requestId

Auth: Bearer token; caller must match :id

Unified cancel/decline endpoint. Reads users/{:id}/buddyRequests/{requestId} to determine intent: type: "sent" → cancel (sender); type: "received" → decline (recipient). Deletes both mirrored docs on success.

Response 200

{ "success": true }

Errors

Status Code Condition
401 UNAUTHORIZED No valid Firebase ID token
403 FORBIDDEN Caller UID ≠ :id, or caller is neither sender nor recipient
404 NOT_FOUND Request does not exist or has expired

Accept Buddy Request

POST /user/:id/buddy-request/:requestId/accept

Auth: Bearer token; caller must match :id

Caller must be the recipientId of the request. On success: writes to both users' buddies subcollections and deletes both mirrored buddyRequests docs.

Response 200

{ "success": true }

Errors

Status Code Condition
401 UNAUTHORIZED No valid Firebase ID token
403 FORBIDDEN Caller UID ≠ :id, or caller is not recipientId
404 NOT_FOUND Request does not exist or has expired
409 ALREADY_BUDDIES Users are already connected (stale request)

List Buddy Requests

GET /user/:id/buddy-requests

Auth: Bearer token; caller must match :id

Returns active (non-expired) requests only, filtered by expiresAt > now().

Response 200

{
  "sent": [
    {
      "id": "req_abc123",
      "type": "sent",
      "otherUserId": "uid_xyz",
      "senderId": "uid_abc",
      "recipientId": "uid_xyz",
      "createdAt": "2026-05-24T10:00:00.000Z",
      "expiresAt": "2026-08-22T10:00:00.000Z",
    },
  ],
  "received": [],
}

Errors

Status Code Condition
401 UNAUTHORIZED No valid Firebase ID token
403 FORBIDDEN Authenticated user UID ≠ :id

List Buddies

GET /user/:id/buddies

Auth: Bearer token; caller must match :id

Returns the buddies subcollection. Each entry contains only the buddy's UID and connection timestamp — fetch GET /user/:buddyId separately for full profile data.

Response 200

{
  "buddies": [
    {
      "id": "uid_xyz",
      "createdAt": "2026-05-20T10:00:00.000Z",
    },
  ],
}

Errors

Status Code Condition
401 UNAUTHORIZED No valid Firebase ID token
403 FORBIDDEN Authenticated user UID ≠ :id

Remove Buddy

DELETE /user/:id/buddy/:buddyId

Auth: Bearer token; caller must match :id

Removal is mutual — deletes :buddyId from :id's buddies subcollection and :id from :buddyId's buddies subcollection atomically.

Response 200

{ "success": true }

Errors

Status Code Condition
401 UNAUTHORIZED No valid Firebase ID token
403 FORBIDDEN Authenticated user UID ≠ :id
404 NOT_FOUND :buddyId is not a buddy of :id

Block / Unblock User

Planned — not yet live.

All block endpoints require a Firebase ID token in Authorization: Bearer <token>. A missing or invalid token returns 401 UNAUTHORIZED. All endpoints enforce that the authenticated user's UID matches :id — mismatch returns 403 FORBIDDEN. Any authenticated user (free, trial, subscriber, beta) may block or unblock. The blocked user is never notified, and the block list is private to :id.


Block User

POST /user/:id/block/:targetId

Auth: Bearer token; caller must match :id

Writes users/{:id}/blockedUsers/{targetId}. Idempotent — blocking an already-blocked user succeeds silently and returns 200.

Side effects, applied atomically in a single Firestore batch write together with the block doc:

  • Delete users/{:id}/buddies/{targetId} and users/{targetId}/buddies/{:id} if they exist (removes the buddy connection in both directions).
  • Delete any pending buddyRequests docs between the two users in both directions.

Request — empty body

Response 200

{ "success": true }

Errors

Status Code Condition
400 SELF_ACTION :targetId equals :id
401 UNAUTHORIZED No valid Firebase ID token
403 FORBIDDEN Authenticated user UID ≠ :id

Unblock User

DELETE /user/:id/block/:targetId

Auth: Bearer token; caller must match :id

Deletes users/{:id}/blockedUsers/{targetId}. Restores the ability to interact but does not auto-restore any prior buddy connection — the users must re-establish it through the buddy request flow.

Request — empty body

Response 200

{ "success": true }

Errors

Status Code Condition
401 UNAUTHORIZED No valid Firebase ID token
403 FORBIDDEN Authenticated user UID ≠ :id
404 NOT_FOUND :targetId is not on :id's block list

List Blocked Users

GET /user/:id/blocked

Auth: Bearer token; caller must match :id

Returns the blockedUsers subcollection. Each entry contains only the blocked user's UID and block timestamp — same shape as the buddies list. No pagination initially.

Response 200

{
  "blocked": [
    {
      "id": "uid_xyz",
      "createdAt": "2026-05-20T10:00:00.000Z",
    },
  ],
}

Errors

Status Code Condition
401 UNAUTHORIZED No valid Firebase ID token
403 FORBIDDEN Authenticated user UID ≠ :id