On the admin host, over HTTPS. There are two kinds of caller, and which one you are decides what you may do:
- An administrator — a username/password account, authenticated by the session cookie the admin web UI sets on login. Administrators manage client keys and see every frontend.
- A client key (
Authorization: Bearer sk_…) — what a seikan client holds. A client key creates and manages its own frontends, and nothing else. It cannot issue keys, so a key that leaks off a client machine cannot escalate.
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/frontendsrequires 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:
- A request grants nothing. It is a row in a table until an administrator approves it.
- The client key is minted at claim time, not at approval. An approval nobody collects leaves no credential behind, and no raw secret is ever written to disk.
- The poll token is the client's proof, and the confirmation code is the administrator's. The code is printed on the requesting machine and shown in the UI so the person approving can tell one request from another; the poll token ensures the resulting key reaches only the client that asked.
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.