Amba

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

MethodPathDescription
POST/client/auth/anonymousCreate an anonymous user and return session + refresh tokens.
POST/client/auth/socialApple / Google sign-in via identity token exchange.
POST/client/auth/email/signupEmail + password signup (bcrypt-hashed).
POST/client/auth/email/loginEmail + password login.
POST/client/auth/magic-link/requestSend a one-tap email magic-link.
POST/client/auth/magic-link/verifyExchange a magic-link token for a session.
POST/client/auth/linkLink an anonymous / logged-in account to an Apple / Google identity.
POST/client/auth/refreshRotate session + refresh tokens.
POST/client/auth/logoutRevoke a refresh token (idempotent).

Auth token shape

All success responses return:

{
  "data": {
    "session_token": "eyJ…",
    "refresh_token": "eyJ…",
    "user": {
      "id": "…",
      "email": "…",
      "display_name": "…",
      "anonymous_id": "…",
      "auth_providers": [],
      "properties": {},
      "first_seen_at": "…",
      "last_seen_at": "…"
    }
  }
}

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:

{
  "data": {
    "session_token": "…",
    "refresh_token": "…",
    "user": {
      "id": "…",
      "email": null,
      "display_name": "OakHiker",
      "anonymous_id": "anon_…",
      "auth_providers": [],
      "properties": {},
      "first_seen_at": "…",
      "last_seen_at": "…"
    },
    "anonymous_id": "anon_…"
  }
}

Errors

  • 500 CREATE_FAILED.

Try it:

POST/client/auth/anonymous
client auth
curl -X POST 'https://api.amba.dev/v1/client/auth/anonymous'
Loading auth… Configure auth in the settings drawer (top-right) to run this request.

Curl:

curl -X POST '${BASE_URL}/client/auth/anonymous' \
  -H 'X-Api-Key: ${CLIENT_API_KEY}' \
  -H 'Authorization: Bearer ${SESSION_TOKEN}' \
  -H 'Content-Type: application/json' \
  -d '{}'

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

FieldTypeRequiredDescription
provider"apple" | "google"yes
tokenstringyesProvider identity token.
session_tokenstringnoExisting 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:

POST/client/auth/social
public auth
curl -X POST 'https://api.amba.dev/v1/client/auth/social'

Curl:

curl -X POST '${BASE_URL}/client/auth/social' \
  -H 'X-Api-Key: ${CLIENT_API_KEY}' \
  -H 'Content-Type: application/json' \
  -d '{}'

POST /client/auth/email/signup

Request

FieldTypeRequiredDescription
emailstringyes
passwordstringyesHashed server-side with bcrypt at cost factor 10.
display_namestringnoIf 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:

POST/client/auth/email/signup
public auth
curl -X POST 'https://api.amba.dev/v1/client/auth/email/signup'

Curl:

curl -X POST '${BASE_URL}/client/auth/email/signup' \
  -H 'X-Api-Key: ${CLIENT_API_KEY}' \
  -H 'Content-Type: application/json' \
  -d '{}'

POST /client/auth/email/login

Request

FieldTypeRequired
emailstringyes
passwordstringyes

Response 200

Standard auth response.

Errors

  • 400 INVALID_INPUT.
  • 401 INVALID_CREDENTIALS — wrong email / password.
  • 500 LOGIN_FAILED.

Try it:

POST/client/auth/email/login
public auth
curl -X POST 'https://api.amba.dev/v1/client/auth/email/login'

Curl:

curl -X POST '${BASE_URL}/client/auth/email/login' \
  -H 'X-Api-Key: ${CLIENT_API_KEY}' \
  -H 'Content-Type: application/json' \
  -d '{}'

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

BucketLimit
Per IP, per minute10 / 60s
Per IP, per day200 / 24h
Per (project, email), per hour5 / 60min
Per (project, email), per day20 / 24h

Request

