Team & Zugriff

Unternehmen, Mitgliedschaften, Einladungen und die Zugriffsprüfung verwalten.

Tenants, entities, memberships, invitations, and access review.

24 Operationen · Basis-URL https://api.fortlauf.de/v1

Operationen

  1. GET /me/memberships List the authenticated caller's active tenant memberships
  2. GET /me/invitations List the invitations waiting for the authenticated caller
  3. POST /me/invitations/{id}/accept Accept one of the caller's own invitations by id
  4. POST /tenants Create a tenant (self-serve, member-less principal becomes owner)
  5. GET /tenants/{tenantId}/plan The tenant's plan-tier readout
  6. GET /tenants/{tenantId}/entities List the tenant's entities (cursor-paginated)
  7. POST /tenants/{tenantId}/entities Create an entity (admin/owner or API key with entities:write)
  8. GET /tenants/{tenantId}/entities/{id} Get one entity (the settings read)
  9. PATCH /tenants/{tenantId}/entities/{id} Update entity settings (admin/owner or API key with entities:write)
  10. DELETE /tenants/{tenantId}/entities/{id} Soft-delete (deactivate) an entity (admin/owner or API key with entities:write)
  11. POST /tenants/{tenantId}/entities/{id}/activate-live-operation Move this company into live operation — deliberate, audited, once
  12. GET /tenants/{tenantId}/live-operation-status Whether this company operates live
  13. GET /tenants/{tenantId}/memberships List the tenant's members (the Team table)
  14. PATCH /tenants/{tenantId}/memberships/{userId} Change a member's role (admin/owner, inline role change)
  15. DELETE /tenants/{tenantId}/memberships/{userId} Remove a member (admin/owner)
  16. POST /tenants/{tenantId}/memberships/leave Leave the organisation ("Organisation verlassen")
  17. GET /tenants/{tenantId}/access-review The access-review stamp (admin)
  18. POST /tenants/{tenantId}/access-review/mark-reviewed Mark tenant access as reviewed (admin, non-idempotent)
  19. GET /tenants/{tenantId}/invitations List the tenant's pending invitations ("ausstehend")
  20. POST /tenants/{tenantId}/invitations Create or re-invite an invitation (admin/owner)
  21. DELETE /tenants/{tenantId}/invitations/{id} Withdraw a pending invitation (admin/owner, "Zurückziehen")
  22. POST /tenants/{tenantId}/invitations/{id}/resend Resend a pending invitation (admin/owner)
  23. POST /invitations/accept Accept an invitation by token (any signed-in user)
  24. GET /invitations/{token}/preview Preview an invitation by token (UNAUTHENTICATED)

List the authenticated caller's active tenant memberships

GET /me/memberships

Drives the dashboard app's tenant-switcher UI, which needs to enumerate which tenants the logged-in user belongs to BEFORE any of them is selected. Requires only a verified OIDC token (requireAuth()) — no X-Tenant-Id header, since choosing one is exactly the thing this response lets the caller do next. JIT- provisions a users row for a never-before-seen sub, same provisioning path requireTenantContext() uses.

Only active memberships are returned — a pending invitation is not a membership yet. A caller with zero active memberships gets 200 { memberships: [] }, never a 403: the whole point of this route is to let the frontend render a "no access" state from the body, not from a status code.

Erforderlicher Scope
keiner
Zugang
nur mit angemeldeter Sitzung – ein API-Schlüssel wird abgelehnt (oidc-only)

Antworten

  • 200

    The caller's active memberships (possibly empty). Always 200, regardless of membership count.

    application/json object

  • 401

    Missing/invalid OIDC token

List the invitations waiting for the authenticated caller

GET /me/invitations

The invitations addressed to the signed-in person's email that can still be accepted: pending, unexpired, not withdrawn. web asks this before it offers a person with no company the company-creation form, so somebody who was invited is offered the invitation instead (the case where the emailed link's token did not come back with them, for example a verification link opened in another browser).

Matched on the email of the verified session, compared with ASCII case folding only: an address containing any non-ASCII character matches nothing (invited addresses are ASCII). The response never carries the invitation token, the invited address or the tenant id. Requires only a verified OIDC token, no X-Tenant-Id. Always 200; an empty list is the normal answer.

Erforderlicher Scope
keiner
Zugang
nur mit angemeldeter Sitzung – ein API-Schlüssel wird abgelehnt (oidc-only)

Antworten

  • 200

    The caller's acceptable invitations (possibly empty), oldest first.

    application/json object

  • 401

    Missing/invalid OIDC token

Accept one of the caller's own invitations by id

POST /me/invitations/{id}/accept

Accepts an invitation listed by GET /me/invitations. The same state machine, idempotence and audit rows as POST /invitations/accept; the caller's identity comes from the verified session, never the body.

  • 200 { outcome: "accepted", tenant, role } — newly activated.

  • 200 { outcome: "already_member", tenant } — a repeated accept.

  • 404 { error: "invitation_not_found" } — no such invitation, a malformed id, OR an invitation addressed to somebody else. Unlike the token path there is no invitation_email_mismatch: the id is not a secret, so the answer never reveals that another person's invitation exists.

  • 410 { error: "invitation_expired", expiresAt } / 410 { error: "invitation_revoked" } — the invitation is dead.

Erforderlicher Scope
keiner
Zugang
nur mit angemeldeter Sitzung – ein API-Schlüssel wird abgelehnt (oidc-only)

Parameter

Parameter In Typ Beschreibung
id erforderlich Pfad string (uuid)

The id from GET /me/invitations.

Antworten

  • 200

    Accepted (newly, or already a member — see outcome).

    application/json AcceptInvitationResult

  • 401

    No authenticated session.

  • 404

    No invitation with this id is addressed to the caller (invitation_not_found).

  • 410

    The invitation is dead: invitation_expired (past expiry) or invitation_revoked (withdrawn).

Create a tenant (self-serve, member-less principal becomes owner)

POST /tenants

A just-authenticated but MEMBER-LESS principal creates a tenant and becomes its owner. Mounted under the global requireAuth() ONLY (not requireTenantContext()) — the POST /invitations/accept shape; the caller has no tenant context yet. Behind the self-serve registration capability (off by default): when off the route is absent (404). Body is BUSINESS DATA ONLY — never a credential; unknown fields are stripped. Idempotency is by the natural key (the claiming principal): a retry returns the principal's EXISTING tenant (200), never a second. A per-IP velocity limiter (about 5 per IP address per hour, tuned on real data) returns 429 as defense-in-depth; the one-self-serve- tenant-per-principal DB guard is the primary control.

Erforderlicher Scope
keiner
Zugang
angemeldete Sitzung oder API-Schlüssel (authenticated)

Request-Body erforderlich

application/json object

Beispiel-Request
{
  "name": "Muster GmbH",
  "legalAcceptances": [
    {
      "document": "avv",
      "version": "v1"
    },
    {
      "document": "agb",
      "version": "v1"
    }
  ]
}

