System Admin
Platform-level endpoints for cross-workspace administration. They are gated by the system_admin flag on directus_users — a boolean introduced with the registration lockdown (migration 20260722A-add-system-admin-flag), backfilled for users holding a Directus admin-access policy. All other callers receive 403 { "error": "forbidden" } (unauthenticated callers receive 401).
The flag is exposed on the current-user payload (Directus GET /users/me, consumed by the frontend and by the SDK's client.me()), so UIs can show or hide the admin surface.
Registration lockdown (OPEN_REGISTRATION_ENABLED)
Self-service sign-up is controlled by the OPEN_REGISTRATION_ENABLED environment variable on directus-api. It defaults to disabled; only the exact string "true" enables it.
While disabled (the default):
| Flow | Behavior |
|---|---|
POST /wallethero-api/register without invitation_token | 403 { "error": "registration_disabled" } |
POST /wallethero-api/register with a valid invitation_token | Works — the user is created, auto-verified, and joins the inviting workspace |
POST /wallethero-api/auth/sso for an existing user (by SSO id or email) | Works — login / account linking is unaffected |
POST /wallethero-api/auth/sso for a new user with no pending invitation | 403 { "error": "registration_disabled" } |
POST /wallethero-api/auth/sso for a new user with a pending workspace invitation for their email | Works — the user is created and joins the inviting workspace (no default workspace) |
POST /wallethero-api/workspace/create | Restricted to system_admin users — others get 403 { "error": "forbidden" } |
The variable is declared in directus-api/env.template, wired through directus-api/docker-compose.yml (OPEN_REGISTRATION_ENABLED: ${OPEN_REGISTRATION_ENABLED:-false}), and in Kubernetes read from the optional directus-config key open_registration_enabled (missing key = disabled).
Onboarding flow
With registration locked down, new-customer onboarding is:
- A system admin creates the workspace with
POST /system/workspaces, passing the owner's email. - If the owner already has an account, they are immediately the workspace creator and admin member (
owner: "added"). - If not, an admin invitation email is sent (
owner: "invited"); the owner registers through the invitation link (/registerwithinvitation_token) or signs in with Google — both paths are allowed by the lockdown because a pending invitation exists.
List All Workspaces
GET /wallethero-api/system/workspaces
Lists every workspace on the platform with member counts and the owner's email (from user_created, falling back to the first admin-role member).
Auth: Bearer token — system_admin only.
Query Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
limit | number | 50 | Page size (1–200) |
offset | number | 0 | Rows to skip |
Example Request
curl "https://api.wallethero.app/wallethero-api/system/workspaces?limit=50&offset=0" \
-H "Authorization: Bearer YOUR_TOKEN"Response (200)
{
"data": [
{
"id": "11111111-1111-1111-1111-111111111111",
"name": "Acme Coffee Rewards",
"slug": "acme_coffee",
"date_created": "2026-07-01T10:30:00Z",
"member_count": 3,
"owner_email": "[email protected]"
}
],
"pagination": { "limit": 50, "offset": 0, "total": 128 }
}owner_email is null when no owner can be determined.
Create Workspace (with optional owner)
POST /wallethero-api/system/workspaces
Creates a workspace via the standard lifecycle (uniqueness checks, admin junction row, data-model seeding) and optionally hands it to an owner.
Auth: Bearer token — system_admin only.
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Workspace display name (1–100 characters) |
slug | string | Yes | URL identifier (^[a-z_]+$) |
owner_email | string (email) | No | Intended workspace owner |
Owner semantics:
owner_emailbelongs to an existing user → that user is the workspace creator and admin member; the acting system admin is not added. Responseowner: "added".owner_emailis unknown → the workspace is temporarily created with the acting system admin as creator, and an owner-handoff admin invitation is emailed to the address. The intended email is shown asowner_emailwhile pending. On acceptance,user_createdtransfers to the invited user and the temporary system-admin membership is removed. Responseowner: "invited"plus aninvitationobject.owner_emailomitted → the acting system admin is the creator. Responseowner: "none".
Example Request
curl -X POST "https://api.wallethero.app/wallethero-api/system/workspaces" \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "name": "Acme Coffee Rewards", "slug": "acme_coffee", "owner_email": "[email protected]" }'Response (200)
{
"workspace": {
"id": "11111111-1111-1111-1111-111111111111",
"name": "Acme Coffee Rewards",
"slug": "acme_coffee",
"date_created": "2026-07-22T10:30:00Z",
"user_created": "22222222-2222-2222-2222-222222222222"
},
"owner": "invited",
"invitation": {
"id": "33333333-3333-3333-3333-333333333333",
"email": "[email protected]",
"expires_at": "2026-07-29T10:30:00Z"
}
}invitation is present only when owner is "invited".
Errors
| Status | Body | Cause |
|---|---|---|
400 | INVALID_PAYLOAD | Duplicate name/slug, invalid slug format |
401 | UNAUTHENTICATED | Missing/invalid token |
403 | { "error": "forbidden" } | Caller is not a system_admin |
SDK
const { data, pagination } = await client.systemAdmin.listWorkspaces({ limit: 50, offset: 0 });
const result = await client.systemAdmin.createWorkspace({
name: "Acme Coffee Rewards",
slug: "acme_coffee",
owner_email: "[email protected]",
});
// result.owner === "added" | "invited" | "none"