Auth
Client-side auth — anonymous, Apple, Google, email signup / login, magic-link, account linking, refresh, logout.
The client-side auth surface. Every route accepts an X-Api-Key header (the project's client key). All token verification is project-scoped — a token issued for project A is rejected with 401 INVALID_TOKEN when presented for project B, even if the signature is valid.
Endpoints
| Method | Path | Description |
|---|---|---|
| POST | /client/auth/anonymous | Create an anonymous user and return session + refresh tokens. |
| POST | /client/auth/social | Apple / Google sign-in via identity token exchange. |
| POST | /client/auth/email/signup | Email + password signup (bcrypt-hashed). |
| POST | /client/auth/email/login | Email + password login. |
| POST | /client/auth/magic-link/request | Send a one-tap email magic-link. |
| POST | /client/auth/magic-link/verify | Exchange a magic-link token for a session. |
| POST | /client/auth/link | Link an anonymous / logged-in account to an Apple / Google identity. |
| POST | /client/auth/refresh | Rotate session + refresh tokens. |
| POST | /client/auth/logout | Revoke a refresh token (idempotent). |
Auth token shape
All success responses return:
session_token is a short-lived JWT; refresh_token is a long-lived JWT backed by a app_user_sessions row (sha256 hashed). On /refresh we verify the stored hash matches and that the session row hasn't been revoked.
POST /client/auth/anonymous
Create an anonymous user. Returns tokens immediately — no credentials to collect.
A display_name is auto-generated server-side from a 32-adjective × 32-noun wordlist (e.g. "OakHiker", "DustOwl") so leaderboards / friends-list UI have a stable label for every user from the first event onward. The user can override it later via PATCH /client/users/me. See Display names for the full behaviour across signup paths.
Request
No body required.
Response 201
Standard auth response plus anonymous_id:
Errors
500 CREATE_FAILED.
Try it:
/client/auth/anonymouscurl -X POST 'https://api.amba.dev/v1/client/auth/anonymous'Curl:
POST /client/auth/social
Apple / Google sign-in. The identity token is verified against the provider's JWKS with strict audience validation (aud must match the project's bundle_id for Apple, google_oauth_client_id for Google). Missing audience config is fail-closed.
Request
| Field | Type | Required | Description |
|---|---|---|---|
provider | "apple" | "google" | yes | |
token | string | yes | Provider identity token. |
session_token | string | no | Existing session for automatic upgrade. |
Response 200
Standard auth response.
Errors
400 AUDIENCE_NOT_CONFIGURED— project has no configured audience for the provider.401 INVALID_TOKEN— identity token signature or audience verification failed.404 NOT_FOUND— project not found.500 CREATE_FAILED/SOCIAL_LOGIN_FAILED.
Try it:
/client/auth/socialcurl -X POST 'https://api.amba.dev/v1/client/auth/social'Curl:
POST /client/auth/email/signup
Request
| Field | Type | Required | Description |
|---|---|---|---|
email | string | yes | |
password | string | yes | Hashed server-side with bcrypt at cost factor 10. |
display_name | string | no | If omitted, the API auto-generates one (e.g. "OakHiker"). See Display names. |
Response 201
Standard auth response.
Errors
400 INVALID_INPUT— missing email / password.409 EMAIL_EXISTS— email already registered.500 CREATE_FAILED.
Try it:
/client/auth/email/signupcurl -X POST 'https://api.amba.dev/v1/client/auth/email/signup'Curl:
POST /client/auth/email/login
Request
| Field | Type | Required |
|---|---|---|
email | string | yes |
password | string | yes |
Response 200
Standard auth response.
Errors
400 INVALID_INPUT.401 INVALID_CREDENTIALS— wrong email / password.500 LOGIN_FAILED.
Try it:
/client/auth/email/logincurl -X POST 'https://api.amba.dev/v1/client/auth/email/login'Curl:
POST /client/auth/magic-link/request
Mint a magic-link token, store it server-side (sha256 hashed), and email the raw token to the user. The link the user clicks is ${ORIGIN}/auth/verify?token=<raw>, where ORIGIN comes from MAGIC_LINK_REDIRECT_BASE_URL (env override) → the request's Origin header → https://app.amba.dev (default).
This endpoint always returns 200 — even for unknown emails — so an unauthenticated caller can't enumerate registered users via timing or response shape.
Tokens are 32 bytes (base64url, ~256 bits of entropy), expire in 15 minutes, and are single-use.
Rate limits
| Bucket | Limit |
|---|---|
| Per IP, per minute | 10 / 60s |
| Per IP, per day | 200 / 24h |
| Per (project, email), per hour | 5 / 60min |
| Per (project, email), per day | 20 / 24h |
Request
| Field | Type | Required | Description |
|---|---|---|---|
email | string | yes | Recipient email. Whitespace-trimmed and lowercased server-side. |
Response 200
Errors
400 INVALID_INPUT— body is missingemailor the value isn't a plausible address.429 RATE_LIMIT_EXCEEDED— per-IP or per-(project, email) bucket exhausted;Retry-Afterheader set.
Send-time errors (email delivery failure, SMTP rejection) do not surface in the response — they're logged server-side. From the caller's perspective the endpoint either accepts the request (200) or refuses it (400 / 429).
Try it:
/client/auth/magic-link/requestcurl -X POST 'https://api.amba.dev/v1/client/auth/magic-link/request'Curl:
POST /client/auth/magic-link/verify
Exchange a magic-link token for a session. The lookup is by sha256(token); a non-matching, expired, or already-used token all return 401 INVALID_TOKEN with the same generic message — same shape as /email/login so a probing caller can't differentiate between "token doesn't exist" and "token was consumed".
If the email already maps to an existing user, the session is bound to that user. Otherwise a new user is created (passwordless signup); the verify step is the email-verified moment, so this is also how a customer onboards a fresh inbox.
Request
| Field | Type | Required | Description |
|---|---|---|---|
token | string | yes | The raw token from the link's ?token= query parameter. |
Response 200
Standard auth response — same shape as /email/login:
Errors
400 INVALID_INPUT— body missingtoken.401 INVALID_TOKEN— token unknown, expired, or already used.402 MAU_CAP_EXCEEDED— only on the create-new-user branch when the project hit its free-tier MAU cap.500 VERIFY_FAILED.
Try it:
/client/auth/magic-link/verifycurl -X POST 'https://api.amba.dev/v1/client/auth/magic-link/verify'Curl:
POST /client/auth/link
Link an Apple or Google identity to an existing user (typically an anonymous one, to preserve history). The session token must match the API key's project.
Email linking via this endpoint is explicitly refused — email identities must go through a verified signup / login flow.
Request
| Field | Type | Required |
|---|---|---|
provider | "apple" | "google" | yes |
token | string | yes |
session_token | string | yes |
Response 200
Standard auth response.
Errors
400 UNSUPPORTED_PROVIDER—provider = "email".400 AUDIENCE_NOT_CONFIGURED.401 INVALID_SESSION— session token invalid.401 INVALID_TOKEN— identity token invalid.403 FORBIDDEN— session belongs to a different project.404 NOT_FOUND— project or user not found.409 IDENTITY_ALREADY_LINKED— the social identity is already bound to another account.500 LINK_FAILED.
Try it:
/client/auth/linkcurl -X POST 'https://api.amba.dev/v1/client/auth/link'Curl:
POST /client/auth/refresh
Rotate tokens. The old session row is revoked. Replaying a revoked token returns 401 INVALID_TOKEN — this is how token theft is detected.
Request
| Field | Type | Required |
|---|---|---|
refresh_token | string | yes |
Response 200
Errors
400 INVALID_INPUT.401 INVALID_TOKEN— signature invalid, session not found, project mismatch, expired, or revoked.500 REFRESH_FAILED.
Try it:
/client/auth/refreshcurl -X POST 'https://api.amba.dev/v1/client/auth/refresh'Curl:
POST /client/auth/logout
Revoke the session. Idempotent — invalid tokens return success.
Request
| Field | Type | Required |
|---|---|---|
refresh_token | string | yes |
Response 200
Errors
400 INVALID_INPUT.500 LOGOUT_FAILED.
Try it:
/client/auth/logoutcurl -X POST 'https://api.amba.dev/v1/client/auth/logout'Curl: