People

Accounts and signing in

auth-service

Signing up, signing in and out, and the OAuth 2.0 and OpenID Connect server every app signs in through (oauth.md); what proves who somebody is (passwords, codes, passkeys, two-factor authentication) and changing it; invitations and the approval queue; and the installation's description of itself (NodeInfo).

The keys tokens are signed with

GET/.well-known/jwks.json

The public halves, as a JSON Web Key Set (RFC 7517): what every service checks a token's signature with.

Who: Anyone, signed in or not

Answers

Request

curl "https://api.example.org/.well-known/jwks.json"

Answer · 200

{
  "keys": [
    {
      "key": null
    }
  ]
}

Where the NodeInfo document is

GET/.well-known/nodeinfo

Who: Anyone, signed in or not

Answers

Request

curl "https://api.example.org/.well-known/nodeinfo"

Answer · 200

{
  "links": [
    {
      "href": "string",
      "rel": "string"
    }
  ]
}

The OpenID Connect discovery document

GET/.well-known/openid-configuration

Where each endpoint is, and what is supported (OpenID Connect Discovery 1.0).

Who: Anyone, signed in or not

Answers

Request

curl "https://api.example.org/.well-known/openid-configuration"

Answer · 200

{
  "authorization_endpoint": "string",
  "claims_supported": [
    "string"
  ],
  "code_challenge_methods_supported": [
    "string"
  ],
  "grant_types_supported": [
    "string"
  ],
  "id_token_signing_alg_values_supported": [
    "string"
  ],
  "introspection_endpoint": "string",
  "issuer": "string",
  "jwks_uri": "https://example.org/",
  "response_types_supported": [
    "string"
  ],
  "revocation_endpoint": "string",
  "scopes_supported": [
    "string"
  ],
  "subject_types_supported": [
    "string"
  ],
  "token_endpoint": "string",
  "token_endpoint_auth_methods_supported": [
    "string"
  ],
  "userinfo_endpoint": "string"
}

People, groups, events or websites, for cleaning up a site

GET/accounts

Least recently active first, by default; purged ones are not listed. Removing them is the purge (DELETE /people, DELETE /groups, account-lifecycle.md).

Who: A super administrator, with actors:read

Parameters

  • kindstringin query
    person, the default, group, event or website. One of person, group, event, website.
  • qstringin query
    Words in the name or the username. At most 100 characters.
  • tierstringin query
    empty, dormant or active. One of empty, dormant, active.
  • statestringin query
    active, disabled, archived or deleted. One of active, disabled, archived, deleted.
  • last_active_beforestringin query
    Dates, as YYYY-MM-DD. An account matches if it was last active, or created, before the start of that day.
  • created_beforestringin query
  • email_verifiedstringin query
    false for people who never confirmed their address. One of false.
  • onboardedstringin query
    false for people who never went through the walk-through after signing up. One of false.
  • has_adminstringin query
    false for groups with no administrator. One of false.
  • content_maxintegerin query
    ContentMax is a pointer so that 0, "has written nothing", can be told from not asking. At least 0.
  • sortstringin query
    last_active_at, the default, created_at or name. One of last_active_at, created_at, name.
  • dirstringin query
    asc, the default, or desc. One of asc, desc.
  • limitintegerin query
    How many on a page: 50 unless said. At least 0. At most 500.
  • startintegerin query
    How many to skip. At least 0.

Answers

  • 200
    OK
    object application/json
  • 400
    the request is not well formed, or a value in it is not one it may be
  • 401
    no token, or one that has expired
  • 403
    the viewer may not do this

Request

curl "https://api.example.org/accounts" \
  -H "Authorization: Bearer $TOKEN"

Answer · 200

{
  "data": [
    {
      "admin_count": 1,
      "alias": "string",
      "content_count": 1,
      "created_at": "2026-10-27T09:30:00Z",
      "deleted_at": "2026-10-27T09:30:00Z",
      "email_verified": false,
      "follower_count": 1,
      "id": 42,
      "kind": "string",
      "last_active_at": "2026-10-27T09:30:00Z",
      "leader_count": 1,
      "name": "string",
      "onboarded": false,
      "person_type": "string",
      "state": "string",
      "tier": "string"
    }
  ],
  "pagination": {
    "limit": 1,
    "offset": 1,
    "total": 1
  }
}

Change a person's type

PATCH/accounts/{id}/person-type

Registered, administrator or super administrator. An administrator may make somebody registered an administrator; making a super administrator, and taking a type away, are a super administrator's. Nobody changes their own, so the last super administrator cannot be demoted: one who wants to step down makes somebody else a super administrator first. Recorded with who did it (made-registered, made-admin, made-super-admin). It takes effect at the person's next token, within fifteen minutes.

Who: An administrator, with actors:write, who has just proved who they are again

Parameters

  • idintegerrequiredin path
    The person's id.

Body application/json

  • person_typestringrequired
    registered, administrator or super-administrator. One of registered, administrator, super-administrator.

Answers

  • 200
    the type they have now and had before; changed is false when they had it already, and no proof was needed
    auth.PersonTypeChanged application/json
  • 400
    the request is not well formed, or a value in it is not one it may be
  • 401
    no token, or one that has expired
  • 403
    step_up_required: the viewer must prove who they are again first (/reauth); person_type_not_allowed: an administrator asked for what is a super administrator's; or, without a body, the viewer is not an administrator
    Refusal application/json
  • 404
    not_a_person: no person here has that id; a group, a profile from another server and a deleted person have no type to change
    Refusal application/json
  • 409
    own_person_type: it is the viewer's own
    Refusal application/json

Request

curl -X PATCH "https://api.example.org/accounts/<id>/person-type" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "person_type": "registered"
  }'

Answer · 200

{
  "changed": false,
  "id": 42,
  "person_type": "string",
  "previous": "string"
}

Make everything public registered-only

POST/accounts/make-registered

Every public profile, group and post, for an installation that has set MIN_CONTENT_ACCESS=registered. It cannot be undone: the number is typed to confirm it.

Who: A super administrator, with actors:write, who has just proved who they are again

Body application/json

  • confirmstringrequired
    The number of things that will change, typed out: "CONVERT 8675". At most 30 characters.

Answers

  • 200
    what was changed
    auth.PublicContent application/json
  • 400
    confirmation_mismatch: the number is not how much is public now, which it says; or, without a body, the request is not well formed
    auth.ConfirmationMismatch application/json
  • 401
    no token, or one that has expired
  • 403
    step_up_required: the viewer must prove who they are again first (/reauth); or, without a body, the viewer may not
    Refusal application/json
  • 409
    public_content_allowed: new content may still be public here
    Refusal application/json

Request

curl -X POST "https://api.example.org/accounts/make-registered" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "confirm": "string"
  }'

Answer · 200

{
  "groups": 1,
  "people": 1,
  "posts": 1,
  "public_allowed": false,
  "total": 1
}

How much is public

GET/accounts/public-content

What making it all registered-only would change.

Who: A super administrator, with actors:read

Answers

  • 200
    OK
    auth.PublicContent application/json
  • 401
    no token, or one that has expired
  • 403
    the viewer may not do this

Request

curl "https://api.example.org/accounts/public-content" \
  -H "Authorization: Bearer $TOKEN"

Answer · 200

{
  "groups": 1,
  "people": 1,
  "posts": 1,
  "public_allowed": false,
  "total": 1
}

What somebody has accepted

GET/agreements/{person_id}

Their own, or anybody's, for an administrator.

Who: Somebody signed in, with account:read

Parameters

  • person_idintegerrequiredin path
    The person's id.

Answers

  • 200
    OK
    object application/json
  • 400
    the id is not a number
  • 401
    no token, or one that has expired
  • 403
    it is somebody else, and the viewer is not an administrator
  • 404
    there is no such person

Request

curl "https://api.example.org/agreements/<person_id>" \
  -H "Authorization: Bearer $TOKEN"

Answer · 200

{
  "data": {
    "privacy": {
      "accepted": false,
      "accepted_at": "2026-10-27T09:30:00Z",
      "version": "string"
    },
    "rules": {
      "accepted": false,
      "accepted_at": "2026-10-27T09:30:00Z",
      "version": "string"
    },
    "tos": {
      "accepted": false,
      "accepted_at": "2026-10-27T09:30:00Z",
      "version": "string"
    }
  }
}

Accept the Privacy Policy

PATCH/agreements/privacy

The viewer's own acceptance, of the version the web app shows.

Who: Somebody signed in

Body application/json or application/x-www-form-urlencoded or multipart/form-data

  • acceptedboolean
    True.
  • versionstringrequired
    The version accepted: 1.0. At most 20 characters.

Answers

  • 200
    OK
    auth.Accepted application/json
  • 400
    version_malformed: the version is not a dotted number; not_accepted: accepted is false; or, without a body, the request is not well formed
    Refusal application/json
  • 401
    no token, or one that has expired

Request

curl -X PATCH "https://api.example.org/agreements/privacy" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "accepted": false,
    "version": "string"
  }'