Antworten

  • 200

    Idempotent replay — the principal already owns their self-serve tenant; the existing tenant is returned (never a second one).

    application/json SelfServeTenant

  • 201

    Tenant created; the caller is now its owner.

    application/json SelfServeTenant

  • 400

    Invalid request body (error: validation_error), or a named legal document has no published version (error: invalid_legal_version, details.document). Nothing was created in either case.

    application/json ValidationError | InvalidLegalVersionError

  • 403

    No valid token (the requireAuth() default).

  • 409

    One of three conflicts with current state, distinguished by error and never by the status. already_member: the principal already belongs to a tenant (≥1 active membership, e.g. invited into someone else's tenant) and owns no self-serve tenant — the UX routes this to the existing tenant, never shown as an error. legal_acceptance_required / legal_acceptance_superseded: a published legal document was not accepted, or was accepted at a version that is no longer current; details.documents names what is outstanding. acceptor_name_missing: the verified token carries no name for the accepting person, so the agreement cannot record who accepted it — the person completes their name at the identity provider and signs in again. NOTHING IS CREATED by any of them: no tenant, no membership, no entity and no acceptance. The whole registration is rolled back, so the request may simply be retried once the conflict is cleared.

    application/json object | LegalAcceptanceError

  • 429

    Per-IP velocity exceeded (error = rate_limited).

    application/json object

The tenant's plan-tier readout

GET /tenants/{tenantId}/plan

Returns { planTier, entityLimit, entityCount } — the plan tier, the EFFECTIVE active-entity fair-use guard (entityLimit; 50 by default on every plan, raised on request), and the AUTHORITATIVE active-entity count (entityCount, the same number the create-time cap check counts, NOT a page length). Read open to ANY active member (requireTenantContext, no role required) — it mirrors the 409 entity_limit_reached body's { limit, planTier } plus a count, so the pre-cap "X von Y" line and the post-cap prompt can never disagree. Mode-independent (an entity exists across live and test; the guard is a tenant fact). Scoped to the resolved context tenant; the path :tenantId must equal it (else 403 tenant_mismatch).

Erforderlicher Scope
keiner
Zugang
angemeldete Sitzung oder API-Schlüssel (authenticated)

Parameter

Parameter In Typ Beschreibung
tenantId erforderlich Pfad string (uuid)

The tenant being managed. Must equal the caller's active tenant (the resolved auth context) — a mismatch is 403 tenant_mismatch.

Antworten

  • 200

    The tenant's plan-tier readout.

    application/json TenantPlan

  • 403

    Path tenantId ≠ active tenant (tenant_mismatch).

  • 404

    Resolved tenant not found (not_found).

List the tenant's entities (cursor-paginated)

GET /tenants/{tenantId}/entities

The active entities of the caller's tenant, oldest first, cursor- paginated ({ data, nextCursor }, keyset over (created_at, id)). Requires entities:read: readable by ANY active OIDC member (all roles, incl. readonly, hold entities:read via scopesForRole() — the list/picker stays visible-but-restricted) OR an API key carrying entities:read. Soft-deleted (deactivated) entities are omitted. Scoped to the resolved context tenant; the path :tenantId must equal it (else 403 tenant_mismatch).

Erforderlicher Scope
entities:read
Zugang
API-Schlüssel oder angemeldete Sitzung

Parameter

Parameter In Typ Beschreibung
tenantId erforderlich Pfad string (uuid)

The tenant being managed. Must equal the caller's active tenant (the resolved auth context) — a mismatch is 403 tenant_mismatch.

Antworten

  • 200

    A page of the tenant's active entities.

    application/json EntityPage

  • 400

    The query string was refused. Three distinguishable facts share this status, and a client should branch on the error code rather than on the status: unknown_query_parameter — the request named one or more query parameters this deployment does not define, and parameters lists their names. Nothing was filtered and nothing was written. This is a DEPLOY-ORDER signal: it is what a client released ahead of the API sees, and it replaces the older behaviour in which an unrecognised parameter was silently ignored and the request answered 200 OK with the UNFILTERED collection — indistinguishable, to the caller, from the filter having been applied. validation_error — a parameter this endpoint does define carried an unusable value (including a required one being absent or too short). issues[] names the parameter and what was wrong with it. invalid_cursor — on cursor-paginated endpoints, the pagination cursor is malformed. No response on this path ever echoes a submitted VALUE; only parameter names appear.

    application/json UnknownQueryParameterError | ValidationError

  • 403

    Path tenantId ≠ active tenant (tenant_mismatch).

Create an entity (admin/owner or API key with entities:write)

POST /tenants/{tenantId}/entities

Provisions a new entity under the tenant. Requires entities:write AND — for an OIDC/dashboard caller only — the admin role; an API key carrying entities:write may provision entities programmatically (it has no role, so the admin gate does not apply to it). Enforces the entity fair-use guard (50 active entities by default, the same on every plan, raised on request) for BOTH credential paths — exceeding it is 409 entity_limit_reached with a limit/planTier/upgradeUrl, so a scope is access, never a cap bypass. The new entity is usable immediately as a managed resource; note that repository-mediated DOCUMENT issuance for a non-genesis entity is gated on the entity-scoped write path. Honors the optional Idempotency-Key header so a retried provisioning is a replay, not a second entity that would count against the guard.

Erforderlicher Scope
entities:write
Zugang
API-Schlüssel oder angemeldete Sitzung

Parameter

Parameter In Typ Beschreibung
tenantId erforderlich Pfad string (uuid)

The tenant being managed. Must equal the caller's active tenant (the resolved auth context) — a mismatch is 403 tenant_mismatch.

Idempotency-Key Header string

Optional client-supplied key that makes this mutating request safe to retry. The FIRST request runs and its outcome is recorded keyed by (tenant, route, key); a REPLAY (same key + byte-identical body, after the first completed) returns the recorded response verbatim WITHOUT re-executing — so a network-blip retry never creates a duplicate customer/subscription/key. A key reused with a DIFFERENT body is 409 IDEMPOTENCY_KEY_REUSED_WITH_DIFFERENT_BODY; a key whose first request is still in flight is 409 REQUEST_IN_PROGRESS (retryable). Keys expire after 24h — a key reused after expiry is a fresh request, not a replay. Transparent when omitted (the request runs normally).

Länge: max. 255

Request-Body erforderlich

application/json CreateEntity

Antworten

  • 201

    The created entity (or, on an Idempotency-Key replay, the recorded response).

    application/json Entity

  • 400

    Validation error.

  • 403

    Caller lacks the admin role, or path tenantId ≠ active tenant.

  • 409

    A conflict with current state. entity_limit_reached — the account already holds as many active entities as its fair-use guard admits (a guard, the same on every plan, raised on request — not a plan limit; body carries limit/planTier/upgradeUrl). IDEMPOTENCY_KEY_REUSED_WITH_DIFFERENT_BODY — the key was already used with a different request body; REQUEST_IN_PROGRESS — the first request bearing this key is still in flight (retryable).

    application/json EntityLimitError | object

Get one entity (the settings read)

GET /tenants/{tenantId}/entities/{id}

One entity of the caller's tenant. Readable by any active member. 404 if not visible.

Erforderlicher Scope
entities:read
Zugang
API-Schlüssel oder angemeldete Sitzung

Parameter

Parameter In Typ Beschreibung
tenantId erforderlich Pfad string (uuid)

The tenant being managed. Must equal the caller's active tenant (the resolved auth context) — a mismatch is 403 tenant_mismatch.

id erforderlich Pfad string (uuid)

Entity id.

Antworten

  • 200

    The entity.

    application/json Entity

  • 404

    Not found / not in the caller's tenant.

Update entity settings (admin/owner or API key with entities:write)

PATCH /tenants/{tenantId}/entities/{id}

Patch mutable settings (name/VAT/address). Requires entities:write AND — for OIDC callers — the admin role. Only the fields present are changed; at least one is required. The VAT ID is IMMUTABLE once the entity has issued a document — attempting to change it then is 422 immutable_after_invoices (a no-op same-value PATCH passes).

Erforderlicher Scope
entities:write
Zugang
API-Schlüssel oder angemeldete Sitzung

Parameter

Parameter In Typ Beschreibung
tenantId erforderlich Pfad string (uuid)

The tenant being managed. Must equal the caller's active tenant (the resolved auth context) — a mismatch is 403 tenant_mismatch.

id erforderlich Pfad string (uuid)

Entity id.

Request-Body erforderlich

application/json UpdateEntity

Antworten

  • 200

    The updated entity.

    application/json Entity

  • 400

    Validation error (incl. empty patch).

  • 403

    Caller lacks the admin role, or path tenantId ≠ active tenant.

  • 404

    Not found / not in the caller's tenant.

  • 422

    A frozen field (VAT ID) was changed after issuance (immutable_after_invoices).

Soft-delete (deactivate) an entity (admin/owner or API key with entities:write)

DELETE /tenants/{tenantId}/entities/{id}

Requires entities:write AND — for OIDC callers — the admin role. Sets status='deactivated'/deleted_at — blocks new issuance, RETAINS every existing document/archive (true purge is a separate surface, not here). Idempotent. Rejects deactivating the tenant's LAST active entity (409 last_active_entity) so a resolved context always has an entity.

Erforderlicher Scope
entities:write
Zugang
API-Schlüssel oder angemeldete Sitzung

Parameter

Parameter In Typ Beschreibung
tenantId erforderlich Pfad string (uuid)

The tenant being managed. Must equal the caller's active tenant (the resolved auth context) — a mismatch is 403 tenant_mismatch.

id erforderlich Pfad string (uuid)

Entity id.

Antworten

  • 204

    Deactivated (or already deactivated — idempotent).

  • 403

    Caller lacks the admin role, or path tenantId ≠ active tenant.

  • 404

    Not found / not in the caller's tenant.

  • 409

    Would deactivate the tenant's last active entity (last_active_entity).

Move this company into live operation — deliberate, audited, once

POST /tenants/{tenantId}/entities/{id}/activate-live-operation

The one act that takes a company which has only ever rehearsed and lets it operate for real. It stamps liveOperationActivatedAt exactly once and answers 200 with the entity; repeating the call replays the same timestamp and writes no second audit row (the act is idempotent — the repository's IS NULL latch makes it exactly-once even without an Idempotency-Key).

Restricted to an interactive admin session: an API credential is refused outright (403 credential_not_accepted), because a key carries its own immutable environment and the act must be performed by a person, once, audited. Never a side effect of a settings save — the general PATCH cannot reach it.

There is NO de-activation verb: putting a live company back into the rehearsal environment would hide its whole history behind row-level security. When the capability is switched off this path does not exist at all (404).

Entering live operation requires a plan that includes live issuance: an account on the free sandbox plan is refused with 402 entitlement_required. The refusal applies to the TRANSITION only — a company that already carries liveOperationActivatedAt keeps replaying 200 whatever the plan is now, so a later plan change never turns a settled activation into an error. The plan refusal and the credential refusal are independent: an API credential on a paid plan is still a 403, and an admin session on the free plan is still a 402.

Erforderlicher Scope
entities:write
Zugang
nur mit angemeldeter Sitzung – ein API-Schlüssel wird abgelehnt (oidc-only)
Verfügbarkeit
noch nicht freigeschaltet (not-enabled)

Parameter

Parameter In Typ Beschreibung
tenantId erforderlich Pfad string (uuid)

The tenant being managed. Must equal the caller's active tenant (the resolved auth context) — a mismatch is 403 tenant_mismatch.

id erforderlich Pfad string (uuid)

Entity id.

Idempotency-Key Header string

Optional client-supplied key that makes this mutating request safe to retry. The FIRST request runs and its outcome is recorded keyed by (tenant, route, key); a REPLAY (same key + byte-identical body, after the first completed) returns the recorded response verbatim WITHOUT re-executing — so a network-blip retry never creates a duplicate customer/subscription/key. A key reused with a DIFFERENT body is 409 IDEMPOTENCY_KEY_REUSED_WITH_DIFFERENT_BODY; a key whose first request is still in flight is 409 REQUEST_IN_PROGRESS (retryable). Keys expire after 24h — a key reused after expiry is a fresh request, not a replay. Transparent when omitted (the request runs normally).

Länge: max. 255

Antworten

  • 200

    The entity, now carrying liveOperationActivatedAt (the same timestamp on every replay).

    application/json Entity

  • 402

    The plan does not include live issuance, so this company cannot enter live operation. Nothing was stamped and no audit record was written; the company is exactly as it was. Cleared by moving to the paid plan, never by retrying. Only a company that has NOT yet entered live operation can be refused this way.

    application/json EntitlementRequiredError

  • 403

    Caller lacks the admin role, presented an API credential (credential_not_accepted), or path tenantId ≠ active tenant.

  • 404

    Not found / not in the caller's tenant — or the capability is off.

  • 409

    The tenant has not accepted the current version of a required legal document. legal_acceptance_required when nothing is on record; legal_acceptance_superseded when the agreement has changed since it was last accepted. Nothing was stamped and no audit record was written. Resolve by accepting the current version, then retry — with a FRESH Idempotency-Key, because a refusal is a recorded outcome for the key that produced it. Evaluated AFTER the plan check, so a tenant missing both is refused on the plan. Only a company that has NOT yet entered live operation can be refused this way. The agreement is held by the TENANT, so a second company under the same tenant needs no second acceptance.

    application/json LegalAcceptanceError

Whether this company operates live

GET /tenants/{tenantId}/live-operation-status

The single platform predicate behind a fresh session's default environment. Answers for the company resolved from the caller's context (X-Entity-Id, or the tenant's oldest-active entity when absent) and ECHOES which company it answered for. Open to every active member including readonly; carries no plan, no tier and no payment state. The answer is INDEPENDENT of the environment the caller states — see basis.

Erforderlicher Scope
keiner
Zugang
nur mit angemeldeter Sitzung – ein API-Schlüssel wird abgelehnt (oidc-only)

Parameter

Parameter In Typ Beschreibung
tenantId erforderlich Pfad string (uuid)

The tenant being managed. Must equal the caller's active tenant (the resolved auth context) — a mismatch is 403 tenant_mismatch.

Antworten

  • 200

    The live-operation signal for the resolved company.

    application/json LiveOperationStatus

  • 403

    tenant_mismatch — the path tenantId is not the caller's active tenant — or an API credential was presented (credential_not_accepted): the only callers are first-party session seeders, and an API key has no session to seed.

    application/json ValidationError

List the tenant's members (the Team table)

GET /tenants/{tenantId}/memberships

Every active (and pending) membership of the tenant, each joined with the member's email — the "Team & Zugriff" roster. Readable by ANY active member (visible-but-restricted for readonly/member). Revoked (removed) members are omitted.

Erforderlicher Scope
keiner
Zugang
nur mit angemeldeter Sitzung – ein API-Schlüssel wird abgelehnt (oidc-only)

Parameter

Parameter In Typ Beschreibung
tenantId erforderlich Pfad string (uuid)

The tenant being managed. Must equal the caller's active tenant (the resolved auth context) — a mismatch is 403 tenant_mismatch.

Antworten

  • 200

    The tenant's members.

    application/json object

  • 403

    Path tenantId ≠ active tenant (tenant_mismatch).

Change a member's role (admin/owner, inline role change)

PATCH /tenants/{tenantId}/memberships/{userId}

Sets a member's role. Admin/owner only. Guardrails, surfaced as machine-readable errors the UI maps: you cannot change your OWN role (409 cannot_change_own_role); you cannot demote the tenant's LAST owner (409 last_owner). Only an owner may make someone an owner or change an owner's role (403 owner_only); an admin manages members, read-only users and other admins. Role is re-read per request, so the change takes effect on the target's very next call.

Erforderlicher Scope
keiner
Zugang
nur mit angemeldeter Sitzung – ein API-Schlüssel wird abgelehnt (oidc-only)

Parameter

Parameter In Typ Beschreibung
tenantId erforderlich Pfad string (uuid)

The tenant being managed. Must equal the caller's active tenant (the resolved auth context) — a mismatch is 403 tenant_mismatch.

userId erforderlich Pfad string (uuid)

The member's users.id (the membership is keyed by (tenant, user)).

Request-Body erforderlich

application/json UpdateMembershipRole

Antworten

  • 200

    The updated membership.

    application/json Membership

  • 400

    Validation error (unknown role).

  • 403

    Caller lacks the admin role, path tenantId ≠ active tenant, or owner_only (the target is an owner, or the new role is owner, and the caller is not an owner; action is grant or change).

  • 404

    No active membership for that user in the tenant.

  • 409

    cannot_change_own_role (self) or last_owner (would demote the sole owner).

Remove a member (admin/owner)

DELETE /tenants/{tenantId}/memberships/{userId}

Soft-revokes the membership (status='revoked') — the member loses access on their very next request. Admin/owner only. Rejects removing the tenant's LAST owner (409 last_owner). Only an owner may remove another owner (403 owner_only). Self-removal is allowed (subject to the last-owner rule).

Erforderlicher Scope
keiner
Zugang
nur mit angemeldeter Sitzung – ein API-Schlüssel wird abgelehnt (oidc-only)

Parameter

Parameter In Typ Beschreibung
tenantId erforderlich Pfad string (uuid)

The tenant being managed. Must equal the caller's active tenant (the resolved auth context) — a mismatch is 403 tenant_mismatch.

userId erforderlich Pfad string (uuid)

The member's users.id (the membership is keyed by (tenant, user)).

Antworten

  • 204

    Removed.

  • 403

    Caller lacks the admin role, path tenantId ≠ active tenant, or owner_only with action: remove (the target is another owner and the caller is not an owner).

  • 404

    No active membership for that user in the tenant.

  • 409

    Would remove the tenant's last owner (last_owner).

Leave the organisation ("Organisation verlassen")

POST /tenants/{tenantId}/memberships/leave

The caller leaves the tenant themselves — the self-service half of member management. Open to ANY active member (NO admin role required): the target is the CALLER'S OWN userId, derived from the verified OIDC token (never a path/body param), so it is unspoofable — a caller can only ever leave for themselves. A dashboard OIDC session only; API-key credentials are refused (oidc-only). Soft-revokes the caller's membership (status='revoked') — they lose access on their very next request. The SOLE owner cannot leave (409 last_owner): the org must always keep ≥1 active owner, so the UI must explain the guard (transfer ownership first, or route to tenant closure) rather than surfacing a blank 409. On losing the caller's LAST active membership anywhere, an identity-provider deactivate is enqueued (best-effort, and only where directory provisioning is enabled for the deployment). Audit membership.revoked, actor = the leaver.

Erforderlicher Scope
keiner
Zugang
nur mit angemeldeter Sitzung – ein API-Schlüssel wird abgelehnt (oidc-only)

Parameter

Parameter In Typ Beschreibung
tenantId erforderlich Pfad string (uuid)

The tenant being managed. Must equal the caller's active tenant (the resolved auth context) — a mismatch is 403 tenant_mismatch.

Antworten

  • 204

    Left the organisation.

  • 401

    No verified OIDC session (unauthorized).

  • 403

    No active membership in the tenant (no_tenant_membership), path tenantId ≠ active tenant (tenant_mismatch), or an API-key credential was used (OIDC-only).

  • 409

    The caller is the tenant's sole owner and leaving would orphan the org (last_owner, with an action field) — transfer ownership first.

The access-review stamp (admin)

GET /tenants/{tenantId}/access-review

The persistent "Zugriff zuletzt geprüft am {Datum} von {Person}" stamp for the Team & Zugriff header — the MOST RECENT access_review.marked_reviewed for the tenant. Admin-open (does NOT ride the owner-only audit-log read). { lastReviewedAt: null, lastReviewedBy: null } when the tenant has never been reviewed.

Erforderlicher Scope
keiner
Zugang
nur mit angemeldeter Sitzung – ein API-Schlüssel wird abgelehnt (oidc-only)

Parameter

Parameter In Typ Beschreibung
tenantId erforderlich Pfad string (uuid)

The tenant being managed. Must equal the caller's active tenant (the resolved auth context) — a mismatch is 403 tenant_mismatch.

Antworten

  • 200

    The latest review stamp (nulls when never reviewed).

    application/json AccessReviewStatus

  • 403

    Caller lacks the admin role, or path tenantId ≠ active tenant.

Mark tenant access as reviewed (admin, non-idempotent)

POST /tenants/{tenantId}/access-review/mark-reviewed

Records that an admin reviewed the tenant's access — writes ONE audit-log entry (access_review.marked_reviewed, actor = the reviewing admin). Deliberately NON-idempotent: a review is a repeatable dated act, so each call is a new artifact (two presses → two rows). Returns the fresh stamp. Admin/owner only; path :tenantId cross-checked.

Erforderlicher Scope
keiner
Zugang
nur mit angemeldeter Sitzung – ein API-Schlüssel wird abgelehnt (oidc-only)

Parameter

Parameter In Typ Beschreibung
tenantId erforderlich Pfad string (uuid)

The tenant being managed. Must equal the caller's active tenant (the resolved auth context) — a mismatch is 403 tenant_mismatch.

Antworten

  • 200

    The review was recorded; returns the fresh stamp.

    application/json AccessReviewStatus

  • 403

    Caller lacks the admin role, or path tenantId ≠ active tenant.

List the tenant's pending invitations ("ausstehend")

GET /tenants/{tenantId}/invitations

Every still-pending invitation of the tenant (consumed_at IS NULL, and not withdrawn), newest first. Readable by ANY active member. The show-once token is NEVER returned (only its SHA-256 hash is stored, and not even the hash reaches the wire).

Erforderlicher Scope
keiner
Zugang
nur mit angemeldeter Sitzung – ein API-Schlüssel wird abgelehnt (oidc-only)

Parameter

Parameter In Typ Beschreibung
tenantId erforderlich Pfad string (uuid)

The tenant being managed. Must equal the caller's active tenant (the resolved auth context) — a mismatch is 403 tenant_mismatch.

Antworten

  • 200

    The tenant's pending invitations.

    application/json object

  • 403

    Path tenantId ≠ active tenant (tenant_mismatch).

Create or re-invite an invitation (admin/owner)

POST /tenants/{tenantId}/invitations

Invites email at role. Admin/owner only (requireRole('admin')). Re-invite-as-UPDATE: if the (tenant, email) already has a pending invitation, its token/expiry/role are REFRESHED in place (the same row, never a second one) and the previously emailed link dies immediately — so this is idempotent and always returns 201. DB-first and NO IdP call at invite time — the row commits, then the invitation email is enqueued unconditionally (delivery is no longer coupled to identity-provider availability; the IdP account is minted by the provider's enrollment flow when the invitee authenticates). A genuinely concurrent second pending INSERT for the same email is 409 pending_invitation_exists (the partial-unique index race — the caller should refresh, not duplicate). No membership is created here — that only happens on ACCEPT. Only an owner may invite at role: owner, or re-invite over a pending owner invitation (403 owner_only, nothing written); the same holds for resending or withdrawing an owner invitation. Honors the optional Idempotency-Key header so a retried create is a replay, not a duplicate side effect.

Erforderlicher Scope
keiner
Zugang
nur mit angemeldeter Sitzung – ein API-Schlüssel wird abgelehnt (oidc-only)

Parameter

Parameter In Typ Beschreibung
tenantId erforderlich Pfad string (uuid)

The tenant being managed. Must equal the caller's active tenant (the resolved auth context) — a mismatch is 403 tenant_mismatch.

Idempotency-Key Header string

Optional client-supplied key that makes this mutating request safe to retry. The FIRST request runs and its outcome is recorded keyed by (tenant, route, key); a REPLAY (same key + byte-identical body, after the first completed) returns the recorded response verbatim WITHOUT re-executing — so a network-blip retry never creates a duplicate customer/subscription/key. A key reused with a DIFFERENT body is 409 IDEMPOTENCY_KEY_REUSED_WITH_DIFFERENT_BODY; a key whose first request is still in flight is 409 REQUEST_IN_PROGRESS (retryable). Keys expire after 24h — a key reused after expiry is a fresh request, not a replay. Transparent when omitted (the request runs normally).

Länge: max. 255

Request-Body erforderlich

application/json CreateInvitation

Antworten

  • 201

    The created (or re-invited) pending invitation (or, on an Idempotency-Key replay, the recorded response). delivery.outcome says whether a mail was actually ENQUEUED — announce „sent" on that, never on the status code, which only covers the invitation row.

    application/json InvitationWithDelivery

  • 400

    Validation error (bad email/role).

  • 403

    Caller lacks the admin role, path tenantId ≠ active tenant, or owner_only with action: invite (an owner invitation, and the caller is not an owner).

  • 409

    A concurrent pending invitation already exists (pending_invitation_exists), OR an Idempotency-Key conflict (IDEMPOTENCY_KEY_REUSED_WITH_DIFFERENT_BODY / REQUEST_IN_PROGRESS).

  • 429

    system_mail_daily_ceiling_reached — this company has reached its daily limit of 50 invitation and reply-address confirmation emails (counted together, per company, over the Europe/Berlin calendar day: emails sent today plus emails still waiting to be sent). Nothing was created, changed or sent. The limit is fixed and resets at midnight Europe/Berlin; Retry-After gives the seconds until then and limit the number.

    • Retry-After integer

      Seconds until the next business day starts (Europe/Berlin midnight).

    application/json object

Withdraw a pending invitation (admin/owner, "Zurückziehen")

DELETE /tenants/{tenantId}/invitations/{id}

Withdraws a pending invitation by setting a dedicated revoked_at (distinct from consumed_at/accepted, for audit clarity). Admin/owner only. The live invitation link dies immediately — a subsequent accept of that token returns 410 invitation_revoked. Idempotent: re-withdrawing an already-withdrawn invite is a no-op success (204). Withdrawing an already-ACCEPTED invite is 409 invitation_already_accepted (you cannot un-accept a membership this way — revoking an active membership is a separate DELETE .../memberships/:userId operation). A pending invite's (tenant, email) becomes free to invite again after withdrawal (the partial-unique index excludes revoked rows). Writes an invitation.withdrawn audit row on the actual transition.

Erforderlicher Scope
keiner
Zugang
nur mit angemeldeter Sitzung – ein API-Schlüssel wird abgelehnt (oidc-only)

Parameter

Parameter In Typ Beschreibung
tenantId erforderlich Pfad string (uuid)

The tenant being managed. Must equal the caller's active tenant (the resolved auth context) — a mismatch is 403 tenant_mismatch.

id erforderlich Pfad string (uuid)

Invitation id.

Antworten

  • 204

    Withdrawn (or already withdrawn — idempotent).

  • 403

    Caller lacks the admin role, path tenantId ≠ active tenant, or owner_only with action: invite (an owner invitation, and the caller is not an owner).

  • 404

    No such invitation in the tenant (not_found).

  • 409

    The invitation has already been accepted (invitation_already_accepted).

Resend a pending invitation (admin/owner)

POST /tenants/{tenantId}/invitations/{id}/resend

Refreshes a specific pending invite's token/expiry so a new link can be emailed; the old link dies immediately. Admin/owner only. 404 if no such pending invite exists in the tenant; 409 invitation_already_accepted if it has already been accepted (you cannot resend a consumed invite).

Erforderlicher Scope
keiner
Zugang
nur mit angemeldeter Sitzung – ein API-Schlüssel wird abgelehnt (oidc-only)

Parameter

Parameter In Typ Beschreibung
tenantId erforderlich Pfad string (uuid)

The tenant being managed. Must equal the caller's active tenant (the resolved auth context) — a mismatch is 403 tenant_mismatch.

id erforderlich Pfad string (uuid)

Invitation id.

Antworten

  • 200

    The refreshed invitation, with delivery.outcome for the new mail.

    application/json InvitationWithDelivery

  • 403

    Caller lacks the admin role, path tenantId ≠ active tenant, or owner_only with action: invite (an owner invitation, and the caller is not an owner).

  • 404

    No pending invitation with that id in the tenant.

  • 409

    The invitation has already been accepted (invitation_already_accepted).

  • 429

    system_mail_daily_ceiling_reached — this company has reached its daily limit of 50 invitation and reply-address confirmation emails (counted together, per company, over the Europe/Berlin calendar day: emails sent today plus emails still waiting to be sent). Nothing was created, changed or sent. The limit is fixed and resets at midnight Europe/Berlin; Retry-After gives the seconds until then and limit the number.

    • Retry-After integer

      Seconds until the next business day starts (Europe/Berlin midnight).

    application/json object

Accept an invitation by token (any signed-in user)

POST /invitations/accept

The token-based acceptance endpoint the web /invite/[token] page calls. Gated by requireAuth() ONLY — NOT requireTenantContext(): the accepting user is a signed-in OIDC session that is NOT yet a member of the target tenant, so a membership-resolving gate would 403 them before they could join. The tenant is resolved from the TOKEN. The caller's identity (sub/email) comes from the verified session, never the body. Idempotent — a re-click after acceptance returns already_member, never a duplicate membership.

Machine-readable per-state outcomes (the page keys its copy off these — an HTTP status alone can't distinguish success from already-member, or expired from revoked):

  • 200 { outcome: "accepted", tenant, role } — newly activated (State 1).

  • 200 { outcome: "already_member", tenant } — idempotent re-click (State 2).

  • 403 { error: "invitation_email_mismatch" } — the signed-in email differs from the invited address (State 3). The invited address is NEVER echoed back (GDPR/data-minimization).

  • 404 { error: "invitation_not_found" } — no row for this token: a garbage token OR a superseded re-invite token (State 4; the old token dies the instant a re-invite/resend refreshes the row).

  • 410 { error: "invitation_expired", expiresAt } — a live token past its expiry (State 5).

  • 410 { error: "invitation_revoked" } — the invite was withdrawn (State 7).

Erforderlicher Scope
keiner
Zugang
nur mit angemeldeter Sitzung – ein API-Schlüssel wird abgelehnt (oidc-only)

Request-Body erforderlich

application/json AcceptInvitation

Antworten

  • 200

    Accepted (newly, or already a member — see outcome).

    application/json AcceptInvitationResult

  • 400

    Validation error (missing token).

  • 401

    No authenticated session.

  • 403

    The signed-in account does not match the invited email (invitation_email_mismatch).

  • 404

    No invitation matches this token (invitation_not_found).

  • 410

    The link is dead: invitation_expired (past expiry) or invitation_revoked (withdrawn).

Preview an invitation by token (UNAUTHENTICATED)

GET /invitations/{token}/preview

The public, signed-out preview the web /invite/[token] page calls BEFORE the invitee authenticates. An EXPLICIT carve-out of the default-deny gate — the ONLY unauthenticated route under /invitations (the accept POST /invitations/accept stays gated). Privacy / disclosure boundary: a live/pending token discloses the tenantName, the invitee's own role, and best-effort tenant branding (logoUrl/primaryColor) — all gated behind possession of the 32-byte token. EVERY other case — expired, revoked, consumed, or nonexistent — returns the BYTE-IDENTICAL { valid: false } at HTTP 200, so the endpoint can never distinguish invitation states. The invited email, the INVITER's name (post-auth-only), and the tenant id are NEVER returned. No side effects — the invitation row is never consumed/extended/revoked/logged. Rate-limited per client IP (in-process; the 32-byte token makes brute force infeasible regardless). authHint is constant 'password' in v1 (the SSO routing hook).

Erforderlicher Scope
keiner
Zugang
ohne Anmeldung (public)

Parameter

Parameter In Typ Beschreibung
token erforderlich Pfad string

The show-once invitation token from the emailed link (32 random bytes, base64url).

Länge: min. 1, max. 512

Antworten

  • 200

    The preview. { valid: true, tenantName, authHint } for a live/pending token; the byte-identical { valid: false } for every other (expired/revoked/consumed/nonexistent) case.

    application/json InvitationPreview

  • 429

    Rate limit exceeded for the client IP (rate_limited).

Datenmodelle

Jedes Datenmodell dieser Gruppe einmal, mit allen Feldern – auch den verschachtelten. Verschachtelte Objekte sind aufklappbar; ein benannter Typ verweist auf seinen eigenen Abschnitt.

MyMembership object

One active tenant membership for the authenticated caller (GET /me/memberships). status is always active in this response today (pending/revoked rows are filtered out server-side) — included for forward compatibility rather than omitted, so a future caller doesn't have to assume.

Feld Typ Beschreibung
tenantId erforderlich string (uuid)
tenantName erforderlich string
tenantSlug erforderlich string
role erforderlich string
Werte: "owner", "admin", "member", "readonly"
status erforderlich string
Werte: "pending", "active", "revoked"

Verwendet von: GET /me/memberships

MyPendingInvitation object

One invitation waiting for the authenticated caller (GET /me/invitations). Never carries the token, the invited address or the tenant id; id is only usable by the person it is addressed to (POST /me/invitations/{id}/accept).

Feld Typ Beschreibung
id erforderlich string (uuid)
tenantName erforderlich string
role erforderlich MembershipRole
expiresAt erforderlich string (date-time)
inviterName erforderlich string | null

The inviter's display name (their email today), or null for a system invite or a since-removed inviter.

Verwendet von: GET /me/invitations

AcceptInvitationResult object

The 200 acceptance outcome. outcome=accepted carries the granted role; outcome=already_member omits it (role may have changed since, resolved fresh inside the app). Both outcomes carry inviterName — who invited them, resolved from invitations.created_by → users.email, nullable and degrade-safe; surfaced POST-auth only (the invitee already received it in the invite email — GDPR entitled-to-see; the invited email is NEVER echoed). Non-200 states are error bodies keyed by error (see the endpoint description).

Feld Typ Beschreibung
outcome erforderlich string
Werte: "accepted", "already_member"
tenant erforderlich object
Felder
Feld Typ Beschreibung
id erforderlich string (uuid)
name erforderlich string
role MembershipRole
inviterName string | null

The inviter's display name (their email today), or null for a genesis/system invite (created_by IS NULL) or a since-removed inviter. Present on both accepted and already_member. Disclosed only post-auth; never the invited email.

Verwendet von: POST /me/invitations/{id}/accept , POST /invitations/accept

SelfServeTenant object

The created (201) or replayed (200) tenant. Business data only — never a credential. membership.role is always owner.

Feld Typ Beschreibung
tenant object
Felder
Feld Typ Beschreibung
id string (uuid)
slug string
name string
planTier string
Werte: "launch", "entry", "platform", "sovereign"
membership object
Felder
Feld Typ Beschreibung
role string
Werte: "owner"
entity object

The tenant's first entity, created in the same transaction.

Felder
Feld Typ Beschreibung
id string (uuid)
name string

Verwendet von: POST /tenants

ValidationError object

A refused request body. error is validation_error when the body was read and failed validation (each problem is listed in issues), or invalid_json when the body is not valid JSON at all (then issues is absent). An unparsable body is answered 400 invalid_json on every operation that takes a JSON body. The submitted body is never echoed.

Feld Typ Beschreibung
error string
issues Array<object>
Felder
Feld Typ Beschreibung
path string
message string
code string

Optional machine-readable reason for the refusal, for a client that shows its own wording instead of message. Present only where an endpoint documents it; an issue without it is refused for the reason message states. Today: invalid_address (an entry of a copy list is not one valid address), too_many_addresses (more than 5 entries in one copy list), copies_require_email (copy recipients are set but email is empty), and service_timing_conflict (POST /invoices, PUT /invoices/{id}: the invoice would state both a single service date and a service period; the issue's path is deliveryDate). On the invoice saves and POST /invoices/preview-totals also reverse_charge_buyer_vat_id_required (a reverse-charge line needs the buyer's VAT identification number), vat_category_unavailable (the line's VAT category is not available), vat_rate_category_mismatch (the rate does not fit the line's VAT category), vat_exemption_reason_required / vat_exemption_reason_not_allowed (the exemption reason is missing, or not allowed for that category), and, only where the deployment enforces the check, unit_code_unknown, currency_code_unknown and vat_id_prefix_unknown (the same three also appear on quote and recurring-template saves, with the same paths). The set may grow, so treat an unknown value like an absent one. Never carries a submitted value.

Verwendet von: POST /tenants , GET /tenants/{tenantId}/entities , GET /tenants/{tenantId}/live-operation-status

InvalidLegalVersionError object

The request named a legal document that has no published version, so there was nothing to accept and nothing was written. Re-read GET /legal/current-versions and submit what it reports. Resolve it by error, not by the status.

Feld Typ Beschreibung
error erforderlich string
Werte: "invalid_legal_version"
message erforderlich string
details erforderlich object
Felder
Feld Typ Beschreibung
document erforderlich string

Which named document has nothing published. The version the caller sent is deliberately not echoed back.

Verwendet von: POST /tenants

LegalAcceptanceError object

A required legal document is not currently accepted by this tenant. The error code is the contract, not the status: resolve it by code and render any unknown code as a generic unavailable state.

Feld Typ Beschreibung
error erforderlich string
Werte: "legal_acceptance_required", "legal_acceptance_superseded"
message erforderlich string
details erforderlich object
Felder
Feld Typ Beschreibung
documents erforderlich Array<object>

Every document this tenant must accept before the act can proceed. Tenant-grained: there is deliberately no company identifier and no acceptor here.

Felder
Feld Typ Beschreibung
document erforderlich string

Which document, e.g. avv or agb.

currentVersion erforderlich string

The version that must be accepted.

acceptedVersion erforderlich string | null

The version this tenant last accepted, or null if they never have.

acceptedAt erforderlich string (date-time) | null

Verwendet von: POST /tenants , POST /tenants/{tenantId}/entities/{id}/activate-live-operation

TenantPlan object

The tenant's plan-tier readout (the dashboard's "X von Y — Tarif" line). entityLimit is the EFFECTIVE fair-use guard (a per-account override, else 50 — the same on every plan); null = unbounded. entityCount is the AUTHORITATIVE active-entity count (the same number the create-time cap check counts), never a page length. Mode-independent.

Feld Typ Beschreibung
planTier erforderlich string
Werte: "launch", "entry", "platform", "sovereign"
entityLimit erforderlich integer | null

The effective active-entity fair-use guard; null = unbounded.

entityCount erforderlich integer

The tenant's authoritative active-entity count.

Verwendet von: GET /tenants/{tenantId}/plan

EntityPage object

Feld Typ Beschreibung
data erforderlich Array<Entity>
nextCursor erforderlich string | null

Opaque cursor for the next page; null on the last page.

Verwendet von: GET /tenants/{tenantId}/entities

UnknownQueryParameterError object

The request named query parameters this endpoint does not define. A list endpoint refuses them rather than ignoring them, so a caller can never mistake an UNFILTERED collection for a filtered one.

Feld Typ Beschreibung
error erforderlich string
Werte: "unknown_query_parameter"
parameters erforderlich Array<string>

The unrecognised parameter NAMES, deduplicated and sorted. Bounded at ten entries, and a name longer than 64 characters is omitted — a parameter name is caller-supplied text, and this response never reflects unbounded caller input. The array may therefore be EMPTY while the refusal still stands: the refusal is decided by the request, not by the length of this list.

message string
requestId string

Verwendet von: GET /tenants/{tenantId}/entities

CreateEntity object

Feld Typ Beschreibung
name erforderlich string
Länge: min. 1, max. 200
vatId string | null
addressLine1 string | null
postalCode string | null
city string | null
countryCode string

ISO 3166-1 alpha-2.

Länge: min. 2, max. 2

Verwendet von: POST /tenants/{tenantId}/entities

Entity object

A billing entity (Unternehmen) under a tenant. Manages entity EXISTENCE and identity master data; the full seller profile (IBAN, invoice-number prefix, default text) is edited in web.

Feld Typ Beschreibung
id erforderlich string (uuid)
tenantId erforderlich string (uuid)
name erforderlich string
vatId string | null

USt-IdNr; immutable once the entity has issued a document.

addressLine1 string | null
postalCode string | null
city string | null
countryCode string | null

ISO 3166-1 alpha-2 (upper-cased).

status erforderlich string
Werte: "active", "deactivated", "purged"
deletedAt string (date-time) | null
createdAt erforderlich string (date-time)
liveOperationActivatedAt string (date-time) | null

When this company was deliberately moved into live operation through the dedicated activation act, or null — and null is the true value for every company that predates the act (no backfill, on purpose: stamping a company that already holds live records would name an act nobody performed). Published to every active member including readonly; WHO performed the act is deliberately NOT on this read (it stays behind the audit-log gate).

Verwendet von: POST /tenants/{tenantId}/entities , GET /tenants/{tenantId}/entities/{id} , PATCH /tenants/{tenantId}/entities/{id} , POST /tenants/{tenantId}/entities/{id}/activate-live-operation

EntityLimitError object

409 body when the account already holds as many active entities as its fair-use guard admits. The guard is the same on every plan (50 by default) and is raised on request; it is not a plan limit and no upgrade clears it. Clear it by deactivating an entity or by asking — it is a conflict with the tenant's current state, not an unpaid bill. ⚠ It was 402 Payment Required until 2026-08-14; a client branching on the status code needs updating, the error code is unchanged.

Feld Typ Beschreibung
error erforderlich string
Werte: "entity_limit_reached"
message string
limit erforderlich integer
planTier erforderlich string
Werte: "launch", "entry", "platform", "sovereign"
upgradeUrl string

Relative dashboard hint to the plan surface — where the cap is EXPLAINED. Not a promise that an upgrade is purchasable.

Verwendet von: POST /tenants/{tenantId}/entities

UpdateEntity object

All fields optional, but at least one must be present (empty patch is a 400).

  • Mindestens 1 Feld(er)
Feld Typ Beschreibung
name string
Länge: min. 1, max. 200
vatId string | null

Rejected with 422 if changed after issuance.

addressLine1 string | null
postalCode string | null
city string | null
countryCode string
Länge: min. 2, max. 2

Verwendet von: PATCH /tenants/{tenantId}/entities/{id}

EntitlementRequiredError object

402 body when the tenant's plan tier does not include the capability the request needs. Cleared by an upgrade, never by the caller — which is what distinguishes it from entity_limit_reached (409), a conflict with current state that the same caller clears by deactivating an entity. Nothing already issued is affected: an entitlement change never touches a document, never touches the archive and never revokes a read.

Feld Typ Beschreibung
error erforderlich string
Werte: "entitlement_required"
message string
capability erforderlich string

The capability the current plan does not include.

Werte: "live_issuance", "live_api_keys"
planTier erforderlich string
Werte: "launch", "entry", "platform", "sovereign"
upgradeUrl string

Relative dashboard hint to the plan surface.

Verwendet von: POST /tenants/{tenantId}/entities/{id}/activate-live-operation

LiveOperationStatus object

Feld Typ Beschreibung
operatesLive erforderlich boolean

True when this company has been deliberately activated for live operation, OR already holds any live record. False means neither — never "we could not find out", which is a non-200.

basis erforderlich string

WHY the answer is what it is. activated = the deliberate activation act was performed; records = the company holds live records (any live invoice, draft included, offer or customer); none = neither. Diagnostic and honest-copy input; carries no plan, tier or payment state.

Werte: "activated", "records", "none"
activatedAt string (date-time) | null

The activation timestamp for the RESOLVED company, or null. Already published on the Entity read to the same audience.

entityId erforderlich string (uuid)

WHICH company was answered for. Required, because a caller that sends no X-Entity-Id would otherwise not know, and a session seal keyed on the wrong company would recreate the very defect this read exists to close.

Verwendet von: GET /tenants/{tenantId}/live-operation-status

Membership object

A tenant membership joined with the member's email (the Team table). The role/status here drive the inline role-change and remove affordances. lastActiveAt + keysOwned are additive read fields for the access-review columns.

Feld Typ Beschreibung
id erforderlich string (uuid)
tenantId erforderlich string (uuid)
userId erforderlich string (uuid)
email erforderlich string
role erforderlich MembershipRole
status erforderlich string
Werte: "active", "pending", "revoked"
createdAt erforderlich string (date-time)
lastActiveAt string (date-time) | null

The member's LAST AUDITED ACTION timestamp (MAX over the tenant's the audit log for this actor), computed N+1-free. null when the member has no audited action yet. This is "last thing they did" ("Zuletzt aktiv"), NOT "last page view".

keysOwned object

Count of API keys the member created, mode-split. { live: 0, test: 0 } when they own none. created_by has no FK, so the count is correct even if the creator's row is gone.

Felder
Feld Typ Beschreibung
live erforderlich integer
test erforderlich integer

Verwendet von: GET /tenants/{tenantId}/memberships , PATCH /tenants/{tenantId}/memberships/{userId}

UpdateMembershipRole object

Feld Typ Beschreibung
role erforderlich MembershipRole

Verwendet von: PATCH /tenants/{tenantId}/memberships/{userId}

AccessReviewStatus object

The persistent access-review stamp for the Team header (the MAX over the tenant's access_review.marked_reviewed audit rows). Both fields null when the tenant has never been reviewed.

Feld Typ Beschreibung
lastReviewedAt erforderlich string (date-time) | null

When the tenant's access was last marked reviewed, or null.

lastReviewedBy erforderlich string | null

The reviewing admin's email (display name), or null.

Verwendet von: GET /tenants/{tenantId}/access-review , POST /tenants/{tenantId}/access-review/mark-reviewed

Invitation object

A tenant invitation. Public projection only — the show-once token and its hash are never on the wire. consumedAt set ⇒ accepted; revokedAt set ⇒ withdrawn; otherwise pending until expiresAt.

Feld Typ Beschreibung
id erforderlich string (uuid)
tenantId erforderlich string (uuid)
email erforderlich string (email)
role erforderlich MembershipRole
expiresAt erforderlich string (date-time)
consumedAt erforderlich string (date-time) | null
revokedAt erforderlich string (date-time) | null

Set when withdrawn; distinct from consumedAt/accepted.

createdBy erforderlich string (uuid) | null
createdAt erforderlich string (date-time)

Verwendet von: GET /tenants/{tenantId}/invitations

CreateInvitation object

Feld Typ Beschreibung
email erforderlich string (email)

Trimmed + lower-cased server-side.

Länge: max. 320
role erforderlich MembershipRole

Verwendet von: POST /tenants/{tenantId}/invitations

InvitationWithDelivery object

Feld Typ Beschreibung
id erforderlich string (uuid)
tenantId erforderlich string (uuid)
email erforderlich string (email)
role erforderlich MembershipRole
expiresAt erforderlich string (date-time)
consumedAt erforderlich string (date-time) | null
revokedAt erforderlich string (date-time) | null

Set when withdrawn; distinct from consumedAt/accepted.

createdBy erforderlich string (uuid) | null
createdAt erforderlich string (date-time)
delivery erforderlich InvitationDelivery

Verwendet von: POST /tenants/{tenantId}/invitations , POST /tenants/{tenantId}/invitations/{id}/resend

AcceptInvitation object

Feld Typ Beschreibung
token erforderlich string

The show-once invitation token from the emailed link.

Länge: min. 1, max. 512

Verwendet von: POST /invitations/accept

InvitationPreview object

The UNAUTHENTICATED invitation-preview projection. valid: true carries tenantName, authHint, the invitee's own role, and best-effort tenant branding (logoUrl, primaryColor) — all gated behind possession of a 32-byte token. valid: false is the byte-identical response for EVERY other case (expired/revoked/consumed/nonexistent) and carries no other field — the endpoint never distinguishes invitation states, and NEVER returns the invited email, the inviter's name (that is post-auth-only), or the tenant id.

Feld Typ Beschreibung
valid erforderlich boolean

True only for a live/pending token; false (with no other field) for every other case.

tenantName string

Present ONLY when valid is true — the inviting tenant's display name.

authHint string

Present ONLY when valid is true. Constant password in v1 (the SSO routing hook — shipped now so the web branch exists before it is needed).

Werte: "password", "sso"
role MembershipRole

Present ONLY when valid is true. The invitee's OWN invited role ("what you are being invited as") — about them, low-sensitivity.

logoUrl string | null

Present ONLY when valid is true. Best-effort tenant logo from the primary entity's company profile; null when unset → the pre-auth screen degrades to the env-branded card.

primaryColor string | null

Present ONLY when valid is true. Tenant primary-colour hint. ALWAYS null today — the company profile carries no colour and no migration ships one; the field exists so a future column is a one-line wire-up. Consumers treat null as "unset".

Verwendet von: GET /invitations/{token}/preview

MembershipRole string

Total order (owner > admin > member > readonly).

  • Werte: "owner", "admin", "member", "readonly"

Verwendet von: GET /me/invitations , POST /me/invitations/{id}/accept , GET /tenants/{tenantId}/memberships , PATCH /tenants/{tenantId}/memberships/{userId} , GET /tenants/{tenantId}/invitations , POST /tenants/{tenantId}/invitations , POST /tenants/{tenantId}/invitations/{id}/resend , POST /invitations/accept , GET /invitations/{token}/preview

InvitationDelivery object

Whether a mail was enqueued for this invitation. enqueued — an outbox row exists and delivery is the dispatcher's; no_active_entity — the tenant has no active entity to scope the mail to, nothing was enqueued; failed — the enqueue itself failed, nothing was enqueued. The invitation row is committed in every case; only enqueued means a person will receive anything.

Feld Typ Beschreibung
outcome erforderlich string
Werte: "enqueued", "no_active_entity", "failed"

Verwendet von: POST /tenants/{tenantId}/invitations , POST /tenants/{tenantId}/invitations/{id}/resend

Nach oben · Alle Ressourcengruppen · OpenAPI-Spezifikation (YAML)