Team & Zugriff
Unternehmen, Mitgliedschaften, Einladungen und die Zugriffsprüfung verwalten.
Tenants, entities, memberships, invitations, and access review.
Operationen
-
GET/me/membershipsList the authenticated caller's active tenant memberships -
GET/me/invitationsList the invitations waiting for the authenticated caller -
POST/me/invitations/{id}/acceptAccept one of the caller's own invitations by id -
POST/tenantsCreate a tenant (self-serve, member-less principal becomes owner) -
GET/tenants/{tenantId}/planThe tenant's plan-tier readout -
GET/tenants/{tenantId}/entitiesList the tenant's entities (cursor-paginated) -
POST/tenants/{tenantId}/entitiesCreate an entity (admin/owner or API key with entities:write) -
GET/tenants/{tenantId}/entities/{id}Get one entity (the settings read) -
PATCH/tenants/{tenantId}/entities/{id}Update entity settings (admin/owner or API key with entities:write) -
DELETE/tenants/{tenantId}/entities/{id}Soft-delete (deactivate) an entity (admin/owner or API key with entities:write) -
POST/tenants/{tenantId}/entities/{id}/activate-live-operationMove this company into live operation — deliberate, audited, once -
GET/tenants/{tenantId}/live-operation-statusWhether this company operates live -
GET/tenants/{tenantId}/membershipsList the tenant's members (the Team table) -
PATCH/tenants/{tenantId}/memberships/{userId}Change a member's role (admin/owner, inline role change) -
DELETE/tenants/{tenantId}/memberships/{userId}Remove a member (admin/owner) -
POST/tenants/{tenantId}/memberships/leaveLeave the organisation ("Organisation verlassen") -
GET/tenants/{tenantId}/access-reviewThe access-review stamp (admin) -
POST/tenants/{tenantId}/access-review/mark-reviewedMark tenant access as reviewed (admin, non-idempotent) -
GET/tenants/{tenantId}/invitationsList the tenant's pending invitations ("ausstehend") -
POST/tenants/{tenantId}/invitationsCreate or re-invite an invitation (admin/owner) -
DELETE/tenants/{tenantId}/invitations/{id}Withdraw a pending invitation (admin/owner, "Zurückziehen") -
POST/tenants/{tenantId}/invitations/{id}/resendResend a pending invitation (admin/owner) -
POST/invitations/acceptAccept an invitation by token (any signed-in user) -
GET/invitations/{token}/previewPreview 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
-
200The caller's active memberships (possibly empty). Always 200, regardless of membership count.
application/jsonobject -
401Missing/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
-
200The caller's acceptable invitations (possibly empty), oldest first.
application/jsonobject -
401Missing/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 noinvitation_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 |
Antworten
-
200Accepted (newly, or already a member — see
outcome).application/jsonAcceptInvitationResult -
401No authenticated session.
-
404No invitation with this id is addressed to the caller (
invitation_not_found). -
410The invitation is dead:
invitation_expired(past expiry) orinvitation_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
{
"name": "Muster GmbH",
"legalAcceptances": [
{
"document": "avv",
"version": "v1"
},
{
"document": "agb",
"version": "v1"
}
]
} Antworten
-
200Idempotent replay — the principal already owns their self-serve tenant; the existing tenant is returned (never a second one).
application/jsonSelfServeTenant -
201Tenant created; the caller is now its owner.
application/jsonSelfServeTenant -
400Invalid 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/jsonValidationError | InvalidLegalVersionError -
403No valid token (the
requireAuth()default). -
409One of three conflicts with current state, distinguished by
errorand 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.documentsnames 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/jsonobject | LegalAcceptanceError -
429Per-IP velocity exceeded (
error=rate_limited).application/jsonobject
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 |
Antworten
-
200The tenant's plan-tier readout.
application/jsonTenantPlan -
403Path tenantId ≠ active tenant (
tenant_mismatch). -
404Resolved 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 |
Antworten
-
200A page of the tenant's active entities.
application/jsonEntityPage -
400The query string was refused. Three distinguishable facts share this status, and a client should branch on the
errorcode rather than on the status:unknown_query_parameter— the request named one or more query parameters this deployment does not define, andparameterslists 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 answered200 OKwith 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/jsonUnknownQueryParameterError | ValidationError -
403Path 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 |
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 Länge: max. 255 |
Request-Body erforderlich
application/json CreateEntity
Antworten
-
201The created entity (or, on an Idempotency-Key replay, the recorded response).
application/jsonEntity -
400Validation error.
-
403Caller lacks the admin role, or path tenantId ≠ active tenant.
-
409A 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 carrieslimit/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/jsonEntityLimitError | 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 |
id erforderlich | Pfad | string (uuid) | Entity id. |
Antworten
-
200The entity.
application/jsonEntity -
404Not 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 |
id erforderlich | Pfad | string (uuid) | Entity id. |
Request-Body erforderlich
application/json UpdateEntity
Antworten
-
200The updated entity.
application/jsonEntity -
400Validation error (incl. empty patch).
-
403Caller lacks the admin role, or path tenantId ≠ active tenant.
-
404Not found / not in the caller's tenant.
-
422A 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 |
id erforderlich | Pfad | string (uuid) | Entity id. |
Antworten
-
204Deactivated (or already deactivated — idempotent).
-
403Caller lacks the admin role, or path tenantId ≠ active tenant.
-
404Not found / not in the caller's tenant.
-
409Would 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 |
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 Länge: max. 255 |
Antworten
-
200The entity, now carrying
liveOperationActivatedAt(the same timestamp on every replay).application/jsonEntity -
402The 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/jsonEntitlementRequiredError -
403Caller lacks the admin role, presented an API credential (
credential_not_accepted), or path tenantId ≠ active tenant. -
404Not found / not in the caller's tenant — or the capability is off.
-
409The tenant has not accepted the current version of a required legal document.
legal_acceptance_requiredwhen nothing is on record;legal_acceptance_supersededwhen 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 FRESHIdempotency-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/jsonLegalAcceptanceError
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 |
Antworten
-
200The live-operation signal for the resolved company.
application/jsonLiveOperationStatus -
403tenant_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/jsonValidationError
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 |
Antworten
-
200The tenant's members.
application/jsonobject -
403Path 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 |
userId erforderlich | Pfad | string (uuid) | The member's |
Request-Body erforderlich
application/json UpdateMembershipRole
Antworten
-
200The updated membership.
application/jsonMembership -
400Validation error (unknown role).
-
403Caller lacks the admin role, path tenantId ≠ active tenant, or
owner_only(the target is an owner, or the new role isowner, and the caller is not an owner;actionisgrantorchange). -
404No active membership for that user in the tenant.
-
409cannot_change_own_role(self) orlast_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 |
userId erforderlich | Pfad | string (uuid) | The member's |
Antworten
-
204Removed.
-
403Caller lacks the admin role, path tenantId ≠ active tenant, or
owner_onlywithaction: remove(the target is another owner and the caller is not an owner). -
404No active membership for that user in the tenant.
-
409Would 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 |
Antworten
-
204Left the organisation.
-
401No verified OIDC session (
unauthorized). -
403No active membership in the tenant (
no_tenant_membership), path tenantId ≠ active tenant (tenant_mismatch), or an API-key credential was used (OIDC-only). -
409The caller is the tenant's sole owner and leaving would orphan the org (
last_owner, with anactionfield) — 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 |
Antworten
-
200The latest review stamp (nulls when never reviewed).
application/jsonAccessReviewStatus -
403Caller 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 |
Antworten
-
200The review was recorded; returns the fresh stamp.
application/jsonAccessReviewStatus -
403Caller 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 |
Antworten
-
200The tenant's pending invitations.
application/jsonobject -
403Path 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 |
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 Länge: max. 255 |
Request-Body erforderlich
application/json CreateInvitation
Antworten
-
201The created (or re-invited) pending invitation (or, on an Idempotency-Key replay, the recorded response).
delivery.outcomesays whether a mail was actually ENQUEUED — announce „sent" on that, never on the status code, which only covers the invitation row.application/jsonInvitationWithDelivery -
400Validation error (bad email/role).
-
403Caller lacks the admin role, path tenantId ≠ active tenant, or
owner_onlywithaction: invite(an owner invitation, and the caller is not an owner). -
409A concurrent pending invitation already exists (
pending_invitation_exists), OR anIdempotency-Keyconflict (IDEMPOTENCY_KEY_REUSED_WITH_DIFFERENT_BODY/REQUEST_IN_PROGRESS). -
429system_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-Aftergives the seconds until then andlimitthe number.-
Retry-AfterintegerSeconds until the next business day starts (Europe/Berlin midnight).
application/jsonobject -
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 |
id erforderlich | Pfad | string (uuid) | Invitation id. |
Antworten
-
204Withdrawn (or already withdrawn — idempotent).
-
403Caller lacks the admin role, path tenantId ≠ active tenant, or
owner_onlywithaction: invite(an owner invitation, and the caller is not an owner). -
404No such invitation in the tenant (
not_found). -
409The 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 |
id erforderlich | Pfad | string (uuid) | Invitation id. |
Antworten
-
200The refreshed invitation, with
delivery.outcomefor the new mail.application/jsonInvitationWithDelivery -
403Caller lacks the admin role, path tenantId ≠ active tenant, or
owner_onlywithaction: invite(an owner invitation, and the caller is not an owner). -
404No pending invitation with that id in the tenant.
-
409The invitation has already been accepted (
invitation_already_accepted). -
429system_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-Aftergives the seconds until then andlimitthe number.-
Retry-AfterintegerSeconds until the next business day starts (Europe/Berlin midnight).
application/jsonobject -
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
-
200Accepted (newly, or already a member — see
outcome).application/jsonAcceptInvitationResult -
400Validation error (missing token).
-
401No authenticated session.
-
403The signed-in account does not match the invited email (
invitation_email_mismatch). -
404No invitation matches this token (
invitation_not_found). -
410The link is dead:
invitation_expired(past expiry) orinvitation_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
-
200The preview.
{ valid: true, tenantName, authHint }for a live/pending token; the byte-identical{ valid: false }for every other (expired/revoked/consumed/nonexistent) case.application/jsonInvitationPreview -
429Rate 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 |
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
| |||||||||
role | MembershipRole | ||||||||||
inviterName | string | null | The inviter's display name (their |
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
| |||||||||||||||
membership | object | Felder
| |||||||||||||||
entity | object | The tenant's first entity, created in the same transaction. Felder
|
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
|
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
|
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
|
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; |
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 |
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. 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 |
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. | |||||||||
keysOwned | object | Count of API keys the member created, mode-split. Felder
|
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 |
authHint | string | Present ONLY when Werte: "password", "sso" |
role | MembershipRole | Present ONLY when |
logoUrl | string | null | Present ONLY when |
primaryColor | string | null | Present ONLY when |
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)