Answer · 200

{
  "accepted": false,
  "version": "string"
}

Agree to the site's rules

PATCH/agreements/rules

The viewer's own agreement, each rule ticked, of the version GET /site has now (roadmap row 102).

Who: Somebody signed in

Body application/json or application/x-www-form-urlencoded or multipart/form-data

  • acceptedboolean
    True.
  • versionstringrequired
    The version accepted: 1.0. At most 20 characters.

Answers

  • 200
    OK
    auth.Accepted application/json
  • 400
    version_malformed: the version is not a dotted number; not_accepted: accepted is false; or, without a body, the request is not well formed
    Refusal application/json
  • 401
    no token, or one that has expired
  • 409
    version_not_current: the site's rules are at another version now
    Refusal application/json

Request

curl -X PATCH "https://api.example.org/agreements/rules" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "accepted": false,
    "version": "string"
  }'

Answer · 200

{
  "accepted": false,
  "version": "string"
}

Accept the Terms of Service

PATCH/agreements/tos

The viewer's own acceptance, of the version the web app shows.

Who: Somebody signed in

Body application/json or application/x-www-form-urlencoded or multipart/form-data

  • acceptedboolean
    True.
  • versionstringrequired
    The version accepted: 1.0. At most 20 characters.

Answers

  • 200
    OK
    auth.Accepted application/json
  • 400
    version_malformed: the version is not a dotted number; not_accepted: accepted is false; or, without a body, the request is not well formed
    Refusal application/json
  • 401
    no token, or one that has expired

Request

curl -X PATCH "https://api.example.org/agreements/tos" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "accepted": false,
    "version": "string"
  }'

Answer · 200

{
  "accepted": false,
  "version": "string"
}

Where somebody is signed in

GET/auth-logs/{id}

Each session: the device, roughly where, when it began, whether it is still live, and which is the viewer's own. Their own, or anybody's, for an administrator.

Who: Somebody signed in, with account:read

Parameters

  • idintegerrequiredin path
    The person's id.

Answers

  • 200
    OK
    list of auth.AuthLogItem application/json
  • 400
    the request is not well formed, or a value in it is not one it may be
  • 401
    no token, or one that has expired
  • 403
    it is somebody else, and the viewer is not an administrator
  • 404
    there is no such person

Request

curl "https://api.example.org/auth-logs/<id>" \
  -H "Authorization: Bearer $TOKEN"

Answer · 200

[
  {
    "city": "string",
    "client_id": "string",
    "country": "string",
    "created_at": "2026-10-27T09:30:00Z",
    "device": "string",
    "id": 42,
    "is_active": false,
    "is_viewer": false,
    "name": "string",
    "os": "string"
  }
]

Sign a session out

DELETE/auth-logs/{uid}/{id}

Somebody whose session an administrator ended is told by mail.

Who: Somebody signed in, with account:write

Parameters

  • uidintegerrequiredin path
    The person's id.
  • idintegerrequiredin path
    The session's id.

Answers

  • 200
    signed out
  • 400
    the request is not well formed, or a value in it is not one it may be
  • 401
    no token, or one that has expired
  • 403
    it is somebody else's, and the viewer is not an administrator
  • 404
    there is no such person, or no such session of theirs

Request

curl -X DELETE "https://api.example.org/auth-logs/<uid>/<id>" \
  -H "Authorization: Bearer $TOKEN"

Whether an email address has an account

GET/auth/is/email

For the forms that ask for one.

Who: Anyone, signed in or not

Parameters

  • emailstring (email)requiredin query
    The address. At least 10 characters. At most 100 characters.

Answers

  • 200
    it has one
  • 400
    the request is not well formed, or a value in it is not one it may be
  • 404
    it has none

Request

curl "https://api.example.org/auth/is/email?email=<email>"

Whether an account has two-factor authentication

GET/auth/is/totp

For the sign-in page, before there is a session.

Who: Anyone, signed in or not

Parameters

  • emailstring (email)requiredin query
    The address. At least 10 characters. At most 100 characters.

Answers

  • 200
    it has
  • 400
    the request is not well formed, or a value in it is not one it may be
  • 404
    it has not, or there is no such account

Request

curl "https://api.example.org/auth/is/totp?email=<email>"

Whether a username is taken

GET/auth/is/username

By a person, a group or an event: a username is the address of a profile, and anybody may see it. The signup form asks as the field is left.

Who: Anyone, signed in or not

Parameters

  • usernamestringrequiredin query
    The username. At least 3 characters. At most 30 characters.

Answers

  • 200
    it is taken
  • 400
    the request is not well formed, or a value in it is not one it may be
  • 404
    it is free

Request

curl "https://api.example.org/auth/is/username?username=<username>"

The apps that may sign people in

GET/clients

Every one, without its secret: a secret is shown once, when it is made.

Who: A super administrator, with oauth:read

Answers

  • 200
    OK
    object application/json
  • 401
    no token, or one that has expired
  • 403
    the viewer may not do this

Request

curl "https://api.example.org/clients" \
  -H "Authorization: Bearer $TOKEN"

Answer · 200

{
  "data": [
    {
      "active": false,
      "client_id": "string",
      "confidential": false,
      "created_at": "2026-10-27T09:30:00Z",
      "first_party": false,
      "grant_types": [
        "string"
      ],
      "id": 42,
      "name": "string",
      "redirect_uris": [
        "string"
      ],
      "scopes": [
        "string"
      ],
      "skip_consent": false,
      "token_expiry": 1
    }
  ]
}

Register an app

POST/clients

A confidential one is answered with its secret, this once.

Who: A super administrator, with oauth:write, who has just proved who they are again

Body application/json

  • activeboolean
    Whether it may sign people in.
  • client_idstringrequired
    What it calls itself when it signs people in: lower-case letters, digits, _ and -. At least 2 characters. At most 64 characters.
  • confidentialboolean
    Whether it can keep a secret: an app's own server can, an app in a browser or on a phone cannot.
  • first_partyboolean
    Whether it is one of the installation's own apps.
  • grant_typeslist of stringrequired
    How it may get tokens: authorization_code, refresh_token, client_credentials. At least 1 of them.
  • namestringrequired
    What people are shown it is called. At least 3 characters. At most 128 characters.
  • redirect_urislist of string (uri)required
    Where people may be sent back to after signing in. At least 1 of them.
  • scopeslist of stringrequired
    What it may ask to do (oauth.md).
  • skip_consentboolean
    Whether people are not asked to allow it.
  • token_expiryinteger
    How long its access tokens last, in seconds: 300 to 86400. At least 300. At most 86400.

Answers

  • 201
    with client_secret when it is confidential
  • 400
    invalid_scope: a scope that does not exist; or, without a body, the request is not well formed
    Refusal application/json
  • 401
    no token, or one that has expired
  • 403
    step_up_required: the viewer must prove who they are again first (/reauth)
    Refusal application/json

Request

curl -X POST "https://api.example.org/clients" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "active": false,
    "client_id": "string",
    "confidential": false,
    "first_party": false,
    "grant_types": [
      "authorization_code"
    ],
    "name": "string",
    "redirect_uris": [
      "https://example.org/"
    ],
    "scopes": [
      "string"
    ],
    "skip_consent": false,
    "token_expiry": 300
  }'

Answer · 201

{
  "active": false,
  "client_id": "string",
  "client_secret": "string",
  "confidential": false,
  "created_at": "2026-10-27T09:30:00Z",
  "first_party": false,
  "grant_types": [
    "string"
  ],
  "id": 42,
  "name": "string",
  "redirect_uris": [
    "string"
  ],
  "scopes": [
    "string"
  ],
  "skip_consent": false,
  "token_expiry": 1
}

One app

GET/clients/{id}

Who: A super administrator, with oauth:read

Parameters

  • idintegerrequiredin path
    The app's id.

Answers

  • 200
    OK
    auth.OAuthClientItem application/json
  • 400
    the request is not well formed, or a value in it is not one it may be
  • 401
    no token, or one that has expired
  • 403
    the viewer may not do this
  • 404
    there is no such app

Request

curl "https://api.example.org/clients/<id>" \
  -H "Authorization: Bearer $TOKEN"

Answer · 200

{
  "active": false,
  "client_id": "string",
  "confidential": false,
  "created_at": "2026-10-27T09:30:00Z",
  "first_party": false,
  "grant_types": [
    "string"
  ],
  "id": 42,
  "name": "string",
  "redirect_uris": [
    "string"
  ],
  "scopes": [
    "string"
  ],
  "skip_consent": false,
  "token_expiry": 1
}

Change an app

PATCH/clients/{id}

Everything but its id and secret: a new secret is rotate-secret.

Who: A super administrator, with oauth:write, who has just proved who they are again

Parameters

  • idintegerrequiredin path
    The app's id.

