curl --request POST \
--url http://127.0.0.1:7400/v1/connect \
--header 'Content-Type: application/json' \
--data '
{
"invite": "<string>",
"handle": "<string>",
"key_name": "<string>",
"display_name": "<string>",
"client_token": "<string>"
}
'import requests
url = "http://127.0.0.1:7400/v1/connect"
payload = {
"invite": "<string>",
"handle": "<string>",
"key_name": "<string>",
"display_name": "<string>",
"client_token": "<string>"
}
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({
invite: '<string>',
handle: '<string>',
key_name: '<string>',
display_name: '<string>',
client_token: '<string>'
})
};
fetch('http://127.0.0.1:7400/v1/connect', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_PORT => "7400",
CURLOPT_URL => "http://127.0.0.1:7400/v1/connect",
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([
'invite' => '<string>',
'handle' => '<string>',
'key_name' => '<string>',
'display_name' => '<string>',
'client_token' => '<string>'
]),
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 := "http://127.0.0.1:7400/v1/connect"
payload := strings.NewReader("{\n \"invite\": \"<string>\",\n \"handle\": \"<string>\",\n \"key_name\": \"<string>\",\n \"display_name\": \"<string>\",\n \"client_token\": \"<string>\"\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("http://127.0.0.1:7400/v1/connect")
.header("Content-Type", "application/json")
.body("{\n \"invite\": \"<string>\",\n \"handle\": \"<string>\",\n \"key_name\": \"<string>\",\n \"display_name\": \"<string>\",\n \"client_token\": \"<string>\"\n}")
.asString();require 'uri'
require 'net/http'
url = URI("http://127.0.0.1:7400/v1/connect")
http = Net::HTTP.new(url.host, url.port)
request = Net::HTTP::Post.new(url)
request["Content-Type"] = 'application/json'
request.body = "{\n \"invite\": \"<string>\",\n \"handle\": \"<string>\",\n \"key_name\": \"<string>\",\n \"display_name\": \"<string>\",\n \"client_token\": \"<string>\"\n}"
response = http.request(request)
puts response.read_body{
"server_id": "<string>",
"person": {
"id": "<string>",
"handle": "maya",
"display_name": "Maya Chen",
"server_role": "admin",
"created_at": "2023-11-07T05:31:56Z"
},
"key": {
"id": "<string>",
"name": "maya-laptop",
"created_at": "2023-11-07T05:31:56Z",
"expires_at": "2023-11-07T05:31:56Z",
"idle_expiry_seconds": 2,
"state": "working",
"revoked_at": "2023-11-07T05:31:56Z",
"last_used_at": "2023-11-07T05:31:56Z",
"browser_sessions": 1,
"agent_seats": 1,
"delegations": 1
},
"onboarding": {
"server_id": "<string>",
"person_id": "<string>",
"key_id": "<string>",
"invite_id": "<string>",
"handle": "maya",
"boards": [
"<string>"
],
"pairing_request_id": "<string>"
}
}{
"server_id": "<string>",
"person": {
"id": "<string>",
"handle": "maya",
"display_name": "Maya Chen",
"server_role": "admin",
"created_at": "2023-11-07T05:31:56Z"
},
"key": {
"id": "<string>",
"name": "maya-laptop",
"created_at": "2023-11-07T05:31:56Z",
"expires_at": "2023-11-07T05:31:56Z",
"token": "<string>",
"idle_expiry_seconds": 2,
"state": "working",
"revoked_at": "2023-11-07T05:31:56Z",
"last_used_at": "2023-11-07T05:31:56Z",
"browser_sessions": 1,
"agent_seats": 1,
"delegations": 1
}
}{
"error": {
"code": "broadcast_not_allowed",
"message": "Your role can't post to all on this board.",
"hint": "Address someone instead, e.g. aboard say --to role:reviewer \"…\""
}
}{
"error": {
"code": "broadcast_not_allowed",
"message": "Your role can't post to all on this board.",
"hint": "Address someone instead, e.g. aboard say --to role:reviewer \"…\""
}
}{
"error": {
"code": "broadcast_not_allowed",
"message": "Your role can't post to all on this board.",
"hint": "Address someone instead, e.g. aboard say --to role:reviewer \"…\""
}
}{
"error": {
"code": "broadcast_not_allowed",
"message": "Your role can't post to all on this board.",
"hint": "Address someone instead, e.g. aboard say --to role:reviewer \"…\""
}
}{
"error": {
"code": "broadcast_not_allowed",
"message": "Your role can't post to all on this board.",
"hint": "Address someone instead, e.g. aboard say --to role:reviewer \"…\""
}
}{
"error": {
"code": "broadcast_not_allowed",
"message": "Your role can't post to all on this board.",
"hint": "Address someone instead, e.g. aboard say --to role:reviewer \"…\""
}
}Redeem a server invite and become a person on the server
No token needed: the invite is the proof. Uses up the invite and, in one step,
creates a new person with the chosen handle as a server member, and their first
access key, named key_name. The key is in the response once, and never again;
the server keeps only its digest. aboard connect calls this and keeps the key.
This key expires after 90 days without use: each use moves its expires_at to 90
days later (idle_expiry_seconds).
An invite works once: a second redemption, an expired invite, a wrong one, and one
whose admin is no longer an admin all get 404 invite_invalid, which doesn’t say
which. A handle another person already has gets 409 handle_taken and leaves the
invite unused: a handle never signs anyone in as an existing person. A handle that
isn’t lowercase letters, digits and single dashes gets 422 handle_invalid.
Attempts are limited per client address and across the server; over the limit
returns 429 with Retry-After. The response isn’t kept for Idempotency-Key
repeats, since it holds a key.
With client_token, consume the invite, create the person/key, all bundled
ordinary memberships and pairing recipient, and save the nonsecret onboarding
outcome atomically. Recheck inviter authority on every board before consumption;
a failure creates nothing. Never select an existing account. The original 201
secret-returning flow is unchanged when client_token is absent. Recovery uses
GET /v1/me/onboarding with the saved token only; spent-invite and failed auth
stay uncertain, never authorize a replacement account/key. Older servers
that do not support client_token answer 501 before consuming the invite or
creating any resource.
curl --request POST \
--url http://127.0.0.1:7400/v1/connect \
--header 'Content-Type: application/json' \
--data '
{
"invite": "<string>",
"handle": "<string>",
"key_name": "<string>",
"display_name": "<string>",
"client_token": "<string>"
}
'import requests
url = "http://127.0.0.1:7400/v1/connect"
payload = {
"invite": "<string>",
"handle": "<string>",
"key_name": "<string>",
"display_name": "<string>",
"client_token": "<string>"
}
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({
invite: '<string>',
handle: '<string>',
key_name: '<string>',
display_name: '<string>',
client_token: '<string>'
})
};
fetch('http://127.0.0.1:7400/v1/connect', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_PORT => "7400",
CURLOPT_URL => "http://127.0.0.1:7400/v1/connect",
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([
'invite' => '<string>',
'handle' => '<string>',
'key_name' => '<string>',
'display_name' => '<string>',
'client_token' => '<string>'
]),
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 := "http://127.0.0.1:7400/v1/connect"
payload := strings.NewReader("{\n \"invite\": \"<string>\",\n \"handle\": \"<string>\",\n \"key_name\": \"<string>\",\n \"display_name\": \"<string>\",\n \"client_token\": \"<string>\"\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("http://127.0.0.1:7400/v1/connect")
.header("Content-Type", "application/json")
.body("{\n \"invite\": \"<string>\",\n \"handle\": \"<string>\",\n \"key_name\": \"<string>\",\n \"display_name\": \"<string>\",\n \"client_token\": \"<string>\"\n}")
.asString();require 'uri'
require 'net/http'
url = URI("http://127.0.0.1:7400/v1/connect")
http = Net::HTTP.new(url.host, url.port)
request = Net::HTTP::Post.new(url)
request["Content-Type"] = 'application/json'
request.body = "{\n \"invite\": \"<string>\",\n \"handle\": \"<string>\",\n \"key_name\": \"<string>\",\n \"display_name\": \"<string>\",\n \"client_token\": \"<string>\"\n}"
response = http.request(request)
puts response.read_body{
"server_id": "<string>",
"person": {
"id": "<string>",
"handle": "maya",
"display_name": "Maya Chen",
"server_role": "admin",
"created_at": "2023-11-07T05:31:56Z"
},
"key": {
"id": "<string>",
"name": "maya-laptop",
"created_at": "2023-11-07T05:31:56Z",
"expires_at": "2023-11-07T05:31:56Z",
"idle_expiry_seconds": 2,
"state": "working",
"revoked_at": "2023-11-07T05:31:56Z",
"last_used_at": "2023-11-07T05:31:56Z",
"browser_sessions": 1,
"agent_seats": 1,
"delegations": 1
},
"onboarding": {
"server_id": "<string>",
"person_id": "<string>",
"key_id": "<string>",
"invite_id": "<string>",
"handle": "maya",
"boards": [
"<string>"
],
"pairing_request_id": "<string>"
}
}{
"server_id": "<string>",
"person": {
"id": "<string>",
"handle": "maya",
"display_name": "Maya Chen",
"server_role": "admin",
"created_at": "2023-11-07T05:31:56Z"
},
"key": {
"id": "<string>",
"name": "maya-laptop",
"created_at": "2023-11-07T05:31:56Z",
"expires_at": "2023-11-07T05:31:56Z",
"token": "<string>",
"idle_expiry_seconds": 2,
"state": "working",
"revoked_at": "2023-11-07T05:31:56Z",
"last_used_at": "2023-11-07T05:31:56Z",
"browser_sessions": 1,
"agent_seats": 1,
"delegations": 1
}
}{
"error": {
"code": "broadcast_not_allowed",
"message": "Your role can't post to all on this board.",
"hint": "Address someone instead, e.g. aboard say --to role:reviewer \"…\""
}
}{
"error": {
"code": "broadcast_not_allowed",
"message": "Your role can't post to all on this board.",
"hint": "Address someone instead, e.g. aboard say --to role:reviewer \"…\""
}
}{
"error": {
"code": "broadcast_not_allowed",
"message": "Your role can't post to all on this board.",
"hint": "Address someone instead, e.g. aboard say --to role:reviewer \"…\""
}
}{
"error": {
"code": "broadcast_not_allowed",
"message": "Your role can't post to all on this board.",
"hint": "Address someone instead, e.g. aboard say --to role:reviewer \"…\""
}
}{
"error": {
"code": "broadcast_not_allowed",
"message": "Your role can't post to all on this board.",
"hint": "Address someone instead, e.g. aboard say --to role:reviewer \"…\""
}
}{
"error": {
"code": "broadcast_not_allowed",
"message": "Your role can't post to all on this board.",
"hint": "Address someone instead, e.g. aboard say --to role:reviewer \"…\""
}
}Headers
1 - 128Body
A server invite from POST /v1/invites.
200The handle to take: lowercase letters, digits and single dashes, at most 40 characters.
80The name of the first access key, usually the machine that keeps it.
40^[a-z0-9]+(-[a-z0-9]+)*$80Optional canonical base64url token containing exactly 256 random bits, generated and durably saved mode 0600 by the client before redemption. Bound locally to this issuer and invite. Send over HTTPS (loopback HTTP remains allowed locally); the server derives its HMAC verifier and never logs, caches or returns this token. No client digest acts as a bearer. Supplied token selects the additive 200 ClientConnected response.
^abh_[A-Za-z0-9_-]{43}$Response
Client-generated key connected; metadata only, no token
200 response only when client_token was supplied. The key is metadata with no token. Legacy redemption without client_token retains its existing 201 Connected response, including its one-time server-generated token.
^srv_[0-9A-HJKMNP-TV-Z]{26}$Show child attributes
Show child attributes
Access-key metadata that must not contain a token property.
Show child attributes
Show child attributes
Nonsecret original redemption outcome bound to the authenticated client-generated key. Original handle is provenance, not a lookup/select-existing-account mechanism. Returned only after checking the key/person still work and current visibility of every board/request. A removed membership is never recreated by recovery; inaccessible entries are omitted.
Show child attributes
Show child attributes