Skip to content
Docs

Visitor sign-in

Site-scoped browser sessions for website visitors (customers, guests, members), on the site’s own address, through https://simple-host.app/auth.js (SH.mount, SH.requireSignIn, SH.me, SH.signOut). The only sign-in a page ever uses. Never returns an API key.

POST /v1/site-signin

The form on a named-viewers site’s sign-in page posts here, on the site’s own host; no key, no JavaScript. With email alone it sends a sign-in code (to any address, listed or not, so the form never says who is on the list) and answers the code page (401, text/html). With email and code it signs the visitor in to this site on this host (the same visitor session as auth.js) and answers 303 to next. Only a form from a page on the same host is accepted (403 origin_not_allowed otherwise). On a site that is not specific it answers 303 to next. Rate limited per address and per email like visitor sign-in.

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

Responses:

Status Description
303 Signed in; go back to next
400 Not a form post
401 The code page, or the sign-in page again with the reason (text/html, noindex, no-store)
403 Origin/Referer is not one of the site’s own addresses (code origin_not_allowed). An agent or script sends the site owner’s X-API-Key and no Origin or Referer, which needs no page.

POST /v1/site-signout

The Switch account and Sign out buttons on “This site is private” post here, on the site’s own host. Ends this host’s visitor session and answers 303 to next, which then shows the sign-in page. Only a form from a page on the same host is accepted.

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

Responses:

Status Description
303 Signed out; go back to next
400 Not a form post
403 Origin/Referer is not one of the site’s own addresses (code origin_not_allowed). An agent or script sends the site owner’s X-API-Key and no Origin or Referer, which needs no page.

POST /v1/site-unlock

The form on the “This site is protected” page posts here, on the site’s own host; no key. The site is the one the host names (on a person host, the first path segment of next). Only a form from a page on the same host is accepted (another origin gets 403 origin_not_allowed as JSON). A right passcode answers 303 to next with the unlock cookie for this host. A wrong one answers the gate page again (401). Wrong tries are limited per site and address (RATE_LIMIT_PASSCODE_IP, default 5 per 3 minutes, then PASSCODE_LOCKOUT_MINUTES, default 15) and per site from everyone together (RATE_LIMIT_PASSCODE_SITE, default 60 a minute, then PASSCODE_SITE_LOCKOUT_MINUTES, default 15, when every try is refused; visitors already let in are not affected): 429 with Retry-After.

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

Responses:

Status Description
303 Right passcode; go back to next
400 Not a form post
401 Wrong passcode: the gate page again, saying so (text/html, noindex, no-store)
403 Origin/Referer is not one of the site’s own addresses (code origin_not_allowed). An agent or script sends the site owner’s X-API-Key and no Origin or Referer, which needs no page.
429 Too many wrong tries: the gate page saying when to try again (text/html)

GET /v1/sites/{sitename}/me

Pages call it same-origin on the site’s own address — its address https://<site>.<handle>.simple-host.app/ (only that site answers there), or its domain if it has one. Origin/Referer-gated. When the site has a connected domain and the call comes through another address, it returns signed_in false, sign_in_available false and code use_custom_domain with domain naming it, so the page can send the visitor to sign in there. Where sign-in is not offered (the legacy path address) the code is custom_domain_required. Checks the host, site and session expiry without extending the session. No CSRF header required. Anonymous visitors receive 200.

Parameters:

Name In Type Required Description
sitename path string yes

Responses:

Status Description
200 Signed-in identity or anonymous visitor
403 Origin/Referer is not one of the site’s own addresses (code origin_not_allowed). An agent or script sends the site owner’s X-API-Key and no Origin or Referer, which needs no page.
404 Not found
500 Internal server error

POST /v1/sites/{sitename}/visitor/auth

