Connections¶
A connection links a TEBHOS account to something outside TEBHOS: a domain the user has proved they control, or an account on a supported external service. Verified connections that the viewer is allowed to see appear in connected_accounts on the user's profile (see Users).
Every /users/@me/connections route requires a user session token, except that List connections also accepts an OAuth2 access token with the connections scope. A valid bot token is rejected with 403 ACCESS_DENIED (an invalid one with 401), and so is a valid access token on any other connection route. Connection IDs are strings, not snowflakes.
Account connection limit¶
An account can hold at most 20 connections across all types. Creating one more returns 400 BAD_REQUEST.
Connection object¶
A connection is identified by the pair of its type and id. Both appear in the path of every per-connection route.
Structure¶
| Field | Type | Description |
|---|---|---|
| id | string | The connection ID, unique within its type. For domain it is 64 lowercase hexadecimal characters and stays the same if the account recreates the same domain. For a provider type it is the account ID on that service |
| type | string | The connection type |
| name | string | The label shown for the connection: the domain, or the account's display name on the external service |
| verified | boolean | Whether the most recent proof succeeded |
| visibility_flags | integer | Connection visibility flags |
| sort_order | integer | The position in the account's connection list. A new connection gets the number of connections that existed before it |
Example¶
{
"id": "12345678",
"type": "github",
"name": "partyparrot",
"verified": true,
"visibility_flags": 1,
"sort_order": 0
}
Connection types¶
| Value | Name | How it is created |
|---|---|---|
| domain | Domain | Initiate connection, then Verify and create connection |
| github | GitHub | Start provider authorisation |
| spotify | Spotify | Start provider authorisation |
| steam | Steam | Start provider authorisation |
| twitch | Twitch | Start provider authorisation |
| Start provider authorisation | ||
| X | Start provider authorisation | |
| riot | Riot Games | Start provider authorisation |
A provider type is usable only when the instance has credentials for it. List connection providers reports which ones are enabled. Steam uses OpenID and is always enabled.
Not supported on TEBHOS
Bluesky (bsky) connections are not available. The Bluesky routes exist for compatibility and always report that Bluesky is not enabled; see Bluesky routes.
Connection visibility flags¶
The flags decide which viewers of the owner's profile see a connection.
| Value | Name | Description |
|---|---|---|
| 1 << 0 | EVERYONE | Anyone who can view the profile sees the connection |
| 1 << 1 | FRIENDS | Friends see the connection |
| 1 << 2 | MUTUAL_GUILDS | Members of a guild the owner is also in see the connection |
A new connection gets EVERYONE (1) unless the request sets another value. A value of 0 hides the connection from everyone, including the owner's view of their own profile. An unverified connection never appears on a profile, whatever its flags. List connections ignores the flags and returns every connection the account holds.
Connection verification object¶
Returned by Initiate connection. No connection exists until Verify and create connection succeeds.
Structure¶
| Field | Type | Description |
|---|---|---|
| token | string | The value to publish at the domain as proof, 64 lowercase hexadecimal characters |
| type | string | Always domain |
| id | string | The domain, exactly as submitted |
| instructions | string | Human-readable instructions for publishing the token |
| initiation_token | string | A signed token accepted by Verify and create connection. It expires 30 minutes after issue and only works for the account that requested it |
The proof token depends only on the account and the lowercased domain, so repeating Initiate connection for the same domain returns the same token, and a published record can stay in place across attempts.
Protect the initiation token
Do not log or forward initiation_token. Only token is meant to be published.
Domain ownership proof¶
Publish the token in one of two ways:
- DNS: a TXT record at
_tebhos.<domain>whose value is exactlytebhos-verification=<token>. TEBHOS resolves the record over DNS-over-HTTPS, so allow time for DNS changes to propagate before verifying. - HTTPS: serve the bare token at
https://<domain>/.well-known/tebhos-verification. The response must have a 2xx status and a body of at most 4096 bytes that equals the token after trimming whitespace. Redirects are not followed, and the request times out after 5 seconds.
Only the DNS record has the prefix
The DNS value is tebhos-verification=<token>; the HTTPS document contains the token alone.
The domain must contain a . and only ASCII letters, digits, ., and -. Any other value fails the proof.
List connections¶
GET /users/@me/connections
Returns an array of every connection object the account holds, sorted by sort_order. An account with no connections receives an empty array.
Also accepts an OAuth2 access token with the connections scope; a token without it receives 403 MISSING_OAUTH_SCOPE.
Rate limit¶
60 requests per minute for each account, on the connection:list bucket.
List connection providers¶
GET /users/@me/connections/providers
Returns the provider connection types and whether each is enabled on this instance.
Response¶
| Status | Body | Condition |
|---|---|---|
| 200 | array[object] | Each entry is {type, name, enabled}, for example {"type": "github", "name": "GitHub", "enabled": true} |
Initiate connection¶
POST /users/@me/connections
Starts domain ownership verification and returns a connection verification object. No connection is created yet.
JSON body¶
| Field | Type | Description |
|---|---|---|
| type | string | Must be domain |
| identifier | string | The domain to prove ownership of |
Response¶
| Status | Body | Condition |
|---|---|---|
| 201 | connection verification object | Verification was started |
| 400 | error response | The type is not domain (including bsky), or the account already has 20 connections |
| 409 | error response | The account already has a connection for this domain |
Rate limit¶
5 requests per minute for each account, on the connection:create bucket.
Verify and create connection¶
POST /users/@me/connections/verify
Checks the domain ownership proof for an initiation token, and on success creates and returns the connection object with status 201.
JSON body¶
| Field | Type | Description |
|---|---|---|
| initiation_token | string | The token from Initiate connection |
| visibility_flags? | integer | Connection visibility flags (default EVERYONE) |
Response¶
| Status | Body | Condition |
|---|---|---|
| 201 | connection object | The proof succeeded and the connection was created |
| 400 | error response | The initiation token is invalid, expired, or belongs to another account; the account already has 20 connections; or the proof failed |
| 409 | error response | The domain is already connected |
The initiation token stays usable until it expires, so a client whose proof was not yet visible can retry with the same token.
Side effects¶
Success sends the complete, sorted list to the account's sessions in a USER_CONNECTIONS_UPDATE Gateway event with the payload {"connections": [...]}.
Rate limit¶
5 requests per minute for each account, on the connection:verify_and_create bucket.
Re-verify a connection¶
POST /users/@me/connections/{type}/{connection_id}/verify
Runs the ownership proof of an existing domain connection again. Returns the updated connection object on success.
Path parameters¶
| Field | Type | Description |
|---|---|---|
| type | string | The connection type; must be domain |
| connection_id | string | The connection ID |
Response¶
| Status | Body | Condition |
|---|---|---|
| 200 | connection object | The proof succeeded |
| 400 | error response | The connection is not a domain connection, or the proof failed |
| 404 | error response | No such connection on the account |
A failed proof marks a previously verified connection as unverified, which removes it from the profile. A background task that re-checks an account's verified domain connections can do the same.
Rate limit¶
5 requests per minute for each account, on the connection:verify bucket.
Update connection¶
PATCH /users/@me/connections/{type}/{connection_id}
Updates the visibility or position of one connection. An omitted field is left unchanged. Returns the updated connection object.
Path parameters¶
| Field | Type | Description |
|---|---|---|
| type | string | The connection type |
| connection_id | string | The connection ID |
JSON body¶
| Field | Type | Description |
|---|---|---|
| visibility_flags? | integer | Connection visibility flags |
| sort_order? | integer | The new position |
Both path parameters must name the same connection: a correct ID under the wrong type returns 404. Two connections may share a sort_order.
Response¶
| Status | Body | Condition |
|---|---|---|
| 200 | connection object | The connection was updated |
| 404 | error response | No such connection on the account |
Side effects¶
Sends the complete list in USER_CONNECTIONS_UPDATE.
Rate limit¶
30 requests per minute for each account, on the connection:update bucket, shared with Reorder connections.
Delete connection¶
DELETE /users/@me/connections/{type}/{connection_id}
Removes one connection. Returns 204.
Path parameters¶
| Field | Type | Description |
|---|---|---|
| type | string | The connection type |
| connection_id | string | The connection ID |
Response¶
| Status | Body | Condition |
|---|---|---|
| 204 | empty | The connection was deleted |
| 404 | error response | No such connection on the account |
Deletion is immediate and cannot be undone. Other connections keep their sort_order. Deleting a provider connection does not revoke anything on the external service.
Side effects¶
Sends the remaining list in USER_CONNECTIONS_UPDATE.
Rate limit¶
10 requests per minute for each account, on the connection:delete bucket.
Reorder connections¶
PATCH /users/@me/connections/reorder
Sets the display order of the listed connections from their position in the array. Returns 204.
JSON body¶
| Field | Type | Description |
|---|---|---|
| connection_ids | array[string] | Connection IDs in their new order |
Each listed connection gets its zero-based array index as sort_order. Unknown IDs are ignored, and connections that are not listed keep their current value, so a partial list can leave two connections sharing a position.
Side effects¶
Sends the complete list in USER_CONNECTIONS_UPDATE, even when nothing changed.
Rate limit¶
30 requests per minute for each account, on the connection:update bucket, shared with Update connection.
Start provider authorisation¶
POST /users/@me/connections/{provider}/authorize
Starts linking an external account. Returns {"authorize_url": string}; open that URL in a browser window (TEBHOS opens a pop-up).
Path parameters¶
| Field | Type | Description |
|---|---|---|
| provider | string | A provider connection type: github, spotify, steam, twitch, reddit, twitter, or riot |
Response¶
| Status | Body | Condition |
|---|---|---|
| 200 | {"authorize_url": string} |
Send the user to authorize_url |
| 400 | error response | The provider is unknown or not enabled on this instance |
Completing the flow¶
The external service returns the browser to GET /connections/{provider}/callback on the API. Clients do not call that route themselves. It answers with a small HTML page that reports the result and closes the window after a moment.
The authorisation state is valid for ten minutes and can be completed once. Spotify and X use PKCE.
On success, TEBHOS creates a verified connection whose id is the account ID on the external service and whose name is its display name, with visibility EVERYONE, and sends USER_CONNECTIONS_UPDATE. Linking the same external account again refreshes its name and verification time instead of creating a duplicate. The page reports a failure when the user cancelled, the state expired, the exchange failed, or the account already has 20 connections.
Bluesky routes¶
These routes exist so that clients built for Bluesky connections receive a clear answer. Bluesky is not enabled on TEBHOS.
| Route | Response |
|---|---|
POST /users/@me/connections/bluesky/authorize |
400 BAD_REQUEST, Bluesky is not enabled. Requires a user session; 5 requests per minute on the connection:create bucket |
GET /connections/bluesky/client-metadata.json |
404 with {"error": "Bluesky OAuth is not enabled"} |
GET /connections/bluesky/jwks.json |
404 with {"error": "Bluesky OAuth is not enabled"} |
GET /connections/bluesky/callback |
302 to https://tebhos.com/connection-callback?status=error&reason=not_enabled |