Body application/json

  • activeboolean
    Whether it may sign people in.
  • confidentialboolean
    Whether it can keep a secret: an app's own server can, an app in a browser or on a phone cannot.
  • first_partyboolean
    Whether it is one of the installation's own apps.
  • grant_typeslist of stringrequired
    How it may get tokens: authorization_code, refresh_token, client_credentials. At least 1 of them.
  • namestringrequired
    What people are shown it is called. At least 3 characters. At most 128 characters.
  • redirect_urislist of string (uri)required
    Where people may be sent back to after signing in. At least 1 of them.
  • scopeslist of stringrequired
    What it may ask to do (oauth.md).
  • skip_consentboolean
    Whether people are not asked to allow it.
  • token_expiryinteger
    How long its access tokens last, in seconds: 300 to 86400. At least 300. At most 86400.

Answers

  • 200
    OK
    auth.OAuthClientItem application/json
  • 400
    invalid_scope: a scope that does not exist; or, without a body, the request is not well formed
    Refusal application/json
  • 401
    no token, or one that has expired
  • 403
    step_up_required: the viewer must prove who they are again first (/reauth)
    Refusal application/json
  • 404
    there is no such app

Request

curl -X PATCH "https://api.example.org/clients/<id>" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "active": false,
    "confidential": false,
    "first_party": false,
    "grant_types": [
      "authorization_code"
    ],
    "name": "string",
    "redirect_uris": [
      "https://example.org/"
    ],
    "scopes": [
      "string"
    ],
    "skip_consent": false,
    "token_expiry": 300
  }'

Answer · 200

{
  "active": false,
  "client_id": "string",
  "confidential": false,
  "created_at": "2026-10-27T09:30:00Z",
  "first_party": false,
  "grant_types": [
    "string"
  ],
  "id": 42,
  "name": "string",
  "redirect_uris": [
    "string"
  ],
  "scopes": [
    "string"
  ],
  "skip_consent": false,
  "token_expiry": 1
}

Remove an app

DELETE/clients/{id}

Its tokens stop working.

Who: A super administrator, with oauth:write, who has just proved who they are again

Parameters

  • idintegerrequiredin path
    The app's id.

Answers

  • 200
    the app as it was
    auth.OAuthClientItem application/json
  • 400
    the request is not well formed, or a value in it is not one it may be
  • 401
    no token, or one that has expired
  • 403
    step_up_required: the viewer must prove who they are again first (/reauth)
    Refusal application/json
  • 404
    there is no such app

Request

curl -X DELETE "https://api.example.org/clients/<id>" \
  -H "Authorization: Bearer $TOKEN"

Answer · 200

{
  "active": false,
  "client_id": "string",
  "confidential": false,
  "created_at": "2026-10-27T09:30:00Z",
  "first_party": false,
  "grant_types": [
    "string"
  ],
  "id": 42,
  "name": "string",
  "redirect_uris": [
    "string"
  ],
  "scopes": [
    "string"
  ],
  "skip_consent": false,
  "token_expiry": 1
}

Give an app a new secret

POST/clients/{id}/rotate-secret

The old one stops working, and so do the tokens issued with it.

Who: A super administrator, with oauth:write, who has just proved who they are again

Parameters

  • idintegerrequiredin path
    The app's id.

Answers

  • 200
    with the new client_secret, this once
  • 400
    public_client: the app is public and has no secret; or, without a body, the request is not well formed
    Refusal application/json
  • 401
    no token, or one that has expired
  • 403
    step_up_required: the viewer must prove who they are again first (/reauth)
    Refusal application/json
  • 404
    there is no such app

Request

curl -X POST "https://api.example.org/clients/<id>/rotate-secret" \
  -H "Authorization: Bearer $TOKEN"

Answer · 200

{
  "active": false,
  "client_id": "string",
  "client_secret": "string",
  "confidential": false,
  "created_at": "2026-10-27T09:30:00Z",
  "first_party": false,
  "grant_types": [
    "string"
  ],
  "id": 42,
  "name": "string",
  "redirect_uris": [
    "string"
  ],
  "scopes": [
    "string"
  ],
  "skip_consent": false,
  "token_expiry": 1
}

Begin changing the viewer's email address

PATCH/email/change

Mails a code to the address the account has now: that inbox approves the move, so a stolen session alone cannot.

Who: Somebody signed in, who has just proved who they are again

Body application/json

  • emailstring (email)required
    The new address. At least 10 characters. At most 100 characters.

Answers

  • 204
    the code is sent, or the address is the one the account has
  • 400
    the request is not well formed, or a value in it is not one it may be
  • 401
    no token, or one that has expired
  • 403
    step_up_required: the viewer must prove who they are again first (/reauth)
    Refusal application/json
  • 409
    another account has that address

Request

curl -X PATCH "https://api.example.org/email/change" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "email": "picard@example.org"
  }'

Change the viewer's email address

POST/email/change/confirm

With the code mailed to the old address. Every session is signed out, this one too.

Who: Somebody signed in

Body application/json

  • codestring
    The six digits mailed to the old address.

Answers

  • 204
    changed
  • 400
    it is not six digits; nothing was counted
  • 401
    no token, or one that has expired
  • 409
    another account has taken the address since
  • 410
    no change is waiting, or its code ran out, was replaced or had too many wrong tries: start again
  • 422
    the code is wrong; it says how many tries are left
    auth.CodeMismatch application/json

Request

curl -X POST "https://api.example.org/email/change/confirm" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "code": "string"
  }'

The invitations the viewer has sent

GET/invites

Their own only, administrators' too: an invitation carries somebody's address. With how many are waiting; how many may is the viewer's entitlement, in userinfo.

Who: Somebody signed in, with actors:read

Parameters

  • startintegerin query
    How many to skip. At least 0.
  • limitintegerin query
    How many on a page. At least 0. At most 100. 10 unless given.
  • qstringin query
    Words to look for, where a list looks. At most 100 characters.
  • orderingstringin query
    What to sort by, where a list sorts. At most 32 characters.

Answers

  • 200
    OK
    auth.InviteList application/json
  • 400
    the request is not well formed, or a value in it is not one it may be
  • 401
    no token, or one that has expired
  • 403
    the viewer may not invite

Request

curl "https://api.example.org/invites" \
  -H "Authorization: Bearer $TOKEN"

Answer · 200

{
  "data": [
    {
      "author": null,
      "created_at": "2026-10-27T09:30:00Z",
      "id": 42,
      "recipient_email": "string",
      "status": "string"
    }
  ],
  "pagination": {
    "limit": 1,
    "offset": 1,
    "total": 1
  },
  "pending": 1
}

Invite somebody

POST/invites

Mails them a link. Who may invite is the installation's choice (INVITES_FROM), and so is how many may wait at once (INVITE_PENDING_MAX).

Who: Somebody signed in, with actors:write

Body application/json

  • emailstring (email)required
    Who to invite. At least 10 characters. At most 100 characters.

Answers

  • 200
    OK
    auth.InviteItem application/json
  • 400
    the request is not well formed, or a value in it is not one it may be
  • 401
    no token, or one that has expired
  • 403
    invitations_closed: the site takes no one new; or, without a body, the viewer may not invite
    Refusal application/json
  • 409
    too_many_invitations: the viewer has as many waiting as they may, and it says how one frees; or, without a body, the address has an account
    Refusal application/json
  • 429
    too many invitations, from the viewer or to the address, for now

Request

curl -X POST "https://api.example.org/invites" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "email": "picard@example.org"
  }'

Answer · 200

{
  "author": {
    "alias": "string",
    "avatar_urls": {
      "key": null
    },
    "id": 42,
    "name": "string",
    "type": "string"
  },
  "created_at": "2026-10-27T09:30:00Z",
  "id": 42,
  "recipient_email": "string",
  "status": "string"
}

Take an invitation back

DELETE/invites/{id}

Its link stops working, and the person invited is told.

Who: Somebody signed in, with actors:write

Parameters

  • idintegerrequiredin path
    The invitation's id.

Answers

  • 200
    the invitation as it was
    auth.InviteItem application/json
  • 400
    the request is not well formed, or a value in it is not one it may be
  • 401
    no token, or one that has expired
  • 403
    it is somebody else's, and the viewer is not an administrator
  • 404
    there is no such invitation

Request

curl -X DELETE "https://api.example.org/invites/<id>" \
  -H "Authorization: Bearer $TOKEN"

Answer · 200

{
  "author": {
    "alias": "string",
    "avatar_urls": {
      "key": null
    },
    "id": 42,
    "name": "string",
    "type": "string"
  },
  "created_at": "2026-10-27T09:30:00Z",
  "id": 42,
  "recipient_email": "string",
  "status": "string"
}

The page an invitation's link opens

GET/invites/accept

It asks for a username, a name and a password.

Who: Anyone, signed in or not

Parameters

  • tokenstringin query
    From the link: 128 characters.

Answers

  • 200
    the page, or one that says why the link does not work
    string text/html

Request

curl "https://api.example.org/invites/accept"

Accept an invitation

POST/invites/accept

Makes the account; where registration is by approval, asks for it instead.

Who: Anyone, signed in or not

