GET
/admin/spaces
List accessible spaces

Returns every Space the authenticated user is a direct member of, along with their role and member/project counts in each. Returns an empty array rather than a 404 when the user has no memberships.

Example response

{
  "spaces": [
    {
      "id": 1,
      "name": "Acme Corp",
      "role": "owner",
      "locale": "en",
      "memberCount": 1,
      "projectCount": 0,
      "createdAt": "2026-01-01T00:00:00.000Z"
    }
  ]
}
POST
/admin/spaces
Create a space

Creates a new Space owned by the authenticated user, who is automatically added as its owner. A default subscription against the free plan is created at the same time. A name only needs to be unique among the Spaces you own - two different owners may each have a Space called the same thing. locale is optional and defaults to en - it controls what locale generated template data uses for every Project under this Space, unless a Project overrides it (see Projects docs). See the locales list endpoint below for every supported code.

Request

{
  "name": "Acme Corp",
  "locale": "de"
}

Response (201)

{
  "id": 1,
  "name": "Acme Corp",
  "role": "owner",
  "locale": "de",
  "createdAt": "2026-01-01T00:00:00.000Z"
}

Returns 422 if name is missing, empty, or longer than 255 characters, or if locale isn't one of the supported codes. Returns 409 if you already have a Space with this name.

GET
/admin/spaces/{spaceId}
Get a space

Returns Space summary information, including counts of Projects, template records, and API keys owned by the Space. Returns 404 if the Space doesn't exist or the caller has no membership - the two cases are indistinguishable to avoid disclosing that a Space exists.

Example response

{
  "id": 1,
  "name": "Acme Corp",
  "role": "owner",
  "locale": "en",
  "projectCount": 0,
  "templateRecordCount": 0,
  "apiKeyCount": 0,
  "createdAt": "2026-01-01T00:00:00.000Z"
}
PATCH
/admin/spaces/{spaceId}
Update a space

Updates the Space's name and/or locale. Requires the caller to be a Space owner or admin - other roles receive 403. The same per-owner name uniqueness rule from creation applies - renaming to a name the Space's owner already uses elsewhere returns 409. Changing locale only affects future generations - it never retroactively changes an already-generated template, and never changes any Project's own locale that was already set (defaulted or overridden) at that Project's creation time.

Request

{
  "name": "Acme Corporation",
  "locale": "fr"
}

Response

{
  "id": 1,
  "name": "Acme Corporation",
  "locale": "fr",
  "updatedAt": "2026-01-01T00:00:00.000Z"
}

Returns 422 if locale isn't one of the supported codes.

DELETE
/admin/spaces/{spaceId}
Delete a space

Permanently deletes the Space and everything it owns - every Project within it (schemas, relationships, routes, template data, runtime dataset instances, memberships, invitations, and API keys for each, exactly as a direct Project delete would), plus the Space's own members, invitations, subscription, and any Space-owned Starter Kits. Requires owner or admin - the same bar as updating the Space. Irreversible. Every API key belonging to every Project in the Space stops working immediately.

Returns 204 No Content on success, 404 if the Space doesn't exist or the caller has no membership, 403 for an insufficiently-privileged member, and 409 if a concurrent conflicting change is detected (retry).

GET
/admin/spaces/{spaceId}/members
List space members

Returns users with direct membership of the Space. Any Space member may view this list.

Example response

{
  "members": [
    {
      "user": {
        "id": 1001,
        "name": "Jane Doe",
        "email": "jane@example.com"
      },
      "role": "owner",
      "joinedAt": "2026-01-01T00:00:00.000Z"
    }
  ]
}
POST
/admin/spaces/{spaceId}/invitations
Invite a user to a space

Creates a pending invitation granting Space-level membership once accepted. Requires the caller to be a Space owner or admin. role must be admin, member, or viewer - an invitation can never grant ownership. Invitations expire after 7 days - a background sweep flips a stale pending invitation to expired automatically (checked every 5 minutes) rather than waiting for someone to actually try the accept endpoint, so a genuinely stale invitation stops blocking a fresh invite to the same email even if nobody ever visits its link.

Request

{
  "email": "jane@example.com",
  "role": "member"
}

Response (201)

{
  "id": 1,
  "email": "jane@example.com",
  "scope": "space",
  "role": "member",
  "status": "pending",
  "expiresAt": "2026-01-08T00:00:00.000Z",
  "token": ""
}

token is shown only in this response - this service has no outbound email delivery, so it must be copied and used directly with the accept endpoint below. Returns 409 if the email already belongs to a member, or already has a pending invitation.

GET
/admin/spaces/{spaceId}/invitations
List space invitations

Returns every invitation ever issued for this Space, most recent first, regardless of status - an audit view, not just what's still pending. Requires owner or admin, the same bar as issuing one. Never includes the raw or hashed token.

Example response

{
  "invitations": [
    {
      "id": 1,
      "email": "jane@example.com",
      "scope": "space",
      "role": "member",
      "status": "pending",
      "invitedBy": {
        "id": 1001,
        "name": "Jane Doe",
        "email": "jane@example.com"
      },
      "expiresAt": "2026-01-08T00:00:00.000Z",
      "createdAt": "2026-01-01T00:00:00.000Z"
    }
  ]
}
DELETE
/admin/spaces/{spaceId}/invitations/{invitationId}
Cancel an invitation

Cancels a still-pending invitation, making its token permanently unusable. Requires owner or admin, the same bar as issuing one. Implemented as a status change to cancelled (not a row deletion) so it stays visible in the list above, and immediately frees the email for a fresh invitation to this Space. Returns 404 for an invitation that doesn't exist, belongs to a different Space, or isn't pending anymore (already accepted, cancelled, or expired).

PATCH
/admin/spaces/{spaceId}/members/{userId}
Change a member's role

Changes an existing member's role. Requires owner or admin. Only a Space owner may grant admin - an admin attempting to do so receives 403. role can never be set to owner through this endpoint.

Request

{
  "role": "admin"
}

Response

{
  "userId": 1002,
  "role": "admin",
  "updatedAt": "2026-01-01T00:00:00.000Z"
}
DELETE
/admin/spaces/{spaceId}/members/{userId}
Remove a space member

Removes a user's Space membership. Requires owner or admin. Owners cannot be removed through this endpoint (403). Also revokes the removed user's personal API keys for this Space's Projects, since Space removal is (for now) the only source of their access to them.

POST
/admin/invitations/{token}/accept
Accept an invitation

Accepts a pending Space or Project invitation (see Projects docs) using the authenticated caller's identity - the response's scope field says which. The invited email must match the caller's account email - a mismatch, an unknown token, or a token belonging to someone else's already-accepted invitation all return 404 without revealing which. An expired token returns 410. Calling this endpoint is optional: every authenticated request to any Admin API endpoint already auto-links pending invitations matching the caller's email, so an invited user is granted membership the first time they authenticate at all, via any endpoint - even if they never see this endpoint or its token. If auto-link already claimed this exact invitation (typically true by the time this handler runs, since auto-link runs earlier in the same request), this endpoint still returns the same success response for its rightful owner rather than a confusing 404.

Example response

{
  "scope": "space",
  "spaceId": 1,
  "role": "member"
}