FieldTypeRequiredDescription
emailstringyesRecipient email. Whitespace-trimmed and lowercased server-side.

Response 200

{ "data": { "ok": true } }

Errors

  • 400 INVALID_INPUT — body is missing email or the value isn't a plausible address.
  • 429 RATE_LIMIT_EXCEEDED — per-IP or per-(project, email) bucket exhausted; Retry-After header 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:

POST/client/auth/magic-link/request
public auth
curl -X POST 'https://api.amba.dev/v1/client/auth/magic-link/request'

Curl:

curl -X POST '${BASE_URL}/client/auth/magic-link/request' \
  -H 'X-Api-Key: ${CLIENT_API_KEY}' \
  -H 'Origin: https://app.example.com' \
  -H 'Content-Type: application/json' \
  -d '{ "email": "alice@example.com" }'

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

FieldTypeRequiredDescription
tokenstringyesThe raw token from the link's ?token= query parameter.

Response 200

Standard auth response — same shape as /email/login:

{
  "data": {
    "session_token": "eyJ…",
    "refresh_token": "eyJ…",
    "user": {
      "id": "…",
      "email": "alice@example.com",
      "display_name": "…",
      "anonymous_id": "anon_…"
    }
  }
}

Errors

  • 400 INVALID_INPUT — body missing token.
  • 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:

POST/client/auth/magic-link/verify
public auth
curl -X POST 'https://api.amba.dev/v1/client/auth/magic-link/verify'

Curl:

curl -X POST '${BASE_URL}/client/auth/magic-link/verify' \
  -H 'X-Api-Key: ${CLIENT_API_KEY}' \
  -H 'Content-Type: application/json' \
  -d '{ "token": "<raw-token-from-email-link>" }'

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

FieldTypeRequired
provider"apple" | "google"yes
tokenstringyes
session_tokenstringyes

Response 200

Standard auth response.

Errors

  • 400 UNSUPPORTED_PROVIDERprovider = "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:

POST/client/auth/link
client auth
curl -X POST 'https://api.amba.dev/v1/client/auth/link'
Loading auth… Configure auth in the settings drawer (top-right) to run this request.

Curl:

curl -X POST '${BASE_URL}/client/auth/link' \
  -H 'X-Api-Key: ${CLIENT_API_KEY}' \
  -H 'Authorization: Bearer ${SESSION_TOKEN}' \
  -H 'Content-Type: application/json' \
  -d '{}'

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

FieldTypeRequired
refresh_tokenstringyes

Response 200

{ "data": { "session_token": "…", "refresh_token": "…" } }

Errors

  • 400 INVALID_INPUT.
  • 401 INVALID_TOKEN — signature invalid, session not found, project mismatch, expired, or revoked.
  • 500 REFRESH_FAILED.

Try it:

POST/client/auth/refresh
public auth
curl -X POST 'https://api.amba.dev/v1/client/auth/refresh'

Curl:

curl -X POST '${BASE_URL}/client/auth/refresh' \
  -H 'X-Api-Key: ${CLIENT_API_KEY}' \
  -H 'Content-Type: application/json' \
  -d '{}'

POST /client/auth/logout

Revoke the session. Idempotent — invalid tokens return success.

Request

FieldTypeRequired
refresh_tokenstringyes

Response 200

{ "data": { "success": true } }

Errors

  • 400 INVALID_INPUT.
  • 500 LOGOUT_FAILED.

Try it:

POST/client/auth/logout
client auth
curl -X POST 'https://api.amba.dev/v1/client/auth/logout'
Loading auth… Configure auth in the settings drawer (top-right) to run this request.

Curl:

curl -X POST '${BASE_URL}/client/auth/logout' \
  -H 'X-Api-Key: ${CLIENT_API_KEY}' \
  -H 'Authorization: Bearer ${SESSION_TOKEN}' \
  -H 'Content-Type: application/json' \
  -d '{}'