Body application/x-www-form-urlencoded or multipart/form-data

  • namestring
    Optional, and offered rather than required. At most 64 characters.
  • passwordstringrequired
    At least 15 characters. At least 15 characters. At most 150 characters.
  • tokenstringrequired
    From the link. At least 128 characters. At most 128 characters.
  • usernamestringrequired
    The username, chosen rather than derived. At least 3 characters. At most 30 characters.

Answers

  • 200
    the page again, saying what was wrong, or a page that says what happened
    string text/html
  • 303
    on to the web app, signed in

Request

curl -X POST "https://api.example.org/invites/accept" \
  --data-urlencode "name=string" \
  --data-urlencode "password=string" \
  --data-urlencode "token=string" \
  --data-urlencode "username=string"

What this installation is (NodeInfo 2.1)

GET/nodeinfo/2.1

The software, the protocols it speaks, whether it takes registrations, and how many people use it. Its names are NodeInfo's, in camelCase.

Who: Anyone, signed in or not

Answers

Request

curl "https://api.example.org/nodeinfo/2.1"

Answer · 200

{
  "metadata": {
    "key": null
  },
  "openRegistrations": false,
  "protocols": [
    "string"
  ],
  "services": {
    "inbound": [
      "string"
    ],
    "outbound": [
      "string"
    ]
  },
  "software": {
    "name": "string",
    "repository": "string",
    "version": "string"
  },
  "usage": {
    "localPosts": 1,
    "users": {
      "activeHalfyear": null,
      "activeMonth": null,
      "total": null
    }
  },
  "version": "string"
}

The keys tokens are signed with

GET/oauth-signing-keys

Every one, live or not, newest first.

Who: A super administrator, with oauth:read

Answers

  • 200
    OK
    object application/json
  • 401
    no token, or one that has expired
  • 403
    insufficient_scope: the token has no oauth:read; forbidden: the viewer is not a super administrator
    Refusal application/json

Request

curl "https://api.example.org/oauth-signing-keys" \
  -H "Authorization: Bearer $TOKEN"

Answer · 200

{
  "data": [
    {
      "active": false,
      "algorithm": "string",
      "backend": "string",
      "created_at": "2026-10-27T09:30:00Z",
      "expires_at": "2026-10-27T09:30:00Z",
      "id": 42,
      "key_id": "string"
    }
  ]
}

One signing key

GET/oauth-signing-keys/{kid}

Who: A super administrator, with oauth:read

Parameters

  • kidstringrequiredin path
    The key's kid.

Answers

  • 200
    OK
    auth.OAuthSigningKeyItem application/json
  • 400
    the request is not well formed, or a value in it is not one it may be
  • 401
    no token, or one that has expired
  • 403
    insufficient_scope: the token has no oauth:read; forbidden: the viewer is not a super administrator
    Refusal application/json
  • 404
    there is no such key

Request

curl "https://api.example.org/oauth-signing-keys/<kid>" \
  -H "Authorization: Bearer $TOKEN"

Answer · 200

{
  "active": false,
  "algorithm": "string",
  "backend": "string",
  "created_at": "2026-10-27T09:30:00Z",
  "expires_at": "2026-10-27T09:30:00Z",
  "id": 42,
  "key_id": "string"
}

Sign with a key, or stop

PATCH/oauth-signing-keys/{kid}

A key no longer signed with still checks the tokens it signed.

Who: A super administrator, with oauth:write, who has just proved who they are again

Parameters

  • kidstringrequiredin path
    The key's kid.

Body application/json

  • activebooleanrequired
    Whether to sign new tokens with it.

Answers

  • 200
    OK
    auth.OAuthSigningKeyItem application/json
  • 400
    the request is not well formed, or a value in it is not one it may be
  • 401
    no token, or one that has expired
  • 403
    step_up_required: the viewer must prove who they are again first (/reauth); insufficient_scope: the token has no oauth:write; forbidden: the viewer is not a super administrator
    Refusal application/json
  • 404
    there is no such key
  • 409
    last_active_key: it is the only key signed with, and nothing could be signed without it
    Refusal application/json

Request

curl -X PATCH "https://api.example.org/oauth-signing-keys/<kid>" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "active": false
  }'

Answer · 200

{
  "active": false,
  "algorithm": "string",
  "backend": "string",
  "created_at": "2026-10-27T09:30:00Z",
  "expires_at": "2026-10-27T09:30:00Z",
  "id": 42,
  "key_id": "string"
}

Ask to act for somebody (OAuth 2.0)

GET/oauth/authorize

The authorization endpoint (RFC 6749, 4.1.1), with PKCE (RFC 7636). The person signs in first if they have to, and is asked whether to allow it unless the app is the installation's own.

Who: Anyone, signed in or not

Parameters

  • response_typestringrequiredin query
    code. One of code.
  • client_idstringrequiredin query
    The app's client_id.
  • redirect_uristring (uri)requiredin query
    RedirectURI uses the uri tag, not url. The url tag in go-playground/validator rejects any URI whose scheme isn't http/https — but RFC 8252 (OAuth 2.0 for Native Apps) explicitly recommends custom schemes for desktop and mobile apps because they route the callback to the registered app via the OS rather than through a browser. The uri tag accepts any valid URI with a scheme, which is the right syntactic check here.
  • scopestringrequiredin query
    What the app asks to do, separated by spaces (oauth.md).
  • statestringrequiredin query
    Anything; it comes back unchanged with the code, for the app to check.
  • code_challengestringin query
    PKCE (RFC 7636): the hash of a secret only the app knows, given again with the code. At least 43 characters. At most 128 characters.
  • code_challenge_methodstringin query
    S256 or plain. One of S256, plain.
  • noncestringin query
    Anything; it is put in the ID token (OpenID Connect).

Answers

  • 200
    a page that says why it cannot go on: no such app, or a redirect address it has not registered; or, for an app on a device, the page that hands it the code
    string text/html
  • 302
    back to the app's redirect address with code and state, or with error and error_description; or on to sign in, or to the consent page
  • 400
    the request is not well formed, or a value in it is not one it may be

Request

curl "https://api.example.org/oauth/authorize?response_type=<response_type>&client_id=<client_id>&redirect_uri=<redirect_uri>&scope=<scope>&state=<state>"

The page that asks whether to allow an app

GET/oauth/consent

Who: Anyone, signed in or not

Answers

  • 200
    the page, or one that says why it cannot go on
    string text/html
  • 302
    on to sign in, when the browser is not signed in

Request

curl "https://api.example.org/oauth/consent"

Allow an app, or not

POST/oauth/consent

Who: Anyone, signed in or not

Body application/x-www-form-urlencoded or multipart/form-data

  • approvedboolean
    Whether the person allows it.
  • csrf_tokenstringrequired
    The token the page was given, which proves the form is this site's.

Answers

  • 200
    a page that says why it cannot go on; or, for an app on a device, the page that hands it the code
    string text/html
  • 302
    back to the app's redirect address with code and state, or with error=access_denied; or on to sign in

Request

curl -X POST "https://api.example.org/oauth/consent" \
  --data-urlencode "approved=false" \
  --data-urlencode "csrf_token=string"

What a token is (OAuth 2.0)

POST/oauth/introspect

RFC 7662: whether it is live, whose it is, and what it may do.

Who: Anyone, signed in or not

Body application/x-www-form-urlencoded or multipart/form-data

  • client_idstring
    The app's client_id, unless it is in an Authorization: Basic header.
  • client_secretstring
    The app's secret, likewise; a public app has none.
  • tokenstringrequired
    The token.
  • token_type_hintstring
    access_token or refresh_token, to look for it as first. One of access_token, refresh_token.

Answers

Request

curl -X POST "https://api.example.org/oauth/introspect" \
  --data-urlencode "client_id=string" \
  --data-urlencode "client_secret=string" \
  --data-urlencode "token=string" \
  --data-urlencode "token_type_hint=access_token"

Answer · 200

{
  "active": false,
  "client_id": "string",
  "exp": 1,
  "iat": 1,
  "iss": "string",
  "jti": "string",
  "scope": "string",
  "sub": "string"
}

The sign-in page

GET/oauth/login

Who: Anyone, signed in or not

Parameters

  • errorstringin query
    Why the page is shown again: session_expired or too_many_attempts.

Answers

  • 200
    the page
    string text/html
  • 302
    on, when the browser is signed in already
  • 400
    the request is not well formed, or a value in it is not one it may be

Request

curl "https://api.example.org/oauth/login"

Sign in with a password

POST/oauth/login

For an account with two-factor authentication, on to the page for the passcode. For one being deleted, the page that offers to restore it.

Who: Anyone, signed in or not

Body application/x-www-form-urlencoded or multipart/form-data

  • csrf_tokenstring
    The token the page was given, which proves it was this site's page.
  • identifierstring (email)required
    The email address. At most 128 characters.
  • passwordstringrequired
    The password. At least 8 characters. At most 72 characters.

