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.
/v1/auth/signup{ email, password, invite? }{ user, session: { token }, seats }/v1/auth/login{ email, password }{ user, session: { token }, seats }/v1/auth/magic-link{ email }{ sent: true } — the same answer once an address has had 5 requests in the hour: nothing is sent/v1/auth/magic-link/verify?token=…{ user, session: { token }, seats }/v1/auth/logoutX-Session){ logged_out: true }/v1/auth/meX-Session){ user, seats }/v1/auth/me{ confirmation_token } on the second call{ deleted, rows_destroyed, rows_kept_frozen, spaces_left, files_destroyed }/v1/auth/reset-password/:token{ token } — the link is valid, nothing is consumed yet/v1/auth/reset-password{ token, password }{ reset: true }/v1/invitesX-Session){ 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 asX-Sessionon every later call. It comes in the body, never in aSet-Cookie: this API has no ambient identity.seats={ used, cap }.capisnullwhen 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#
- A seat is taken at sign-up.
seats.usedcounts 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 — answers402 quota_exceededand creates nothing. Signing in is never refused by the cap. Deleting a user frees their seat. - The token comes in the body. Store it where you decide; the SDK keeps it in
localStorageby default. GET …/magic-link/verifyreturns the JSON envelope, never a redirect. The app decides where to send the person next. The same goes forGET …/reset-password/:token.- Consuming a reset link kills every open session of that user, on every device. Verifying the link (
GET) does not consume it; only thePOSTdoes. - Sessions live 30 rolling days, 90 days at most. A deleted user's session answers
401immediately. - 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_limitedwithRetry-After. See Limits and quotas.
Refusals#
| Case | Refusal |
|---|---|
An sk_ or ak_ key | 401 unauthorized |
pk_ issued by an agent alone (inert) on sign-up | — every publishable key signs users up |
| Invalid body, e-mail already registered | 400 validation_error |
| Wrong credentials on login | 401 unauthorized |
| Magic-link token unknown, expired or already used | 401 — the same body for the three |
| Reset token unknown, expired or already used | 401 — the same body, and the same as above |
| Password out of bounds on reset | 400 validation_error |
| No session on logout, me, delete me | 401 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 usable | 402 quota_exceeded |
| A new person on an app in read-only mode — sign-up, invitation or a first magic link | 402 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 count | 503 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 toPOST /v1/auth/signupas theinvitefield, 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
urlyourself. - 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.
