/api/v1/users/:user_id/user-links
Links to external accounts that are not games: Discord, GitLab, and verified
email addresses. Defined in crates/rest/src/users.rs.
This is the generic user_link mechanism — one row per link, discriminated by
its type — and it is a different thing from the per-server game links served
by links and shown as links on a user.
A member creates one of these by going through the provider's OAuth flow; the callbacks are listed in Internal endpoints. There is no route here that creates a link, only ones that read and remove.
GET /:user_id/user-links
Lists the account's external links, as a plain array.
Visibility is applied per link. A link marked Public is returned to
anybody; one marked Private or Friend is returned only to the account's
owner and to administrators. Friend is read as Private for now — there is
no friendship model yet, and that is the conservative reading. A guest
therefore gets a possibly shorter array rather than a 403.
Example
GET /api/v1/users/28dbb0bf-0fdc-40fe-ae5a-dde193f9fea8/user-links Cookie: sid=b8be19ef-2d61-44de-b7d2-9c34ccb8a763
[
{
"type": "Discord",
"id": "5c0d4f2b-9a1e-4c7d-8f3a-6b2e1d0c9a44",
"user": {"type": "User", "id": "28dbb0bf-0fdc-40fe-ae5a-dde193f9fea8"},
"period": {"start": "2026-08-01T12:00:00.000Z", "end": null},
"visibility": {
"now": {"value": "Private"},
"then": {"value": "Private"}
},
"rank": {"now": {"value": 0}, "then": {"value": 0}},
"target": {
"user": {
"type": "DiscordUser",
"id": "123456789012345678",
"display_name": "alice"
}
}
}
]
| Field | Type | Meaning |
|---|---|---|
type | string | "Discord", "Gitlab" or "Email". |
id | UUID | The link's own identifier, which is what a DELETE names. |
user | user reference | The Eternaltwin account. |
period | object | {"start": …, "end": null} while the link holds. |
visibility | object | Who may see it. Private, Friend or Public. |
rank | object | Sort order among the account's links. |
target | object | The external account. |
visibility and rank are given at two revisions, now and then, each
wrapping its value in {"value": …}.
The target depends on the type: {"user": {"type": "DiscordUser", …}} for
Discord, {"user": {"type": "GitlabUser", …}} for GitLab, and
{"email": "…"} for an email link.
Errors
| Status | Body | When |
|---|---|---|
| 403 | {"error": "forbidden"} | The store refused the read. |
| 500 | {"error": "internal error"} | — |
DELETE /:user_id/user-links/:user_link_id
Removes one link. :user_link_id is the id of the link, not the identifier
of the external account.
The caller must be the owner of the account or an administrator.
Example
DELETE /api/v1/users/28dbb0bf-0fdc-40fe-ae5a-dde193f9fea8/user-links/5c0d4f2b-9a1e-4c7d-8f3a-6b2e1d0c9a44 Cookie: sid=b8be19ef-2d61-44de-b7d2-9c34ccb8a763
null
The body is the JSON literal null: the route answers with a value rather
than with 204 because the site's REST client requires something to parse
on every call.
Errors
| Status | Body | When |
|---|---|---|
| 403 | {"error": "forbidden"} | Neither the owner nor an administrator. |
| 404 | {"error": "not found"} | No link under that identifier. |
| 500 | {"error": "internal error"} | — |