Settings, renames, exports and account deletion

Embed
Preview of the card
<a href="https://forumulate.com/guides/settings-exports-and-deletion"><img src="https://forumulate.com/guides/_api/public/orgs/046a9bc0-d530-43ae-9bae-af7945008cda/spaces/36aa4df0-1e95-4cef-8ae9-795169441345/pages/82c88f0c-82c9-49a2-abfb-627032a91f6b/social-card.svg?v=3e7ae33eec8b" alt="Settings, renames, exports and account deletion" width="600" height="315"></a>

The four administrative doors that have a reference page each and belonged on one page together: the organization's settings registry, renaming an organization or a space, getting your data out, and closing an account.

Everything here is the API. Where a console screen does the same thing it is named, but the API is the authority — the console calls these endpoints.

The settings registry

An organization's behaviour is a flat list of keys. There is one catalogue and it is served, not written down:

GET /api/orgs/{orgId}/settings

Each entry carries everything needed to drive it:

Field

What it tells you

key

What you PUT to. community.max_tags_per_topic.

label, description

What a person reads. The console shows these.

category

How the console groups it.

type

boolean, integer, string or enum.

default

The platform default, which applies until you override it.

min, max

Integer bounds, where the key has them.

options

The allowed values, for an enum.

maxLength

The ceiling, for a string.

value

What this organization currently gets.

isDefault

Whether that value is the default or an override you set.

advanced

Machinery; the console hides it behind a disclosure.

Read it before you write. It is the only published list of valid keys, and a key that is not in it is a 404 rather than a stored unknown — we do not keep settings we do not honour.

Setting one key

PUT /api/orgs/{orgId}/settings/community.max_tags_per_topic
{ "value": 8 }
  • value must be the type the key declares. A wrong type is a 422.

  • An unknown key is a 404.

  • Idempotent: sending the value it already has answers 200 and writes nothing.

  • The change is audit-logged against the caller, so GET /api/orgs/{orgId}/audit says who moved it and when.

  • Answers { "setting": { … } } — the whole entry, including the new value and isDefault: false.

Putting one back

DELETE /api/orgs/{orgId}/settings/community.max_tags_per_topic

This is not "set it to false" or "set it to zero". It clears your override so the key follows the platform default, including if we ever change that default. Explicitly writing today's default value would pin you to it. The reply is the setting as it now reads, with isDefault: true.

Who may

Both writes need the org.manage_settings capability — owners and admins have it, and a custom role can be built with it. A token also needs the write scope; GET needs read.

Renaming an organization or a space

A slug is not cosmetic: it is the first segment of every public address the organization owns, and an accepted value for {orgId} on every route under /orgs/{orgId}/….

PATCH /api/orgs/{orgId}/slug                      { "slug": "acme" }
PATCH /api/orgs/{orgId}/spaces/{spaceId}/slug     { "slug": "handbook" }

Both answer { "slug": "acme", "previous": "acme-inc" }. previous is what a redirect table or a cache purge needs, which is why it comes back rather than having to be remembered.

The rules, which are the same ones POST /api/orgs and space create enforce — there is one answer to "what may I call it?", not one per door:

  • lowercase letters, digits and hyphens; never starting or ending with one;

  • at least 2 characters, and at most 63 for an organization (it is handed out as a DNS label, acme.forumulate.com, and 63 is the limit on one) or 48 for a space, which sits in a path and has no reason to be longer;

  • an inner double dash (a--b) is accepted for an organization and refused for a space, because the signup door has always minted organization names that way and tightening it would make an existing name unrenamable to itself;

  • globally unique, and a name another organization used before stays theirs;

  • a reserved platform name — api, docs, admin, login, status, the reader prefixes — is refused with a 409, so a rename cannot shadow our own routes.

The uuid never changes and keeps working, whatever you rename things to. Any route that takes {orgId} takes either form.

What happens to the old address

That is a setting, and it is the one worth knowing about before you rename anything in public:

links.keep_renamed

After a rename

true (the default)

The old address keeps working and redirects to the new one.

false

The old name is released immediately and stops resolving.

Turn it off when the old name was a mistake or gives something away, and leave it on otherwise — a released name can be taken by somebody else.

A space remembers every name it has answered to, and you can read and prune that list:

GET    /api/orgs/{orgId}/spaces/{spaceId}/slug/history
DELETE /api/orgs/{orgId}/spaces/{spaceId}/slug/history/{slug}

The DELETE stops the old name answering and releases it. It is the per-name version of turning links.keep_renamed off, after the fact.

Getting your data out

Two exports, with different subjects. Both are synchronous JSON attachments — a database read, not a job, so there is nothing to poll.

GET /api/orgs/{orgId}/export     # the organization: every space, collection, page, topic
GET /api/users/me/export         # the caller's own account, across every org it belongs to

The organization export is the document POST /api/orgs/{orgId}/import takes, so an export of one organization is a migration into another. The import is idempotent — matched by slug at every level, so re-importing updates what it created rather than making a second copy — and additive: nothing is deleted because the document does not mention it.

