API Reference
← Developer Docs · Core Concepts · Agent skill: phuze-skill.md
Base URL: https://phuze.edato.me/api/v1. All bodies are JSON. Auth schemes (Publishable / Secret / CLI) are described on the main docs page. Errors use { "error": { "code", "message" } } — see Errors.
Never validate a response with a closed/strict schema (additionalProperties: false, zod .strict(), Pydantic extra="forbid") — new fields may be added. Destructure the fields you need.
Client Management
POST /clients — Register
No auth. Registers a client and emails a 6-digit activation code. name: 1–80 chars; domains: 1–5 valid domains.
POST /api/v1/clients
{ "name": "My App", "email": "you@example.com", "domains": ["myapp.com"],
"invite_code": "XXXX-XXXX-XXXX" } // invite_code required unless your email is whitelisted
→ 201 { "client_id": "cl_...", "status": "pending", "message": "Check your email for an activation code." }Errors: 400 invalid name/email/domains or undeliverable email domain · 403 alpha gate (email not approved and no valid invite code) · 429 per-email client limit reached ({ limit, count }).
POST /clients/activate — Activate
No auth. Exchanges the emailed code for your keys. Returned once.
POST /api/v1/clients/activate
{ "email": "you@example.com", "code": "123456" }
→ 200 {
"client_id": "cl_...",
"publishable_key": "pk_live_...",
"secret_key": "sk_live_...",
"cli_token": "cli_...",
"warning": "Store the secret_key and cli_token securely — they will not be shown again."
}Errors: 404 no pending activation · 400 code already used, expired, or invalid (repeated wrong codes invalidate it — re-register).
GET /clients/me — Client info
Auth CLI or Secret. Returns your client record (never the secret-key hash/prefix).
→ 200 {
"client_id": "cl_...", "name": "My App", "email": "you@example.com",
"authorized_domains": ["myapp.com"], "publishable_key": "pk_live_...",
"status": "active", "type": "general", "plan": "free",
"from_name": "My App", // present only if set
"created_at": "...", "activated_at": "..."
}PATCH /clients/me — Update
Auth CLI. Any of name (1–80), domains (1–5), from_name. from_name is 1–40 chars (control characters and <>"@ stripped); null clears it; settable on any plan, shown on emails for Builder+.
PATCH /api/v1/clients/me
{ "domains": ["myapp.com", "www.myapp.com"], "from_name": "My App" }
→ 200 { "updated": true }POST /clients/me/keys/rotate — Rotate secret key
Auth CLI. Issues a new secret_key and invalidates the old one immediately. Rate-limited; owner is emailed.
→ 200 { "secret_key": "sk_live_...",
"warning": "Store the new secret_key securely — it will not be shown again. The previous key is now invalid." }GET /clients/me/quota — Quota usage
Auth CLI or Secret. Live effective limits + current-period counters. See Core Concepts → Quotas.
→ 200 {
"plan": "free",
"limits": { "mau": 50000, "logins_per_day": 100, "logins_hard_cap_per_day": 100 },
"current_period": {
"start": "...", "mau_count": 12, "daily_login_count": 3,
"daily_reset_at": "...", "overage_count": 0
},
"test_accounts": { "limit": 1, "used": 1, "entries": [ { "email": "...", "expires_at": "..." } ] }
}GET /clients/me/users — List your users
Auth CLI or Secret. Paginated: ?limit=1..25&cursor=<uid>; follow next_cursor until null. last_active_at is the user's last login (the signal behind billing MAU / mau_current). Client-scoped — never reveals a user's other Phuze clients.
→ 200 {
"users": [ {
"uid": "u_...", "email": "user@example.com",
"first_seen_at": "...", "last_active_at": "...", "login_count": 4, "mau_current": true,
"tags": [ { "tag": "pro", "assigned_at": "...", "issued_by": { "type": "client", "id": "cl_..." } } ]
} ],
"next_cursor": null,
"mau": { "count": 12, "limit": 50000 }
}Test accounts — POST / DELETE /clients/me/test-accounts
Auth CLI (secret key is rejected here by design). Declares an address of your own for relaxed send limits — 10/min · 60/hr vs the default 1/min · 5/hr — for 14 days (re-adding resets the clock). Plan cap: free 1 · builder 5. Adding an address emails its owner (audited).
POST /api/v1/clients/me/test-accounts Authorization: CLI cli_...
{ "email": "dev@yourapp.com" }
→ 200 { "email": "dev@yourapp.com", "expires_at": "...", "refreshed": false,
"test_accounts": { "limit": 1, "used": 1 } }
// at the plan cap: 429 quota_exceeded { limit, used }
DELETE /api/v1/clients/me/test-accounts/dev%40yourapp.com Authorization: CLI cli_...
→ 200CLI tokens — GET / POST /clients/me/cli-tokens, DELETE /clients/me/cli-tokens/:prefix
Auth CLI. Manage the CLI tokens for your client. Issuing and revoking are rate-limited, audited, and email the owner. Rotation = issue a new token, then revoke the old one.
GET /api/v1/clients/me/cli-tokens
→ 200 { "cli_tokens": [ { "prefix": "cli_abc123...", "created_at": "...", "last_used_at": "..." } ] }
// metadata only — token values are never returned; revoked tokens are omitted
POST /api/v1/clients/me/cli-tokens
→ 200 { "cli_token": "cli_...", "prefix": "cli_...",
"warning": "Store this CLI token securely — it will not be shown again." }
DELETE /api/v1/clients/me/cli-tokens/cli_abc123...
→ 200 { "revoked": true, "prefix": "cli_abc123..." }
// 404 if no token with that prefix; revocation is enforced in real timePOST /clients/me/dashboard-link — One-time dashboard link
Auth CLI. Mints a one-time URL that opens a logged-in dashboard for this client. Short-lived (60s) and single-use — open it on your own machine. Rate-limited.
→ 200 { "url": "https://phuze.edato.me/auth/handoff?token=...", "expires_in": 60 }POST /feedback — Feedback & feature interest
Auth CLI or Secret. topic is "custom_domains" or "other"; message (≤2000 chars) is required for other, optional for custom_domains. Lightly rate-limited.
POST /api/v1/feedback
{ "topic": "custom_domains", "message": "Would love to send from auth.myapp.com" }
→ 201 { "message": "Thanks — recorded." }⚠ Dangerous — Client Account Deletion
Deleting a client is permanent and irreversible. All sessions, CLI tokens, quota records, and the client itself are deleted. Your end-users will no longer be able to sign in via this client. Two steps, both unauthenticated (guarded by an emailed code).
POST /api/v1/clients/me/delete-request
{ "email": "owner@example.com" }
→ 200 { "message": "If a client is registered to this email, a deletion code has been sent to its owner. Run delete-confirm with the code." }
// always 200 (enumeration-safe) — a code is sent only if the email owns a client
POST /api/v1/clients/me/delete-confirm
{ "email": "owner@example.com", "code": "123456" }
→ 200 { "deleted": true, "client_id": "cl_..." }
// 400 if the code is wrong, used, or expiredAuth Flow
POST /auth/magic-link — Send a sign-in email
Auth Publishable + Origin. Only email is required. pending_id enables the no-widget session handoff (see Custom login flow); redirect_url must be an authorized domain; locale sets the email + verify-page language.
POST /api/v1/auth/magic-link Authorization: Publishable pk_live_... Origin: https://myapp.com
{ "email": "user@example.com",
"pending_id": "0f9a...-uuid-v4", // optional — enables cross-browser pickup
"redirect_url": "https://myapp.com/welcome", // optional — must be a registered domain
"locale": "en" } // optional
→ 200 { "message": "Check your email for a sign-in link." }Always returns 200, even for an unregistered email (no account enumeration). The magic link expires after 15 minutes and is single-use.
Errors: 403 forbidden — origin not authorized, or the client account is not active · 429 quota_exceeded — daily login cap reached (Free: at quota; paid: at the hard cap) · 429 rate_limited — send throttle hit.
Rate limits (abuse-tuned): 1/min · 5/hr per target email, 3/min · 20/hr per requesting IP. Every rate_limited body carries retry_after_seconds (plus a standard Retry-After header) and an optional machine-readable reason — branch on reason, never the message text:
reason | Meaning | What to do |
|---|---|---|
test_account_limit | Test-account preset (10/min · 60/hr) hit for a declared address | Back off retry_after_seconds |
test_account_expired (+ expires_at) | The address's 14-day test grant lapsed | Re-POST /clients/me/test-accounts to refresh, then retry |
not_test_account | You have test accounts, but this address isn't one | Declare the address, or accept default limits |
| (absent) | Ordinary rate limit | Back off retry_after_seconds |
GET /auth/verify — the emailed link (a page, not an API call)
Auth —. This is the URL inside the sign-in email (phuze.edato.me/auth/verify?token=&client_id=) — a browser page, not under /api/v1, and you never request it yourself. It verifies the token, creates the session, and shows a success page with a "Return to app" button linking to redirect_url (no auto-redirect).
Sessions
POST /sessions/verify — Verify a session token
Auth Secret (server-to-server). The primary call your backend makes.
POST /api/v1/sessions/verify Authorization: Secret sk_live_...
{ "token": "st_..." }
→ 200 {
"valid": true, "uid": "u_...", "email": "user@example.com",
"session_id": "s_...", "client_id": "cl_...",
"created_at": "...", "expires_at": "...", "type": "user"
}Field notes: email is optional (absent in rare cases, e.g. the user record no longer exists); every other field is present when valid is true. client_id is always your own client. type is always "user" today. When the token is not valid:
→ 200 { "valid": false, "reason": "expired" } // reason: expired | revoked | not_foundPOST /sessions/revoke — Revoke a session
Auth Secret. Force-logout from your backend (account deletion, suspicious activity). Idempotent.
POST /api/v1/sessions/revoke Authorization: Secret sk_live_...
{ "token": "st_..." }
→ 200 { "revoked": true }Custom login flow — without the widget
Only for a fully custom login UI. You replicate what the widget does internally: request a magic link with a pending_id, then poll for the session after the user clicks the link.
⚠ Security invariants — the widget enforces these; your flow must too
Generate pending_id with crypto.randomUUID(), never Math.random() — it's the guessable key to an in-flight session. All calls require a registered Origin + HTTPS. Pickup is one-time within a ~5-minute window after the click. Treat the returned session_token like a session cookie.
// 1. Request the link with a random handoff id
POST /api/v1/auth/magic-link Authorization: Publishable pk_live_... Origin: https://myapp.com
{ "email": "user@example.com", "pending_id": "<crypto.randomUUID()>" }
// 2. Poll pickup ~every 2s (up to ~10 min). Publishable + Origin.
GET /api/v1/sessions/pickup?pending_id=<same-id>
→ { "ready": false } // until the link is clicked
→ { "ready": true, "session_token": "st_...", "uid": "u_...", // then once (one-time)
"email": "...", "expires_at": "..." }
// polling faster than ~2s from one IP → 429 rate_limited { retry_after_seconds: 1 }
// 3. Store session_token, then verify it on your backend (POST /sessions/verify).Two more Publishable + Origin helpers for a stored token (both via the X-Session-Token header):
GET /api/v1/sessions/current → { "active": true, "uid", "email", "expires_at" } or { "active": false }
POST /api/v1/sessions/logout → { "logged_out": true }The device that clicks the link gets the session. Without pending_id the user is still logged in on whatever device clicks, but there is no way to hand the session back to your original tab.
Users
GET /users/:uid — Look up a user by UID
Auth Secret. Client-scoped: 404 if the user has not logged in through your client (no cross-tenant lookup).
→ 200 {
"uid": "u_...", "email": "user@example.com",
"created_at": "...", "email_verified_at": "...",
"first_seen_at": "...", "last_active_at": "...", "login_count": 4, "mau_current": true,
"tags": [ { "tag": "pro", "assigned_at": "...", "issued_by": { "type": "client", "id": "cl_..." } } ]
}The first_seen_at / last_active_at / login_count / mau_current / tags fields are the same client-scoped data as a user-list row (last_active_at = last login).
GET /users/by-email/:email — Look up a user by email
Auth Secret. Same response as above. 404 if the email has not logged in through your client.
POST /users/me/delete-request — End-user self-deletion (trusted clients only)
Auth Publishable + Origin + X-Session-Token. Gated by client type — only trusted clients may call it; general clients get 403 forbidden. Emails the user a confirmation link; clicking it permanently deletes their account and revokes all sessions.
POST /api/v1/users/me/delete-request
Authorization: Publishable pk_live_... Origin: https://myapp.com X-Session-Token: st_...
→ 200 { "message": "Check your email to confirm account deletion." }Tags
String labels you attach to your own users — tiers, roles, feature flags, cohorts. Phuze stores and serves them; your app owns the gating logic. Names match [a-z0-9-]{1,64}, up to 64 per user, scoped to your client (never visible to other clients). All three calls use Secret auth and return 404 if the uid was never served by your client.
POST /api/v1/users/:uid/tags { "tag": "premium" } → 200 { "tag": "premium", "uid": "u_..." } // idempotent
GET /api/v1/users/:uid/tags → 200 { "uid": "u_...", "tags": ["beta-tester", "premium"] } // sorted
DELETE /api/v1/users/:uid/tags/:tag → 204 // idempotent, whether or not the tag was presentErrors
{ "error": { "code": "unauthorized", "message": "Missing Authorization header" } }Codes: unauthorized · forbidden · not_found · rate_limited · quota_exceeded · invalid_request · server_error.
429 rate_limited bodies carry retry_after_seconds (plus a Retry-After header) and an optional reason — branch on those, never the message text (see magic-link rate limits above). Re-sending to your own address while developing? Declare it a test account instead of fighting the throttle.
Phuze · phuze.edato.me · Developer Docs · Core Concepts