Answers

  • 200
    the page again, saying what was wrong, or the page that offers to restore the account
    string text/html
  • 302
    on to where the person was going: back to the authorization that sent them to sign in, or to the web app; or on to the page for the passcode

Request

curl -X POST "https://api.example.org/oauth/login" \
  --data-urlencode "csrf_token=string" \
  --data-urlencode "identifier=picard@example.org" \
  --data-urlencode "password=string"

The page that asks who is signing in with a code

GET/oauth/login/code

Who: Anyone, signed in or not

Parameters

  • errorstringin query
    Why the page is shown again: identifier_required, session_expired or code_expired.

Answers

  • 200
    the page
    string text/html
  • 400
    the request is not well formed, or a value in it is not one it may be

Request

curl "https://api.example.org/oauth/login/code"

Say who is signing in with a code

POST/oauth/login/code

An email address or a username.

Who: Anyone, signed in or not

Body application/x-www-form-urlencoded or multipart/form-data

  • csrf_tokenstring
    The token the page was given, which proves the form is this site's.
  • identifierstring
    An email address or a username.

Answers

  • 200
    the page again, saying what was wrong, or a page that says what happened
    string text/html
  • 303
    on to choose where the code goes, or back when it was not given

Request

curl -X POST "https://api.example.org/oauth/login/code" \
  --data-urlencode "csrf_token=string" \
  --data-urlencode "identifier=string"

The page that asks where the code should go

GET/oauth/login/code/method

Who: Anyone, signed in or not

Answers

  • 200
    the page
    string text/html
  • 302
    back to the start, or on, when there is only one place to send it

Request

curl "https://api.example.org/oauth/login/code/method"

Send the code

POST/oauth/login/code/method

The answer is the same whether or not there is such an account.

Who: Anyone, signed in or not

Body application/x-www-form-urlencoded or multipart/form-data

  • channelstring
    email.
  • csrf_tokenstring
    The token the page was given, which proves the form is this site's.

Answers

  • 200
    the page again, saying what was wrong, or a page that says what happened
    string text/html
  • 303
    on to the page for the code

Request

curl -X POST "https://api.example.org/oauth/login/code/method" \
  --data-urlencode "channel=string" \
  --data-urlencode "csrf_token=string"

Send a new code

POST/oauth/login/code/resend

Who: Anyone, signed in or not

Body application/x-www-form-urlencoded or multipart/form-data

  • channelstring
    email.
  • csrf_tokenstring
    The token the page was given, which proves the form is this site's.

Answers

  • 200
    the page again, saying what was wrong, or a page that says what happened
    string text/html
  • 303
    back to the page for the code

Request

curl -X POST "https://api.example.org/oauth/login/code/resend" \
  --data-urlencode "channel=string" \
  --data-urlencode "csrf_token=string"

The page that asks for the code

GET/oauth/login/code/verify

Who: Anyone, signed in or not

Answers

  • 200
    the page
    string text/html
  • 302
    back to the start, when no code is waiting

Request

curl "https://api.example.org/oauth/login/code/verify"

Sign in with the code

POST/oauth/login/code/verify

Who: Anyone, signed in or not

Body application/x-www-form-urlencoded or multipart/form-data

  • codestring
    The six digits that were sent.
  • csrf_tokenstring
    The token the page was given, which proves the form is this site's.

Answers

  • 200
    the page again, saying what was wrong and how many tries are left
    string text/html
  • 302
    on to where the person was going: back to the authorization that sent them to sign in, or to the web app; or on to the page for the passcode
  • 303
    back to the start, when the code ran out

Request

curl -X POST "https://api.example.org/oauth/login/code/verify" \
  --data-urlencode "code=string" \
  --data-urlencode "csrf_token=string"

Begin signing in with a passkey

POST/oauth/login/passkey/begin

The options to hand to navigator.credentials.get(). The browser offers whichever of its passkeys are for this site.

Who: Anyone, signed in or not

Answers

  • 200
    the options (WebAuthn)
    anything application/json

Request

curl -X POST "https://api.example.org/oauth/login/passkey/begin"

Answer · 200

null

Sign in with a passkey

POST/oauth/login/passkey/finish

What navigator.credentials.get() answered, as the browser gives it. No passcode follows: the gesture that unlocked the passkey is the second factor.

Who: Anyone, signed in or not

Body application/json

anything

Answers

  • 200
    OK
    auth.PasskeySignedIn application/json
  • 400
    ceremony_expired: it was begun too long ago, or not at all
    Refusal application/json
  • 401
    assertion_failed: the passkey was not accepted
    Refusal application/json

Request

curl -X POST "https://api.example.org/oauth/login/passkey/finish" \
  -H "Content-Type: application/json" \
  -d 'null'

Answer · 200

{
  "redirect_to": "string"
}

The page that offers to restore an account being deleted

GET/oauth/login/restore

Who: Anyone, signed in or not

Answers

  • 200
    the page
    string text/html
  • 302
    back to the sign-in page, when no password was just given for such an account

Request

curl "https://api.example.org/oauth/login/restore"

Restore an account being deleted

POST/oauth/login/restore

Then the person signs in again.

Who: Anyone, signed in or not

Answers

  • 200
    a page that says it was restored, or why not
    string text/html
  • 302
    back to the sign-in page, when no password was just given for such an account

Request

curl -X POST "https://api.example.org/oauth/login/restore"

The page that asks for the passcode

GET/oauth/login/totp

The second step of signing in, for an account with two-factor authentication.

Who: Anyone, signed in or not

Answers

  • 200
    the page
    string text/html
  • 302
    back to the sign-in page, when the first step ran out

Request

curl "https://api.example.org/oauth/login/totp"

Sign in with the passcode

POST/oauth/login/totp

Who: Anyone, signed in or not

Body application/x-www-form-urlencoded or multipart/form-data

  • csrf_tokenstring
    The token the page was given, which proves the form is this site's.
  • totp_passcodestring
    The six digits the authenticator shows now.

Answers

  • 200
    the page again, saying what was wrong
    string text/html
  • 302
    on to where the person was going: back to the authorization that sent them to sign in, or to the web app; or back to the sign-in page, when the first step ran out or there were too many wrong passcodes

Request

curl -X POST "https://api.example.org/oauth/login/totp" \
  --data-urlencode "csrf_token=string" \
  --data-urlencode "totp_passcode=string"

Sign out

GET/oauth/logout

Navigated to, not called: it signs out the app's session, then the sign-in session, each on its own host, and ends at return_to.

Who: Anyone, signed in or not

Parameters

  • return_tostringin query
    One of the installation's own apps. Anything else, or nothing, is the web app.

Answers

  • 302
    on to the next host, or to return_to

Request

curl "https://api.example.org/oauth/logout"

Revoke a token (OAuth 2.0)

POST/oauth/revoke

RFC 7009. A refresh token is revoked; an access token is short-lived and runs out by itself. The answer is the same whether or not the token was known.

Who: Anyone, signed in or not

Body application/x-www-form-urlencoded or multipart/form-data

  • client_idstring
    The app's client_id, unless it is in an Authorization: Basic header.
  • client_secretstring
    The app's secret, likewise; a public app has none.
  • tokenstringrequired
    The token.
  • token_type_hintstring
    access_token or refresh_token, to look for it as first. One of access_token, refresh_token.

Answers

  • 200
    done
  • 400
    the request is not well formed, or a value in it is not one it may be
  • 401
    invalid_client: the app did not prove which it is
    auth.OAuthErrorResponse application/json

Request

curl -X POST "https://api.example.org/oauth/revoke" \
  --data-urlencode "client_id=string" \
  --data-urlencode "client_secret=string" \
  --data-urlencode "token=string" \
  --data-urlencode "token_type_hint=access_token"

Start an app's session

POST/oauth/session

For an app's own server (oauth.md): the authorization code is exchanged as /oauth/token would, and the tokens are kept in the session, not sent.

Who: Anyone, signed in or not

Body application/x-www-form-urlencoded or multipart/form-data

  • client_idstring
    The app's client_id, unless it is in an Authorization: Basic header.
  • client_secretstring
    The app's secret, likewise; a public app has none.
  • codestring
    For authorization_code: what the app was sent back with.
  • code_verifierstring
    For authorization_code with PKCE: the secret the challenge was the hash of. At least 43 characters. At most 128 characters.
  • grant_typestringrequired
    authorization_code, refresh_token or client_credentials. One of authorization_code, refresh_token, client_credentials.
  • redirect_uristring (uri)
    For authorization_code: the one the code was asked for with.
  • refresh_tokenstring
    For refresh_token.
  • scopestring
    For client_credentials: what the app asks to do, separated by spaces.

Answers

Request

curl -X POST "https://api.example.org/oauth/session" \
  --data-urlencode "client_id=string" \
  --data-urlencode "client_secret=string" \
  --data-urlencode "code=string" \
  --data-urlencode "code_verifier=string" \
  --data-urlencode "grant_type=authorization_code" \
  --data-urlencode "redirect_uri=https://example.org/" \
  --data-urlencode "refresh_token=string" \
  --data-urlencode "scope=string"

