Eternaltwin

Home | /api | v1 | users

/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"
      }
    }
  }
]
FieldTypeMeaning
typestring"Discord", "Gitlab" or "Email".
idUUIDThe link's own identifier, which is what a DELETE names.
useruser referenceThe Eternaltwin account.
periodobject{"start": …, "end": null} while the link holds.
visibilityobjectWho may see it. Private, Friend or Public.
rankobjectSort order among the account's links.
targetobjectThe 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

StatusBodyWhen
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

StatusBodyWhen
403{"error": "forbidden"}Neither the owner nor an administrator.
404{"error": "not found"}No link under that identifier.
500{"error": "internal error"}—