Authentication
These endpoints handle user login and token refresh. They do not require an access token.
POST /v1/auth/login
Authenticate a user via an OAuth provider (Apple or Google). If the user doesn't exist yet, an account is created automatically.
Request Body
{
"provider": "google",
"token": "<oauth_authorization_code_or_id_token>",
"name": "John"
}
| Field | Type | Required | Description |
|---|---|---|---|
provider | string | Yes | "google" or "apple" |
token | string | Yes | OAuth token from the provider |
name | string | Apple only | User's display name (Apple only sends this on first sign-in) |
Provider Details
Google: Send the ID token from Google Sign-In. The backend validates it against the appropriate client ID based on the Client-Info os field (iOS vs web).
Apple: Send the authorization code from Sign in with Apple. The backend exchanges it with Apple's servers. Include name on the first login — Apple only provides it once.
Response
{
"status": 200,
"data": {
"access_token": "eyJhbGciOiJIUzI1NiIs...",
"expire_at": "1735689600000",
"refresh_token": "dGhpcyBpcyBhIHJlZnJl...",
"user_id": "1234567890123456789"
}
}
| Field | Type | Description |
|---|---|---|
access_token | string | JWT for authenticating subsequent requests |
expire_at | string | Token expiry as Unix timestamp in milliseconds |
refresh_token | string | Opaque token for refreshing the session |
user_id | string | Snowflake ID of the authenticated user |
Error Responses
| Status | Error | Cause |
|---|---|---|
| 400 | VALIDATION_FAILED | Missing token or provider, or missing Client-Device-Id header |
| 401 | UNAUTHORIZED | OAuth token is invalid or expired |
Session Policy
Only one active session is allowed per user. Logging in from a new device invalidates all previous refresh tokens.
POST /v1/auth/refresh
Exchange a valid refresh token for a new access token + refresh token pair. The old refresh token is rotated (invalidated and replaced).
Request Body
{
"refresh_token": "dGhpcyBpcyBhIHJlZnJl..."
}
| Field | Type | Required | Description |
|---|---|---|---|
refresh_token | string | Yes | The refresh token from login or a previous refresh |
Response
{
"status": 200,
"data": {
"access_token": "eyJhbGciOiJIUzI1NiIs...",
"expire_at": "1735689600000",
"refresh_token": "bmV3IHJlZnJlc2ggdG9r...",
"user_id": "1234567890123456789"
}
}
Same shape as the login response. The refresh_token is a new token — the old one is no longer valid.
Error Responses
| Status | Error | Cause |
|---|---|---|
| 400 | VALIDATION_FAILED | Missing refresh_token |
| 401 | UNAUTHORIZED | Token is invalid, expired, or the Client-Device-Id doesn't match the session |
Device Binding
The refresh token is bound to the Client-Device-Id header sent during login. If the device ID on the refresh request doesn't match, the request is rejected with 401.
Token Lifecycle
1. User signs in with Google/Apple
2. POST /v1/auth/login → access_token + refresh_token
3. Use access_token for all authenticated requests
4. When access_token expires (check expire_at):
POST /v1/auth/refresh → new access_token + new refresh_token
5. Repeat from step 3
Token Expiry
| Token | Lifetime |
|---|---|
| Access token | Check the expire_at field in the response |
| Refresh token | 3 months |