curl --request POST \
--url https://api.example.com/api/setup \
--header 'Content-Type: application/json' \
--data '
{
"owner_display_name": "<string>",
"key_description": "owner key",
"key_user_id": "<string>",
"login": {
"email": "<string>",
"password": "<string>",
"kind": "password"
},
"owner_user_id": "<string>",
"setup_token": ""
}
'import requests
url = "https://api.example.com/api/setup"
payload = {
"owner_display_name": "<string>",
"key_description": "owner key",
"key_user_id": "<string>",
"login": {
"email": "<string>",
"password": "<string>",
"kind": "password"
},
"owner_user_id": "<string>",
"setup_token": ""
}
headers = {"Content-Type": "application/json"}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
owner_display_name: '<string>',
key_description: 'owner key',
key_user_id: '<string>',
login: {email: '<string>', password: '<string>', kind: 'password'},
owner_user_id: '<string>',
setup_token: ''
})
};
fetch('https://api.example.com/api/setup', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://api.example.com/api/setup",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode([
'owner_display_name' => '<string>',
'key_description' => 'owner key',
'key_user_id' => '<string>',
'login' => [
'email' => '<string>',
'password' => '<string>',
'kind' => 'password'
],
'owner_user_id' => '<string>',
'setup_token' => ''
]),
CURLOPT_HTTPHEADER => [
"Content-Type: application/json"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://api.example.com/api/setup"
payload := strings.NewReader("{\n \"owner_display_name\": \"<string>\",\n \"key_description\": \"owner key\",\n \"key_user_id\": \"<string>\",\n \"login\": {\n \"email\": \"<string>\",\n \"password\": \"<string>\",\n \"kind\": \"password\"\n },\n \"owner_user_id\": \"<string>\",\n \"setup_token\": \"\"\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://api.example.com/api/setup")
.header("Content-Type", "application/json")
.body("{\n \"owner_display_name\": \"<string>\",\n \"key_description\": \"owner key\",\n \"key_user_id\": \"<string>\",\n \"login\": {\n \"email\": \"<string>\",\n \"password\": \"<string>\",\n \"kind\": \"password\"\n },\n \"owner_user_id\": \"<string>\",\n \"setup_token\": \"\"\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.example.com/api/setup")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["Content-Type"] = 'application/json'
request.body = "{\n \"owner_display_name\": \"<string>\",\n \"key_description\": \"owner key\",\n \"key_user_id\": \"<string>\",\n \"login\": {\n \"email\": \"<string>\",\n \"password\": \"<string>\",\n \"kind\": \"password\"\n },\n \"owner_user_id\": \"<string>\",\n \"setup_token\": \"\"\n}"
response = http.request(request)
puts response.read_body{
"data": {
"api_key": "<string>",
"key_fingerprint": "<string>",
"key_user_id": "<string>",
"login_attached": true,
"owner_user_id": "<string>",
"invite_token": "<string>",
"login_path": "<string>"
}
}{
"error": "<string>",
"code": "<string>"
}{
"error": "<string>",
"code": "<string>"
}{
"error": "<string>",
"code": "<string>"
}{
"error": "<string>",
"code": "<string>"
}Initialize the deployment (owner + first key)
Initialize the deployment once behind the secure-by-default setup-token gate.
Only meaningful on a serviceable access-controlled install: it refuses loudly with a
501 when the door cannot initialize — access control off, no configured key-minting
identity provider, or the access-control Redis unset. The per-IP backoff is consulted
BEFORE the token, so a wrong-token flood escalates a lockout that turns further attempts away
without ever comparing; a wrong/absent token is a generic 403 (no oracle for the
initialized state). Under one mint mutex: 409 “Already initialized” when ANY principal
exists; else the owner principal (kind=human) is created, granted the admin role,
its first key (owned by the owner) is minted and returned ONCE, and — when a configured
accounts provider can attach a login and the request carries one — the owner’s login is
attached (a password set now, or an invite whose one-time link is returned). A failure
past the owner create is compensated so setup stays retriable; a correctable login
credential is compensated then mapped to the caller (a too-short password → 400, a
login/email collision → 409) rather than surfaced as a 500.
curl --request POST \
--url https://api.example.com/api/setup \
--header 'Content-Type: application/json' \
--data '
{
"owner_display_name": "<string>",
"key_description": "owner key",
"key_user_id": "<string>",
"login": {
"email": "<string>",
"password": "<string>",
"kind": "password"
},
"owner_user_id": "<string>",
"setup_token": ""
}
'import requests
url = "https://api.example.com/api/setup"
payload = {
"owner_display_name": "<string>",
"key_description": "owner key",
"key_user_id": "<string>",
"login": {
"email": "<string>",
"password": "<string>",
"kind": "password"
},
"owner_user_id": "<string>",
"setup_token": ""
}
headers = {"Content-Type": "application/json"}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
owner_display_name: '<string>',
key_description: 'owner key',
key_user_id: '<string>',
login: {email: '<string>', password: '<string>', kind: 'password'},
owner_user_id: '<string>',
setup_token: ''
})
};
fetch('https://api.example.com/api/setup', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://api.example.com/api/setup",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode([
'owner_display_name' => '<string>',
'key_description' => 'owner key',
'key_user_id' => '<string>',
'login' => [
'email' => '<string>',
'password' => '<string>',
'kind' => 'password'
],
'owner_user_id' => '<string>',
'setup_token' => ''
]),
CURLOPT_HTTPHEADER => [
"Content-Type: application/json"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://api.example.com/api/setup"
payload := strings.NewReader("{\n \"owner_display_name\": \"<string>\",\n \"key_description\": \"owner key\",\n \"key_user_id\": \"<string>\",\n \"login\": {\n \"email\": \"<string>\",\n \"password\": \"<string>\",\n \"kind\": \"password\"\n },\n \"owner_user_id\": \"<string>\",\n \"setup_token\": \"\"\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://api.example.com/api/setup")
.header("Content-Type", "application/json")
.body("{\n \"owner_display_name\": \"<string>\",\n \"key_description\": \"owner key\",\n \"key_user_id\": \"<string>\",\n \"login\": {\n \"email\": \"<string>\",\n \"password\": \"<string>\",\n \"kind\": \"password\"\n },\n \"owner_user_id\": \"<string>\",\n \"setup_token\": \"\"\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.example.com/api/setup")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["Content-Type"] = 'application/json'
request.body = "{\n \"owner_display_name\": \"<string>\",\n \"key_description\": \"owner key\",\n \"key_user_id\": \"<string>\",\n \"login\": {\n \"email\": \"<string>\",\n \"password\": \"<string>\",\n \"kind\": \"password\"\n },\n \"owner_user_id\": \"<string>\",\n \"setup_token\": \"\"\n}"
response = http.request(request)
puts response.read_body{
"data": {
"api_key": "<string>",
"key_fingerprint": "<string>",
"key_user_id": "<string>",
"login_attached": true,
"owner_user_id": "<string>",
"invite_token": "<string>",
"login_path": "<string>"
}
}{
"error": "<string>",
"code": "<string>"
}{
"error": "<string>",
"code": "<string>"
}{
"error": "<string>",
"code": "<string>"
}{
"error": "<string>",
"code": "<string>"
}Body
The body of POST /api/setup.
setup_token is checked against the deployment's setup token.
owner_user_id and key_user_id are optional — the door mints ids when
they are absent. owner_display_name is required and non-empty (the
principals door rejects an empty display name). login attaches the owner's
interactive login when a login-attaching provider is configured; a keys-only
deployment sends None.
1A login credential that sets the principal's password now.
- PasswordCredential
- InviteCredential
Show child attributes
Show child attributes
Response
Success.
The result of a successful POST /api/setup.
api_key is the owner key's plaintext, returned exactly once.
login_attached is whether an interactive login was attached;
invite_token and login_path are set only when the login was an invite
the operator completes later.
Show child attributes
Show child attributes