Answer · 200

{
  "status": "authenticated"
}

Get tokens (OAuth 2.0)

POST/oauth/token

The token endpoint (RFC 6749, 3.2): an authorization code, a refresh token or the app's own credentials for an access token. A refresh token is used once, and the answer has the next. A refusal of a refresh token says which in error_code: refresh_raced alone is not the end of the session, but another request refreshing first.

Who: Anyone, signed in or not

Body application/x-www-form-urlencoded or multipart/form-data

  • client_idstring
    The app's client_id, unless it is in an Authorization: Basic header.
  • client_secretstring
    The app's secret, likewise; a public app has none.
  • codestring
    For authorization_code: what the app was sent back with.
  • code_verifierstring
    For authorization_code with PKCE: the secret the challenge was the hash of. At least 43 characters. At most 128 characters.
  • grant_typestringrequired
    authorization_code, refresh_token or client_credentials. One of authorization_code, refresh_token, client_credentials.
  • redirect_uristring (uri)
    For authorization_code: the one the code was asked for with.
  • refresh_tokenstring
    For refresh_token.
  • scopestring
    For client_credentials: what the app asks to do, separated by spaces.

Answers

  • 200
    OK
    auth.OAuthTokenResponse application/json
  • 400
    invalid_grant: the code or refresh token is wrong, used or expired; unsupported_grant_type: the app may not use this grant; invalid_scope: it asked for a scope it does not have; or, without a body, the request is not well formed
    auth.OAuthErrorResponse application/json
  • 401
    invalid_client: the app did not prove which it is
    auth.OAuthErrorResponse application/json
  • 403
    unauthorized_client: the app may not act for itself
    auth.OAuthErrorResponse application/json

Request

curl -X POST "https://api.example.org/oauth/token" \
  --data-urlencode "client_id=string" \
  --data-urlencode "client_secret=string" \
  --data-urlencode "code=string" \
  --data-urlencode "code_verifier=string" \
  --data-urlencode "grant_type=authorization_code" \
  --data-urlencode "redirect_uri=https://example.org/" \
  --data-urlencode "refresh_token=string" \
  --data-urlencode "scope=string"

Answer · 200

{
  "access_token": "string",
  "expires_in": 1,
  "id_token": "string",
  "refresh_token": "string",
  "scope": "string",
  "token_type": "string"
}

Who the token is for (OpenID Connect)

GET/oauth/userinfo

The claims its scopes allow: sub always; with profile, name, preferred_username, alias, avatar_urls, id, type, enabled, person_type, totp_enabled, last_visited_at and the rest of what the web app reads of the viewer; with email, email and email_verified. With profile, also entitlements: what the person may do, and how much, where the installation sets a limit; entitlements.invites.max_pending is how many invitations may wait at once, and invites is null for somebody who may not invite.

Who: Somebody signed in, with openid

Answers

Request

curl "https://api.example.org/oauth/userinfo" \
  -H "Authorization: Bearer $TOKEN"

Answer · 200

{
  "key": null
}

The access token of the session

GET/oauth/validate

For the gateway, on every request: the token is in the X-OAuth-Token header of the answer, refreshed when it had expired, and missing when there is none. Always 200.

Who: Anyone, signed in or not

Answers

  • 200
    the token, in a header, or none

Request

curl "https://api.example.org/oauth/validate"

What the walk-through after signing up needs

GET/onboarding

Who: Somebody signed in

Answers

Request

curl "https://api.example.org/onboarding" \
  -H "Authorization: Bearer $TOKEN"

Answer · 200

{
  "invited_by": "string"
}

Say the viewer has been through the walk-through

PATCH/onboarding

Whether they finished it or skipped it: either way they are not sent back. Only the first time is kept.

Who: Somebody signed in

Answers

  • 200
    OK
    auth.Onboarded application/json
  • 401
    no token, or one that has expired

Request

curl -X PATCH "https://api.example.org/onboarding" \
  -H "Authorization: Bearer $TOKEN"

Answer · 200

{
  "onboarded_at": "2026-10-27T09:30:00Z"
}

Change the viewer's password

PATCH/password/change

Signs out every other session. Passkeys are kept.

Who: Somebody signed in, who has just proved who they are again

Body application/json

  • passwordstringrequired
    The new password: at least 15 characters. At least 15 characters. At most 150 characters.

Answers

  • 200
    OK
    auth.PasswordChanged application/json
  • 400
    the request is not well formed, or a value in it is not one it may be
  • 401
    no token, or one that has expired
  • 403
    step_up_required: the viewer must prove who they are again first (/reauth)
    Refusal application/json
  • 409
    it is the password the account has
  • 429
    too many tries; wait

Request

curl -X PATCH "https://api.example.org/password/change" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "password": "string"
  }'

Answer · 200

{
  "revoked_sessions": 1
}

The page that asks for a reset code

GET/password/forgot

Where somebody who has forgotten their password starts.

Who: Anyone, signed in or not

Parameters

  • errorstringin query
    expired, when the code ran out.

Answers

  • 200
    the page
    string text/html
  • 400
    the request is not well formed, or a value in it is not one it may be

Request

curl "https://api.example.org/password/forgot"

Send a reset code

POST/password/forgot

Mails a six-digit code to the address. The answer is the same whether or not it has an account.

Who: Anyone, signed in or not

Body application/x-www-form-urlencoded or multipart/form-data

  • csrf_tokenstring
    The token the page was given, which proves the form is this site's.
  • emailstring (email)required
    The account's address.

Answers

  • 200
    the page again, saying what was wrong, or a page that says what happened
    string text/html
  • 303
    on to the page for the code

Request

curl -X POST "https://api.example.org/password/forgot" \
  --data-urlencode "csrf_token=string" \
  --data-urlencode "email=picard@example.org"

The page that asks for a new password

GET/password/reset

Reached from a reset link, or after the code.

Who: Anyone, signed in or not

Parameters

  • tokenstringin query
    From the link: 128 characters.

Answers

  • 200
    the page
    string text/html
  • 302
    back to the start, when there is neither a link nor a code
  • 303
    back to the start, when the code ran out

Request

curl "https://api.example.org/password/reset"

Set a new password

POST/password/reset

Signs the account out everywhere, and removes its passkeys.

Who: Anyone, signed in or not

Body application/x-www-form-urlencoded or multipart/form-data

  • passwordstringrequired
    The new password: at least 15 characters. At least 15 characters. At most 150 characters.
  • tokenstring
    From the link. None after the code: the session says whose it is. At least 128 characters. At most 128 characters.

Answers

  • 200
    a page that says it was changed, or what was wrong
    string text/html
  • 303
    back to the start, when the code ran out
  • 400
    the request is not well formed, or a value in it is not one it may be

Request

curl -X POST "https://api.example.org/password/reset" \
  --data-urlencode "password=string" \
  --data-urlencode "token=string"

The page that asks for the reset code

GET/password/reset/verify

Who: Anyone, signed in or not

Answers

  • 200
    the page
    string text/html
  • 302
    back to the start, when no code is waiting, or on, when it was given

Request

curl "https://api.example.org/password/reset/verify"

Check the reset code

POST/password/reset/verify

Who: Anyone, signed in or not

Body application/x-www-form-urlencoded or multipart/form-data

  • codestring
    The six digits that were mailed.
  • csrf_tokenstring
    The token the page was given, which proves the form is this site's.

Answers

  • 200
    the page again, saying what was wrong, or a page that says what happened
    string text/html
  • 303
    on to the new password, or back to the start when the code ran out

Request

curl -X POST "https://api.example.org/password/reset/verify" \
  --data-urlencode "code=string" \
  --data-urlencode "csrf_token=string"

Begin proving who the viewer is, with a passkey

POST/reauth/passkey/begin

The options to hand to navigator.credentials.get(): one of the viewer's own passkeys.

Who: Somebody signed in

Answers

  • 200
    the options (WebAuthn)
    anything application/json
  • 401
    no token, or one that has expired
  • 409
    the viewer has no passkeys: use the password

Request

curl -X POST "https://api.example.org/reauth/passkey/begin" \
  -H "Authorization: Bearer $TOKEN"

Answer · 200

null

Prove who the viewer is, with a passkey

POST/reauth/passkey/finish

What navigator.credentials.get() answered, as the browser gives it.

Who: Somebody signed in

Body application/json

anything

Answers

  • 204
    proved
  • 400
    ceremony_expired: it was begun too long ago, or not at all
    Refusal application/json
  • 401
    assertion_failed: the passkey was not accepted; or, without a body, no token
    Refusal application/json

Request

curl -X POST "https://api.example.org/reauth/passkey/finish" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d 'null'

Prove who the viewer is, with their password

POST/reauth/password

And their passcode, when they have two-factor authentication. Good for five minutes, and for one change.

Who: Somebody signed in

Body application/json

  • passcodestring
    The passcode, when they have two-factor authentication. At most 64 characters.
  • passwordstringrequired
    The viewer's password. At most 150 characters.