Content, never credentials. An export carries no tokens, no secrets, no password hashes and no webhook signing keys. It is safe to hand to a customer and it is not enough to clone an installation.

The organization export needs org.view_audit. The account export needs nothing but the account: it is yours.

For the privacy framing of the same two endpoints — which regime asks for which, and what to tell a reader who writes to you — see Privacy, GDPR and data requests.

Streaming the audit log to your SIEM

The audit log records who did what in your organization. If your security team keeps its own record, point the organization at an HTTPS collector and every audit event is pushed to it as it happens. Both routes need org.manage_settings.

PUT /orgs/{orgId}/siem
{ "url": "https://siem.example.com/ingest/forumulate", "token": "s3cret-collector-token" }
  • url must be https; anything else is a 422. Send null to turn the stream off, which also clears the token.

  • token is optional. When set, each delivery carries it as Authorization: Bearer <token>. It is stored encrypted and never returned. Leave it out to keep the stored one; send null to remove it.

GET /orgs/{orgId}/siem answers { "url": ..., "configured": true, "tokenSet": true }. When nothing is set it is { "url": null, "configured": false, "tokenSet": false }, not a 404.

Each event is one POST with a JSON body of { "type": "audit", "event": { ... } }. The event has organizationId, actorUserId, impersonatorUserId, action, targetType, targetId, source and at. Delivery goes through a retrying queue, so a collector that is down for a few minutes catches up rather than losing that stretch. A 4xx or a timeout counts as a failure and is retried; events that exhaust their retries are kept on our side as the record of what your collector never received. A change to the URL or token takes effect at once.

Closing an account

POST /api/users/me/delete
{ "confirm": "you@example.com" }

Irreversible. It anonymises the account and revokes every session, including the one making the request — the session cookie is cleared in the response, so a client must treat a 200 as a logout.

confirm is the account's own email address. Credentials live in Atlas, so there is no local password to re-check; a live session plus the address is what proves the human. Anything else is a 400.

Content the account authored is detached from it rather than deleted, so a forum thread does not lose its replies and an answer somebody relies on does not disappear out of the middle of a conversation.

Export first:

GET /api/users/me/export

Afterwards there is nothing left to export.

Audit records outlive the account on purpose. An audit log that can be erased by the person it describes is not an audit log.

Your own account, short of deleting it

These are separate endpoints rather than fields on one profile patch, because each is a deliberate act with its own rules:

Endpoint

What it changes

GET /api/users/me

Read it all back.

PATCH /api/users/me

name, avatarUrl, bio, location, websiteUrl.

PATCH /api/users/me/username

The @handle that mentions and /u/ links resolve to. 3–20 characters, letters, digits and underscores; uniqueness-checked.

PUT /api/users/me/locale

The UI language, platform-wide. null clears it and the browser decides again.

PUT /api/users/me/profile-visibility

{ "hidden": true } takes you out of the public profile surfaces and the member directory. Gated on the organization allowing it.

In the API

Every operation below is also reachable from the other surfaces without a dedicated command. forumulate api <Operation> calls it from the CLI, the JavaScript SDK has a typed method for each one (the last column names it; the Python and Go clients are generated from the same spec), and an agent reaches the organization-scoped ones through the MCP call_api tool. Where a CLI command or MCP tool exists too, the last column names it. Operation names link to the API reference.

Org siem

Operation

Request

What it does

Shortcuts

Org_getSiem

GET /orgs/{orgId}/siem

Get the SIEM in an organization

client.orgs.getSiem()

Org_setSiem

PUT /orgs/{orgId}/siem

Replace the SIEM in an organization

client.orgs.setSiem()

Space slug

Operation

Request

What it does

Shortcuts

Space_rename

PATCH /orgs/{orgId}/spaces/{spaceId}/slug

Update the slug in a space

forumulate spaces rename<br>client.spaces.rename()

Space_slugHistory

GET /orgs/{orgId}/spaces/{spaceId}/slug/history

Get the history in a space

forumulate spaces slugs list<br>client.spaces.slugHistory()

Space_forgetSlug

DELETE /orgs/{orgId}/spaces/{spaceId}/slug/history/{slug}

Delete the history

forumulate spaces slugs forget<br>client.spaces.forgetSlug()

User

Operation

Request

What it does

Shortcuts

User_me

GET /users/me

Your account

client.users.me()

User_update

PATCH /users/me

Update your account

client.users.update()

User_setLocale

PUT /users/me/locale

Set your interface language

client.users.setLocale()

User_changeUsername

PATCH /users/me/username

Change your @handle

client.users.changeUsername()

User_setProfileVisibility

PUT /users/me/profile-visibility

Hide or show your public profile

client.users.setProfileVisibility()

User_exportData

GET /users/me/export

Export this account’s personal data (GDPR Article 15)

client.users.exportData()

User_deleteAccount

POST /users/me/delete

Erase this account (GDPR Article 17)

client.users.deleteAccount()

The Forumulate teamUpdated