Sign-in methods
Password, email codes, magic links, Google and GitHub, password reset, and the methods each user has linked.
An app's users can sign in five ways: password, a six-digit email code, a magic link, Google, or GitHub. Each door does its own verification and then hands the account to the same session issuer, so every one returns the same payload (or the same two-factor challenge), and code that consumes a session never needs to know how the user signed in.
Ask what is available
curl "$AUTHORIC_URL/$APP_UID/auth/methods"{
"password": true,
"magicLink": true,
"emailOtp": true,
"oauth": [
{ "slug": "google", "label": "Google" },
{ "slug": "github", "label": "GitHub" }
],
"signup": { "magicLink": false, "emailOtp": false, "oauth": true, "mode": "open" },
"botCheck": null
}Render your sign-in screen from this rather than hard-coding it. oauth lists only the providers
the deployment has credentials for. signup says which passwordless doors may create an
account for an address that has none: by default the email doors only sign existing users in, and
social sign-in may create an account.
Password
POST /{appUid}/auth/login with email and password. See Auth for the request and
the response. A user created by Google or a magic link has no password, and that is a supported
state rather than a broken one: the password door answers the same generic failure for them, and
they can add one through password reset.
Password reset
Two calls. The first always answers the same message, whether or not the address has an account.
# 1. Email a reset link
curl -X POST "$AUTHORIC_URL/$APP_UID/auth/request-password-reset" \
-H "Content-Type: application/json" \
-d '{ "email": "dana@example.com" }'
# 2. Set the new password with the token from the link
curl -X POST "$AUTHORIC_URL/$APP_UID/auth/confirm-password-reset" \
-H "Content-Type: application/json" \
-d '{ "token": "<token from the link>", "newPassword": "N3w!Passphrase" }'To send the reset link to your own page, add redirectTo to the first call (see
Where links land).
A successful reset ends every session the account has and forgets every browser it had
remembered for two-factor authentication. A bad or expired token answers 400 Invalid or expired reset token.
Email code
The user asks for a code, then types it. The first call returns a requestId you send back with
the code.
# 1. Email a six-digit code
curl -X POST "$AUTHORIC_URL/$APP_UID/auth/otp/request" \
-H "Content-Type: application/json" \
-d '{ "email": "dana@example.com" }'
# → { "message": "...", "requestId": "..." }
# 2. Sign in with it
curl -X POST "$AUTHORIC_URL/$APP_UID/auth/otp/verify" \
-H "Content-Type: application/json" \
-d '{ "requestId": "<requestId>", "code": "482913" }'A code lasts 10 minutes, works once, and allows five attempts. An address with no account gets a
requestId too, one that fails exactly like a wrong code, so the response never reveals whether
the address is registered.
The code is the door that stays inside your UI
The email code never sends the user anywhere: they read it and type it into your own screen. If your app's sign-in lives on your own domain, it is the passwordless door to reach for (see Where links land).
Magic link
POST /{appUid}/auth/magic-link/request emails a one-click link carrying a token. The page the link
opens posts that token to POST /{appUid}/auth/magic-link/verify and gets the session.
curl -X POST "$AUTHORIC_URL/$APP_UID/auth/magic-link/request" \
-H "Content-Type: application/json" \
-d '{ "email": "dana@example.com" }'
curl -X POST "$AUTHORIC_URL/$APP_UID/auth/magic-link/verify" \
-H "Content-Type: application/json" \
-d '{ "token": "<token from the link>" }'A link lasts 15 minutes and works once. The request always answers the same message, account or
not. Add redirectTo to the request to have the link open your own page (see
Where links land).
Google and GitHub
Start
POST /{appUid}/auth/oauth/{provider}/start (google or github) returns authorizeUrl. Send
the browser there. Pass redirectTo, an address on one of your app's
allowed domains, to have the user come back to your own
callback page.
The provider sends the browser back
It returns to the deployment's single registered callback, which redirects to your redirectTo
(or the console's /auth/callback without one) with status=ok&code=…&appUid=… (or
status=error&reason=<CODE>). The code is a single-use hand-off that lasts two minutes. No token
ever travels in a URL.
Exchange the code
POST /{appUid}/auth/oauth/exchange with { "code": "..." } returns the session payload.
curl -X POST "$AUTHORIC_URL/$APP_UID/auth/oauth/google/start" \
-H "Content-Type: application/json" \
-d '{ "redirectTo": "https://yourapp.com/auth/callback" }'
curl -X POST "$AUTHORIC_URL/$APP_UID/auth/oauth/exchange" \
-H "Content-Type: application/json" \
-d '{ "code": "<code from the callback>" }'Which account a social sign-in becomes
- An existing link wins. A Google or GitHub account already linked to a user signs that user in, even if its email has since changed.
- Otherwise the email is matched, only if the provider has verified it. An unverified address
is refused with
OAUTH_EMAIL_UNVERIFIED, because matching on one is a classic takeover. - Otherwise a new account is created, if the deployment allows social sign-up
(
OAUTH_SIGNUP_DISABLEDif not).
Where links land
Magic links, password-reset links and the social sign-in callback land on your own page when
the request names one in redirectTo. It must be on one of your app's
allowed domains (set in App settings › Domains) or on
localhost; any other address is refused with 400 REDIRECT_NOT_ALLOWED before an email is sent
or a redirect happens. The link carries the same parameters either way, and any query already on
redirectTo is kept:
| Flow | With redirectTo | Without |
|---|---|---|
| Magic link | redirectTo?appUid=…&token=…&accountType=user | /auth/magic-link?appUid=…&token=… |
| Password reset | redirectTo?appUid=…&token=…&accountType=user | /reset-password?appUid=…&token=… |
| Google / GitHub | redirectTo?status=…&code=…&appUid=… | /auth/callback?status=…&code=…&appUid=… |
Without redirectTo, a link goes to the public address the deployment is configured with, with
the app named in the query string.
curl -X POST "$AUTHORIC_URL/$APP_UID/auth/request-password-reset" \
-H "Content-Type: application/json" \
-d '{ "email": "dana@example.com", "redirectTo": "https://yourapp.com/reset" }'Password and the email code work entirely from your own screens. The emails themselves go out as your app: its sender and its branding, when you have set them up under App integrations › Email and App settings › Branding.
Linked methods and switching one off
A signed-in user can see and manage their own ways in. These routes act only on the caller's own account, taken from the session, never from the request body.
| Method | Path | Does |
|---|---|---|
GET | /{appUid}/auth/identities | List every way in: password, email code, magic link and each linked provider |
PATCH | /{appUid}/auth/identities/{methodId} | { "enabled": false } switches one off; true switches it back on |
DELETE | /{appUid}/auth/identities/{identityUid} | Unlink a Google or GitHub account |
{
"methods": [
{ "kind": "password", "methodId": "password", "identityUid": null, "enabled": true },
{ "kind": "google", "methodId": "0199a3d0-0b44-7e1a-b3c9-55d2a4e81f06", "identityUid": "0199a3d0-0b44-7e1a-b3c9-55d2a4e81f06", "enabled": true },
{ "kind": "magic_link", "methodId": "magic_link", "identityUid": null, "enabled": true },
{ "kind": "email_otp", "methodId": "email_otp", "identityUid": null, "enabled": true }
]
}Use each entry's methodId in the PATCH path: a linked provider's identity uid, or the literals
password, magic_link and email_otp for the doors that work without anything linked. (Each
entry also carries label, email, lastLoginAt and createdAt.)
- Off is not unlinked. A switched-off method stays listed and can be switched back on, and a
linked provider account stays claimed by this user. Every door enforces it: the password and
email doors refuse with their usual generic answer (an email door sends nothing), and a
switched-off provider answers
OAUTH_METHOD_DISABLED. - The last way in cannot be switched off or unlinked. Both refuse with a
400naming the problem. The count includes the email doors, which always work for the account's own address.
Rate limits and enumeration
| What | Limit (defaults) |
|---|---|
| Anything that sends mail (magic link, email code, password reset) | 5 per address and 20 per IP address, per 15 minutes |
| Anything that spends a credential (password, code, link, OAuth start and exchange, reset confirm) | 30 per IP address per 15 minutes |
A throttled request answers 429, and says nothing about whether the address exists. Every dead
link or code (missing, expired, used, revoked, wrong, out of attempts) is the same
404 LINK_TOKEN_INVALID.
Errors to branch on
| Code | Status | Meaning |
|---|---|---|
LINK_TOKEN_INVALID | 404 | A link, code or OAuth state is invalid, expired, used or revoked. Start again. |
OAUTH_PROVIDER_UNAVAILABLE | 400 | The provider is unknown or not configured on this deployment. |
OAUTH_EMAIL_UNVERIFIED | 400 | The provider returned no verified email, so the account cannot be matched safely. |
OAUTH_SIGNUP_DISABLED | 400 | No account matches, and social sign-up is off. |
OAUTH_METHOD_DISABLED | 400 | The user switched this provider off for their account. |
OAUTH_EXCHANGE_FAILED | 400 | The provider exchange failed. Usually transient: retry. |
REDIRECT_NOT_ALLOWED | 400 | redirectTo is not on one of your app's allowed domains or localhost. Nothing was sent. |
A sign-in that succeeds on any of these doors can still return a two-factor challenge instead of a session. See Two-factor authentication.