Answers

  • 204
    proved
  • 400
    the request is not well formed, or a value in it is not one it may be
  • 401
    the password is wrong
  • 403
    the passcode is wrong, or missing
  • 429
    too many tries; wait

Request

curl -X POST "https://api.example.org/reauth/password" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "passcode": "string",
    "password": "string"
  }'

The signup page

GET/signup

Or a page that says the site takes no registrations, or only by invitation.

Who: Anyone, signed in or not

Answers

  • 200
    the page
    string text/html

Request

curl "https://api.example.org/signup"

Sign up

POST/signup

Mails a code to confirm the address. Where registration is by approval, the request waits for an administrator once it is confirmed.

Who: Anyone, signed in or not

Body application/x-www-form-urlencoded or multipart/form-data

  • bodystring
    At most 500 characters.
  • csrf_tokenstringrequired
    The CSRF token, matched against the one put in the session when the form was rendered.
  • emailstring (email)required
    Their address: a code is mailed to it. At least 3 characters. At most 64 characters.
  • namestring
    Collected in approval mode only, and required there. At most 64 characters.
  • passwordstringrequired
    At least 15 characters. At least 15 characters. At most 150 characters.
  • usernamestringrequired
    The username: letters, digits, - and _. It is the address of their profile. At least 3 characters. At most 30 characters.
  • website_urlstring
    At most 255 characters.

Answers

  • 200
    the page again, saying what was wrong, or a page that says what happened
    string text/html
  • 303
    on to the page for the code

Request

curl -X POST "https://api.example.org/signup" \
  --data-urlencode "body=string" \
  --data-urlencode "csrf_token=string" \
  --data-urlencode "email=picard@example.org" \
  --data-urlencode "name=string" \
  --data-urlencode "password=string" \
  --data-urlencode "username=string" \
  --data-urlencode "website_url=https://example.org/"

The requests to join waiting for an answer

GET/signup-requests

Where registration is by approval: oldest first, each with whether the username asked for is still free.

Who: An administrator, with actors:read

Parameters

  • limitintegerin query
    How many on a page: 50 unless said. At least 0. At most 100.
  • startintegerin query
    How many to skip. At least 0.

Answers

  • 200
    OK
    object application/json
  • 400
    the request is not well formed, or a value in it is not one it may be
  • 401
    no token, or one that has expired
  • 403
    the viewer may not do this

Request

curl "https://api.example.org/signup-requests" \
  -H "Authorization: Bearer $TOKEN"

Answer · 200

{
  "data": [
    {
      "body": "string",
      "created_at": "2026-10-27T09:30:00Z",
      "created_ip": "string",
      "email": "picard@example.org",
      "id": 42,
      "invited_by": 1,
      "name": "string",
      "status": "string",
      "username": "string",
      "username_taken": false,
      "website_url": "https://example.org/"
    }
  ],
  "pagination": {
    "limit": 1,
    "offset": 1,
    "total": 1
  }
}

Approve a request to join

POST/signup-requests/{id}/approve

Makes the account, and tells them by mail.

Who: An administrator, with actors:write

Parameters

  • idintegerrequiredin path
    The request's id.

Body application/json

  • notestring
    Why, for the record: the administrators see it, the person does not.

Answers

  • 200
    OK
    auth.SignupDecided application/json
  • 400
    the request is not well formed, or a value in it is not one it may be
  • 401
    no token, or one that has expired
  • 403
    the viewer may not do this
  • 404
    there is no such request
  • 409
    already_decided: it has been answered; not_verified: they have not confirmed their address; username_taken: somebody has the username now
    Refusal application/json

Request

curl -X POST "https://api.example.org/signup-requests/<id>/approve" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "note": "string"
  }'

Answer · 200

{
  "person_id": 42,
  "status": "string"
}

Turn down a request to join

POST/signup-requests/{id}/reject

They are told by mail; the note is not sent.

Who: An administrator, with actors:write

Parameters

  • idintegerrequiredin path
    The request's id.

Body application/json

  • notestring
    Why, for the record: the administrators see it, the person does not.

Answers

  • 200
    OK
    auth.SignupDecided application/json
  • 400
    the request is not well formed, or a value in it is not one it may be
  • 401
    no token, or one that has expired
  • 403
    the viewer may not do this
  • 404
    there is no such request
  • 409
    already_decided: it has been answered; not_verified: they have not confirmed their address
    Refusal application/json

Request

curl -X POST "https://api.example.org/signup-requests/<id>/reject" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "note": "string"
  }'

Answer · 200

{
  "person_id": 42,
  "status": "string"
}

The page that asks for the signup code

GET/signup/confirm

Who: Anyone, signed in or not

Answers

  • 200
    the page
    string text/html
  • 302
    on to ask for a new code, when none is waiting

Request

curl "https://api.example.org/signup/confirm"

Confirm the address

POST/signup/confirm

Makes the account, or sends the request to the administrators.

Who: Anyone, signed in or not

Body application/x-www-form-urlencoded or multipart/form-data

  • codestring
    The six digits that were mailed.
  • csrf_tokenstring
    The token the page was given, which proves the form is this site's.

Answers

  • 200
    the page again, saying what was wrong, or a page that says what happened
    string text/html
  • 303
    on to ask for a new code, when this one ran out

Request

curl -X POST "https://api.example.org/signup/confirm" \
  --data-urlencode "code=string" \
  --data-urlencode "csrf_token=string"

The page that asks for a new signup code

GET/signup/confirm/resend

Who: Anyone, signed in or not

Parameters

  • errorstringin query
    expired, when the code ran out.

Answers

  • 200
    the page
    string text/html
  • 400
    the request is not well formed, or a value in it is not one it may be

Request

curl "https://api.example.org/signup/confirm/resend"

Send a new signup code

POST/signup/confirm/resend

The answer is the same whether or not a signup is waiting.

Who: Anyone, signed in or not

Body application/x-www-form-urlencoded or multipart/form-data

  • csrf_tokenstring
    The token the page was given, which proves the form is this site's.
  • emailstring (email)required
    The address signed up with. At most 255 characters.

Answers

  • 200
    the page again, saying what was wrong, or a page that says what happened
    string text/html
  • 303
    on to the page for the code

Request

curl -X POST "https://api.example.org/signup/confirm/resend" \
  --data-urlencode "csrf_token=string" \
  --data-urlencode "email=picard@example.org"

What this site says about itself

GET/site

Its rules, which everybody agrees to by ticking each one, with the version agreed to; its promises, for its About page; and the rules a new group is offered to start from. Every text in every language it is written in, by the web app's language names. The operator's, in site.json (configuration.md, SITE_DEFAULTS_PATH).

Who: Anyone, signed in or not

Answers

Request

curl "https://api.example.org/site"

Answer · 200

{
  "promises": [
    {
      "body": {
        "key": null
      },
      "title": {
        "key": null
      }
    }
  ],
  "rule_suggestions": [
    {
      "body": {
        "key": null
      },
      "title": {
        "key": null
      }
    }
  ],
  "rules": {
    "items": [
      null
    ],
    "version": "string"
  }
}

The pages' styles, scripts and pictures

GET/static/{path}

Who: Anyone, signed in or not

Parameters

  • pathstringrequiredin path
    The rest of the address: the file's path under the directory.

Answers

  • 200
    the file
    file application/octet-stream
  • 404
    there is no such file

Request

curl "https://api.example.org/static/<path>"

Whether the viewer has two-factor authentication

GET/totp

Who: Somebody signed in

Answers

  • 200
    OK
    object application/json
  • 401
    no token, or one that has expired

Request

curl "https://api.example.org/totp" \
  -H "Authorization: Bearer $TOKEN"

Answer · 200

{
  "data": {
    "enabled": false,
    "expired": false
  }
}

Begin setting up two-factor authentication

POST/totp

Answers the QR code to pair an authenticator with. It is not on until /totp/recovery-codes and PATCH /totp.

Who: Somebody signed in

Body application/x-www-form-urlencoded or multipart/form-data

  • passwordstringrequired
    The viewer's password. At least 15 characters. At most 150 characters.

Answers

  • 201
    the QR code
    file image/png
  • 400
    the request is not well formed, or a value in it is not one it may be
  • 401
    the password is wrong; or no token
  • 404
    the viewer has no account
  • 409
    it is on: turn it off first

Request

curl -X POST "https://api.example.org/totp" \
  -H "Authorization: Bearer $TOKEN" \
  --data-urlencode "password=string"

Turn two-factor authentication on

PATCH/totp

Once it is set up: paired, and the recovery codes handed out.

Who: Somebody signed in

Answers

  • 200
    on
  • 401
    no token, or one that has expired
  • 404
    the viewer has no account
  • 409
    it is not set up

Request

curl -X PATCH "https://api.example.org/totp" \
  -H "Authorization: Bearer $TOKEN"

Whether somebody has two-factor authentication

GET/totp/{person_id}

For an administrator about to turn it off.

Who: Somebody signed in, with account:read

Parameters

  • person_idintegerrequiredin path
    The person's id.

