Skip to content
esc

Search pages and sections. Try a tool name, an error code, a route.

↑↓ navigate↵ openesc close

API reference · Data plane

End-user authentication

Sign up, sign in and manage the sessions of the people who use an app. Each project has its own users; they are not Manybases accounts.

Host: https://proj-<id>.manybases.com · Key: pk_ — any publishable key of the project, whoever created it (see below). sk_ and ak_ have no path to these routes and receive 401.

POST/v1/auth/signup
Body{ email, password, invite? }
Success201
data{ user, session: { token }, seats }
POST/v1/auth/login
Body{ email, password }
Success200
data{ user, session: { token }, seats }
POST/v1/auth/magic-link
Body{ email }
Success200
data{ sent: true } — the same answer once an address has had 5 requests in the hour: nothing is sent
GET/v1/auth/magic-link/verify?token=…
Body—
Success200
data{ user, session: { token }, seats }
POST/v1/auth/logout
Body— (with X-Session)
Success200
data{ logged_out: true }
GET/v1/auth/me
Body— (with X-Session)
Success200
data{ user, seats }
DELETE/v1/auth/me
Body{ confirmation_token } on the second call
Success200
data{ deleted, rows_destroyed, rows_kept_frozen, spaces_left, files_destroyed }
GET/v1/auth/reset-password/:token
Body—
Success200
data{ token } — the link is valid, nothing is consumed yet
POST/v1/auth/reset-password
Body{ token, password }
Success200
data{ reset: true }
POST/v1/invites
Body— (with X-Session)
Success201
data{ invite }

These paths are the only ones served. The underlying authentication library exposes more; none of it is mounted.

The objects#

  • user = { id, email, created_at }. Nothing else: no password hash, no provider token.
  • session = { token } — the value to send as X-Session on every later call. It comes in the body, never in a Set-Cookie: this API has no ambient identity.
  • seats = { used, cap }. cap is null when the plan's cap could not be read; the API then lets the request through rather than refusing on ignorance.
  • invite = { id, url, expires_at }.

Six rules#

  1. A seat is taken at sign-up. seats.used counts the people signed up to the app, and a sign-up response counts the person who just signed up. At the cap (3 on Free, 1 000 on Builder and Studio), a sign-up — through an invitation link or a first magic link included — answers 402 quota_exceeded and creates nothing. Signing in is never refused by the cap. Deleting a user frees their seat.
  2. The token comes in the body. Store it where you decide; the SDK keeps it in localStorage by default.
  3. GET …/magic-link/verify returns the JSON envelope, never a redirect. The app decides where to send the person next. The same goes for GET …/reset-password/:token.
  4. Consuming a reset link kills every open session of that user, on every device. Verifying the link (GET) does not consume it; only the POST does.
  5. Sessions live 30 rolling days, 90 days at most. A deleted user's session answers 401 immediately.
  6. The magic link is rate-limited. 20 requests per hour per IP address; 5 per hour per email address, silently — the answer does not change and nothing is sent; 50 sign-in emails per UTC day per app and 70 across all apps. Beyond the IP or the daily limits, 429 rate_limited with Retry-After. See Limits and quotas.

Refusals#

CaseRefusal
An sk_ or ak_ key401 unauthorized
A pk_ issued by an agent alone (inert) on sign-up (removed 2026-09-07)— every publishable key signs users up
Invalid body, e-mail already registered400 validation_error
Wrong credentials on login401 unauthorized
Magic-link token unknown, expired or already used401 — the same body for the three
Reset token unknown, expired or already used401 — the same body, and the same as above
Password out of bounds on reset400 validation_error
No session on logout, me, delete me401 unauthorized
Seat cap reached, at sign-up — including through an invitation link, or magic-link/verify for an address the app does not know; nothing is created, and the invitation link stays usable402 quota_exceeded
A new person on an app in read-only mode — sign-up, invitation or a first magic link402 quota_exceeded
Magic link: more than 20 requests an hour from one IP, or today's sign-in emails used up — the app's, or all apps'429 rate_limited, with Retry-After — the same body for both days
Magic link: the shared counter cannot count503 internal — nothing is sent

The two reset routes require no session: the token itself is the authority. The pk_ is still required, as on every route here.

The inert publishable key — removed on 2026-09-07#

A pk_ created by an account key alone — by an agent, without a signed-in human — used to be inert: it worked for everything except POST /v1/auth/signup, which answered 401, and a human had to issue the publishable key from the dashboard.

That rule is gone. Every publishable key signs users up, whoever created it: an agent can build the whole backend and enroll its first users. The key card still carries an inert field, always false — see Project keys.

Invitations#

POST /v1/invites mints a single-use link to the app's sign-up page. It requires a signed-in user (anonymous → 401; keys → 401), and the project must have declared its app URL: the link is built from that URL, because the sign-up page belongs to the app, not to Manybases. Without an app URL the route answers 409 conflict and the hint names the gesture.

  • The link is <app URL>/i/<token>. The app's page at that path passes the token to POST /v1/auth/signup as the invite field, which consumes it.
  • The link is valid 7 days, single-use: two people racing on the same link cannot both succeed.
  • An invitation reserves no seat and sends no e-mail — you share the url yourself.
  • Sign-up stays open to anyone holding the pk_: an invitation attributes and shares, it closes nothing.
  • The token lives only in url; it is never returned in a field of its own and never stored in clear.

The account key no longer mints invitation links: POST /v1/projects/:pid/invites and the invite_user tool were removed on 2026-09-22, and the route answers 404. A link already issued stays valid until it expires.

Deleting oneself#

DELETE /v1/auth/me erases the person and what is attributed to them. It requires a re-authenticated session — an authentication less than 5 minutes old, by whatever means the app uses — and confirms in two steps: the first call answers 428 with a numbered summary of what would be destroyed and a confirmation_token; the second call executes and returns the same numbers. Both calls must fall within the 5-minute window.

What goes: the rows of prive and journal tables, every file they uploaded (public files included), their sessions and space memberships. What stays: rows in the other presets, kept with a frozen owner_id — nobody can edit them any more, they can only be deleted. The text of past SQL statements is not attributed to anyone and is not covered by this cascade. See Retention.