Skip to content
runlot

Dashboard and CLI login

Covers 29 API operations in the auth category.

MethodPathDescription
POST/v1/auth/dev/loginSign in with the dev login method
POST/v1/auth/password/sign-upStart a dashboard sign-up by email
POST/v1/auth/password/sign-inSign in to the dashboard with email and password
POST/v1/auth/totpFinish a sign-in with the second factor
POST/v1/auth/password/forgotRequest a dashboard password reset
POST/v1/auth/password/resetReset a dashboard password
POST/v1/auth/email/verifyVerify a dashboard email address and choose its password
GET/v1/auth/github/startStart GitHub OAuth
GET/v1/auth/github/callbackGitHub 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}/cancelRunlot sign-in: back to the app
POST/v1/app-auth/choose/{id}/continueRunlot sign-in: continue as the remembered Runlot user
POST/v1/app-auth/choose/{id}/passkey/optionsRunlot sign-in: passkey assertion options
POST/v1/app-auth/choose/{id}/passkeyRunlot sign-in: verify a passkey assertion
POST/v1/app-auth/passkey/register/optionsPasskey offer: creation options
POST/v1/app-auth/passkey/registerPasskey offer: store the new passkey
POST/v1/app-auth/choose/{id}/passwordRunlot sign-in: email and password
GET/v1/auth/{provider}/callback/appManaged app sign-in: provider callback
GET/v1/auth/google/startStart Google OAuth
GET/v1/auth/google/callbackGoogle OAuth callback
GET/v1/auth/oidc/startStart a generic OIDC login
GET/v1/auth/oidc/callbackGeneric OIDC callback
GET/v1/configPublic names and settings for this deployment
GET/v1/meThe current user and their organizations
GET/v1/me/totpThis account's two-factor state
DELETE/v1/me/totpTurn two-factor authentication off
POST/v1/me/totp/enrollGenerate an authenticator secret
POST/v1/me/totp/confirmConfirm 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 codeDescriptionResponse body
200Session tokenSession
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 codeDescriptionResponse body
202Sign-up request acceptedAuthEmailAccepted
400——
429Too 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 codeDescriptionResponse body
200A 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——
401Email or password is invalid (invalid_credentials)Error
403The email address has not been verified (email_unverified)Error
429Too 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 codeDescriptionResponse body
200Session tokenSession
400——
401The code did not match (invalid_code), or the pending token expired (mfa_expired)Error
409This account has no authenticator (totp_disabled)Error
429Too many attempts (5 per 5 minutes per account); retry after Retry-After—
503This deployment cannot read the account's secret (totp_unreadable, no_totp_key) — use a recovery codeError

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 codeDescriptionResponse body
202Password reset email accepted for deliveryAuthEmailAccepted
400——
429Too 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 codeDescriptionResponse body
200New session tokenSession
400——
401——
429Too 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 codeDescriptionResponse body
200Session tokenSession
400——
401——
409A 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

ParameterLocationRequiredTypeDescription
redirectqueryYesstring—
Status codeDescriptionResponse body
302to 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

ParameterLocationRequiredTypeDescription
codequeryNostring—
statequeryYesstring—
errorqueryNostring—
Status codeDescriptionResponse body
302to 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

ParameterLocationRequiredTypeDescription
idpathYesstring—
idpqueryNostring—
login_hintqueryNostring—
error_nextqueryNostring—
promptqueryNostring—
Status codeDescriptionResponse body
302to the provider—
400sign_in_expired — unknown, expired or used id—
503managed_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

ParameterLocationRequiredTypeDescription
idpathYesstring—
Status codeDescriptionResponse body
200The chooser pagestring
400sign_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=&lt;id&gt;.

operationId appAuthChooseCancel

ParameterLocationRequiredTypeDescription
idpathYesstring—
Status codeDescriptionResponse body
302to the app host with auth_error=cancelled—
400sign_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

ParameterLocationRequiredTypeDescription
idpathYesstring—
Status codeDescriptionResponse body
200the chooser (no session)string
303to the app host with a grant—

