Skip to content

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
reddit Reddit Start provider authorisation
twitter 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 exactly tebhos-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