Origin-gated; sign-in works on the site’s own address (its address https://<site>.<handle>.simple-host.app/, or its domain if it has one), not on the apex or the legacy path address. Uses the shared account email-code limiter and site IP limiter. Normalizes and validates email, then sends a 6-digit Resend code valid for 15 minutes. Does not create the account yet.

No authentication required.

Parameters:

Name In Type Required Description
sitename path string yes

Request body:

Name Type Required Description
email string yes

Responses:

Status Description
202 Code sent; no dashboard magic link.
400 Invalid request, missing fields, apex host, or the legacy path address (custom_domain_required)
401 Invalid code. code stays invalid_code. reason is code_wrong, code_used_up, code_expired, or link_other_browser; tries_left is set on code_wrong. Verification only.
403 Origin forbidden or missing CSRF (verification)
404 Site not found
429 IP or shared per-email rate limit exceeded
500 Internal error or email delivery failed

POST /v1/sites/{sitename}/visitor/auth/verify

Origin-gated; sign-in works on the site’s own address (its address https://<site>.<handle>.simple-host.app/, or its domain if it has one), not on the apex or the legacy path address. Uses the shared account email-code limiter and dedicated visitor auth IP limiter. Codes are bound to this site and the visitor purpose; dashboard codes are not accepted. Checks the latest unused, unexpired token, caps attempts, and creates the account lazily. Never returns an API key. Session has 30-day absolute and 14-day idle lifetimes. Send X-SH-CSRF: 1 is required explicitly; application/json does not satisfy this requirement.

No authentication required.

Parameters:

Name In Type Required Description
sitename path string yes
X-SH-CSRF header string, one of 1 Required on cookie-authenticated writes

Request body:

Name Type Required Description
code string yes 6-digit email code
email string yes

Responses:

Status Description
200 Signed in; sets the existing host-only visitor session cookie. Email/provider are included; requests through the legacy path address return 400 custom_domain_required.
400 Invalid request, missing fields, apex host, or the legacy path address (custom_domain_required)
401 Invalid code. code stays invalid_code. reason is code_wrong, code_used_up, code_expired, or link_other_browser; tries_left is set on code_wrong. Verification only.
403 Origin forbidden or missing CSRF (verification)
404 Site not found
429 IP or shared per-email rate limit exceeded
500 Internal error or email delivery failed

POST /v1/sso/consent

Same-origin form POST with Sec-Fetch-Site validation and a one-use form token bound to the current SSO pass and server-stored hop. No account API accepts the SSO cookie.

No authentication required.

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

Responses:

Status Description
302 Consent remembered; navigate to the site’s establish endpoint
400 Invalid or expired hop
401 Sign in on the apex first
403 Invalid form token or origin
429 IP rate limit exceeded

DELETE /v1/sso/consents/{site}

Same-origin account-key request. The next hop asks again, including for named viewers. Does not revoke existing sessions. The apex SSO cookie alone cannot authorize this operation. No session-list UI.

Auth: an API key (X-API-Key header, or a REST-audience connector token as Authorization: Bearer).

Parameters:

Name In Type Required Description
site path string yes Site UUID

Responses:

Status Description
204 Consent forgotten or already absent
401 Missing or invalid API key — send the X-API-Key header, not Authorization Bearer. The body’s code is missing_api_key, wrong_auth_header, invalid_api_key, key_expired or key_expired_idle (create a new key from the Keys panel).
403 Wrong origin or not a full account key

GET /v1/sso/hop

Uses only the apex SSO cookie. Shows the shared account sign-in card if absent, asks consent once per person and site including named viewers, or redirects with a 60-second host- and nonce-bound one-time establish code. The return address is read only from server state. No-store, no-referrer, and frame-ancestors none.

No authentication required.

Parameters:

Name In Type Required Description
state query string yes
token query string Browser-bound owner provider token
cn query string Provider browser nonce hash

Responses:

Status Description
200 Apex sign-in or consent card
302 Navigate to the site’s establish endpoint
400 Missing
403 Wrong origin or non-document request
429 IP rate limit exceeded

POST /v1/sso/session

Same-origin apex POST authenticated by a live stored full account key. Sets a pass cookie if the browser does not already hold a live pass for this key. Browser sign-in calls this after verification; the dashboard also calls it after loading. Replacing a different browser pass revokes its old parent key and site sessions. Issuance is limited per IP and key and retains at most one pass per parent key. Deploy keys and connector credentials cannot bootstrap it.

Auth: an API key (X-API-Key header, or a REST-audience connector token as Authorization: Bearer).

Responses:

Status Description
204 Apex SSO cookie ready
401 Missing
403 Wrong host or origin
429 Pass issuance rate limit exceeded

GET /v1/sso/signed-out

Rate-limited top-level navigation after site sign-out. Consumes the one-use ticket silently only when its live requesting site session’s parent hash matches this browser’s apex cookie. Otherwise shows a shared-card confirmation with a form token and Cancel link. On completion, clears the cookie and serves an apex page that revokes and removes the browser’s localStorage dashboard key before navigating to the server-stored return address, even when the pass row has been swept.

No authentication required.

Parameters:

Name In Type Required Description
token query string yes

Responses:

Status Description
200 Confirmation form or final dashboard-key sign-out page
400 Invalid
403 Wrong host or non-document request
429 IP rate limit exceeded

POST /v1/sso/signed-out

Same-origin apex form POST with Origin and Fetch Metadata checks, a one-use form token bound to the cookie that opened the confirmation, and a server-stored return address. Revokes the current family, clears the cookie, and serves the final dashboard-key sign-out page. Cannot be framed.

No authentication required.

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

Responses:

Status Description
200 Final apex page revokes the stored dashboard key and returns to the site
400 Invalid
403 Wrong host or origin
429 IP rate limit exceeded

GET /v1/sso/start

Stores the validated return address and nonce hash server-side, sets the host-only nonce cookie, and redirects to the account apex. Requires an unlocked passcode site and refuses offline or suspended sites. Top-level navigation only. Fetch metadata must indicate navigate and document when present; prefetch and prerender are refused. Rate-limited per IP.

No authentication required.

Parameters:

Name In Type Required Description
return_to query string yes

Responses:

Status Description
302 Nonce cookie set; navigate to the apex hop
400 Invalid site return address or navigation
403 Site locked
429 Rate limited (code rate_limited). Saved-data reads allow 30 a second per address with a burst of 60 (SAVED_DATA_READ_PER_SEC, SAVED_DATA_READ_BURST).

GET /v1/turnstile/frame

Served on the app’s own address only (PUBLIC_BASE_URL’s host; 404 elsewhere, and 404 when Turnstile is not set up). A small HTML page with Cloudflare Turnstile that any origin may frame (frame-ancestors *). auth.js embeds it when a save answers spam_check_required. It starts only when its parent window posts {type: “sh-turnstile-start”, resource}, renders the widget with action sh-post and a cdata that binds the token to the parent’s origin (the browser’s event.origin) and the resource, and posts {type: “sh-turnstile-token”, token} to that origin only. auth.js accepts messages only from this frame’s window and the app’s origin. The server then checks the token with siteverify: success, action sh-post, this app’s hostname, that cdata, a challenge under 300 seconds old, and single use. The visitor’s address is never sent to Cloudflare.

Responses:

Status Description
200 The frame page
404 Not the app’s own address

GET /v1/u/{handle}/sites/{sitename}/me

Pages call it same-origin on the site’s own address — its address https://<site>.<handle>.simple-host.app/ (only that site answers there), or its domain if it has one. Origin/Referer-gated. When the site has a connected domain and the call comes through another address, it returns signed_in false, sign_in_available false and code use_custom_domain with domain naming it, so the page can send the visitor to sign in there. Where sign-in is not offered (the legacy path address) the code is custom_domain_required. Checks the host, site and session expiry without extending the session. No CSRF header required. Anonymous visitors receive 200.

Parameters:

Name In Type Required Description
sitename path string yes
handle path string yes

Responses:

Status Description
200 Signed-in identity or anonymous visitor
403 Origin/Referer is not one of the site’s own addresses (code origin_not_allowed). An agent or script sends the site owner’s X-API-Key and no Origin or Referer, which needs no page.
404 Not found
500 Internal server error

POST /v1/u/{handle}/sites/{sitename}/visitor/auth

Origin-gated; sign-in works on the site’s own address (its address https://<site>.<handle>.simple-host.app/, or its domain if it has one), not on the apex or the legacy path address. Uses the shared account email-code limiter and site IP limiter. Normalizes and validates email, then sends a 6-digit Resend code valid for 15 minutes. Does not create the account yet.

No authentication required.

Parameters:

Name In Type Required Description
handle path string yes
sitename path string yes

Request body:

Name Type Required Description
email string yes

Responses:

Status Description
202 Code sent; no dashboard magic link.
400 Invalid request, missing fields, apex host, or the legacy path address (custom_domain_required)
401 Invalid code. code stays invalid_code. reason is code_wrong, code_used_up, code_expired, or link_other_browser; tries_left is set on code_wrong. Verification only.
403 Origin forbidden or missing CSRF (verification)
404 Site not found
429 IP or shared per-email rate limit exceeded
500 Internal error or email delivery failed

POST /v1/u/{handle}/sites/{sitename}/visitor/auth/verify

Origin-gated; sign-in works on the site’s own address (its address https://<site>.<handle>.simple-host.app/, or its domain if it has one), not on the apex or the legacy path address. Uses the shared account email-code limiter and dedicated visitor auth IP limiter. Codes are bound to this site and the visitor purpose; dashboard codes are not accepted. Checks the latest unused, unexpired token, caps attempts, and creates the account lazily. Never returns an API key. Session has 30-day absolute and 14-day idle lifetimes. Send X-SH-CSRF: 1 is required explicitly; application/json does not satisfy this requirement.

No authentication required.

Parameters:

Name In Type Required Description
handle path string yes
sitename path string yes
X-SH-CSRF header string, one of 1 Required on cookie-authenticated writes

Request body:

Name Type Required Description
code string yes 6-digit email code
email string yes

Responses:

Status Description
200 Signed in; sets the existing host-only visitor session cookie. Email/provider are included; requests through the legacy path address return 400 custom_domain_required.
400 Invalid request, missing fields, apex host, or the legacy path address (custom_domain_required)
401 Invalid code. code stays invalid_code. reason is code_wrong, code_used_up, code_expired, or link_other_browser; tries_left is set on code_wrong. Verification only.
403 Origin forbidden or missing CSRF (verification)
404 Site not found
429 IP or shared per-email rate limit exceeded
500 Internal error or email delivery failed

GET /v1/visitor/establish

One-time hop on the site’s own address (its <site>.<handle> host or domain). Consumes once, host-matches, requires the __Host-sh_vnonce cookie set by GET /v1/visitor/oauth/{provider} in this browser (its hash must match the one bound to the code; the code is burned on any failure), sets __Host-sh_vsess (or sh_vsess on local HTTP), clears the nonce cookie, then 302s to the stored return_to with no query mutation. Never sets a cookie on the apex or the legacy path address. Failures are HTML, no cookie, no redirect.

Parameters:

Name In Type Required Description
once query string yes

Responses:

Status Description
302 Cookie set; redirect to the stored return_to
400 Bad, replayed, expired, host-mismatch, missing or wrong nonce cookie, or apex Host
429 Rate limited (code rate_limited). Saved-data reads allow 30 a second per address with a burst of 60 (SAVED_DATA_READ_PER_SEC, SAVED_DATA_READ_BURST).

POST /v1/visitor/logout

For a hop session, deletes its parent dashboard key, apex pass, and all linked site sessions. Returns redirect_url for a top-level navigation through the apex. A matching family signs out silently; expired, missing, legacy, or mismatched sessions require confirmation. The final apex page revokes and removes the browser’s stored dashboard key before returning. Instances without an account apex end the local session with 204. Requires same-origin. Requires X-SH-CSRF: 1 or Content-Type: application/json (not a simple form post).

Parameters:

Name In Type Required Description
X-SH-CSRF header string, one of 1

Responses:

Status Description
200 Navigate the top-level window to redirect_url to clear the apex cookie
204 Session deleted and cookie cleared
403 Missing CSRF header (and not a JSON request)
429 Rate limited (code rate_limited). Saved-data reads allow 30 a second per address with a burst of 60 (SAVED_DATA_READ_PER_SEC, SAVED_DATA_READ_BURST).

GET /v1/visitor/oauth/{provider}

Visitor sign-in start, served on the site’s own address (its <site>.<handle> host, person address, claimed name or bound domain). return_to must be a page on this same host that belongs to the site served here. Sets a short-lived host-only nonce cookie (__Host-sh_vnonce, HttpOnly, Secure, SameSite=Lax, 10 minutes; sh_vnonce on local HTTP), stores only its SHA-256 with the OAuth state, and 302s to the provider with PKCE S256. GET /v1/visitor/establish later sets the session only in the browser holding that nonce, so a sign-in finished elsewhere cannot be completed in another browser (login CSRF). auth.js SH.signIn() navigates here.

Parameters:

Name In Type Required Description
provider path string, one of google, github, oidc yes
return_to query string yes Absolute URL of a page on this host to return to after sign-in

Responses:

Status Description
302 Nonce cookie set; redirect to the identity provider
400 Missing return_to, or one that is not a page of the site on this host
404 Unknown or disabled provider
429 Rate limited (code rate_limited). Saved-data reads allow 30 a second per address with a burst of 60 (SAVED_DATA_READ_PER_SEC, SAVED_DATA_READ_BURST).