POST /v1/app-auth/choose/{id}/passkey/options

Runlot sign-in: passkey assertion options

operationId appAuthPasskeyOptions

ParameterLocationRequiredTypeDescription
idpathYesstring—
Status codeDescriptionResponse body
200&#123;challengeId, publicKey&#125; — WebAuthn PublicKeyCredentialRequestOptions (discoverable)object
404——

POST /v1/app-auth/choose/{id}/passkey

Runlot sign-in: verify a passkey assertion

operationId appAuthPasskeyVerify

ParameterLocationRequiredTypeDescription
idpathYesstring—
challengequeryYesstring—

Request body: application/json · object

Status codeDescriptionResponse body
200&#123;redirect&#125; — the app URL carrying the grantobject
401passkey_failed—

POST /v1/app-auth/passkey/register/options

Passkey offer: creation options

operationId appAuthPasskeyRegisterOptions

ParameterLocationRequiredTypeDescription
ticketqueryYesstring—
Status codeDescriptionResponse body
200&#123;publicKey&#125; — PublicKeyCredentialCreationOptionsobject
400——

POST /v1/app-auth/passkey/register

Passkey offer: store the new passkey

operationId appAuthPasskeyRegister

ParameterLocationRequiredTypeDescription
ticketqueryYesstring—

Request body: application/json · object

Status codeDescriptionResponse body
200&#123;redirect&#125;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=&lt;grant&gt;; a refusal re-renders the chooser with the error.

operationId appAuthChoosePassword

ParameterLocationRequiredTypeDescription
idpathYesstring—

Request body: application/x-www-form-urlencoded · object

Status codeDescriptionResponse body
200The chooser re-rendered with an errorstring
302to the app host with a grant—
400sign_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://&lt;app host&gt;/__runlot/auth/callback/managed?code=&lt;grant&gt; 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 &lt;public origin&gt;/v1/auth/google/callback/app on the Google OAuth client. GitHub accepts it as a subpath of the app's registered &lt;public origin&gt;/v1/auth/github/callback.

operationId appAuthCallback

ParameterLocationRequiredTypeDescription
providerpathYesstring—
codequeryNostring—
statequeryYesstring—
errorqueryNostring—
Status codeDescriptionResponse body
302to the app host—
400sign_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

ParameterLocationRequiredTypeDescription
redirectqueryYesstring—
Status codeDescriptionResponse body
302to 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

ParameterLocationRequiredTypeDescription
codequeryNostring—
statequeryYesstring—
errorqueryNostring—
Status codeDescriptionResponse body
302to 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

ParameterLocationRequiredTypeDescription
redirectqueryYesstring—
Status codeDescriptionResponse body
302to the provider's authorization endpoint—
400——
404No identity provider is configured on this deployment—
502The 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:&lt;issuer host&gt;", 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

ParameterLocationRequiredTypeDescription
codequeryNostring—
statequeryYesstring—
errorqueryNostring—
Status codeDescriptionResponse body
302to the client callback—
400——
404No 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 codeDescriptionResponse body
200Public settings for the deploymentobject

GET /v1/me

The current user and their organizations

operationId getMe

Status codeDescriptionResponse body
200The current user and their organizationsobject
401——

GET /v1/me/totp

This account's two-factor state

operationId getTotp

Status codeDescriptionResponse body
200The account's two-factor stateTotpStatus
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 codeDescriptionResponse body
204Two-factor authentication is off—
400——
401The code did not match (invalid_code)Error
409This account has no authenticator (totp_disabled)Error
429Too 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 codeDescriptionResponse body
200The secret and the URI to scanTotpEnrollment
401——
409Two-factor authentication is already on (totp_enabled)Error
503This 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 codeDescriptionResponse body
200Two-factor authentication is on; here are the recovery codesTotpRecoveryCodes
400——
401The code did not match (invalid_code)Error
409There is nothing to confirm (no_enrollment), or it is already on (totp_enabled)Error
429Too many attempts; retry after Retry-After—

On this page