{"openapi":"3.1.0","info":{"title":"FluxDesk API","version":"0.20.28","description":"Public API for the FluxDesk internal-OS. Used by internal automation (fd1) and partner integrators to deliver inbound events, claim per-employee tokens, and manage catalog approvals. All endpoints under `/api/v1/*` follow stable contracts; breaking changes require a major version bump.\n\n**MCP surface:** `POST /mcp` (`tools/list`) returns the full agentic surface (16+ tools — tasks, feedback, catalog, projects, …). The REST API documented here is for SDK-style integrations; agent-style clients should prefer MCP.","license":{"name":"Proprietary — internal use","identifier":"LicenseRef-FluxDesk-Internal"},"contact":{"name":"FluxDesk platform team","url":"https://docs.fluxdesk.net"}},"servers":[{"url":"https://my.fluxdesk.net","description":"Production"},{"url":"https://fluxdesk.predithub.net","description":"Test"},{"url":"http://localhost:3000","description":"Local dev"}],"security":[{"BearerAuth":[]}],"tags":[{"name":"events","description":"Inbound event publishing. Each event is identified by `event_id` and namespaced by the bearer token's integrator (e.g. `fd1.*`)."},{"name":"tokens","description":"Per-employee token claim flow. A tools-token (`flux-tools-…`) issued to an integrator can mint event-publishing tokens (`flux-in-pers-…`) for pre-approved employees without admin intervention."},{"name":"catalog","description":"/team-support catalog approval management. Admin-only."},{"name":"introspection","description":"Discovery endpoints — list event types you have permission to publish, list integrators (admin), fetch this spec."},{"name":"health","description":"Liveness / version probes."}],"components":{"securitySchemes":{"BearerAuth":{"type":"http","scheme":"bearer","bearerFormat":"FluxDesk bearer token","description":"HTTP `Authorization: Bearer <token>` header. Tokens come in two flavors:\n\n- `flux-tools-…` — integrator tools-token. Long-lived, admin-issued via `/settings/integrations`. Used for tokens-claim, catalog approvals, and bulk fan-out. Treated as the integrator's identity.\n- `flux-in-pers-…` — personal event-publishing token. Per-employee, rotatable (24h overlap). Used for single inbound event POSTs."}},"parameters":{"IdempotencyKeyHeader":{"name":"Idempotency-Key","in":"header","required":false,"description":"Optional opaque key (≤200 chars). When supplied on a mutation, the server caches the response for 24h and returns the same envelope on replay with an identical body. Replay with a different body returns 409.","schema":{"type":"string","maxLength":200},"example":"idem_fd1_onboard_2026-05-21_8aXcQp"},"RequestIdHeader":{"name":"X-Request-Id","in":"header","required":false,"description":"Optional client-supplied request id (≤120 chars, `[A-Za-z0-9_-]`). Echoed back as a response header and embedded in the response envelope to correlate logs. If absent, the server mints one (`req_<nanoid>`).","schema":{"type":"string","pattern":"^[A-Za-z0-9_-]{1,120}$"}}},"headers":{"X-Request-Id":{"description":"Correlates the response with server logs.","schema":{"type":"string"}},"X-RateLimit-Limit":{"description":"The integrator/token's request budget for the window.","schema":{"type":"integer","minimum":0}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current window.","schema":{"type":"integer","minimum":0}},"X-RateLimit-Reset":{"description":"Unix epoch seconds when the current window resets, after which the budget refills.","schema":{"type":"integer","minimum":0}},"Retry-After":{"description":"Seconds the client should wait before retrying.","schema":{"type":"integer","minimum":0}}},"schemas":{"Severity":{"type":"string","enum":["critical","warn","info","success"],"description":"Technical severity of the underlying source-system event. Used for filtering / rate-budget weighting. Visual rendering uses `tone` (decoupled)."},"Tone":{"type":"string","enum":["neutral","positive","negative"],"description":"UI tone hint, decoupled from `severity`. Drives inbox icon + color: e.g. a `success` event can still be `neutral` if you don't want green emphasis."},"MarkdownRendering":{"type":"string","enum":["collapsed","expanded","preview"],"description":"How the inbox should surface `markdown_body`. `collapsed` (default) hides behind a 「📋 详情」 disclosure; `expanded` inlines without disclosure; `preview` uses the first ~200 chars of the body as the toggle text."},"ExternalStatus":{"type":"string","enum":["firing","resolved","pending","approved","rejected","withdrawn"],"description":"Lifecycle hint about the underlying source-system entity."},"ActionType":{"type":"string","enum":["url","webhook"],"description":"Click outcome for an inline action button. `url` opens a hyperlink in a new tab. `webhook` POSTs HMAC-signed callback to the source system."},"Locale":{"type":"string","description":"BCP-47 language tag. Used for per-recipient inbox rendering.","example":"zh-CN"},"Actor":{"type":"object","additionalProperties":false,"required":["email"],"properties":{"email":{"type":"string","format":"email","maxLength":120},"name":{"type":"string","maxLength":80}}},"ActionButton":{"type":"object","additionalProperties":false,"required":["label","action_type"],"properties":{"label":{"type":"string","minLength":1,"maxLength":40},"action_type":{"$ref":"#/components/schemas/ActionType"},"url":{"type":"string","maxLength":2000,"description":"Required when `action_type` is `url`. Must start with `https://`."},"webhook_url":{"type":"string","maxLength":2000,"description":"Required when `action_type` is `webhook`. Must start with `https://` and not target `localhost`."}},"description":"Inline button rendered under the event summary. Up to 4 per event."},"RecipientHint":{"type":"object","additionalProperties":false,"description":"Optional hint about who the event is for. ONLY consumed by moderation-mode tokens (admin's triage queue); regular tokens persist it verbatim but never auto-route. At least one of `email`, `user_id`, `display_hint` must be present.","properties":{"email":{"type":"string","format":"email","maxLength":120},"user_id":{"type":"string","format":"uuid"},"display_hint":{"type":"string","maxLength":80}}},"Labels":{"type":"object","description":"Up to 20 string-valued labels (each ≤80 chars). Surfaced as inbox chips and queryable by integration admins.","additionalProperties":{"type":"string","maxLength":80},"maxProperties":20},"InboundEvent":{"type":"object","additionalProperties":false,"required":["spec_version","event_id","event_type","severity","title","occurred_at"],"properties":{"spec_version":{"type":"string","enum":["2"],"description":"Must be the literal `\"2\"`. v1 is deprecated and rejected at `/api/v1/events`."},"event_id":{"type":"string","minLength":1,"maxLength":120,"pattern":"^[a-zA-Z0-9_-]+$","description":"Stable per-source-system id. Combined with the token, used as the dedup key — sending the same `event_id` twice returns 200 + `deduped: true`."},"event_type":{"type":"string","minLength":1,"maxLength":60,"description":"Namespaced `<integrator-slug>.<entity>.<action>`. Must start with the bearer's `allowed_event_prefix` (e.g. `fd1.`).","example":"fd1.repo.access_granted"},"severity":{"$ref":"#/components/schemas/Severity"},"title":{"type":"string","minLength":1,"maxLength":200,"description":"Single-line headline shown in inbox."},"occurred_at":{"type":"string","format":"date-time","description":"ISO-8601 with offset. Must be within (now - 24h, now + 5min)."},"summary":{"type":"string","maxLength":500,"description":"Short prose body. Plain text, no markdown."},"markdown_body":{"type":"string","maxLength":8000,"description":"Optional long-form markdown (≤8 KB). For content that doesn't fit `summary` — commands, instructions, multi-paragraph descriptions."},"markdown_body_rendering":{"$ref":"#/components/schemas/MarkdownRendering"},"external_url":{"type":"string","maxLength":2000,"description":"Optional deep-link to the source-system entity. Must start with `https://` and must not include credential query params."},"external_status":{"$ref":"#/components/schemas/ExternalStatus"},"actor":{"$ref":"#/components/schemas/Actor"},"labels":{"$ref":"#/components/schemas/Labels"},"actions":{"type":"array","maxItems":4,"items":{"$ref":"#/components/schemas/ActionButton"}},"recipient_hint":{"allOf":[{"$ref":"#/components/schemas/RecipientHint"}],"deprecated":true,"description":"Legacy. Use `recipient: {type, value}` instead. Still accepted for moderation-mode tokens but no longer evolved; `recipient` and `recipient_hint` are mutually exclusive (XOR) on the same event."},"project_slug":{"type":"string","minLength":1,"maxLength":80,"description":"Optional project attribution. When set, sender's tools-token owner must be admin (org or project) AND recipient must be a project member; else 403 `cross_project_blocked`."},"tone":{"$ref":"#/components/schemas/Tone"},"locale":{"$ref":"#/components/schemas/Locale"}},"example":{"spec_version":"2","event_id":"fd1-evt-2026-05-21-001","event_type":"fd1.repo.access_granted","severity":"info","tone":"positive","title":"你被加入 acme/widget-pro","summary":"alice@example.com 把你加为协作者。","occurred_at":"2026-05-21T10:00:00+08:00","external_url":"https://github.com/acme/widget-pro","actor":{"email":"alice@example.com","name":"Alice"},"labels":{"repo":"widget-pro","role":"write"}}},"Recipient":{"oneOf":[{"type":"object","additionalProperties":false,"required":["type","email"],"properties":{"type":{"type":"string","enum":["email"]},"email":{"type":"string","format":"email","maxLength":120}}},{"type":"object","additionalProperties":false,"required":["type","user_id"],"properties":{"type":{"type":"string","enum":["user_id"]},"user_id":{"type":"string","format":"uuid"}}},{"type":"object","additionalProperties":false,"required":["type","slug"],"properties":{"type":{"type":"string","enum":["group"]},"slug":{"type":"string","minLength":1,"maxLength":80}}}],"discriminator":{"propertyName":"type","mapping":{"email":"#/components/schemas/Recipient","user_id":"#/components/schemas/Recipient","group":"#/components/schemas/Recipient"}},"description":"Fan-out target. `email` is the most common; `user_id` for pre-resolved targets; `group` for project / group slugs (sender's tools-token owner must be a group admin)."},"BulkEventRequest":{"type":"object","additionalProperties":false,"required":["event","recipients"],"properties":{"event":{"$ref":"#/components/schemas/InboundEvent"},"recipients":{"type":"array","minItems":1,"maxItems":100,"items":{"$ref":"#/components/schemas/Recipient"}},"auto_claim":{"type":"boolean","default":true,"description":"v0.20.0 — when true (default) and a recipient has no personal token claimable via this tools-token, the server mints one inline before delivering. Adds one DB round-trip per unclaimed recipient. Set false to retain the old `no_approved_request` failure (recommended when caller has already pre-batched `/api/v1/tokens/claim/batch`)."}},"description":"Bulk fan-out: one event template + N recipients. Each recipient gets an isolated delivery row (idempotency + dedup tracked per (recipient, event_id))."},"BulkEventDeliveryResult":{"type":"object","additionalProperties":true,"required":["email"],"properties":{"email":{"type":"string","format":"email"},"deliveryId":{"type":"string","format":"uuid"},"deduped":{"type":"boolean"},"pushed":{"type":"boolean"},"autoClaimed":{"type":"boolean","description":"v0.20.0 — present (and `true`) only when the personal token was minted inline during this broadcast. Absent on the pre-claimed happy path; equivalent to `false`."},"error":{"type":"string","description":"Per-recipient failure code (e.g. `no_token`, `no_approved_request`, `user_not_found`, `user_inactive`, `token_disabled`, `schema_invalid:…`, `auto_claim_lookup_failed`). Mutually exclusive with the success fields above."}},"description":"One row per recipient in the BulkEvent response. Either has `deliveryId` + `deduped` + `pushed` (success) or `error` (failure)."},"Delivery":{"type":"object","additionalProperties":true,"required":["deliveryId","eventType","severity","title","occurredAt","createdAt"],"properties":{"deliveryId":{"type":"string","format":"uuid"},"eventType":{"type":"string"},"severity":{"$ref":"#/components/schemas/Severity"},"tone":{"$ref":"#/components/schemas/Tone"},"title":{"type":"string"},"summary":{"type":"string"},"externalUrl":{"type":"string"},"externalStatus":{"$ref":"#/components/schemas/ExternalStatus"},"occurredAt":{"type":"string","format":"date-time"},"createdAt":{"type":"string","format":"date-time"},"resolvedAt":{"type":"string","format":"date-time","nullable":true},"resolutionNote":{"type":"string","nullable":true},"fireCount":{"type":"integer","minimum":1},"deduped":{"type":"boolean","description":"True when this row was returned from the dedup cache rather than a fresh write."},"ok":{"type":"boolean","description":"Legacy envelope flag (back-compat). Clients should rely on HTTP 2xx, not this field."},"pushed":{"type":"boolean","description":"True when the recipient's inbox / web-push was actually nudged. False when deduped or push channel disabled."},"requestId":{"type":"string","description":"Echoes the response X-Request-Id header."},"deprecation":{"type":"string","description":"Optional human-readable deprecation notice. Currently emitted when caller used legacy `recipient_hint` instead of `recipient`."},"locale":{"type":"string","description":"BCP-47, echoes payload."},"recipientUserId":{"type":"string","format":"uuid"},"projectId":{"type":"string","format":"uuid","nullable":true},"supplements":{"type":"array","description":"Append-only follow-ups added by the sender after the original delivery. Original `title` / `summary` / `markdown_body` never mutate (Charter Art 9).","items":{"type":"object","additionalProperties":false,"required":["id","markdownBody","createdAt"],"properties":{"id":{"type":"string","format":"uuid"},"markdownBody":{"type":"string"},"createdAt":{"type":"string","format":"date-time"}}}}}},"ResolveDeliveryRequest":{"type":"object","additionalProperties":false,"properties":{"resolution_note":{"type":"string","maxLength":500,"description":"Optional resolution note shown next to the resolved chip."}}},"SupplementDeliveryRequest":{"type":"object","additionalProperties":false,"required":["markdown_body"],"properties":{"markdown_body":{"type":"string","minLength":1,"maxLength":8000,"description":"Markdown body for the follow-up."}}},"ClaimTokenRequest":{"type":"object","additionalProperties":false,"required":["email"],"properties":{"email":{"type":"string","format":"email","maxLength":120},"label":{"type":"string","maxLength":80,"description":"Human-readable label that will appear in the employee's /me/integrations list."},"severity_ceiling":{"$ref":"#/components/schemas/Severity"},"daily_limit":{"type":"integer","minimum":1,"maximum":1000,"description":"Per-token daily delivery cap. Default 50; admins set higher via /team-support."}}},"ClaimedToken":{"type":"object","additionalProperties":false,"required":["token","tokenId","ownerUserId","expiresPriorAt"],"properties":{"token":{"type":"string","description":"The new event-publishing token (`flux-in-pers-…`)."},"tokenId":{"type":"string","format":"uuid"},"ownerUserId":{"type":"string","format":"uuid"},"expiresPriorAt":{"type":"string","format":"date-time","description":"Until this timestamp, the previous token (if any) is also accepted (rotation overlap)."}}},"ClaimTokenBatchRequest":{"type":"object","additionalProperties":false,"required":["claims"],"properties":{"claims":{"type":"array","minItems":1,"maxItems":100,"items":{"$ref":"#/components/schemas/ClaimTokenRequest"}}}},"CatalogApprovalAssignRequest":{"type":"object","additionalProperties":false,"required":["email","catalog_slug"],"properties":{"email":{"type":"string","format":"email"},"catalog_slug":{"type":"string","minLength":1,"maxLength":80,"description":"Stable catalog identifier, e.g. `\"fd1\"`, `\"network-vpn-access\"`."}}},"CatalogApprovalReviewRequest":{"type":"object","additionalProperties":false,"required":["request_id","decision"],"properties":{"request_id":{"type":"string","format":"uuid"},"decision":{"type":"string","enum":["approve","reject"]},"note":{"type":"string","maxLength":500}}},"CatalogApprovalListItem":{"type":"object","additionalProperties":false,"properties":{"id":{"type":"string","format":"uuid"},"email":{"type":"string","format":"email"},"catalogSlug":{"type":"string"},"status":{"type":"string","enum":["pending","approved","rejected","revoked"]},"requestedAt":{"type":"string","format":"date-time"},"reviewedAt":{"type":"string","format":"date-time","nullable":true},"reviewedByEmail":{"type":"string","nullable":true}}},"AssetType":{"type":"string","enum":["saas_account","api_key","server_access","repo_access","ai_server","account_resource","other"],"description":"Asset class of the catalog row. Drives which approval / claim flow the row hooks into. Immutable post-create."},"CatalogItem":{"type":"object","additionalProperties":false,"required":["id","name","type","categoryDisplay","use_cases","is_pinned","approval_required","display_order","created_at","updated_at"],"properties":{"id":{"type":"string","format":"uuid"},"slug":{"type":"string","nullable":true,"description":"Stable lower-ASCII slug (`^[a-z0-9][a-z0-9._-]{0,79}$`). Legacy rows may have null. Immutable post-create."},"name":{"type":"string","maxLength":200},"type":{"$ref":"#/components/schemas/AssetType"},"description":{"type":"string","nullable":true},"category":{"type":"string","nullable":true,"description":"Raw machine category slug (e.g. `llm-backend`). See docs/MCP_GUIDE.md §13.3 for the canonical list."},"categoryDisplay":{"type":"string","description":"Server's best-effort English display label derived from `category`. Empty string when `category` is null."},"url":{"type":"string","nullable":true},"application_guide":{"type":"string","nullable":true},"use_cases":{"type":"array","description":"Opaque JSON array of use-case rows surfaced to the inbox. Schema deliberately loose; clients render as-is.","items":{}},"is_pinned":{"type":"boolean"},"approval_required":{"type":"boolean","description":"When true, employees must go through the apply→review flow before access is granted."},"display_order":{"type":"integer"},"contact_user_id":{"type":"string","format":"uuid","nullable":true},"linked_tools_token_id":{"type":"string","format":"uuid","nullable":true,"description":"Tools-token id of the integrator that owns this catalog row (used for inbound-token claim eligibility)."},"archived_at":{"type":"string","format":"date-time","nullable":true,"description":"Soft-archive stamp (Charter Art 13). Lists filter these out by default unless `active_only=false`."},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"}}},"CatalogItemCreateRequest":{"type":"object","additionalProperties":false,"required":["slug","name","type"],"properties":{"slug":{"type":"string","pattern":"^[a-z0-9][a-z0-9._-]{0,79}$","description":"Immutable post-create. Lowercase ASCII + `._-`."},"name":{"type":"string","minLength":1,"maxLength":200},"type":{"$ref":"#/components/schemas/AssetType"},"description":{"type":"string","nullable":true},"category":{"type":"string","nullable":true},"url":{"type":"string","nullable":true},"application_guide":{"type":"string","nullable":true},"use_cases":{"type":"array","items":{}},"display_order":{"type":"integer"},"is_pinned":{"type":"boolean","default":false},"approval_required":{"type":"boolean","default":true},"contact_user_id":{"type":"string","format":"uuid","nullable":true},"linked_tools_token_id":{"type":"string","format":"uuid","nullable":true}}},"CatalogItemUpdateRequest":{"type":"object","additionalProperties":false,"description":"Partial update. `slug` and `type` are immutable — sending either returns 400 `{field: ['immutable']}`.","properties":{"name":{"type":"string","minLength":1,"maxLength":200},"description":{"type":"string","nullable":true},"category":{"type":"string","nullable":true},"url":{"type":"string","nullable":true},"application_guide":{"type":"string","nullable":true},"use_cases":{"type":"array","items":{}},"display_order":{"type":"integer"},"is_pinned":{"type":"boolean"},"approval_required":{"type":"boolean"},"contact_user_id":{"type":"string","format":"uuid","nullable":true},"linked_tools_token_id":{"type":"string","format":"uuid","nullable":true}}},"CatalogItemDeleteBlockers":{"type":"object","additionalProperties":false,"required":["liveApprovals","liveAssets","liveTokens"],"description":"Counts of live foreign references that would be orphaned by a hard delete. Returned in the 409 problem+json body when `?hard=true` is refused; soft-delete (default) ignores these.","properties":{"liveApprovals":{"type":"integer","minimum":0},"liveAssets":{"type":"integer","minimum":0},"liveTokens":{"type":"integer","minimum":0}}},"EventTypeRow":{"type":"object","additionalProperties":false,"required":["eventType"],"properties":{"eventType":{"type":"string"},"lastSeenAt":{"type":"string","format":"date-time","description":"Most recent observation within the 90-day rolling window. Null when the type is derived from the integrator prefix but no deliveries exist yet.","nullable":true},"count":{"type":"integer","minimum":0,"description":"Delivery count in the past 90 days for this `event_type`."}}},"EventTypesResponse":{"type":"object","additionalProperties":false,"required":["yourIntegrator","yourEventTypes","fluxdeskInternalTypes"],"properties":{"yourIntegrator":{"type":"object","additionalProperties":false,"required":["slug","allowedEventPrefix"],"properties":{"slug":{"type":"string"},"allowedEventPrefix":{"type":"string"}}},"yourEventTypes":{"type":"array","items":{"$ref":"#/components/schemas/EventTypeRow"}},"fluxdeskInternalTypes":{"type":"array","items":{"$ref":"#/components/schemas/EventTypeRow"},"description":"FluxDesk-internal types (prefix `internal-os.`) visible to every authenticated integrator."},"requestId":{"type":"string"}}},"Integrator":{"type":"object","additionalProperties":false,"required":["id","slug","name","allowedEventPrefix","createdAt"],"properties":{"id":{"type":"string","format":"uuid"},"slug":{"type":"string"},"name":{"type":"string"},"allowedEventPrefix":{"type":"string"},"homepageUrl":{"type":"string","nullable":true},"description":{"type":"string","nullable":true},"createdAt":{"type":"string","format":"date-time"}}},"IntegratorsResponse":{"type":"object","additionalProperties":false,"required":["items","totalCount"],"properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/Integrator"}},"totalCount":{"type":"integer","minimum":0},"requestId":{"type":"string"}}},"Health":{"type":"object","additionalProperties":false,"required":["ok","version"],"properties":{"ok":{"type":"boolean"},"version":{"type":"string","example":"0.20.28"},"serverTime":{"type":"string","format":"date-time"}}},"ProblemDetails":{"type":"object","description":"RFC 7807 problem+json error envelope. Clients SHOULD route on the stable `type` URI, not fuzzy-match `detail`.","required":["type","title","status"],"properties":{"type":{"type":"string","format":"uri","description":"Stable URI identifying the error class (e.g. `https://docs.fluxdesk.net/api/v1/errors/rate-limited`)."},"title":{"type":"string","description":"Short human-readable summary."},"status":{"type":"integer","minimum":100,"maximum":599},"detail":{"type":"string"},"instance":{"type":"string","format":"uri-reference"},"requestId":{"type":"string"},"fieldErrors":{"type":"object","description":"When `type` is the validation-failed URI, may carry per-field error arrays.","additionalProperties":{"type":"array","items":{"type":"string"}}}}},"RateLimitState":{"type":"object","description":"Embed-able rate-limit snapshot (also exposed as response headers).","properties":{"limit":{"type":"integer","minimum":0},"remaining":{"type":"integer","minimum":0},"resetAtUnix":{"type":"integer","minimum":0}}}},"responses":{"Unauthorized":{"description":"Missing / malformed bearer token, or the token has been disabled / revoked.","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"}},"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ProblemDetails"},"example":{"type":"https://docs.fluxdesk.net/api/v1/errors/unauthorized","title":"Unauthorized","status":401,"detail":"Missing or invalid bearer token."}}}},"Forbidden":{"description":"Authenticated, but the caller can't perform this action (admin-only endpoint, cross-project block, or namespace mismatch).","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"}},"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ProblemDetails"},"example":{"type":"https://docs.fluxdesk.net/api/v1/errors/forbidden","title":"Forbidden","status":403,"detail":"Caller is not an admin."}}}},"NotFound":{"description":"The named resource doesn't exist (or isn't visible to you).","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"}},"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ProblemDetails"}}}},"ValidationFailed":{"description":"Body didn't match the schema (missing required field, bad enum, forbidden top-level key, etc.).","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"}},"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ProblemDetails"},"example":{"type":"https://docs.fluxdesk.net/api/v1/errors/validation-failed","title":"Validation failed","status":400,"detail":"event_type must start with \"fd1.\"","fieldErrors":{"event_type":["namespace mismatch"]}}}}},"RateLimited":{"description":"Per-integrator / per-token rate budget exhausted. Retry after `Retry-After` seconds.","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"},"Retry-After":{"$ref":"#/components/headers/Retry-After"}},"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ProblemDetails"}}}},"IdempotencyConflict":{"description":"Same `Idempotency-Key` replayed with a different request body. Pick a new key or send the same body.","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"}},"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ProblemDetails"}}}}}},"paths":{"/api/v1/events":{"post":{"tags":["events"],"summary":"Publish a single inbound event","description":"Single-recipient delivery: the token owner is the recipient. Idempotent on `(token, event_id)` — replaying returns 200 + `deduped: true`. Namespace gate: `event.event_type` must start with the bearer's `allowed_event_prefix`.","parameters":[{"$ref":"#/components/parameters/IdempotencyKeyHeader"},{"$ref":"#/components/parameters/RequestIdHeader"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/InboundEvent"}}}},"responses":{"200":{"description":"Deduped — the same `(token, event_id)` was already delivered. The cached delivery is returned.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Delivery"}}}},"202":{"description":"Accepted — delivery row created and recipient notified.","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Delivery"}}}},"400":{"$ref":"#/components/responses/ValidationFailed"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"409":{"$ref":"#/components/responses/IdempotencyConflict"},"413":{"description":"Body larger than 256 KB.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ProblemDetails"}}}},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/api/v1/events/bulk":{"post":{"tags":["events"],"summary":"Fan-out a single event template to many recipients","description":"Tools-token-only. The same event payload is materialized into one delivery row per recipient (`recipients[].type` is one of `email` / `user_id` / `group`). Per-recipient idempotency: same `event_id` to the same recipient deduplicates. v0.20.0 — recipients without an existing personal token claimable via this tools-token are auto-claimed inline by default; results carry `autoClaimed: true` on the rows where that happened. Disable via `auto_claim: false`.","parameters":[{"$ref":"#/components/parameters/IdempotencyKeyHeader"},{"$ref":"#/components/parameters/RequestIdHeader"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BulkEventRequest"}}}},"responses":{"202":{"description":"All recipients accepted (or deduped per-row).","content":{"application/json":{"schema":{"type":"object","required":["results"],"properties":{"results":{"type":"array","items":{"$ref":"#/components/schemas/BulkEventDeliveryResult"}},"requestId":{"type":"string"}}}}}},"400":{"$ref":"#/components/responses/ValidationFailed"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"409":{"$ref":"#/components/responses/IdempotencyConflict"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/api/v1/events/{deliveryId}":{"get":{"tags":["events"],"summary":"Read back a single delivery","description":"Returns the full delivery row including any append-only supplements. Caller must be the original sender (same tools-token / token-owner).","parameters":[{"name":"deliveryId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Delivery row.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Delivery"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"}}}},"/api/v1/events/{deliveryId}/resolve":{"post":{"tags":["events"],"summary":"Mark a delivery as resolved","description":"Terminal-state stamp; doesn't repush or delete the row. Inbox UI renders a 「已处理」 chip. Original sender only.","parameters":[{"name":"deliveryId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}},{"$ref":"#/components/parameters/IdempotencyKeyHeader"}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ResolveDeliveryRequest"}}}},"responses":{"200":{"description":"Resolved.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Delivery"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"}}}},"/api/v1/events/{deliveryId}/supplement":{"post":{"tags":["events"],"summary":"Append a follow-up markdown body to a delivery","description":"Original `title` / `summary` / `markdown_body` never mutate (Charter Art 9). Supplements are new rows the inbox renders in chronological order under the original. Sender-only.","parameters":[{"name":"deliveryId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}},{"$ref":"#/components/parameters/IdempotencyKeyHeader"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SupplementDeliveryRequest"}}}},"responses":{"201":{"description":"Supplement appended.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Delivery"}}}},"400":{"$ref":"#/components/responses/ValidationFailed"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"}}}},"/api/v1/tokens/claim":{"post":{"tags":["tokens"],"summary":"Claim or rotate a per-employee event-publishing token","description":"Mints (or rotates) a `flux-in-pers-…` token bound to the named employee. The employee must be pre-approved for the integrator's catalog. 24h overlap: the previous token (if any) is still accepted until `expiresPriorAt`.","parameters":[{"$ref":"#/components/parameters/IdempotencyKeyHeader"},{"$ref":"#/components/parameters/RequestIdHeader"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ClaimTokenRequest"}}}},"responses":{"200":{"description":"Token minted or rotated.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ClaimedToken"}}}},"400":{"$ref":"#/components/responses/ValidationFailed"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"Caller is not an admin tools-token, OR the named employee is not pre-approved for the integrator's catalog.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ProblemDetails"}}}},"409":{"$ref":"#/components/responses/IdempotencyConflict"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/api/v1/tokens/claim/batch":{"post":{"tags":["tokens"],"summary":"Claim tokens for many employees in a single call","description":"Atomic per-row; partial failures return per-row `ok: false`.","parameters":[{"$ref":"#/components/parameters/IdempotencyKeyHeader"},{"$ref":"#/components/parameters/RequestIdHeader"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ClaimTokenBatchRequest"}}}},"responses":{"200":{"description":"Per-row results.","content":{"application/json":{"schema":{"type":"object","properties":{"results":{"type":"array","items":{"type":"object","properties":{"email":{"type":"string","format":"email"},"ok":{"type":"boolean"},"token":{"$ref":"#/components/schemas/ClaimedToken"},"reason":{"type":"string"}}}},"requestId":{"type":"string"}}}}}},"400":{"$ref":"#/components/responses/ValidationFailed"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/api/v1/catalog/approvals":{"get":{"tags":["catalog"],"summary":"List catalog approval records","description":"Admin-only. Filterable by email / catalog_slug / status.","parameters":[{"name":"email","in":"query","required":false,"schema":{"type":"string","format":"email"}},{"name":"catalog_slug","in":"query","required":false,"schema":{"type":"string"}},{"name":"status","in":"query","required":false,"schema":{"type":"string","enum":["pending","approved","rejected","revoked"]}}],"responses":{"200":{"description":"Approval rows.","content":{"application/json":{"schema":{"type":"object","properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/CatalogApprovalListItem"}},"requestId":{"type":"string"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"}}},"post":{"tags":["catalog"],"summary":"Admin-assign a catalog approval (bypassing apply→review)","description":"Equivalent of the MCP `assign_catalog_to_employee` tool. Creates an approved request row + an access_assets row. Admin tools-token only.","parameters":[{"$ref":"#/components/parameters/IdempotencyKeyHeader"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CatalogApprovalAssignRequest"}}}},"responses":{"201":{"description":"Approval created.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CatalogApprovalListItem"}}}},"400":{"$ref":"#/components/responses/ValidationFailed"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"409":{"$ref":"#/components/responses/IdempotencyConflict"}}},"delete":{"tags":["catalog"],"summary":"Revoke a catalog approval","description":"Idempotent. Invalidates the matching personal token(s) auto-minted under this approval. Admin tools-token only.","parameters":[{"$ref":"#/components/parameters/IdempotencyKeyHeader"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CatalogApprovalAssignRequest"}}}},"responses":{"200":{"description":"Revoked (or already not-approved — idempotent).","content":{"application/json":{"schema":{"type":"object","properties":{"revoked":{"type":"boolean"},"tokensInvalidated":{"type":"integer","minimum":0}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"}}}},"/api/v1/catalog/items":{"get":{"tags":["catalog"],"summary":"List catalog rows","description":"Admin tools-token only. Returns up to `limit` non-archived rows by default, ordered by `display_order` then `name`. Set `active_only=false` to include soft-archived rows.","parameters":[{"name":"active_only","in":"query","required":false,"description":"Include soft-archived (`archived_at != null`) rows when `false`. Defaults to `true`.","schema":{"type":"boolean","default":true}},{"name":"category","in":"query","required":false,"description":"Filter by exact category slug.","schema":{"type":"string"}},{"name":"type","in":"query","required":false,"description":"Filter by `AssetType`.","schema":{"$ref":"#/components/schemas/AssetType"}},{"name":"integrator_slug","in":"query","required":false,"description":"Filter to rows whose `linked_tools_token_id` belongs to the named integrator (slug). Honoured by both the MCP `catalog_item_list` tool and the REST GET handler since v0.20.4.","schema":{"type":"string"}},{"name":"limit","in":"query","required":false,"description":"1..200, default 50.","schema":{"type":"integer","minimum":1,"maximum":200,"default":50}}],"responses":{"200":{"description":"Catalog rows.","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"type":"object","required":["items","totalCount"],"properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/CatalogItem"}},"totalCount":{"type":"integer","minimum":0},"requestId":{"type":"string"}}}}}},"400":{"$ref":"#/components/responses/ValidationFailed"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/RateLimited"}}},"post":{"tags":["catalog"],"summary":"Create a catalog row","description":"Admin tools-token only. Slug must be unique; 409 on conflict. Idempotency-Key honoured under scope `catalog.item.create`.","parameters":[{"$ref":"#/components/parameters/IdempotencyKeyHeader"},{"$ref":"#/components/parameters/RequestIdHeader"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CatalogItemCreateRequest"}}}},"responses":{"201":{"description":"Created.","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"}},"content":{"application/json":{"schema":{"type":"object","required":["ok","item"],"properties":{"ok":{"type":"boolean"},"item":{"$ref":"#/components/schemas/CatalogItem"},"requestId":{"type":"string"}}}}}},"400":{"$ref":"#/components/responses/ValidationFailed"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"409":{"description":"Either `Idempotency-Key` replayed with a different body, or another row already owns this slug.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ProblemDetails"}}}},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/api/v1/catalog/items/{slug}":{"get":{"tags":["catalog"],"summary":"Fetch a single catalog row","description":"Admin tools-token only. Returns 404 when the slug doesn't exist OR the row is soft-archived (the per-row GET does not surface archived rows; use list with `active_only=false` instead).","parameters":[{"name":"slug","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Catalog row.","content":{"application/json":{"schema":{"type":"object","required":["ok","item"],"properties":{"ok":{"type":"boolean"},"item":{"$ref":"#/components/schemas/CatalogItem"},"requestId":{"type":"string"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}},"patch":{"tags":["catalog"],"summary":"Partial update of a catalog row","description":"Admin tools-token only. `slug` + `type` are immutable; sending either yields 400 `{field: ['immutable']}`. Idempotency-Key honoured under scope `catalog.item.update`.","parameters":[{"name":"slug","in":"path","required":true,"schema":{"type":"string"}},{"$ref":"#/components/parameters/IdempotencyKeyHeader"},{"$ref":"#/components/parameters/RequestIdHeader"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CatalogItemUpdateRequest"}}}},"responses":{"200":{"description":"Updated row.","content":{"application/json":{"schema":{"type":"object","required":["ok","item"],"properties":{"ok":{"type":"boolean"},"item":{"$ref":"#/components/schemas/CatalogItem"},"requestId":{"type":"string"}}}}}},"400":{"$ref":"#/components/responses/ValidationFailed"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/IdempotencyConflict"},"429":{"$ref":"#/components/responses/RateLimited"}}},"delete":{"tags":["catalog"],"summary":"Delete (soft by default) a catalog row","description":"Admin tools-token only. Default soft-archives by stamping `archived_at` (Charter Art 13); `?hard=true` removes the row outright but only when no live approvals / access assets / personal inbound tokens reference it — otherwise 409 with `blockers`. Re-soft-deleting an already-archived row returns 200 with `already_archived: true`. Idempotency-Key honoured under scope `catalog.item.delete`.","parameters":[{"name":"slug","in":"path","required":true,"schema":{"type":"string"}},{"name":"hard","in":"query","required":false,"description":"Set `true` to attempt a hard delete (refused with 409 when blockers > 0). Default `false` = soft-archive.","schema":{"type":"boolean","default":false}},{"$ref":"#/components/parameters/IdempotencyKeyHeader"}],"responses":{"200":{"description":"Soft-archived, hard-deleted, or no-op idempotent replay.","content":{"application/json":{"schema":{"type":"object","required":["ok","mode"],"properties":{"ok":{"type":"boolean"},"mode":{"type":"string","enum":["soft","hard"]},"slug":{"type":"string"},"item":{"allOf":[{"$ref":"#/components/schemas/CatalogItem"}],"nullable":true,"description":"Present on soft-delete; null on hard."},"already_archived":{"type":"boolean"},"requestId":{"type":"string"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"description":"Either `Idempotency-Key` replayed with a different body, or `hard=true` was blocked by live dependents (`blockers` populated).","content":{"application/problem+json":{"schema":{"allOf":[{"$ref":"#/components/schemas/ProblemDetails"},{"type":"object","properties":{"blockers":{"$ref":"#/components/schemas/CatalogItemDeleteBlockers"}}}]}}}},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/api/v1/event-types":{"get":{"tags":["introspection"],"summary":"List event types visible to the caller","description":"Returns the integrator's namespace prefix + any `event_type` strings that have appeared in deliveries in the last 90 days, plus FluxDesk-internal types (`internal-os.*`). Admin tools-tokens can pass `?integrator_slug=…` to inspect another integrator.","parameters":[{"name":"integrator_slug","in":"query","required":false,"description":"Admin-only — list another integrator's event types.","schema":{"type":"string"}}],"responses":{"200":{"description":"Event type listing.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/EventTypesResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"}}}},"/api/v1/integrators":{"get":{"tags":["introspection"],"summary":"List integrators (admin-only)","description":"Returns every non-archived row from the integrators registry. Bearer must be an admin tools-token.","responses":{"200":{"description":"Integrator listing.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/IntegratorsResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"}}}},"/api/v1/openapi.json":{"get":{"tags":["introspection"],"summary":"This OpenAPI spec, as JSON","description":"Public — no authentication required. Discovery clients fetch this to pin / refresh their schema.","security":[],"responses":{"200":{"description":"OpenAPI 3.1 document.","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/api/v1/docs":{"get":{"tags":["introspection"],"summary":"Swagger UI for this spec","description":"Swagger UI HTML rendering of this spec. Returns text/html. Public — no authentication required.","security":[],"responses":{"200":{"description":"Swagger UI one-pager (loads `/api/v1/openapi.json` via the pinned CDN bundle).","content":{"text/html":{}}}}}},"/api/v1/health":{"get":{"tags":["health"],"summary":"Liveness + version probe","description":"Public. No DB hit; returns the process version stamp.","security":[],"responses":{"200":{"description":"Server alive.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Health"}}}}}}}}}