Answers

  • 200
    OK
    object application/json
  • 400
    the id is not a number
  • 401
    no token, or one that has expired
  • 403
    it is somebody else, and the viewer is not an administrator
  • 404
    there is no such person

Request

curl "https://api.example.org/totp/<person_id>" \
  -H "Authorization: Bearer $TOKEN"

Answer · 200

{
  "data": {
    "enabled": false,
    "expired": false
  }
}

Turn two-factor authentication off

DELETE/totp/{person_id}

The viewer's own; or, for an administrator, somebody's who has lost their authenticator. They are told by mail.

Who: Somebody signed in, with actors:write, who has just proved who they are again

Parameters

  • person_idintegerrequiredin path
    The person's id.

Answers

  • 204
    off, or it was
  • 400
    the request is not well formed, or a value in it is not one it may be
  • 401
    no token, or one that has expired
  • 403
    step_up_required: the viewer must prove who they are again first (/reauth); forbidden: the viewer may not act on this account
    Refusal application/json
  • 404
    there is no such person

Request

curl -X DELETE "https://api.example.org/totp/<person_id>" \
  -H "Authorization: Bearer $TOKEN"

Whether the viewer's last passcode is too long ago

GET/totp/is/expired

Who: Somebody signed in

Answers

  • 200
    it is
  • 204
    it is not
  • 401
    no token, or one that has expired
  • 404
    the viewer has no account

Request

curl "https://api.example.org/totp/is/expired" \
  -H "Authorization: Bearer $TOKEN"

Get recovery codes

POST/totp/recovery-codes

With a passcode from the authenticator just paired, which proves it works. Each code signs in once in place of a passcode. A new set replaces the old, and the owner is told by mail.

Who: Somebody signed in

Body application/x-www-form-urlencoded or multipart/form-data

  • passcodestringrequired
    The six digits the authenticator shows now. At least 6 characters. At most 8 characters.

Answers

  • 200
    the codes, shown this once
    list of string application/json
  • 400
    the request is not well formed, or a value in it is not one it may be
  • 401
    no token, or one that has expired
  • 403
    the passcode is wrong
  • 404
    the viewer has no account

Request

curl -X POST "https://api.example.org/totp/recovery-codes" \
  -H "Authorization: Bearer $TOKEN" \
  --data-urlencode "passcode=string"

Answer · 200

[
  "string"
]

Check a passcode

POST/totp/verify

Signing in asks for one again once the last is too long ago; this is that.

Who: Somebody signed in

Body application/x-www-form-urlencoded or multipart/form-data

  • passcodestring
    The six digits the authenticator shows now.

Answers

  • 200
    it is right
  • 400
    the request is not well formed, or a value in it is not one it may be
  • 401
    no token, or one that has expired
  • 403
    it is wrong, or the viewer has no two-factor authentication
  • 404
    the viewer has no account

Request

curl -X POST "https://api.example.org/totp/verify" \
  -H "Authorization: Bearer $TOKEN" \
  --data-urlencode "passcode=string"

Change the viewer's username

PATCH/username/change

Which is also the address of their profile.

Who: Somebody signed in, who has just proved who they are again

Body application/json

  • usernamestringrequired
    The new username: letters, digits, - and _. At least 3 characters. At most 30 characters.

Answers

  • 200
    OK
    auth.UsernameChanged application/json
  • 204
    it is the username they have
  • 400
    the request is not well formed, or a value in it is not one it may be
  • 401
    no token, or one that has expired
  • 403
    step_up_required: the viewer must prove who they are again first (/reauth)
    Refusal application/json
  • 409
    it is taken

Request

curl -X PATCH "https://api.example.org/username/change" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "username": "string"
  }'

Answer · 200

{
  "username": "string"
}

The viewer's passkeys

GET/webauthn/credentials

Enough to tell them apart: never the keys.

Who: Somebody signed in, with account:read

Answers

  • 200
    OK
    object application/json
  • 401
    no token, or one that has expired
  • 403
    the viewer may not do this

Request

curl "https://api.example.org/webauthn/credentials" \
  -H "Authorization: Bearer $TOKEN"

Answer · 200

{
  "data": [
    {
      "clone_warning": false,
      "created_at": "2026-10-27T09:30:00Z",
      "id": 42,
      "last_used_at": "2026-10-27T09:30:00Z",
      "nickname": "string",
      "synced": false,
      "transports": [
        "string"
      ]
    }
  ]
}

Rename a passkey

PATCH/webauthn/credentials/{id}

Who: Somebody signed in, with account:write

Parameters

  • idintegerrequiredin path
    The passkey's id.

Body application/json or application/x-www-form-urlencoded or multipart/form-data

  • nicknamestringrequired
    What to call it. At least 1 characters. At most 64 characters.

Answers

  • 200
    OK
    object application/json
  • 400
    the request is not well formed, or a value in it is not one it may be
  • 401
    no token, or one that has expired
  • 403
    the viewer may not do this
  • 404
    the viewer has no such passkey

Request

curl -X PATCH "https://api.example.org/webauthn/credentials/<id>" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "nickname": "string"
  }'

Answer · 200

{
  "data": {
    "clone_warning": false,
    "created_at": "2026-10-27T09:30:00Z",
    "id": 42,
    "last_used_at": "2026-10-27T09:30:00Z",
    "nickname": "string",
    "synced": false,
    "transports": [
      "string"
    ]
  }
}

Remove a passkey

DELETE/webauthn/credentials/{id}

The owner is told by mail.

Who: Somebody signed in, with account:write

Parameters

  • idintegerrequiredin path
    The passkey's id.

Answers

  • 200
    removed
  • 400
    the request is not well formed, or a value in it is not one it may be
  • 401
    no token, or one that has expired
  • 403
    the viewer may not do this
  • 404
    the viewer has no such passkey

Request

curl -X DELETE "https://api.example.org/webauthn/credentials/<id>" \
  -H "Authorization: Bearer $TOKEN"

Begin adding a passkey

POST/webauthn/credentials/register/begin

The options to hand to navigator.credentials.create().

Who: Somebody signed in, with account:write

Answers

  • 200
    the options (WebAuthn)
    anything application/json
  • 401
    no token, or one that has expired
  • 403
    the viewer may not do this
  • 409
    credential_limit_reached: remove a passkey first
    Refusal application/json

Request

curl -X POST "https://api.example.org/webauthn/credentials/register/begin" \
  -H "Authorization: Bearer $TOKEN"

Answer · 200

null

Add a passkey

POST/webauthn/credentials/register/finish

What navigator.credentials.create() answered, as the browser gives it. It is named after the device; the owner is told by mail.

Who: Somebody signed in, with account:write

Body application/json

anything

Answers

  • 201
    Created
    object application/json
  • 400
    ceremony_expired: it was begun too long ago, or not at all; registration_failed: the passkey could not be checked
    Refusal application/json
  • 401
    no token, or one that has expired
  • 403
    the viewer may not do this

Request

curl -X POST "https://api.example.org/webauthn/credentials/register/finish" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d 'null'

Answer · 201

{
  "data": {
    "clone_warning": false,
    "created_at": "2026-10-27T09:30:00Z",
    "id": 42,
    "last_used_at": "2026-10-27T09:30:00Z",
    "nickname": "string",
    "synced": false,
    "transports": [
      "string"
    ]
  }
}

How many passkeys somebody has

GET/webauthn/credentials/user/{user_id}

A number only: which devices they hold is theirs. Their own, or anybody's, for an administrator.

Who: Somebody signed in, with account:write

Parameters

  • user_idintegerrequiredin path
    The person's id.

Answers

  • 200
    OK
    object application/json
  • 400
    the request is not well formed, or a value in it is not one it may be
  • 401
    no token, or one that has expired
  • 403
    it is somebody else, and the viewer is not an administrator
  • 404
    there is no such person

Request

curl "https://api.example.org/webauthn/credentials/user/<user_id>" \
  -H "Authorization: Bearer $TOKEN"

Answer · 200

{
  "data": {
    "count": 1
  }
}

Remove all of somebody's passkeys

DELETE/webauthn/credentials/user/{user_id}

Their own, or anybody's, for an administrator, who must have just proved who they are; the owner is told by mail. They still sign in with their password.

Who: Somebody signed in, with account:write, who has just proved who they are again

Parameters

  • user_idintegerrequiredin path
    The person's id.

Answers

  • 200
    OK
    object application/json
  • 400
    the request is not well formed, or a value in it is not one it may be
  • 401
    no token, or one that has expired
  • 403
    step_up_required: the viewer must prove who they are again first (/reauth); or, without a body, it is somebody else, and the viewer is not an administrator
    Refusal application/json
  • 404
    there is no such person

Request

curl -X DELETE "https://api.example.org/webauthn/credentials/user/<user_id>" \
  -H "Authorization: Bearer $TOKEN"

Answer · 200

{
  "data": {
    "revoked": 1
  }
}

Written by Claude, reviewed by Rastin Mehr · About this