On the admin host, over HTTPS. There are two kinds of caller, and which one you are decides what you may do:

There is no credential that does both. Setup (below) is how the first administrator comes to exist; registration (below) is how a client comes by a key without one being emailed to it.

Method & path Body / result
POST /api/frontends {"type":"http","domains":["app.example.com","*.example.com"]} → {id, domain, domains, url, token} (a single "domain" is also accepted)
POST /api/frontends {"type":"tcp"} → {id, public_port, token}. Add "public_port": 10000 to request a specific port instead of having one assigned — it must be inside SEIKAN_TCP_RANGE (400 port_out_of_range otherwise) and unclaimed (409 port_in_use, or port_in_use_by_you naming your own frontend so you can delete it). A port that cannot be bound is 409 port_bind_failed.
POST /api/frontends {"type":"peer"} → {id, token, consumer_token} — a private frontend with no domain and no public port. token registers the backend (works like a TCP frontend's token); consumer_token is a second, independent secret that relays a connection in (see seikan forward). Both are shown only in this response.
GET /api/frontends · GET /api/frontends/{id} list / one (no tokens). An administrator sees all; a client key sees only its own (404 otherwise).
DELETE /api/frontends/{id} 204 (404 if the calling client key doesn't own it)
POST /api/frontends/{id}/token Mint a fresh connect token for a frontend, invalidating the previous one → {id, …, token}. Requires a client key that owns it (404 otherwise); 400 for a peer frontend, whose two secrets have no single token to replace. Live tunnel sessions keep running — the token is only checked at the handshake.
POST /api/apikeys · GET · DELETE /api/apikeys/{id} Administrators only (a client key → 403). Create returns {id, key} (key shown once); delete revokes the client key and cascade-deletes its frontends.
GET /api/setup {"complete": bool}. Unauthenticated — the UI asks this before deciding whether to show the setup wizard or the login form.
POST /api/setup {"setup_key":"setup_…","username":"…","password":"…"} → creates the first administrator, signs them in (session cookie), and spends the setup key. Unauthenticated, rate-limited per IP; 409 once any administrator exists, so it can't be replayed.
POST /api/auth/login {"username":"…","password":"…"} → {kind,id,username,admin_host} + sets the /_admin session cookie. Rate-limited per source IP.
POST /api/auth/logout Revokes the current session and clears the cookie. 204.
GET /api/auth/me `{kind,id,username

Rotation is what lets a client recover a frontend it can no longer authenticate to. A connect token is returned once, at creation, and only its hash is stored — so a client that has lost its copy (a container with no persistent state, restarting) has no way to read it back. Without rotation the only route would be deleting and recreating the frontend, which changes its ID and drops its route while it happens. See seikan up.

Note: POST /api/frontends requires a client key. An administrator has no key for the returned connect token to belong to, so creating a frontend is a client's action — administrators read and delete. Frontends are owned by the client key that created them; see multi-tenancy in the Security & FAQ.

Registration by approval

How seikan init --server … with no --api-key gets a client key: the client asks, an administrator answers, the client collects.

Method & path Body / result
POST /api/register {"hostname":"…","os":"…","arch":"…","version":"…"} (all optional) → {id, poll_token, code, expires_at, interval_seconds, admin_url}. Unauthenticated — the caller has no credential yet, which is the thing it is asking for. Capped at 3 live requests per source IP and 32 in total; 429 beyond that.
GET /api/register/{id} Authorization: Bearer <poll_token> → {"status":"pending"|"approved"|"denied"}. On the poll that finds it approved, and only that one, the response also carries {"api_key":"sk_…"} and the request is deleted. 404 once it is gone — claimed, denied-and-read, or expired.
GET /api/registrations Administrators only. Live requests, newest first, with the confirmation code and the client's self-reported details plus its observed source IP.
POST /api/registrations/{id}/approve Administrators only. {"label":"…"} (defaults to the hostname) — the label the minted client key will carry. 410 if it expired, 409 if it was already decided.
POST /api/registrations/{id}/deny Administrators only. Same status codes.

Requests live 15 minutes. Three things are worth knowing about the design:

Polling is deliberately outside the shared auth middleware, which counts unrecognised bearer tokens as failed logins — a client waiting patiently must not lock its own address out.