Dashboard and CLI login
Covers 29 API operations in the auth category.
| Method | Path | Description |
|---|---|---|
| POST | /v1/auth/dev/login | Sign in with the dev login method |
| POST | /v1/auth/password/sign-up | Start a dashboard sign-up by email |
| POST | /v1/auth/password/sign-in | Sign in to the dashboard with email and password |
| POST | /v1/auth/totp | Finish a sign-in with the second factor |
| POST | /v1/auth/password/forgot | Request a dashboard password reset |
| POST | /v1/auth/password/reset | Reset a dashboard password |
| POST | /v1/auth/email/verify | Verify a dashboard email address and choose its password |
| GET | /v1/auth/github/start | Start GitHub OAuth |
| GET | /v1/auth/github/callback | GitHub OAuth callback |
| GET | /v1/app-auth/start/{id} | Managed app sign-in: start leg |
| GET | /v1/app-auth/choose/{id} | Runlot sign-in: chooser page |
| GET | /v1/app-auth/choose/{id}/cancel | Runlot sign-in: back to the app |
| POST | /v1/app-auth/choose/{id}/continue | Runlot sign-in: continue as the remembered Runlot user |
| POST | /v1/app-auth/choose/{id}/passkey/options | Runlot sign-in: passkey assertion options |
| POST | /v1/app-auth/choose/{id}/passkey | Runlot sign-in: verify a passkey assertion |
| POST | /v1/app-auth/passkey/register/options | Passkey offer: creation options |
| POST | /v1/app-auth/passkey/register | Passkey offer: store the new passkey |
| POST | /v1/app-auth/choose/{id}/password | Runlot sign-in: email and password |
| GET | /v1/auth/{provider}/callback/app | Managed app sign-in: provider callback |
| GET | /v1/auth/google/start | Start Google OAuth |
| GET | /v1/auth/google/callback | Google OAuth callback |
| GET | /v1/auth/oidc/start | Start a generic OIDC login |
| GET | /v1/auth/oidc/callback | Generic OIDC callback |
| GET | /v1/config | Public names and settings for this deployment |
| GET | /v1/me | The current user and their organizations |
| GET | /v1/me/totp | This account's two-factor state |
| DELETE | /v1/me/totp | Turn two-factor authentication off |
| POST | /v1/me/totp/enroll | Generate an authenticator secret |
| POST | /v1/me/totp/confirm | Confirm the authenticator and turn the factor on |
POST /v1/auth/dev/login
A local-only sign-in method. It serves the same purpose as GitHub OAuth, but without an
external round trip. In production this route is not registered — it exists only on
servers where RUNLOT_ENABLE_DEV_AUTH is on.
Calling it again with the same subject returns the same user.
operationId devLogin
Request body: application/json · object
| Status code | Description | Response body |
|---|---|---|
| 200 | Session token | Session |
| 400 | — | — |
POST /v1/auth/password/sign-up
For a new email address, this queues a verification message. It does not create a
session: the account becomes usable when the recipient calls /v1/auth/email/verify.
An existing verified account instead receives an "already registered" message at its
own mailbox, pointing at password reset. The response is identical in both cases, so it
never discloses whether an account exists.
No password is sent here. The recipient of the emailed link chooses the password at
/v1/auth/email/verify.
operationId passwordSignUp
Request body: application/json · object
| Status code | Description | Response body |
|---|---|---|
| 202 | Sign-up request accepted | AuthEmailAccepted |
| 400 | — | — |
| 429 | Too many attempts; retry after the number of seconds in Retry-After | — |
POST /v1/auth/password/sign-in
Sign in to the dashboard with email and password
operationId passwordSignIn
Request body: application/json · object
| Status code | Description | Response body |
|---|---|---|
| 200 | A session token — or, for an account with two-factor authentication on, a {"mfa":"totp","pending":…} body carrying no token. Finish with POST /v1/auth/totp. | Session |
| 400 | — | — |
| 401 | Email or password is invalid (invalid_credentials) | Error |
| 403 | The email address has not been verified (email_unverified) | Error |
| 429 | Too many attempts; retry after the number of seconds in Retry-After | — |
POST /v1/auth/totp
Takes the pending token the first factor handed back, plus either the six-digit code
from your authenticator app or one recovery code. On success it issues the real
session.
The pending token carries the account's session revision, so a password reset landing between the two steps invalidates the half-finished login too.
A code can only be spent once. One code is valid across three 30-second steps, and a code whose step has already been used is refused.
operationId completeTotpLogin
Request body: application/json · object
| Status code | Description | Response body |
|---|---|---|
| 200 | Session token | Session |
| 400 | — | — |
| 401 | The code did not match (invalid_code), or the pending token expired (mfa_expired) | Error |
| 409 | This account has no authenticator (totp_disabled) | Error |
| 429 | Too many attempts (5 per 5 minutes per account); retry after Retry-After | — |
| 503 | This deployment cannot read the account's secret (totp_unreadable, no_totp_key) — use a recovery code | Error |
POST /v1/auth/password/forgot
Queues a reset email. The response is deliberately the same whether or not an account exists for the email address.
operationId passwordForgot
Request body: application/json · object
| Status code | Description | Response body |
|---|---|---|
| 202 | Password reset email accepted for delivery | AuthEmailAccepted |
| 400 | — | — |
| 429 | Too many attempts; retry after the number of seconds in Retry-After | — |
POST /v1/auth/password/reset
Consumes a one-hour reset token, changes the password, invalidates sessions issued before the change, and returns a new session.
operationId passwordReset
Request body: application/json · object
| Status code | Description | Response body |
|---|---|---|
| 200 | New session token | Session |
| 400 | — | — |
| 401 | — | — |
| 429 | Too many attempts; retry after the number of seconds in Retry-After | — |
POST /v1/auth/email/verify
Consumes a 24-hour verification token, creates the account with the password chosen here, and returns the account's first session. The password is chosen at this step rather than at sign-up, so it arrives together with proof of mailbox access.
operationId verifyEmail
Request body: application/json · object
| Status code | Description | Response body |
|---|---|---|
| 200 | Session token | Session |
| 400 | — | — |
| 401 | — | — |
| 409 | A verified account appeared after sign-up; use password reset (account_exists) | Error |
GET /v1/auth/github/start
redirect is the client callback to return to, with the token carried as a
fragment, after login finishes (https://dash.runlot.io/login/callback). It must
match a prefix in the server's allowlist (RUNLOT_GITHUB_REDIRECTS, defaulting
to RUNLOT_PUBLIC_URL). This sends a 302 to the GitHub authorize page. Since
the browser follows this path, client libraries don't call it — open it with
window.location.
operationId githubStart
| Parameter | Location | Required | Type | Description |
|---|---|---|---|---|
redirect | query | Yes | string | — |
| Status code | Description | Response body |
|---|---|---|
| 302 | to GitHub authorize | — |
| 400 | — | — |
GET /v1/auth/github/callback
This path is the GitHub app's Authorization callback URL. It exchanges
the code for a session, then issues a 302 to redirect — on success,
#token=…&expiresAt=…&userId=…&subject=…; on failure,
#error=<code>&message=…. It uses a fragment so the token is not
recorded in server logs or the Referer header.
operationId githubCallback
| Parameter | Location | Required | Type | Description |
|---|---|---|---|---|
code | query | No | string | — |
state | query | Yes | string | — |
error | query | No | string | — |
| Status code | Description | Response body |
|---|---|---|
| 302 | to the client callback | — |
| 400 | — | — |
GET /v1/app-auth/start/{id}
The sign-in broker for apps using a managed provider. The app's own
/__runlot/auth/sign-in/<provider> sends the browser here; id was minted
for one project, provider and attached host and lives ten minutes. Sends a 302
to the provider with identity-only scopes (GitHub read:user user:email,
Google openid email profile with PKCE).
For the runlot provider (docs/custom-sign-in.md §12.2) four optional
parameters are validated and kept on the start; an invalid one is dropped.
idp=google|github goes straight to that upstream when the project offers it,
idp=password|passkey opens the chooser in that mode, prompt=none issues a
grant with no page when the browser has a Runlot ID session and the person has
signed in to this project before, else returns auth_error=login_required.
Every failure returns to error_next (or next) on the app host with
auth_error=<code>&auth_request=<id>.
operationId appAuthStart
| Parameter | Location | Required | Type | Description |
|---|---|---|---|---|
id | path | Yes | string | — |
idp | query | No | string | — |
login_hint | query | No | string | — |
error_next | query | No | string | — |
prompt | query | No | string | — |
| Status code | Description | Response body |
|---|---|---|
| 302 | to the provider | — |
| 400 | sign_in_expired — unknown, expired or used id | — |
| 503 | managed_unavailable — no broker credential for that provider | — |
GET /v1/app-auth/choose/{id}
For the runlot provider the start leg sends the browser here instead of straight
to an upstream. An HTML page, branded Runlot and naming the app, offering email and
password, Google and GitHub. Google and GitHub continue through the
/v1/auth/{provider}/callback/app legs; the grant always carries
provider = runlot and a pairwise subject.
operationId appAuthChoose
| Parameter | Location | Required | Type | Description |
|---|---|---|---|---|
id | path | Yes | string | — |
| Status code | Description | Response body |
|---|---|---|
| 200 | The chooser page | string |
| 400 | sign_in_expired — unknown, expired or used id | — |
GET /v1/app-auth/choose/{id}/cancel
The chooser's "back to <app>" link. Consumes the start and returns to the app with
auth_error=cancelled&auth_request=<id>.
operationId appAuthChooseCancel
| Parameter | Location | Required | Type | Description |
|---|---|---|---|---|
id | path | Yes | string | — |
| Status code | Description | Response body |
|---|---|---|
| 302 | to the app host with auth_error=cancelled | — |
| 400 | sign_in_expired | — |
POST /v1/app-auth/choose/{id}/continue
One click for a browser holding the broker's Runlot ID cookie (docs/custom-sign-in.md §12.7). Without a valid cookie the chooser is re-rendered.
operationId appAuthChooseContinue
| Parameter | Location | Required | Type | Description |
|---|---|---|---|---|
id | path | Yes | string | — |
| Status code | Description | Response body |
|---|---|---|
| 200 | the chooser (no session) | string |
| 303 | to the app host with a grant | — |
POST /v1/app-auth/choose/{id}/passkey/options
Runlot sign-in: passkey assertion options
operationId appAuthPasskeyOptions
| Parameter | Location | Required | Type | Description |
|---|---|---|---|---|
id | path | Yes | string | — |
| Status code | Description | Response body |
|---|---|---|
| 200 | {challengeId, publicKey} — WebAuthn PublicKeyCredentialRequestOptions (discoverable) | object |
| 404 | — | — |
POST /v1/app-auth/choose/{id}/passkey
Runlot sign-in: verify a passkey assertion
operationId appAuthPasskeyVerify
| Parameter | Location | Required | Type | Description |
|---|---|---|---|---|
id | path | Yes | string | — |
challenge | query | Yes | string | — |
Request body: application/json · object
| Status code | Description | Response body |
|---|---|---|
| 200 | {redirect} — the app URL carrying the grant | object |
| 401 | passkey_failed | — |
POST /v1/app-auth/passkey/register/options
Passkey offer: creation options
operationId appAuthPasskeyRegisterOptions
| Parameter | Location | Required | Type | Description |
|---|---|---|---|---|
ticket | query | Yes | string | — |
| Status code | Description | Response body |
|---|---|---|
| 200 | {publicKey} — PublicKeyCredentialCreationOptions | object |
| 400 | — | — |
POST /v1/app-auth/passkey/register
Passkey offer: store the new passkey
operationId appAuthPasskeyRegister
| Parameter | Location | Required | Type | Description |
|---|---|---|---|---|
ticket | query | Yes | string | — |
Request body: application/json · object
| Status code | Description | Response body |
|---|---|---|
| 200 | {redirect} | object |
| 400 | — | — |
POST /v1/app-auth/choose/{id}/password
The chooser's email-and-password form. sign-up creates an end-user Runlot account
with no organization. On success, a 302 to the app host's
/__runlot/auth/callback/managed?code=<grant>; a refusal re-renders the chooser
with the error.
operationId appAuthChoosePassword
| Parameter | Location | Required | Type | Description |
|---|---|---|---|---|
id | path | Yes | string | — |
Request body: application/x-www-form-urlencoded · object
| Status code | Description | Response body |
|---|---|---|
| 200 | The chooser re-rendered with an error | string |
| 302 | to the app host with a grant | — |
| 400 | sign_in_expired — unknown, expired or used id | — |
GET /v1/auth/{provider}/callback/app
The provider returns here. The broker exchanges the code, reads the profile and
verified email, and sends a 302 to
https://<app host>/__runlot/auth/callback/managed?code=<grant> with a
single-use grant valid five minutes. A denial or cancel goes to
/__runlot/auth/sign-in?error=provider_denied on the app host. No provider
token ever leaves the broker.
Register <public origin>/v1/auth/google/callback/app on the Google OAuth
client. GitHub accepts it as a subpath of the app's registered
<public origin>/v1/auth/github/callback.
operationId appAuthCallback
| Parameter | Location | Required | Type | Description |
|---|---|---|---|---|
provider | path | Yes | string | — |
code | query | No | string | — |
state | query | Yes | string | — |
error | query | No | string | — |
| Status code | Description | Response body |
|---|---|---|
| 302 | to the app host | — |
| 400 | sign_in_expired — unknown, expired or used state | — |
GET /v1/auth/google/start
redirect is the client callback to return to, with the token carried as a fragment,
after login finishes. The redirect must match the server allowlist. This sends a 302
to Google and uses PKCE for the authorization-code exchange.
operationId googleStart
| Parameter | Location | Required | Type | Description |
|---|---|---|---|---|
redirect | query | Yes | string | — |
| Status code | Description | Response body |
|---|---|---|
| 302 | to Google authorize | — |
| 400 | — | — |
GET /v1/auth/google/callback
This path is the Google OAuth client's callback URL. It exchanges the authorization code and PKCE verifier for a session, then redirects to the client callback with the result in the URL fragment, using the same shape as the GitHub callback.
operationId googleCallback
| Parameter | Location | Required | Type | Description |
|---|---|---|---|---|
code | query | No | string | — |
state | query | Yes | string | — |
error | query | No | string | — |
| Status code | Description | Response body |
|---|---|---|
| 302 | to the client callback | — |
| 400 | — | — |
GET /v1/auth/oidc/start
The provider is configured per installation (RUNLOT_OIDC_ISSUER), and the three
endpoints come from that issuer's /.well-known/openid-configuration rather than from
this document. The two OIDC routes exist only when an issuer is configured; without
one they are not registered and answer 404, which is how a client can tell whether this
deployment federates an identity provider.
redirect is the client callback the token is carried back to, as a fragment, and it
has to match the server allowlist. The redirect to the provider uses PKCE S256 and a
nonce that binds the resulting id_token to this attempt.
operationId oidcStart
| Parameter | Location | Required | Type | Description |
|---|---|---|---|---|
redirect | query | Yes | string | — |
| Status code | Description | Response body |
|---|---|---|
| 302 | to the provider's authorization endpoint | — |
| 400 | — | — |
| 404 | No identity provider is configured on this deployment | — |
| 502 | The provider's discovery document could not be read | — |
GET /v1/auth/oidc/callback
This path is the registered redirect URI of the installation's OIDC client. It exchanges
the authorization code and PKCE verifier at the token endpoint, verifies the
id_token's signature against the issuer's JWKS (RS256 and ES256) along with its
issuer, audience, expiry and nonce, and then redirects to the client callback with the
result in the URL fragment — the same shape as the GitHub and Google callbacks.
The identity is stored as provider = "oidc:<issuer host>", so two installations
federating different providers never collide on a subject.
A sign-up the installation's RUNLOT_SIGNUP policy refuses comes back as
#error=signup_closed, and the account is not created.
operationId oidcCallback
| Parameter | Location | Required | Type | Description |
|---|---|---|---|---|
code | query | No | string | — |
state | query | Yes | string | — |
error | query | No | string | — |
| Status code | Description | Response body |
|---|---|---|
| 302 | to the client callback | — |
| 400 | — | — |
| 404 | No identity provider is configured on this deployment | — |
GET /v1/config
The public values a client needs before anyone signs in: the app hostname suffix, the docs and marketing site origins, the product-analytics key and host, and the caller's country. Nothing here is a secret — these are the same names the landing page shows — so the route needs no authentication.
Feature on/off state is not here — billing, storage, and email each have their own endpoints that report their own state.
operationId getSiteConfig
| Status code | Description | Response body |
|---|---|---|
| 200 | Public settings for the deployment | object |
GET /v1/me
The current user and their organizations
operationId getMe
| Status code | Description | Response body |
|---|---|---|
| 200 | The current user and their organizations | object |
| 401 | — | — |
GET /v1/me/totp
This account's two-factor state
operationId getTotp
| Status code | Description | Response body |
|---|---|---|
| 200 | The account's two-factor state | TotpStatus |
| 401 | — | — |
DELETE /v1/me/totp
Requires a current code or one recovery code. A session alone is not enough.
The account's recovery codes are deleted at the same time, so the next enrollment issues a fresh set.
operationId disableTotp
Request body: application/json · object
| Status code | Description | Response body |
|---|---|---|
| 204 | Two-factor authentication is off | — |
| 400 | — | — |
| 401 | The code did not match (invalid_code) | Error |
| 409 | This account has no authenticator (totp_disabled) | Error |
| 429 | Too many attempts; retry after Retry-After | — |
POST /v1/me/totp/enroll
Generates a fresh 20-byte secret and returns it with the otpauth:// URI to scan.
It does not turn the factor on. POST /v1/me/totp/confirm does that, once your
app has proved it can produce a code.
Re-enrolling over a confirmed authenticator is refused with 409 — turn it off first, which itself requires a code.
operationId enrollTotp
| Status code | Description | Response body |
|---|---|---|
| 200 | The secret and the URI to scan | TotpEnrollment |
| 401 | — | — |
| 409 | Two-factor authentication is already on (totp_enabled) | Error |
| 503 | This deployment has no sealing key (no_totp_key) | Error |
POST /v1/me/totp/confirm
Verifies one code against the secret from enroll (RFC 6238, SHA-1, six digits,
a 30-second step, ±1 step), turns the factor on, and issues ten recovery codes.
The recovery codes are readable only in this response. They are stored hashed, so no endpoint can list them later. Save them now.
operationId confirmTotp
Request body: application/json · object
| Status code | Description | Response body |
|---|---|---|
| 200 | Two-factor authentication is on; here are the recovery codes | TotpRecoveryCodes |
| 400 | — | — |
| 401 | The code did not match (invalid_code) | Error |
| 409 | There is nothing to confirm (no_enrollment), or it is already on (totp_enabled) | Error |
| 429 | Too many attempts; retry after Retry-After | — |