{"openapi":"3.1.0","info":{"title":"Plurism API","version":"1.0.0","summary":"Backend services for product teams: support, feedback, waitlist, changelog, content, feature flags, files, notifications, webhooks, analytics, and transactional email.","description":"Plurism is a multi-tenant backend-as-a-service. This document covers every HTTP surface the API Worker exposes.\n\n**Authentication.** Most endpoints take a secret API key as `Authorization: Bearer sk_live_…`, minted in the Plurism portal and carrying an explicit scope set; each operation lists the scope it needs. Secret keys are server-side only.\n\n**Public surfaces.** Three families need no API key and are marked with an empty `security` block. Browser-facing ingest (`/v1/a/e`, `/v1/el/*`) authenticates with a *publishable* `pa_` key plus an Origin allow-list — safe to ship in client-side code, and bounded by non-depletable per-project rate caps rather than the org's quota. Read-only public content (`/public/changelog/*`, `/public/content/*`) is CORS-open and edge-cached for embedding in marketing sites. Inbound webhooks (`/webhooks/receive/*`, the Stripe and inbound-email endpoints) authenticate by signature or by an unguessable path token.\n\n**Conventions.** Requests and responses are JSON unless noted — multipart for file upload, raw body for inbound webhooks. Successful responses wrap payloads in `{ \"data\": … }`, except `POST /support/threads` and `POST /v1/emails`, which return their envelope at the top level. Errors are `{ \"error\": { \"code\", \"message\", \"details\"? } }` and every response carries `X-Plurism-Request-Id` for support correlation. Deletes are soft by default and return `204`. List endpoints take `limit` (1–500, default 100).","contact":{"name":"Plurism support","url":"https://plurism.dev"}},"servers":[{"url":"https://api.plurism.dev","description":"Production"},{"url":"https://api-staging.plurism.dev","description":"Staging"}],"security":[{"bearerAuth":[]}],"tags":[{"name":"Threads","description":"Support threads — the primary unit of work."},{"name":"Messages","description":"Replies on a thread (public to the reporter or internal to operators)."},{"name":"Tags","description":"Project-scoped tag definitions for organising threads."},{"name":"Reporters","description":"Customers who created threads. Includes merge + GDPR purge."},{"name":"Saved views","description":"Reusable filter sets per project."},{"name":"Custom statuses","description":"Project-level status names that map to one of the four base buckets."},{"name":"Metadata schema","description":"Per-project schema for the per-thread `metadata` JSON object."},{"name":"Audit","description":"Read-only audit log for a thread."},{"name":"Public lookup","description":"Unauthenticated reporter-facing endpoints (browser + email forwarder reach these directly)."},{"name":"Inbound webhooks","description":"Resend `email.received` ingestion."},{"name":"Feature flags","description":"Boolean and percentage-rollout flags with stable per-user bucketing."},{"name":"Waitlist","description":"Pre-launch signups, invite flow, and the notifier event that emails them."},{"name":"Changelog","description":"Release notes, drafted then published."},{"name":"Public changelog","description":"Unauthenticated, CORS-open changelog feed for embedding."},{"name":"Content","description":"Articles, news, FAQs and guides, with schema.org output for answer engines."},{"name":"Public content","description":"Unauthenticated, CORS-open content feed with rendered HTML and JSON-LD."},{"name":"Feedback","description":"NPS-style ratings and comments, plus the aggregate score."},{"name":"Files","description":"Object storage with per-plan byte ceilings."},{"name":"Notifiers","description":"Where events go: email, Discord, or your own HTTPS endpoint."},{"name":"Notifier rules","description":"Which events route to which notifier, with an optional template."},{"name":"Email templates","description":"Reusable subject and body for notifier email."},{"name":"Webhook receivers","description":"Inbound endpoints you hand to third parties."},{"name":"Webhook events","description":"What arrived, what was delivered, and what to retry."},{"name":"Usage","description":"Current plan, ceilings, and consumption."},{"name":"Email","description":"Transactional send."},{"name":"Email domains","description":"Sending-domain onboarding and DNS verification."},{"name":"Analytics","description":"Browser event ingest, the tracker bundle, and revenue attribution."},{"name":"Elements","description":"Drop-in web components for waitlist and feedback."},{"name":"Ops","description":"Liveness and readiness probes."},{"name":"Keys","description":"Provisioning: mint, list and revoke publishable keys."}],"paths":{"/support/threads":{"post":{"operationId":"createThread","summary":"Create a thread","tags":["Threads"],"description":"Creates a new support thread. The reporter is matched to an existing `SupportReporter` by email or auto-created. A best-effort confirmation email is sent in the background; failure does not block the response. `support.thread.created` event fires for any matching webhook subscription.","security":[{"bearerAuth":["support:write"]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateThreadInput"},"example":{"body":"Login button does nothing on Safari iOS.","category":"bug","reporter_email":"alice@example.com","reporter_name":"Alice Lee","subject":"Can't sign in on iPhone","tags":["mobile","auth"],"metadata":{"browser":"Safari 17.4","device":"iPhone 14"}}}}},"responses":{"201":{"description":"Thread created","headers":{"X-Plurism-Request-Id":{"$ref":"#/components/headers/RequestId"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateThreadResponse"},"example":{"id":"thr_01jbx7w9q2k0wqxgkr1d8m2y3a","ref":"PLURISM-42","lookup_url":"https://api.plurism.dev/support/lookup/PLURISM-42","created_at":"2026-04-29T10:42:11.000Z"}}}},"400":{"$ref":"#/components/responses/ValidationError"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/RateLimited"}}},"get":{"operationId":"listThreads","summary":"List threads","tags":["Threads"],"description":"Returns up to `limit` threads (default 50, max 100), reverse-chronological. `next_cursor` is non-null when more pages exist. Array filters (`status`, `category`, `tag_ids`) accept repeated query params (e.g. `?status=open&status=pending`).","security":[{"bearerAuth":["support:read"]}],"parameters":[{"name":"status","in":"query","description":"Repeat for OR.","schema":{"oneOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}]}},{"name":"category","in":"query","schema":{"oneOf":[{"$ref":"#/components/schemas/SupportCategory"},{"type":"array","items":{"$ref":"#/components/schemas/SupportCategory"}}]}},{"name":"tag_ids","in":"query","schema":{"oneOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}]}},{"name":"tag_match","in":"query","schema":{"type":"string","enum":["any","all"],"default":"any"}},{"name":"reporter_id","in":"query","schema":{"type":"string"}},{"name":"reporter_email","in":"query","schema":{"type":"string"}},{"name":"sla_breached","in":"query","schema":{"type":"boolean"}},{"name":"is_suspected_spam","in":"query","schema":{"type":"boolean"}},{"name":"q","in":"query","description":"Full-text search.","schema":{"type":"string","maxLength":200}},{"name":"created_after","in":"query","schema":{"type":"string","format":"date-time"}},{"name":"created_before","in":"query","schema":{"type":"string","format":"date-time"}},{"name":"cursor","in":"query","schema":{"type":"string"}},{"name":"limit","in":"query","schema":{"type":"integer","minimum":1,"maximum":100,"default":50}}],"responses":{"200":{"description":"Paginated list","headers":{"X-Plurism-Request-Id":{"$ref":"#/components/headers/RequestId"}},"content":{"application/json":{"schema":{"type":"object","required":["data","next_cursor"],"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/SupportThread"}},"next_cursor":{"type":["string","null"]}}}}}},"400":{"$ref":"#/components/responses/ValidationError"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/support/threads/bulk-update":{"post":{"operationId":"bulkUpdateThreads","summary":"Bulk-update up to 100 threads","tags":["Threads"],"security":[{"bearerAuth":["support:write"]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BulkUpdateInput"},"example":{"ids":["thr_01a","thr_01b"],"status":"resolved"}}}},"responses":{"200":{"description":"Per-id outcome","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/BulkUpdateResult"}}}}}},"400":{"$ref":"#/components/responses/ValidationError"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/support/threads/bulk-delete":{"post":{"operationId":"bulkDeleteThreads","summary":"Bulk soft-delete up to 100 threads","tags":["Threads"],"security":[{"bearerAuth":["support:delete"]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["ids"],"properties":{"ids":{"type":"array","items":{"type":"string"},"minItems":1,"maxItems":100}}},"example":{"ids":["thr_01a","thr_01b"]}}}},"responses":{"200":{"description":"Per-id outcome","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/BulkDeleteResult"}}}}}},"400":{"$ref":"#/components/responses/ValidationError"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/support/threads/{id}":{"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"get":{"operationId":"getThread","summary":"Get a thread by id","tags":["Threads"],"security":[{"bearerAuth":["support:read"]}],"responses":{"200":{"description":"Thread","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/SupportThread"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"}}},"patch":{"operationId":"updateThread","summary":"Update a thread","tags":["Threads"],"security":[{"bearerAuth":["support:write"]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateThreadInput"},"example":{"status":"pending","tags":["mobile","auth","p1"]}}}},"responses":{"200":{"description":"Updated thread","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/SupportThread"}}}}}},"400":{"$ref":"#/components/responses/ValidationError"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"}}},"delete":{"operationId":"deleteThread","summary":"Soft-delete a thread","tags":["Threads"],"security":[{"bearerAuth":["support:delete"]}],"responses":{"204":{"description":"Deleted"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"}}}},"/support/threads/{id}/restore":{"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"post":{"operationId":"restoreThread","summary":"Restore a soft-deleted thread","tags":["Threads"],"security":[{"bearerAuth":["support:delete"]}],"responses":{"200":{"description":"Restored thread","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/SupportThread"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"}}}},"/support/threads/{id}/audit":{"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"get":{"operationId":"listThreadAudit","summary":"List the audit log for a thread","tags":["Audit"],"description":"Soft-deleted threads keep their audit readable so admins can investigate post-delete.","security":[{"bearerAuth":["support:read"]}],"parameters":[{"name":"cursor","in":"query","schema":{"type":"string"}},{"name":"limit","in":"query","schema":{"type":"integer","minimum":1,"maximum":500,"default":100}}],"responses":{"200":{"description":"Paginated audit entries","content":{"application/json":{"schema":{"type":"object","required":["data","next_cursor"],"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/SupportAuditEntry"}},"next_cursor":{"type":["string","null"]}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"}}}},"/support/threads/{id}/messages":{"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"get":{"operationId":"listMessages","summary":"List messages on a thread","tags":["Messages"],"description":"Add `?include=internal` to include internal-visibility messages. Requires `support:admin`.","security":[{"bearerAuth":["support:read"]}],"parameters":[{"name":"include","in":"query","schema":{"type":"string","enum":["internal"]}}],"responses":{"200":{"description":"Messages","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/SupportMessage"}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"}}},"post":{"operationId":"createMessage","summary":"Reply on a thread","tags":["Messages"],"description":"Public messages email the reporter and fire `support.thread.replied`. Internal messages skip both. `visibility=internal` requires `support:admin`.","security":[{"bearerAuth":["support:write"]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateMessageInput"},"example":{"body":"Thanks — we've shipped a fix in v2.4.1.","visibility":"public"}}}},"responses":{"201":{"description":"Created message + email dispatch status","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateMessageResponse"}}}},"400":{"$ref":"#/components/responses/ValidationError"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"}}}},"/support/threads/{id}/messages/{mid}":{"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}},{"name":"mid","in":"path","required":true,"schema":{"type":"string"}}],"patch":{"operationId":"editMessage","summary":"Edit a message body within the 5-minute window","tags":["Messages"],"security":[{"bearerAuth":["support:write"]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/EditMessageInput"},"example":{"body":"Edited: thanks — fix is live in v2.4.2."}}}},"responses":{"200":{"description":"Edited message","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/SupportMessage"}}}}}},"400":{"$ref":"#/components/responses/ValidationError"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"description":"EDIT_WINDOW_EXPIRED","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"EDIT_WINDOW_EXPIRED","message":"Message can only be edited within 5 minutes of creation"}}}}}}}},"/support/tags":{"get":{"operationId":"listTags","summary":"List tags with usage counts","tags":["Tags"],"security":[{"bearerAuth":["support:read"]}],"responses":{"200":{"description":"Tags","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/SupportTag"}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}},"post":{"operationId":"createTag","summary":"Create or get a tag (idempotent on name)","tags":["Tags"],"description":"Re-posting the same `name` returns the existing tag. Display fields are merged when supplied.","security":[{"bearerAuth":["support:write"]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateTagInput"},"example":{"name":"p1","display_name":"P1","color":"#dc2626"}}}},"responses":{"201":{"description":"Tag","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/SupportTag"}}}}}},"400":{"$ref":"#/components/responses/ValidationError"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"}}}},"/support/tags/{id}":{"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"patch":{"operationId":"updateTag","summary":"Update tag display fields","tags":["Tags"],"description":"`name` is immutable — to rename, soft-delete and re-create.","security":[{"bearerAuth":["support:write"]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateTagInput"}}}},"responses":{"200":{"description":"Tag","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/SupportTag"}}}}}},"400":{"$ref":"#/components/responses/ValidationError"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"}}},"delete":{"operationId":"deleteTag","summary":"Soft-delete a tag","tags":["Tags"],"security":[{"bearerAuth":["support:delete"]}],"responses":{"204":{"description":"Deleted"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"}}}},"/support/reporters":{"get":{"operationId":"listReporters","summary":"Search reporters by email or display name","tags":["Reporters"],"security":[{"bearerAuth":["support:read"]}],"parameters":[{"name":"q","in":"query","schema":{"type":"string","maxLength":200}},{"name":"cursor","in":"query","schema":{"type":"string"}},{"name":"limit","in":"query","schema":{"type":"integer","minimum":1,"maximum":100,"default":50}}],"responses":{"200":{"description":"Paginated reporters","content":{"application/json":{"schema":{"type":"object","required":["data","next_cursor"],"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/SupportReporter"}},"next_cursor":{"type":["string","null"]}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}}},"/support/reporters/merge":{"post":{"operationId":"mergeReporters","summary":"Merge two reporters; secondary's threads repoint to primary","tags":["Reporters"],"security":[{"bearerAuth":["support:admin"]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MergeReportersInput"},"example":{"primary_id":"rep_01a","secondary_id":"rep_01b"}}}},"responses":{"200":{"description":"Merge result","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"type":"object","required":["primary_id","threads_repointed"],"properties":{"primary_id":{"type":"string"},"threads_repointed":{"type":"integer"}}}}}}}},"400":{"$ref":"#/components/responses/ValidationError"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"description":"REPORTER_ALREADY_MERGED","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"REPORTER_ALREADY_MERGED","message":"Secondary reporter has already been merged"}}}}}}}},"/support/reporters/{id}/purge":{"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"delete":{"operationId":"purgeReporter","summary":"GDPR purge — irreversible","tags":["Reporters"],"description":"Removes the reporter, their threads, messages, and attachment bytes. Recorded in audit but PII is scrubbed.","security":[{"bearerAuth":["support:admin"]}],"responses":{"204":{"description":"Purged"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"}}}},"/support/reporters/{id}":{"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"get":{"operationId":"getReporter","summary":"Get reporter detail with thread list","tags":["Reporters"],"security":[{"bearerAuth":["support:read"]}],"responses":{"200":{"description":"Reporter + threads","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"type":"object","required":["reporter","threads"],"properties":{"reporter":{"$ref":"#/components/schemas/SupportReporter"},"threads":{"type":"array","items":{"$ref":"#/components/schemas/SupportThread"}}}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"}}},"patch":{"operationId":"updateReporter","summary":"Update reporter display name, notes, alt emails","tags":["Reporters"],"security":[{"bearerAuth":["support:write"]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateReporterInput"}}}},"responses":{"200":{"description":"Updated reporter","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/SupportReporter"}}}}}},"400":{"$ref":"#/components/responses/ValidationError"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"description":"REPORTER_ALT_EMAIL_CONFLICT","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"object","required":["code","message","conflicting_emails"],"properties":{"code":{"type":"string","enum":["REPORTER_ALT_EMAIL_CONFLICT"]},"message":{"type":"string"},"conflicting_emails":{"type":"array","items":{"type":"string"}}}}}}}}}}}},"/support/saved-views":{"get":{"operationId":"listSavedViews","summary":"List saved views (pinned first, then sort_order, then name)","tags":["Saved views"],"security":[{"bearerAuth":["support:read"]}],"responses":{"200":{"description":"Saved views","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/SupportSavedView"}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}},"post":{"operationId":"createSavedView","summary":"Create a saved view","tags":["Saved views"],"security":[{"bearerAuth":["support:write"]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateSavedViewInput"}}}},"responses":{"201":{"description":"Saved view","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/SupportSavedView"}}}}}},"400":{"$ref":"#/components/responses/ValidationError"},"401":{"$ref":"#/components/responses/Unauthorized"},"409":{"description":"SAVED_VIEW_DUPLICATE_NAME","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/support/saved-views/{id}":{"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"patch":{"operationId":"updateSavedView","summary":"Update a saved view","tags":["Saved views"],"security":[{"bearerAuth":["support:write"]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateSavedViewInput"}}}},"responses":{"200":{"description":"Saved view","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/SupportSavedView"}}}}}},"400":{"$ref":"#/components/responses/ValidationError"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"description":"SAVED_VIEW_DUPLICATE_NAME","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"delete":{"operationId":"deleteSavedView","summary":"Soft-delete a saved view","tags":["Saved views"],"security":[{"bearerAuth":["support:delete"]}],"responses":{"204":{"description":"Deleted"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"}}}},"/support/custom-statuses":{"get":{"operationId":"listCustomStatuses","summary":"List custom statuses","tags":["Custom statuses"],"security":[{"bearerAuth":["support:read"]}],"responses":{"200":{"description":"Statuses","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/SupportCustomStatus"}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}},"post":{"operationId":"createCustomStatus","summary":"Create a custom status","tags":["Custom statuses"],"security":[{"bearerAuth":["support:admin"]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateCustomStatusInput"},"example":{"name":"in-review","display_name":"In review","color":"#0ea5e9","sort_order":10,"treats_as":"open"}}}},"responses":{"201":{"description":"Custom status","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/SupportCustomStatus"}}}}}},"400":{"$ref":"#/components/responses/ValidationError"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"409":{"description":"CUSTOM_STATUS_DUPLICATE","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/support/custom-statuses/{id}":{"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"patch":{"operationId":"updateCustomStatus","summary":"Update a custom status","tags":["Custom statuses"],"security":[{"bearerAuth":["support:admin"]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateCustomStatusInput"}}}},"responses":{"200":{"description":"Custom status","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/SupportCustomStatus"}}}}}},"400":{"$ref":"#/components/responses/ValidationError"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"description":"CUSTOM_STATUS_DUPLICATE","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"delete":{"operationId":"deleteCustomStatus","summary":"Soft-delete a custom status","tags":["Custom statuses"],"security":[{"bearerAuth":["support:admin"]}],"responses":{"204":{"description":"Deleted"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"}}}},"/support/metadata-schema":{"get":{"operationId":"getMetadataSchema","summary":"Get the per-project metadata schema","tags":["Metadata schema"],"description":"Returns `{ schema: null }` when unset.","security":[{"bearerAuth":["support:read"]}],"responses":{"200":{"description":"Schema (or null)","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"type":"object","required":["schema"],"properties":{"schema":{"oneOf":[{"$ref":"#/components/schemas/SupportMetadataSchema"},{"type":"null"}]}}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}},"put":{"operationId":"setMetadataSchema","summary":"Replace the per-project metadata schema","tags":["Metadata schema"],"security":[{"bearerAuth":["support:admin"]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SupportMetadataSchema"},"example":{"fields":[{"key":"order_id","type":"string","required":true},{"key":"tier","type":"enum","required":false,"enum_values":["free","pro","enterprise"]}],"allow_unknown_fields":false}}}},"responses":{"200":{"description":"Resulting schema","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"type":"object","required":["schema"],"properties":{"schema":{"$ref":"#/components/schemas/SupportMetadataSchema"}}}}}}}},"400":{"$ref":"#/components/responses/ValidationError"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"}}}},"/support/lookup/{ref}":{"parameters":[{"name":"ref","in":"path","required":true,"schema":{"type":"string"}},{"name":"t","in":"query","required":true,"description":"Capability token for this thread. Refs are `<PREFIX>-<N>` and therefore enumerable, so the ref alone authorises nothing — every public support route requires this token and returns 404 without it (404, not 403, so a wrong token cannot be used to confirm a ref exists). It is minted at thread creation and delivered in the `lookup_url` returned by `POST /support/threads`. Treat the resulting URL as a secret link.","schema":{"type":"string","pattern":"^[0-9a-f]{32}$"}}],"get":{"operationId":"lookupThreadPublic","summary":"Public lookup for a thread by ref","tags":["Public lookup"],"description":"Content-negotiated. `Accept: application/json` returns the public DTO; otherwise returns mobile-first server-rendered HTML for the reporter to view their thread and reply.","security":[],"responses":{"200":{"description":"Public thread (JSON or HTML)","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/PublicSupportThread"}}}},"text/html":{"schema":{"type":"string"}}}},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/support/lookup/{ref}/messages":{"parameters":[{"name":"ref","in":"path","required":true,"schema":{"type":"string"}},{"name":"t","in":"query","required":true,"description":"Capability token for this thread. Refs are `<PREFIX>-<N>` and therefore enumerable, so the ref alone authorises nothing — every public support route requires this token and returns 404 without it (404, not 403, so a wrong token cannot be used to confirm a ref exists). It is minted at thread creation and delivered in the `lookup_url` returned by `POST /support/threads`. Treat the resulting URL as a secret link.","schema":{"type":"string","pattern":"^[0-9a-f]{32}$"}}],"post":{"operationId":"publicReply","summary":"Public reporter reply (auto-reopens closed/resolved threads)","tags":["Public lookup"],"description":"Accepts both `application/json` (SDK / fetch) and `application/x-www-form-urlencoded` (HTML form). Turnstile-gated when `TURNSTILE_SITE_KEY` is configured. Fires `support.thread.replied`.","security":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["body"],"properties":{"body":{"type":"string","minLength":1,"maxLength":10000},"turnstile_token":{"type":"string"},"attachment_ids":{"type":"array","items":{"type":"string"}}}}},"application/x-www-form-urlencoded":{"schema":{"type":"object","required":["body"],"properties":{"body":{"type":"string","minLength":1,"maxLength":10000},"cf-turnstile-response":{"type":"string"},"attachment_ids":{"type":"array","items":{"type":"string"}}}}}}},"responses":{"201":{"description":"Reply persisted","content":{"application/json":{"schema":{"type":"object","required":["data","thread_reopened"],"properties":{"data":{"$ref":"#/components/schemas/PublicSupportMessage"},"thread_reopened":{"type":"boolean"}}}}}},"400":{"$ref":"#/components/responses/ValidationError"},"403":{"description":"Turnstile rejected","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/support/lookup/{ref}/attachments":{"parameters":[{"name":"ref","in":"path","required":true,"schema":{"type":"string"}},{"name":"t","in":"query","required":true,"description":"Capability token for this thread. Refs are `<PREFIX>-<N>` and therefore enumerable, so the ref alone authorises nothing — every public support route requires this token and returns 404 without it (404, not 403, so a wrong token cannot be used to confirm a ref exists). It is minted at thread creation and delivered in the `lookup_url` returned by `POST /support/threads`. Treat the resulting URL as a secret link.","schema":{"type":"string","pattern":"^[0-9a-f]{32}$"}}],"post":{"operationId":"publicUploadAttachment","summary":"Upload an attachment for a public reply","tags":["Public lookup"],"description":"Two-phase: R2 first then D1, with compensating R2 delete on D1 failure. Returns the file id the reporter then references in `attachment_ids` on their next reply.","security":[],"requestBody":{"required":true,"content":{"multipart/form-data":{"schema":{"type":"object","required":["file"],"properties":{"file":{"type":"string","format":"binary"}}}}}},"responses":{"201":{"description":"File created","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"type":"object","required":["file_id"],"properties":{"file_id":{"type":"string"}}}}}}}},"400":{"$ref":"#/components/responses/ValidationError"},"404":{"$ref":"#/components/responses/NotFound"},"413":{"description":"PAYLOAD_TOO_LARGE","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/support/lookup/{ref}/attachments/{fid}":{"parameters":[{"name":"ref","in":"path","required":true,"schema":{"type":"string"}},{"name":"fid","in":"path","required":true,"schema":{"type":"string"}},{"name":"t","in":"query","required":true,"description":"Capability token for this thread. Refs are `<PREFIX>-<N>` and therefore enumerable, so the ref alone authorises nothing — every public support route requires this token and returns 404 without it (404, not 403, so a wrong token cannot be used to confirm a ref exists). It is minted at thread creation and delivered in the `lookup_url` returned by `POST /support/threads`. Treat the resulting URL as a secret link.","schema":{"type":"string","pattern":"^[0-9a-f]{32}$"}}],"get":{"operationId":"publicDownloadAttachment","summary":"Stream attachment bytes (public-visibility only)","tags":["Public lookup"],"description":"Internal-message attachments 404. Filename header is sanitised to defang header injection. SVGs are forced to download with a strict CSP.","security":[],"responses":{"200":{"description":"Attachment bytes","content":{"application/octet-stream":{"schema":{"type":"string","format":"binary"}}}},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/support/inbound/email":{"post":{"operationId":"inboundEmail","summary":"Resend inbound webhook (Svix-signed)","tags":["Inbound webhooks"],"description":"Verifies the Svix signature against the raw body. Drops with 200 (so Resend stops retrying) when the to-address has no parseable ref or the thread is missing/deleted. Auto-reopens closed/resolved threads when the reporter replies. Fires `support.thread.replied` (always) and `support.thread.status_changed` (on reopen).","security":[],"parameters":[{"name":"svix-id","in":"header","schema":{"type":"string"},"required":true},{"name":"svix-timestamp","in":"header","schema":{"type":"string"},"required":true},{"name":"svix-signature","in":"header","schema":{"type":"string"},"required":true}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/InboundEmailPayload"}}}},"responses":{"200":{"description":"Processed (or intentionally dropped)","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"type":"object","properties":{"dropped":{"type":"boolean"},"reason":{"type":"string"},"message_id":{"type":"string"},"thread_reopened":{"type":"boolean"}}}}}}}},"400":{"$ref":"#/components/responses/ValidationError"},"401":{"description":"SIGNATURE_INVALID","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/flags":{"get":{"operationId":"listFlags","summary":"List feature flags","tags":["Feature flags"],"security":[{"bearerAuth":["flags:read"]}],"responses":{"200":{"description":"Flags","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/FeatureFlag"}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}},"post":{"operationId":"createFlag","summary":"Create a feature flag","tags":["Feature flags"],"security":[{"bearerAuth":["flags:admin"]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateFlagInput"},"example":{"key":"new-checkout","description":"New checkout flow","enabled":true,"rollout_percent":25}}}},"responses":{"201":{"description":"Flag","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/FeatureFlag"}}}}}},"400":{"$ref":"#/components/responses/ValidationError"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"409":{"description":"CONFLICT — Flag key already exists","headers":{"X-Plurism-Request-Id":{"$ref":"#/components/headers/RequestId"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"CONFLICT","message":"Flag key already exists"}}}}}}}},"/flags/{key}":{"parameters":[{"name":"key","in":"path","required":true,"schema":{"type":"string"}}],"patch":{"operationId":"updateFlag","summary":"Update a flag","tags":["Feature flags"],"security":[{"bearerAuth":["flags:admin"]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateFlagInput"}}}},"responses":{"200":{"description":"Flag","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/FeatureFlag"}}}}}},"400":{"$ref":"#/components/responses/ValidationError"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"}}},"delete":{"operationId":"deleteFlag","summary":"Soft-delete a flag","tags":["Feature flags"],"security":[{"bearerAuth":["flags:admin"]}],"responses":{"204":{"description":"Deleted"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"}}}},"/flags/{key}/evaluate":{"parameters":[{"name":"key","in":"path","required":true,"schema":{"type":"string"}}],"get":{"operationId":"evaluateFlag","summary":"Evaluate one flag for a user","tags":["Feature flags"],"description":"KV hot-path cached. A missing flag returns `enabled:false, reason:\"not_found\"` rather than a 404, so callers can safely query optional flags.","security":[{"bearerAuth":["flags:read"]}],"parameters":[{"name":"user_id","in":"query","required":false,"schema":{"type":"string","minLength":1,"maxLength":120},"description":"Bucketing identity. Without it, any rollout below 100% evaluates false."}],"responses":{"200":{"description":"Evaluation","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/FlagEvaluation"}}}}}},"400":{"$ref":"#/components/responses/ValidationError"},"401":{"$ref":"#/components/responses/Unauthorized"}}}},"/flags/evaluate":{"post":{"operationId":"evaluateFlags","summary":"Evaluate up to 100 flags at once","tags":["Feature flags"],"security":[{"bearerAuth":["flags:read"]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BulkEvaluateInput"},"example":{"user_id":"usr_123","keys":["new-checkout","dark-mode"]}}}},"responses":{"200":{"description":"Map of key → evaluation","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"type":"object","additionalProperties":{"$ref":"#/components/schemas/FlagEvaluation"}}}}}}},"400":{"$ref":"#/components/responses/ValidationError"},"401":{"$ref":"#/components/responses/Unauthorized"}}}},"/waitlist":{"post":{"operationId":"addWaitlistEntry","summary":"Add a waitlist entry","tags":["Waitlist"],"description":"Fires `waitlist.entry_added`. Call this from your backend — the Plurism API key must never ship to a browser. For a browser-side form use the `<plurism-waitlist>` element and its publishable key instead.","security":[{"bearerAuth":["waitlist:write"]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AddWaitlistEntryInput"},"example":{"email":"ada@example.com","name":"Ada","referrer":"launch-post"}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/WaitlistCreated"}}}}}},"400":{"$ref":"#/components/responses/ValidationError"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"409":{"description":"CONFLICT — Email already on the waitlist","headers":{"X-Plurism-Request-Id":{"$ref":"#/components/headers/RequestId"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"CONFLICT","message":"Email already on the waitlist"}}}}}}},"get":{"operationId":"listWaitlistEntries","summary":"List waitlist entries","tags":["Waitlist"],"security":[{"bearerAuth":["waitlist:read"]}],"parameters":[{"name":"status","in":"query","required":false,"schema":{"type":"string","enum":["waiting","invited","rejected"]},"description":"Filter by status."},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":500,"default":100},"description":"Max rows. Values outside 1–500 are rejected (500 clamps on some list routes)."}],"responses":{"200":{"description":"Entries","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/WaitlistEntry"}}}}}}},"400":{"$ref":"#/components/responses/ValidationError"},"401":{"$ref":"#/components/responses/Unauthorized"}}}},"/waitlist/{id}":{"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"get":{"operationId":"getWaitlistEntry","summary":"Get a waitlist entry","tags":["Waitlist"],"security":[{"bearerAuth":["waitlist:read"]}],"responses":{"200":{"description":"Entry","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/WaitlistEntry"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"}}},"patch":{"operationId":"updateWaitlistEntry","summary":"Update a waitlist entry","tags":["Waitlist"],"security":[{"bearerAuth":["waitlist:admin"]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateWaitlistEntryInput"}}}},"responses":{"200":{"description":"Entry","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/WaitlistEntry"}}}}}},"400":{"$ref":"#/components/responses/ValidationError"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"}}},"delete":{"operationId":"deleteWaitlistEntry","summary":"Soft-delete a waitlist entry","tags":["Waitlist"],"security":[{"bearerAuth":["waitlist:admin"]}],"responses":{"204":{"description":"Deleted"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"}}}},"/waitlist/{id}/invite":{"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"post":{"operationId":"inviteWaitlistEntry","summary":"Invite a waitlist entry","tags":["Waitlist"],"description":"Flips status to `invited` and fires `waitlist.invited`, which normally routes to an email notifier. Idempotent: the transition is an atomic claim, so concurrent calls send exactly one email and the losers return the unchanged entry.","security":[{"bearerAuth":["waitlist:admin"]}],"responses":{"200":{"description":"Entry","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/WaitlistEntry"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"}}}},"/changelog":{"get":{"operationId":"listChangelogEntries","summary":"List changelog entries","tags":["Changelog"],"security":[{"bearerAuth":["changelog:read"]}],"parameters":[{"name":"state","in":"query","required":false,"schema":{"type":"string","enum":["published","draft"]},"description":"Filter by publish state."},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":500,"default":100},"description":"Max rows. Values outside 1–500 are rejected (500 clamps on some list routes)."}],"responses":{"200":{"description":"Entries","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/ChangelogEntry"}}}}}}},"400":{"$ref":"#/components/responses/ValidationError"},"401":{"$ref":"#/components/responses/Unauthorized"}}},"post":{"operationId":"createChangelogEntry","summary":"Create a changelog entry","tags":["Changelog"],"security":[{"bearerAuth":["changelog:write"]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateChangelogInput"},"example":{"slug":"v2-launch","title":"v2 is live","body_markdown":"## What's new\n\n- Faster search","publish":true}}}},"responses":{"201":{"description":"Entry","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/ChangelogEntry"}}}}}},"400":{"$ref":"#/components/responses/ValidationError"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"409":{"description":"CONFLICT — Slug already exists in this project","headers":{"X-Plurism-Request-Id":{"$ref":"#/components/headers/RequestId"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"CONFLICT","message":"Slug already exists in this project"}}}}}}}},"/changelog/{id}":{"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"get":{"operationId":"getChangelogEntry","summary":"Get a changelog entry","tags":["Changelog"],"security":[{"bearerAuth":["changelog:read"]}],"responses":{"200":{"description":"Entry","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/ChangelogEntry"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"}}},"patch":{"operationId":"updateChangelogEntry","summary":"Update a changelog entry","tags":["Changelog"],"security":[{"bearerAuth":["changelog:write"]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateChangelogInput"}}}},"responses":{"200":{"description":"Entry","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/ChangelogEntry"}}}}}},"400":{"$ref":"#/components/responses/ValidationError"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"description":"CONFLICT — Slug already exists in this project","headers":{"X-Plurism-Request-Id":{"$ref":"#/components/headers/RequestId"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"CONFLICT","message":"Slug already exists in this project"}}}}}}},"delete":{"operationId":"deleteChangelogEntry","summary":"Soft-delete a changelog entry","tags":["Changelog"],"security":[{"bearerAuth":["changelog:delete"]}],"responses":{"204":{"description":"Deleted"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"}}}},"/changelog/{id}/publish":{"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"post":{"operationId":"publishChangelogEntry","summary":"Publish a changelog entry","tags":["Changelog"],"description":"Idempotent. `changelog.published` fires only on the draft → published transition, and re-publishing preserves the original `published_at`.","security":[{"bearerAuth":["changelog:write"]}],"responses":{"200":{"description":"Entry","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/ChangelogEntry"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"}}}},"/public/changelog/{project_slug}":{"parameters":[{"name":"project_slug","in":"path","required":true,"schema":{"type":"string"}}],"get":{"operationId":"listPublicChangelog","summary":"Public changelog feed for a project","tags":["Public changelog"],"description":"Unauthenticated and `Access-Control-Allow-Origin: *`, so it can be fetched straight from a marketing site. Drafts and soft-deleted entries are never served. Cached 60s at the edge; `X-Cache: HIT|MISS` reports which.","security":[],"parameters":[{"name":"limit","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":500,"default":100},"description":"Max rows. Values outside 1–500 are rejected (500 clamps on some list routes)."}],"responses":{"200":{"description":"Published entries, newest first","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"type":"object","required":["project","entries"],"properties":{"project":{"$ref":"#/components/schemas/PublicProjectRef"},"entries":{"type":"array","items":{"$ref":"#/components/schemas/PublicChangelogEntry"}}}}}}}}},"404":{"$ref":"#/components/responses/NotFound"}}}},"/public/changelog/{project_slug}/{entry_slug}":{"parameters":[{"name":"project_slug","in":"path","required":true,"schema":{"type":"string"}},{"name":"entry_slug","in":"path","required":true,"schema":{"type":"string"}}],"get":{"operationId":"getPublicChangelogEntry","summary":"Public changelog entry by slug","tags":["Public changelog"],"security":[],"responses":{"200":{"description":"Published entry","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"type":"object","required":["project","entry"],"properties":{"project":{"$ref":"#/components/schemas/PublicProjectRef"},"entry":{"$ref":"#/components/schemas/PublicChangelogEntry"}}}}}}}},"400":{"$ref":"#/components/responses/ValidationError"},"404":{"$ref":"#/components/responses/NotFound"}}}},"/content":{"get":{"operationId":"listContent","summary":"List content entries","tags":["Content"],"security":[{"bearerAuth":["content:read"]}],"parameters":[{"name":"type","in":"query","required":false,"schema":{"$ref":"#/components/schemas/ContentType"},"description":"Filter by content type."},{"name":"state","in":"query","required":false,"schema":{"type":"string","enum":["published","draft"]},"description":"Filter by publish state."},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":500,"default":100},"description":"Max rows. Values outside 1–500 are rejected (500 clamps on some list routes)."}],"responses":{"200":{"description":"Entries","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/ContentEntry"}}}}}}},"400":{"$ref":"#/components/responses/ValidationError"},"401":{"$ref":"#/components/responses/Unauthorized"}}},"post":{"operationId":"createContent","summary":"Create a content entry","tags":["Content"],"security":[{"bearerAuth":["content:write"]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateContentInput"},"example":{"type":"article","slug":"why-aeo-matters","title":"Why AEO matters","excerpt":"Answer engines read structure.","body_markdown":"Search is changing…","tags":["seo","aeo"],"publish":true}}}},"responses":{"201":{"description":"Entry","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/ContentEntry"}}}}}},"400":{"$ref":"#/components/responses/ValidationError"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"409":{"description":"CONFLICT — Slug already exists in this project","headers":{"X-Plurism-Request-Id":{"$ref":"#/components/headers/RequestId"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"CONFLICT","message":"Slug already exists in this project"}}}}}}}},"/content/{slug}":{"parameters":[{"name":"slug","in":"path","required":true,"schema":{"type":"string"}}],"get":{"operationId":"getContent","summary":"Get a content entry by slug","tags":["Content"],"security":[{"bearerAuth":["content:read"]}],"responses":{"200":{"description":"Entry","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/ContentEntry"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"}}},"patch":{"operationId":"updateContent","summary":"Update a content entry","tags":["Content"],"security":[{"bearerAuth":["content:write"]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateContentInput"}}}},"responses":{"200":{"description":"Entry","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/ContentEntry"}}}}}},"400":{"$ref":"#/components/responses/ValidationError"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"description":"CONFLICT — Slug already exists in this project","headers":{"X-Plurism-Request-Id":{"$ref":"#/components/headers/RequestId"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"CONFLICT","message":"Slug already exists in this project"}}}}}}},"delete":{"operationId":"deleteContent","summary":"Soft-delete a content entry","tags":["Content"],"security":[{"bearerAuth":["content:delete"]}],"responses":{"204":{"description":"Deleted"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"}}}},"/public/content/{project_slug}":{"parameters":[{"name":"project_slug","in":"path","required":true,"schema":{"type":"string"}}],"get":{"operationId":"listPublicContent","summary":"Public content index for a project","tags":["Public content"],"description":"Unauthenticated, CORS-open, 60s edge cache. This is the JSON your marketing site embeds. Bodies are omitted from the index — fetch a single entry for those.","security":[],"parameters":[{"name":"type","in":"query","required":false,"schema":{"$ref":"#/components/schemas/ContentType"},"description":"Filter by content type."},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":500,"default":100},"description":"Max rows. Values outside 1–500 are rejected (500 clamps on some list routes)."}],"responses":{"200":{"description":"Published entries, newest first","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"type":"object","required":["project","entries"],"properties":{"project":{"$ref":"#/components/schemas/PublicProjectRef"},"entries":{"type":"array","items":{"$ref":"#/components/schemas/PublicContentListEntry"}}}}}}}}},"404":{"$ref":"#/components/responses/NotFound"}}}},"/public/content/{project_slug}/{entry_slug}":{"parameters":[{"name":"project_slug","in":"path","required":true,"schema":{"type":"string"}},{"name":"entry_slug","in":"path","required":true,"schema":{"type":"string"}}],"get":{"operationId":"getPublicContentEntry","summary":"Public content entry by slug","tags":["Public content"],"security":[],"parameters":[{"name":"base_url","in":"query","required":false,"schema":{"type":"string","format":"uri"},"description":"Absolute site origin used to build absolute URLs inside the JSON-LD."}],"responses":{"200":{"description":"Published entry with rendered HTML and JSON-LD","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"type":"object","required":["project","entry"],"properties":{"project":{"$ref":"#/components/schemas/PublicProjectRef"},"entry":{"$ref":"#/components/schemas/PublicContentEntry"}}}}}}}},"400":{"$ref":"#/components/responses/ValidationError"},"404":{"$ref":"#/components/responses/NotFound"}}}},"/feedback":{"post":{"operationId":"createFeedback","summary":"Submit feedback","tags":["Feedback"],"description":"Server-side submission — stores `source: \"user\"`. No notifier event fires for feedback by design, so a submission flood can't drain the org's notifier quota.","security":[{"bearerAuth":["feedback:write"]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateFeedbackInput"},"example":{"rating":9,"comment":"Fast and clear.","reporter_email":"ada@example.com"}}}},"responses":{"201":{"description":"Entry","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/FeedbackEntry"}}}}}},"400":{"$ref":"#/components/responses/ValidationError"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"}}},"get":{"operationId":"listFeedback","summary":"List feedback","tags":["Feedback"],"security":[{"bearerAuth":["feedback:read"]}],"parameters":[{"name":"source","in":"query","required":false,"schema":{"type":"string","enum":["user","auto-capture"]},"description":"Filter by source."},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":500,"default":100},"description":"Max rows. Values outside 1–500 are rejected (500 clamps on some list routes)."}],"responses":{"200":{"description":"Entries","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/FeedbackEntry"}}}}}}},"400":{"$ref":"#/components/responses/ValidationError"},"401":{"$ref":"#/components/responses/Unauthorized"}}}},"/feedback/auto-capture":{"post":{"operationId":"autoCaptureFeedback","summary":"Submit agent/automation-captured feedback","tags":["Feedback"],"description":"Same body as `POST /feedback`, but the server forces `source: \"auto-capture\"` regardless of the request. Use it for sentiment lifted out of a support thread or an assistant conversation, so it can be filtered apart from feedback a human deliberately submitted.","security":[{"bearerAuth":["feedback:write"]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateFeedbackInput"}}}},"responses":{"201":{"description":"Entry","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/FeedbackEntry"}}}}}},"400":{"$ref":"#/components/responses/ValidationError"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"}}}},"/feedback/stats":{"get":{"operationId":"getFeedbackStats","summary":"NPS summary","tags":["Feedback"],"security":[{"bearerAuth":["feedback:read"]}],"responses":{"200":{"description":"Stats","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/FeedbackStats"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}}},"/feedback/{id}":{"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"delete":{"operationId":"deleteFeedback","summary":"Soft-delete a feedback entry","tags":["Feedback"],"security":[{"bearerAuth":["feedback:admin"]}],"responses":{"204":{"description":"Deleted"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"}}}},"/files":{"post":{"operationId":"uploadFile","summary":"Upload a file","tags":["Files"],"description":"`multipart/form-data` with a single `file` part. Max 10 MB. Allowed types: `image/png`, `image/jpeg`, `image/webp`, `image/gif`, `image/svg+xml`, `application/pdf`, `text/plain`, `text/markdown`, `application/json`.","security":[{"bearerAuth":["files:write"]}],"responses":{"201":{"description":"File","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/FileEntry"}}}}}},"400":{"$ref":"#/components/responses/ValidationError"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"413":{"description":"PAYLOAD_TOO_LARGE — file exceeds 10 MB","headers":{"X-Plurism-Request-Id":{"$ref":"#/components/headers/RequestId"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"PAYLOAD_TOO_LARGE","message":"File exceeds 10485760 bytes"}}}}},"429":{"description":"QUOTA_EXCEEDED — the org is at its `files_bytes` storage ceiling. Storage is reserved before the bytes are written, so a rejected upload leaves nothing behind.","headers":{"X-Plurism-Request-Id":{"$ref":"#/components/headers/RequestId"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"QUOTA_EXCEEDED","message":"Quota exceeded: files_bytes"}}}}}},"requestBody":{"required":true,"content":{"multipart/form-data":{"schema":{"type":"object","required":["file"],"properties":{"file":{"type":"string","format":"binary"}}}}}}},"get":{"operationId":"listFiles","summary":"List files","tags":["Files"],"security":[{"bearerAuth":["files:read"]}],"parameters":[{"name":"limit","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":500,"default":100},"description":"Max rows. Values outside 1–500 are rejected (500 clamps on some list routes)."}],"responses":{"200":{"description":"Files","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/FileEntry"}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}}},"/files/{id}":{"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"get":{"operationId":"getFile","summary":"Get file metadata","tags":["Files"],"security":[{"bearerAuth":["files:read"]}],"responses":{"200":{"description":"File","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/FileEntry"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"}}},"delete":{"operationId":"deleteFile","summary":"Soft-delete a file","tags":["Files"],"security":[{"bearerAuth":["files:write"]}],"responses":{"204":{"description":"Deleted"},"400":{"$ref":"#/components/responses/ValidationError"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"}}}},"/files/{id}/content":{"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"get":{"operationId":"getFileContent","summary":"Download file bytes","tags":["Files"],"description":"Served `Cache-Control: private` with `X-Content-Type-Options: nosniff`. SVGs additionally get `Content-Disposition: attachment` and a locked-down CSP, because an uploaded SVG can carry script and would otherwise execute when opened directly.","security":[{"bearerAuth":["files:read"]}],"responses":{"200":{"description":"File bytes, streamed from object storage","content":{"application/octet-stream":{"schema":{"type":"string","format":"binary"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"}}}},"/files/{id}/optimize":{"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"get":{"operationId":"getOptimizedImage","summary":"Download an image, optionally resized","tags":["Files"],"description":"Images only — a non-image returns 400. The `w`/`f`/`q` query parameters are accepted and preserved so the URL contract is stable, but a Workers-native object read does not pass through edge image resizing, so today the original bytes are returned.","security":[{"bearerAuth":["files:read"]}],"parameters":[{"name":"w","in":"query","required":false,"schema":{"type":"integer"},"description":"Target width in pixels."},{"name":"f","in":"query","required":false,"schema":{"type":"string"},"description":"Target format."},{"name":"q","in":"query","required":false,"schema":{"type":"integer"},"description":"Quality, 1–100."}],"responses":{"200":{"description":"Image bytes","content":{"application/octet-stream":{"schema":{"type":"string","format":"binary"}}}},"400":{"$ref":"#/components/responses/ValidationError"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"}}}},"/notifiers":{"post":{"operationId":"createNotifier","summary":"Create a notifier","tags":["Notifiers"],"security":[{"bearerAuth":["notifications:admin"]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateNotifierInput"},"example":{"type":"discord","name":"Alerts","config":{"webhook_url":"https://discord.com/api/webhooks/…"}}}}},"responses":{"201":{"description":"Notifier","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/Notifier"}}}}}},"400":{"$ref":"#/components/responses/ValidationError"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"}}},"get":{"operationId":"listNotifiers","summary":"List notifiers","tags":["Notifiers"],"security":[{"bearerAuth":["notifications:read"]}],"responses":{"200":{"description":"Notifiers","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Notifier"}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}}},"/notifiers/{id}":{"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"get":{"operationId":"getNotifier","summary":"Get a notifier","tags":["Notifiers"],"security":[{"bearerAuth":["notifications:read"]}],"responses":{"200":{"description":"Notifier","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/Notifier"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"}}},"patch":{"operationId":"updateNotifier","summary":"Update a notifier","tags":["Notifiers"],"security":[{"bearerAuth":["notifications:admin"]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateNotifierInput"}}}},"responses":{"200":{"description":"Notifier","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/Notifier"}}}}}},"400":{"$ref":"#/components/responses/ValidationError"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"}}},"delete":{"operationId":"deleteNotifier","summary":"Soft-delete a notifier","tags":["Notifiers"],"security":[{"bearerAuth":["notifications:admin"]}],"responses":{"204":{"description":"Deleted"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"}}}},"/notifier-rules":{"post":{"operationId":"createNotifierRule","summary":"Route an event to a notifier","tags":["Notifier rules"],"security":[{"bearerAuth":["notifications:admin"]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateNotifierRuleInput"},"example":{"event":"support.thread.created","notifier_id":"ntf_123"}}}},"responses":{"201":{"description":"Rule","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/NotifierRule"}}}}}},"400":{"$ref":"#/components/responses/ValidationError"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"}}},"get":{"operationId":"listNotifierRules","summary":"List notifier rules","tags":["Notifier rules"],"security":[{"bearerAuth":["notifications:read"]}],"responses":{"200":{"description":"Rules","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/NotifierRule"}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}}},"/notifier-rules/{id}":{"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"patch":{"operationId":"updateNotifierRule","summary":"Update a notifier rule","tags":["Notifier rules"],"security":[{"bearerAuth":["notifications:admin"]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateNotifierRuleInput"}}}},"responses":{"200":{"description":"Rule","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/NotifierRule"}}}}}},"400":{"$ref":"#/components/responses/ValidationError"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"}}},"delete":{"operationId":"deleteNotifierRule","summary":"Soft-delete a notifier rule","tags":["Notifier rules"],"security":[{"bearerAuth":["notifications:admin"]}],"responses":{"204":{"description":"Deleted"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"}}}},"/email-templates":{"post":{"operationId":"createEmailTemplate","summary":"Create an email template","tags":["Email templates"],"security":[{"bearerAuth":["notifications:admin"]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateEmailTemplateInput"}}}},"responses":{"201":{"description":"Template","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/EmailTemplate"}}}}}},"400":{"$ref":"#/components/responses/ValidationError"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"}}},"get":{"operationId":"listEmailTemplates","summary":"List email templates","tags":["Email templates"],"security":[{"bearerAuth":["notifications:read"]}],"responses":{"200":{"description":"Templates","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/EmailTemplate"}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}}},"/email-templates/{id}":{"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"get":{"operationId":"getEmailTemplate","summary":"Get an email template","tags":["Email templates"],"security":[{"bearerAuth":["notifications:read"]}],"responses":{"200":{"description":"Template","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/EmailTemplate"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"}}},"patch":{"operationId":"updateEmailTemplate","summary":"Update an email template","tags":["Email templates"],"security":[{"bearerAuth":["notifications:admin"]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateEmailTemplateInput"}}}},"responses":{"200":{"description":"Template","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/EmailTemplate"}}}}}},"400":{"$ref":"#/components/responses/ValidationError"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"}}},"delete":{"operationId":"deleteEmailTemplate","summary":"Soft-delete an email template","tags":["Email templates"],"security":[{"bearerAuth":["notifications:admin"]}],"responses":{"204":{"description":"Deleted"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"}}}},"/delivery-logs":{"get":{"operationId":"listDeliveryLogs","summary":"List notifier delivery attempts","tags":["Notifiers"],"description":"The audit trail for every dispatch: what fired, where it went, and why it failed.","security":[{"bearerAuth":["notifications:read"]}],"parameters":[{"name":"event","in":"query","required":false,"schema":{"type":"string","maxLength":120},"description":"Filter by event name."},{"name":"status","in":"query","required":false,"schema":{"type":"string","enum":["sent","failed","skipped"]},"description":"Filter by outcome."},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":500,"default":100},"description":"Max rows. Values outside 1–500 are rejected (500 clamps on some list routes)."}],"responses":{"200":{"description":"Logs","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/DeliveryLog"}}}}}}},"400":{"$ref":"#/components/responses/ValidationError"},"401":{"$ref":"#/components/responses/Unauthorized"}}}},"/webhook-receivers":{"post":{"operationId":"createWebhookReceiver","summary":"Create an inbound webhook receiver","tags":["Webhook receivers"],"security":[{"bearerAuth":["webhooks:admin"]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateWebhookReceiverInput"},"example":{"slug":"stripe","forwarding_url":"https://api.example.com/hooks/stripe","signing_secret":"whsec_…","signature_header":"Stripe-Signature"}}}},"responses":{"201":{"description":"Receiver","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/WebhookReceiver"}}}}}},"400":{"$ref":"#/components/responses/ValidationError"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"409":{"description":"CONFLICT — Receiver slug already exists","headers":{"X-Plurism-Request-Id":{"$ref":"#/components/headers/RequestId"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"CONFLICT","message":"Receiver slug already exists"}}}}}}},"get":{"operationId":"listWebhookReceivers","summary":"List webhook receivers","tags":["Webhook receivers"],"security":[{"bearerAuth":["webhooks:read"]}],"responses":{"200":{"description":"Receivers","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/WebhookReceiver"}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}}},"/webhook-receivers/{id}":{"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"patch":{"operationId":"updateWebhookReceiver","summary":"Update a webhook receiver","tags":["Webhook receivers"],"security":[{"bearerAuth":["webhooks:admin"]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateWebhookReceiverInput"}}}},"responses":{"200":{"description":"Receiver","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/WebhookReceiver"}}}}}},"400":{"$ref":"#/components/responses/ValidationError"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"description":"CONFLICT — Receiver slug already exists","headers":{"X-Plurism-Request-Id":{"$ref":"#/components/headers/RequestId"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"CONFLICT","message":"Receiver slug already exists"}}}}}}},"delete":{"operationId":"deleteWebhookReceiver","summary":"Soft-delete a webhook receiver","tags":["Webhook receivers"],"security":[{"bearerAuth":["webhooks:admin"]}],"responses":{"204":{"description":"Deleted"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"}}}},"/webhooks":{"get":{"operationId":"listWebhookEvents","summary":"List received webhook events","tags":["Webhook events"],"security":[{"bearerAuth":["webhooks:read"]}],"parameters":[{"name":"status","in":"query","required":false,"schema":{"type":"string","enum":["pending","delivering","delivered","failed","permanent_failure"]},"description":"Filter by delivery status."},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":500,"default":100},"description":"Max rows. Values outside 1–500 are rejected (500 clamps on some list routes)."}],"responses":{"200":{"description":"Events","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/WebhookEvent"}}}}}}},"400":{"$ref":"#/components/responses/ValidationError"},"401":{"$ref":"#/components/responses/Unauthorized"}}}},"/webhooks/{id}":{"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"get":{"operationId":"getWebhookEvent","summary":"Get an event with its delivery attempts","tags":["Webhook events"],"security":[{"bearerAuth":["webhooks:read"]}],"responses":{"200":{"description":"Event and deliveries","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/WebhookEventDetail"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"}}}},"/webhooks/{id}/retry":{"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"post":{"operationId":"retryWebhookEvent","summary":"Re-queue a failed event","tags":["Webhook events"],"description":"Automatic retries back off at 1, 5, 30, 120 and 720 minutes; after those the event moves to `permanent_failure` and stops on its own. This endpoint is how an operator restarts one.","security":[{"bearerAuth":["webhooks:admin"]}],"responses":{"200":{"description":"Event","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/WebhookEvent"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"description":"CONFLICT — Event is not in a retryable state","headers":{"X-Plurism-Request-Id":{"$ref":"#/components/headers/RequestId"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"CONFLICT","message":"Event is not in a retryable state"}}}}}}}},"/webhooks/receive/{project_slug}/{slug}":{"parameters":[{"name":"project_slug","in":"path","required":true,"schema":{"type":"string"}},{"name":"slug","in":"path","required":true,"schema":{"type":"string"}}],"post":{"operationId":"receiveWebhook","summary":"Inbound webhook endpoint","tags":["Inbound webhooks"],"description":"Give this URL to the third party. The raw body is stored verbatim, the signature is verified against the receiver's `signing_secret`, and delivery to your `forwarding_url` happens asynchronously with retries. Any content type is accepted.","security":[],"responses":{"202":{"description":"Accepted and queued for forwarding","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"type":"object","required":["id"],"properties":{"id":{"type":"string"}}}}}}}},"401":{"description":"Signature verification failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"$ref":"#/components/responses/NotFound"}},"requestBody":{"required":true,"content":{"*/*":{"schema":{"type":"string","description":"Raw provider payload."}}}}}},"/usage":{"get":{"operationId":"getUsage","summary":"Current plan, limits and usage","tags":["Usage"],"description":"Any valid key can read its own org's usage; no specific scope is required and the read is never quota-gated, so it still answers when the org is over its limits. Counters reset monthly except `filesBytes`, `projects` and `seats`, which are point-in-time.","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Usage snapshot","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/UsageSnapshot"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}}},"/v1/emails":{"post":{"operationId":"sendEmail","summary":"Send a transactional email","tags":["Email"],"description":"Transactional only. The `from` address must sit on a domain this project has verified via the sending-domains endpoints, so nothing can send until a domain is onboarded. Rate-limited per IP before authentication and per project after it.","security":[{"bearerAuth":["email:send"]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SendEmailInput"},"example":{"from":"hello@example.com","to":"ada@example.com","subject":"Reset your password","html":"<p>Click to reset.</p>","text":"Click to reset.","idempotency_key":"reset-usr_123-1721980800"}}}},"responses":{"202":{"description":"Accepted for delivery","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SendEmailAccepted"}}}},"400":{"$ref":"#/components/responses/ValidationError"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"409":{"description":"Duplicate `idempotency_key` — the original send stands","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"413":{"description":"PAYLOAD_TOO_LARGE — body over 260 KB","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"$ref":"#/components/responses/RateLimited"},"502":{"description":"UPSTREAM_ERROR — transient provider failure. The message row is `failed`, so retrying with the SAME `idempotency_key` is safe.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"Sending is disabled for this environment","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/emails/domains":{"get":{"operationId":"listSendingDomains","summary":"List sending domains","tags":["Email domains"],"security":[{"bearerAuth":["email:admin"]}],"responses":{"200":{"description":"Domains","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/EmailDomain"}}}}}}},"400":{"$ref":"#/components/responses/ValidationError"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"}}},"post":{"operationId":"createSendingDomain","summary":"Register a sending domain","tags":["Email domains"],"description":"Returns the DNS records to publish. Add them; do not remove an existing MX record or another provider's DKIM. The number of active domains per project is capped by plan.","security":[{"bearerAuth":["email:admin"]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateEmailDomainInput"},"example":{"domain":"mail.example.com"}}}},"responses":{"201":{"description":"Domain with the DNS records to publish","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/EmailDomain"}}}}}},"400":{"$ref":"#/components/responses/ValidationError"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"409":{"description":"CONFLICT — Domain already registered","headers":{"X-Plurism-Request-Id":{"$ref":"#/components/headers/RequestId"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"CONFLICT","message":"Domain already registered"}}}}}}}},"/v1/emails/domains/{id}/verify":{"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"post":{"operationId":"verifySendingDomain","summary":"Re-check domain ownership and DKIM","tags":["Email domains"],"description":"Re-resolves the ownership TXT and asks the provider for DKIM status. Safe to poll after publishing the records — DNS propagation usually takes minutes.","security":[{"bearerAuth":["email:admin"]}],"responses":{"200":{"description":"Domain with updated verification state","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/EmailDomain"}}}}}},"400":{"$ref":"#/components/responses/ValidationError"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"}}}},"/v1/a.js":{"get":{"operationId":"getTrackerScript","summary":"Analytics tracker bundle (rolling)","tags":["Analytics"],"description":"Served from `cdn.plurism.dev`. Rolling `latest` — cached 10 minutes with stale-while-revalidate, so fixes reach embedded sites without anyone re-pasting a snippet. Do NOT pin a Subresource Integrity hash to this URL: the bytes change by design and a stale hash would silently break every embed.","security":[],"responses":{"200":{"description":"JavaScript bundle","content":{"application/javascript":{"schema":{"type":"string"}}}}}}},"/v1.0.0/a.js":{"get":{"operationId":"getTrackerScriptPinned","summary":"Analytics tracker bundle (superseded)","tags":["Analytics"],"description":"Superseded by `/v1/a.{hash}.js`. Still served, but it is NOT immutable: it carries whatever the current deploy built, so the previous `immutable, max-age=31536000` header was a promise this path could not keep. It now uses the same rolling cache policy as `/v1/a.js` and advertises its successor via `Deprecation` and `Link` headers. Use the content-addressed URL for anything you intend to pin.","security":[],"responses":{"200":{"description":"JavaScript bundle","content":{"application/javascript":{"schema":{"type":"string"}}}}}}},"/v1/a/e":{"post":{"operationId":"ingestAnalyticsEvents","summary":"Ingest analytics events","tags":["Analytics"],"description":"Browser-facing beacon. Authenticated by a publishable `pa_` key plus an Origin allow-list, so it is safe to ship in client-side code. Accepts `sendBeacon`'s `text/plain` as well as `application/json`. Body cap 64 KB, 50 events per request.","security":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AnalyticsIngestInput"},"example":{"key":"pa_live_…","events":[{"type":"pageview","path":"/pricing","anonymous_id":"a1b2c3","referrer_host":"news.ycombinator.com"}]}}}},"responses":{"202":{"description":"Accepted. Returned even when events are dropped — over the monthly plan ceiling Plurism stops storing but never breaks the customer's page."},"400":{"$ref":"#/components/responses/ValidationError"},"401":{"description":"Publishable key rejected, or Origin not allow-listed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"405":{"description":"Method Not Allowed. `GET` is refused deliberately — link prefetchers and email scanners would otherwise inflate counts."},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/analytics/stripe/webhook/{projectToken}":{"parameters":[{"name":"projectToken","in":"path","required":true,"schema":{"type":"string"},"description":"Unguessable per-project token issued when the integration is connected."}],"post":{"operationId":"receiveStripeWebhook","summary":"Per-project Stripe revenue webhook","tags":["Analytics"],"description":"Point a Stripe endpoint here to attribute revenue to analytics sessions. Authentication is the unguessable token in the path plus per-project Stripe signature verification — not an API key.","security":[],"responses":{"200":{"description":"Processed"},"400":{"description":"Missing or invalid Stripe signature, or malformed JSON"},"404":{"description":"Unknown webhook token"},"500":{"description":"Server not configured for this integration"}},"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"description":"Stripe event payload."}}}}}},"/analytics/traffic":{"get":{"operationId":"getAnalyticsTraffic","summary":"Traffic overview","tags":["Analytics"],"description":"Computed from the rollup table, which a cron rebuilds on a roughly 5-minute cadence, so figures can lag by up to that. `summary.current_visitors` is the exception: it reads raw events so the live figure is not itself five minutes stale.","security":[{"bearerAuth":["analytics:read"]}],"parameters":[{"name":"range","in":"query","required":false,"schema":{"type":"string","enum":["7d","30d","90d"],"default":"7d"},"description":"Window, ending now. An unrecognised value is a 400 rather than a silent fall back to the default. The list stops at 90 days because raw events are pruned at 90 and the funnel reads them, so a longer window would return a 90-day answer under a longer label."}],"responses":{"200":{"description":"Traffic overview","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/AnalyticsTraffic"}}}}}},"400":{"$ref":"#/components/responses/ValidationError"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/analytics/acquisition":{"get":{"operationId":"getAnalyticsAcquisition","summary":"Acquisition breakdown","tags":["Analytics"],"description":"Computed from the rollup table, which a cron rebuilds on a roughly 5-minute cadence, so figures can lag by up to that. Only the top 500 values per dimension per day are stored, so a very long tail is absent from the source data rather than truncated here.","security":[{"bearerAuth":["analytics:read"]}],"parameters":[{"name":"range","in":"query","required":false,"schema":{"type":"string","enum":["7d","30d","90d"],"default":"7d"},"description":"Window, ending now. An unrecognised value is a 400 rather than a silent fall back to the default. The list stops at 90 days because raw events are pruned at 90 and the funnel reads them, so a longer window would return a 90-day answer under a longer label."}],"responses":{"200":{"description":"Acquisition breakdown","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/AnalyticsAcquisition"}}}}}},"400":{"$ref":"#/components/responses/ValidationError"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/analytics/funnel":{"get":{"operationId":"getAnalyticsFunnel","summary":"Visit to signup to activation to paid","tags":["Analytics"],"description":"Reads RAW events, which are pruned at 90 days — the reason no window longer than 90d is offered. Each step counts visitors ACTIVE IN THE WINDOW, so this is not a first-touch cohort: someone who signed up last month and visited today counts at both `visit` and `signup` here.","security":[{"bearerAuth":["analytics:read"]}],"parameters":[{"name":"range","in":"query","required":false,"schema":{"type":"string","enum":["7d","30d","90d"],"default":"7d"},"description":"Window, ending now. An unrecognised value is a 400 rather than a silent fall back to the default. The list stops at 90 days because raw events are pruned at 90 and the funnel reads them, so a longer window would return a 90-day answer under a longer label."}],"responses":{"200":{"description":"Visit to signup to activation to paid","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/AnalyticsFunnel"}}}}}},"400":{"$ref":"#/components/responses/ValidationError"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/analytics/revenue":{"get":{"operationId":"getAnalyticsRevenue","summary":"Revenue summary","tags":["Analytics"],"description":"Derived from your own Stripe events via the per-project webhook. Attribution is attribute-once: the acquisition takes channel credit and renewals carry none.","security":[{"bearerAuth":["analytics:read"]}],"parameters":[{"name":"range","in":"query","required":false,"schema":{"type":"string","enum":["7d","30d","90d"],"default":"7d"},"description":"Window, ending now. An unrecognised value is a 400 rather than a silent fall back to the default. The list stops at 90 days because raw events are pruned at 90 and the funnel reads them, so a longer window would return a 90-day answer under a longer label."}],"responses":{"200":{"description":"Revenue summary","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/AnalyticsRevenue"}}}}}},"400":{"$ref":"#/components/responses/ValidationError"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/v1/elements.js":{"get":{"operationId":"getElementsScript","summary":"Elements web-component bundle (rolling)","tags":["Elements"],"description":"Registers the `<plurism-waitlist>` and `<plurism-feedback>` custom elements. Drop the tag on any page. If the host site sets a Content-Security-Policy it must allow `script-src https://cdn.plurism.dev` and `connect-src https://api.plurism.dev`, or the element silently never registers.","security":[],"responses":{"200":{"description":"JavaScript bundle","content":{"application/javascript":{"schema":{"type":"string"}}}}}}},"/v1.0.0/elements.js":{"get":{"operationId":"getElementsScriptPinned","summary":"Elements bundle (superseded)","tags":["Elements"],"description":"Superseded by `/v1/elements.{hash}.js`. Still served, but it is NOT immutable: it carries whatever the current deploy built, so the previous `immutable, max-age=31536000` header was a promise this path could not keep. It now uses the same rolling cache policy as `/v1/elements.js` and advertises its successor via `Deprecation` and `Link` headers. Use the content-addressed URL for anything you intend to pin.","security":[],"responses":{"200":{"description":"JavaScript bundle","content":{"application/javascript":{"schema":{"type":"string"}}}}}}},"/v1/el/waitlist":{"post":{"operationId":"submitWaitlistWidget","summary":"Waitlist widget submission","tags":["Elements"],"description":"Public endpoint behind `<plurism-waitlist>`. Authenticated by a publishable `pa_` key with `waitlist:ingest` plus an Origin allow-list, and bounded by a per-project rate cap rather than a depletable quota, so a flood cannot exhaust the customer's own allowance. Includes a honeypot field whose rejection is indistinguishable from success.","security":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ElementsWaitlistInput"},"example":{"key":"pa_live_…","email":"ada@example.com","name":"Ada"}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/ElementsCreated"}}}}}},"400":{"$ref":"#/components/responses/ValidationError"},"401":{"description":"Publishable key rejected, or Origin not allow-listed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"405":{"description":"Method Not Allowed"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/v1/el/feedback":{"post":{"operationId":"submitFeedbackWidget","summary":"Feedback widget submission","tags":["Elements"],"description":"Public endpoint behind `<plurism-feedback>`. Requires `feedback:ingest`. `metadata` is deliberately not accepted here — an anonymous browser must not be able to write arbitrary JSON into the tenant's database — and `reporter_email` is untrusted display-only text, so never build reply or merge flows on it.","security":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ElementsFeedbackInput"},"example":{"key":"pa_live_…","rating":9,"comment":"Really useful."}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/ElementsCreated"}}}}}},"400":{"$ref":"#/components/responses/ValidationError"},"401":{"description":"Publishable key rejected, or Origin not allow-listed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"405":{"description":"Method Not Allowed"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/health":{"get":{"operationId":"getHealth","summary":"Liveness probe","tags":["Ops"],"description":"Always 200 while the Worker is reachable. Also reports scheduled-trigger liveness: `cron_stale: true` means webhook retries and pruning have stopped even though HTTP serving looks fine.","security":[],"responses":{"200":{"description":"Service is reachable","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/HealthStatus"}}}}}}}}},"/health/ready":{"get":{"operationId":"getReadiness","summary":"Deep readiness probe","tags":["Ops"],"description":"Round-trips D1, KV and R2. Use it to gate a deploy; use `/health` for high-frequency uptime probes.","security":[],"responses":{"200":{"description":"All dependencies healthy","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/ReadinessStatus"}}}}}},"503":{"description":"One or more dependencies are failing","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/ReadinessStatus"}}}}}}}}},"/openapi.json":{"get":{"operationId":"getOpenapiSpec","summary":"This API description","tags":["Ops"],"description":"The machine-readable description of every endpoint here, served without authentication and CORS-open so an API explorer or a coding agent can read it before it has a key. Cached 5 minutes; the document changes only on deploy.","security":[],"responses":{"200":{"description":"OpenAPI 3.1 document","content":{"application/json":{"schema":{"type":"object","description":"An OpenAPI 3.1 document.","additionalProperties":true}}}}}}},"/v1/keys":{"post":{"operationId":"mintPublishableKey","summary":"Mint a publishable key","tags":["Keys"],"description":"Creates a browser-safe `pa_` key so an agent can finish an integration without a portal session.\n\n**Publishable keys only.** A key that could mint secret keys would be a privilege-escalation primitive, so the mintable scope set is a fixed three-scope allowlist rather than \"a subset of the caller's scopes\" — a key holding every scope in the catalog mints exactly what a key holding only `keys:write` mints. `keys:write` is itself absent from that allowlist, so a minted key can never mint.\n\nSecret keys are minted in the portal by an org admin. The plaintext is returned once and cannot be read back.","security":[{"bearerAuth":["keys:write"]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MintKeyInput"},"example":{"name":"marketing site widgets","allowed_origins":["https://example.com"],"scopes":["analytics:ingest","waitlist:ingest"]}}}},"responses":{"201":{"description":"Key created — `key` is shown once","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/MintedKey"}}}}}},"400":{"$ref":"#/components/responses/ValidationError"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"description":"Rate-limited, or the project is at its active-publishable-key ceiling (`QUOTA_EXCEEDED`)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"get":{"operationId":"listKeys","summary":"List this project's API keys","tags":["Keys"],"description":"Metadata only — the key hash is never returned and the plaintext does not exist server-side.","security":[{"bearerAuth":["keys:write"]}],"responses":{"200":{"description":"Keys, newest first","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/PublishableKey"}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"}}}},"/v1/keys/{id}":{"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"delete":{"operationId":"revokePublishableKey","summary":"Revoke a publishable key","tags":["Keys"],"description":"Idempotent. Takes effect immediately — the cached authorization entry is invalidated rather than left to expire.\n\n**Publishable keys only.** A secret key returns 404, including the one you are authenticating with: otherwise a compromised minting key could lock the owner out of their own project and destroy the credential trail. 404 rather than 403 so this cannot be used to probe which key ids exist.","security":[{"bearerAuth":["keys:write"]}],"responses":{"204":{"description":"Revoked"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"}}}},"/v1/a.{hash}.js":{"get":{"operationId":"getTrackerScriptByHash","summary":"Analytics tracker bundle (pinned, immutable)","tags":["Analytics"],"description":"Content-addressed build of the bundle: the `{hash}` segment IS the sha256 of the bytes, so what is served at this URL can never change. Genuinely `immutable` for a year and safe to pin with Subresource Integrity. Get the current `{hash}` and its `sha384` `integrity` value from `/v1/bundles.json`, then embed `<script src=\"https://cdn.plurism.dev/v1/a.<hash>.js\" integrity=\"sha384-…\" crossorigin=\"anonymous\">`. Moving to a newer build is deliberately manual — that is the point of pinning.","security":[],"parameters":[{"name":"hash","in":"path","required":true,"description":"16 lowercase hex characters — the build's content address.","schema":{"type":"string","pattern":"^[0-9a-f]{16}$"}}],"responses":{"200":{"description":"JavaScript bundle","content":{"application/javascript":{"schema":{"type":"string"}}}},"404":{"description":"No build with that content hash. Fetch `/v1/bundles.json` for the current one; never fall back to the rolling URL, whose bytes will not match your pinned `integrity`."}}}},"/v1/elements.{hash}.js":{"get":{"operationId":"getElementsScriptByHash","summary":"Elements bundle (pinned, immutable)","tags":["Elements"],"description":"Content-addressed build of the bundle: the `{hash}` segment IS the sha256 of the bytes, so what is served at this URL can never change. Genuinely `immutable` for a year and safe to pin with Subresource Integrity. Get the current `{hash}` and its `sha384` `integrity` value from `/v1/bundles.json`, then embed `<script src=\"https://cdn.plurism.dev/v1/elements.<hash>.js\" integrity=\"sha384-…\" crossorigin=\"anonymous\">`. Moving to a newer build is deliberately manual — that is the point of pinning.","security":[],"parameters":[{"name":"hash","in":"path","required":true,"description":"16 lowercase hex characters — the build's content address.","schema":{"type":"string","pattern":"^[0-9a-f]{16}$"}}],"responses":{"200":{"description":"JavaScript bundle","content":{"application/javascript":{"schema":{"type":"string"}}}},"404":{"description":"No build with that content hash. Fetch `/v1/bundles.json` for the current one; never fall back to the rolling URL, whose bytes will not match your pinned `integrity`."}}}},"/v1/bundles.json":{"get":{"operationId":"getBundleManifest","summary":"Current pinned URL and SRI hash for each browser bundle","tags":["Elements"],"description":"Public and unauthenticated — you need it before you have a key. Returns, per bundle, the rolling URL, the content-addressed pinned URL, and the `sha384` value to put in `integrity`. Absolute URLs are built from the origin you fetched this from, so call it on `cdn.plurism.dev` to get `cdn.plurism.dev` URLs.","security":[],"responses":{"200":{"description":"Bundle manifest","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","additionalProperties":{"type":"object","properties":{"hash":{"type":"string","example":"0363fa43e13d920c"},"bytes":{"type":"integer","example":2417},"rolling_url":{"type":"string","example":"https://cdn.plurism.dev/v1/a.js"},"pinned_url":{"type":"string","example":"https://cdn.plurism.dev/v1/a.0363fa43e13d920c.js"},"integrity":{"type":"string","example":"sha384-…"}}}}}}}}}}}}},"components":{"headers":{"RequestId":{"description":"Plurism request correlation ID — log this alongside your own trace ID for support.","schema":{"type":"string"}}},"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","bearerFormat":"Plurism API key (`sk_live_…` / `sk_test_…`)","description":"Mint API keys in the Plurism portal. Each key carries a scope set; the scopes required for a given endpoint are listed in its `security` block."}},"responses":{"Unauthorized":{"description":"UNAUTHORIZED — missing or invalid API key","headers":{"X-Plurism-Request-Id":{"$ref":"#/components/headers/RequestId"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"UNAUTHORIZED","message":"Missing or invalid API key"}}}}},"Forbidden":{"description":"FORBIDDEN — API key is missing the required scope","headers":{"X-Plurism-Request-Id":{"$ref":"#/components/headers/RequestId"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"FORBIDDEN","message":"Insufficient scope: requires support:write"}}}}},"NotFound":{"description":"NOT_FOUND — id doesn't exist in this tenant (or is soft-deleted)","headers":{"X-Plurism-Request-Id":{"$ref":"#/components/headers/RequestId"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"NOT_FOUND","message":"Thread thr_x not found"}}}}},"ValidationError":{"description":"VALIDATION — request body or query params failed validation","headers":{"X-Plurism-Request-Id":{"$ref":"#/components/headers/RequestId"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"VALIDATION","message":"Invalid payload","details":{"issues":"body: String must contain at least 1 character(s)"}}}}}},"RateLimited":{"description":"RATE_LIMITED — too many requests; retry after `Retry-After` seconds","headers":{"X-Plurism-Request-Id":{"$ref":"#/components/headers/RequestId"},"Retry-After":{"schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"RATE_LIMITED","message":"Rate limit exceeded"}}}}}},"schemas":{"Error":{"type":"object","required":["error"],"properties":{"error":{"type":"object","required":["code","message"],"properties":{"code":{"type":"string"},"message":{"type":"string"},"details":{"type":"object","additionalProperties":{"type":"string"}}}}}},"SupportCategory":{"type":"string","enum":["general","bug","feedback","billing","other"]},"SupportBaseStatus":{"type":"string","enum":["open","pending","resolved","closed"]},"SupportVisibility":{"type":"string","enum":["public","internal"]},"SupportAuthorType":{"type":"string","enum":["reporter","agent","system"]},"SupportSource":{"type":"string","enum":["api","admin_spa","lookup_page","inbound_email","import","system"]},"SupportThread":{"type":"object","required":["id","tenant_id","reporter_id","ref","ref_seq","subject","body","category","status","reporter_email_snapshot","reporter_name_snapshot","metadata","tags","first_agent_response_at","is_suspected_spam","is_sla_breached","prior_threads_count","source","attachment_count","created_at","updated_at","deleted_at"],"properties":{"id":{"type":"string"},"tenant_id":{"type":"string"},"reporter_id":{"type":"string"},"ref":{"type":"string","description":"<PROJECT_PREFIX>-<SEQ>, e.g. PLURISM-42"},"ref_seq":{"type":"integer"},"subject":{"type":"string"},"body":{"type":"string"},"category":{"$ref":"#/components/schemas/SupportCategory"},"status":{"type":"string","description":"Base or custom status name"},"reporter_email_snapshot":{"type":"string"},"reporter_name_snapshot":{"type":"string"},"metadata":{"type":["object","null"],"additionalProperties":true},"tags":{"type":"array","items":{"type":"string"}},"first_agent_response_at":{"type":["string","null"],"format":"date-time"},"is_suspected_spam":{"type":"boolean"},"is_sla_breached":{"type":"boolean"},"prior_threads_count":{"type":"integer"},"source":{"$ref":"#/components/schemas/SupportSource"},"attachment_count":{"type":"integer"},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"},"deleted_at":{"type":["string","null"],"format":"date-time"},"lookup_token":{"type":["string","null"],"pattern":"^[0-9a-f]{32}$","description":"Capability token for this thread's public lookup page. Returned on authed reads so an agent can rebuild or re-send the reporter's link; deliberately absent from `PublicSupportThread`. Anyone holding it can read and reply to the thread, so treat it as a secret. `null` only for rows predating the token migration."}}},"SupportMessage":{"type":"object","required":["id","thread_id","tenant_id","author_type","author_email","author_name","visibility","body","source","inbound_email_id","attachments","created_at"],"properties":{"id":{"type":"string"},"thread_id":{"type":"string"},"tenant_id":{"type":"string"},"author_type":{"$ref":"#/components/schemas/SupportAuthorType"},"author_email":{"type":["string","null"]},"author_name":{"type":["string","null"]},"visibility":{"$ref":"#/components/schemas/SupportVisibility"},"body":{"type":"string"},"source":{"$ref":"#/components/schemas/SupportSource"},"inbound_email_id":{"type":["string","null"]},"attachments":{"type":"array","items":{"$ref":"#/components/schemas/SupportAttachment"}},"created_at":{"type":"string","format":"date-time"}}},"SupportAttachment":{"type":"object","required":["id","file_id","filename","content_type","size_bytes","visibility"],"properties":{"id":{"type":"string"},"file_id":{"type":"string"},"filename":{"type":"string"},"content_type":{"type":"string"},"size_bytes":{"type":"integer"},"visibility":{"$ref":"#/components/schemas/SupportVisibility"}}},"SupportReporter":{"type":"object","required":["id","tenant_id","primary_email","alt_emails","display_name","notes","thread_count","first_seen_at","last_seen_at","metadata"],"properties":{"id":{"type":"string"},"tenant_id":{"type":"string"},"primary_email":{"type":"string"},"alt_emails":{"type":"array","items":{"type":"string"}},"display_name":{"type":["string","null"]},"notes":{"type":["string","null"]},"thread_count":{"type":"integer"},"first_seen_at":{"type":"string","format":"date-time"},"last_seen_at":{"type":"string","format":"date-time"},"metadata":{"type":["object","null"],"additionalProperties":true}}},"SupportTag":{"type":"object","required":["id","tenant_id","name","display_name","color","description","usage_count"],"properties":{"id":{"type":"string"},"tenant_id":{"type":"string"},"name":{"type":"string","description":"Canonical id (lowercased, hyphenated)."},"display_name":{"type":"string"},"color":{"type":"string","description":"#RRGGBB"},"description":{"type":["string","null"]},"usage_count":{"type":"integer"}}},"SupportSavedView":{"type":"object","required":["id","project_id","name","filters","sort_order","is_pinned"],"properties":{"id":{"type":"string"},"project_id":{"type":"string"},"name":{"type":"string"},"filters":{"$ref":"#/components/schemas/SupportListFilters"},"sort_order":{"type":"integer"},"is_pinned":{"type":"boolean"}}},"SupportListFilters":{"type":"object","additionalProperties":true,"properties":{"status":{"type":"array","items":{"type":"string"}},"category":{"type":"array","items":{"$ref":"#/components/schemas/SupportCategory"}},"tag_ids":{"type":"array","items":{"type":"string"}},"tag_match":{"type":"string","enum":["any","all"]},"reporter_id":{"type":"string"},"reporter_email":{"type":"string"},"sla_breached":{"type":"boolean"},"is_suspected_spam":{"type":"boolean"},"q":{"type":"string"},"created_after":{"type":"string","format":"date-time"},"created_before":{"type":"string","format":"date-time"}}},"SupportCustomStatus":{"type":"object","required":["id","project_id","name","display_name","color","sort_order","treats_as"],"properties":{"id":{"type":"string"},"project_id":{"type":"string"},"name":{"type":"string"},"display_name":{"type":"string"},"color":{"type":"string"},"sort_order":{"type":"integer"},"treats_as":{"$ref":"#/components/schemas/SupportBaseStatus"}}},"SupportMetadataSchema":{"type":"object","required":["fields","allow_unknown_fields"],"properties":{"fields":{"type":"array","items":{"$ref":"#/components/schemas/SupportMetadataSchemaField"}},"allow_unknown_fields":{"type":"boolean"}}},"SupportMetadataSchemaField":{"type":"object","required":["key","type","required"],"properties":{"key":{"type":"string"},"type":{"type":"string","enum":["string","number","bool","enum","url","date"]},"required":{"type":"boolean"},"enum_values":{"type":"array","items":{"type":"string"}},"render":{"oneOf":[{"type":"object","required":["kind","href_template"],"properties":{"kind":{"const":"link"},"href_template":{"type":"string"}}},{"type":"object","required":["kind"],"properties":{"kind":{"const":"badge"},"color_map":{"type":"object","additionalProperties":{"type":"string"}}}},{"type":"object","required":["kind"],"properties":{"kind":{"const":"date"},"format":{"type":"string"}}}]}}},"SupportAuditEntry":{"type":"object","required":["id","thread_id","actor_kind","actor_id","action","meta_json","created_at"],"properties":{"id":{"type":"string"},"thread_id":{"type":"string"},"actor_kind":{"type":"string","enum":["agent","consumer","reporter","system"]},"actor_id":{"type":["string","null"]},"action":{"type":"string"},"meta_json":{"type":["object","null"],"additionalProperties":true},"created_at":{"type":"string","format":"date-time"}}},"PublicSupportThread":{"type":"object","required":["id","ref","subject","body","category","status","reporter_name","tags","messages","created_at","updated_at"],"properties":{"id":{"type":"string"},"ref":{"type":"string"},"subject":{"type":"string"},"body":{"type":"string"},"category":{"$ref":"#/components/schemas/SupportCategory"},"status":{"type":"string"},"reporter_name":{"type":"string"},"tags":{"type":"array","items":{"type":"string"}},"messages":{"type":"array","items":{"$ref":"#/components/schemas/PublicSupportMessage"}},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"}}},"PublicSupportMessage":{"type":"object","required":["id","thread_id","author_type","author_name","body","attachments","created_at"],"properties":{"id":{"type":"string"},"thread_id":{"type":"string"},"author_type":{"$ref":"#/components/schemas/SupportAuthorType"},"author_name":{"type":["string","null"]},"body":{"type":"string"},"attachments":{"type":"array","items":{"$ref":"#/components/schemas/PublicSupportAttachment"}},"created_at":{"type":"string","format":"date-time"}}},"PublicSupportAttachment":{"type":"object","required":["id","filename","content_type","size_bytes"],"properties":{"id":{"type":"string"},"filename":{"type":"string"},"content_type":{"type":"string"},"size_bytes":{"type":"integer"}}},"CreateThreadInput":{"type":"object","required":["body","category","reporter_email","reporter_name"],"properties":{"body":{"type":"string","minLength":1,"maxLength":5000},"category":{"$ref":"#/components/schemas/SupportCategory"},"reporter_email":{"type":"string","format":"email","maxLength":254},"reporter_name":{"type":"string","minLength":1,"maxLength":120},"subject":{"type":"string","minLength":1,"maxLength":200},"tags":{"type":"array","items":{"type":"string","minLength":1,"maxLength":60},"maxItems":20},"metadata":{"type":"object","additionalProperties":true},"attachment_ids":{"type":"array","items":{"type":"string"},"maxItems":10}}},"CreateThreadResponse":{"type":"object","required":["id","ref","lookup_url","created_at"],"properties":{"id":{"type":"string"},"ref":{"type":"string"},"lookup_url":{"type":"string","format":"uri"},"created_at":{"type":"string","format":"date-time"}}},"UpdateThreadInput":{"type":"object","minProperties":1,"properties":{"status":{"type":"string","minLength":1,"maxLength":60},"category":{"$ref":"#/components/schemas/SupportCategory"},"subject":{"type":"string","minLength":1,"maxLength":200},"tags":{"type":"array","items":{"type":"string","minLength":1,"maxLength":60},"maxItems":20},"metadata":{"type":["object","null"],"additionalProperties":true}}},"BulkUpdateInput":{"type":"object","required":["ids"],"properties":{"ids":{"type":"array","items":{"type":"string"},"minItems":1,"maxItems":100},"status":{"type":"string"},"category":{"$ref":"#/components/schemas/SupportCategory"},"subject":{"type":"string"},"tags":{"type":"array","items":{"type":"string"}},"metadata":{"type":["object","null"],"additionalProperties":true}}},"BulkUpdateResult":{"type":"object","required":["updated"],"properties":{"updated":{"type":"array","items":{"type":"string"}},"failed":{"type":"array","items":{"type":"object","required":["id","reason"],"properties":{"id":{"type":"string"},"reason":{"type":"string"}}}}}},"BulkDeleteResult":{"type":"object","required":["deleted"],"properties":{"deleted":{"type":"array","items":{"type":"string"}},"failed":{"type":"array","items":{"type":"object","required":["id","reason"],"properties":{"id":{"type":"string"},"reason":{"type":"string"}}}}}},"CreateMessageInput":{"type":"object","required":["body"],"properties":{"body":{"type":"string","minLength":1,"maxLength":10000},"visibility":{"$ref":"#/components/schemas/SupportVisibility","default":"public"},"attachment_ids":{"type":"array","items":{"type":"string"},"maxItems":10}}},"CreateMessageResponse":{"type":"object","required":["data","email"],"properties":{"data":{"$ref":"#/components/schemas/SupportMessage"},"email":{"type":"object","required":["status"],"properties":{"status":{"type":"string","enum":["sent","skipped","failed"]},"reason":{"type":"string"}}}}},"EditMessageInput":{"type":"object","required":["body"],"properties":{"body":{"type":"string","minLength":1,"maxLength":10000}}},"CreateTagInput":{"type":"object","required":["name"],"additionalProperties":false,"properties":{"name":{"type":"string","minLength":1,"maxLength":60},"display_name":{"type":"string","minLength":1,"maxLength":60},"color":{"type":"string","pattern":"^#[0-9a-fA-F]{6}$"},"description":{"type":["string","null"],"maxLength":500}}},"UpdateTagInput":{"type":"object","minProperties":1,"additionalProperties":false,"properties":{"display_name":{"type":"string","minLength":1,"maxLength":60},"color":{"type":"string","pattern":"^#[0-9a-fA-F]{6}$"},"description":{"type":["string","null"],"maxLength":500}}},"UpdateReporterInput":{"type":"object","minProperties":1,"additionalProperties":false,"properties":{"display_name":{"type":"string","minLength":1,"maxLength":120},"notes":{"type":["string","null"],"maxLength":10000},"add_alt_emails":{"type":"array","items":{"type":"string","format":"email"},"maxItems":50},"remove_alt_emails":{"type":"array","items":{"type":"string","format":"email"},"maxItems":50}}},"MergeReportersInput":{"type":"object","required":["primary_id","secondary_id"],"additionalProperties":false,"properties":{"primary_id":{"type":"string"},"secondary_id":{"type":"string"}}},"CreateSavedViewInput":{"type":"object","required":["name"],"additionalProperties":false,"properties":{"name":{"type":"string","minLength":1,"maxLength":120},"filters":{"$ref":"#/components/schemas/SupportListFilters"},"sort_order":{"type":"integer"},"is_pinned":{"type":"boolean"}}},"UpdateSavedViewInput":{"type":"object","minProperties":1,"additionalProperties":false,"properties":{"name":{"type":"string","minLength":1,"maxLength":120},"filters":{"$ref":"#/components/schemas/SupportListFilters"},"sort_order":{"type":"integer"},"is_pinned":{"type":"boolean"}}},"CreateCustomStatusInput":{"type":"object","required":["name","display_name","color","sort_order","treats_as"],"additionalProperties":false,"properties":{"name":{"type":"string","minLength":1,"maxLength":60},"display_name":{"type":"string","minLength":1,"maxLength":60},"color":{"type":"string","pattern":"^#[0-9a-fA-F]{6}$"},"sort_order":{"type":"integer"},"treats_as":{"$ref":"#/components/schemas/SupportBaseStatus"}}},"UpdateCustomStatusInput":{"type":"object","minProperties":1,"additionalProperties":false,"properties":{"name":{"type":"string","minLength":1,"maxLength":60},"display_name":{"type":"string","minLength":1,"maxLength":60},"color":{"type":"string","pattern":"^#[0-9a-fA-F]{6}$"},"sort_order":{"type":"integer"},"treats_as":{"$ref":"#/components/schemas/SupportBaseStatus"}}},"InboundEmailPayload":{"type":"object","required":["type","data"],"properties":{"type":{"type":"string","const":"email.received"},"data":{"type":"object","required":["id","from","to"],"properties":{"id":{"type":"string"},"from":{"type":"string"},"to":{"type":"array","items":{"type":"string"}},"subject":{"type":"string"},"text":{"type":"string"},"html":{"type":"string"},"headers":{"type":"object","additionalProperties":true},"attachments":{"type":"array","items":{"type":"object","required":["filename","content","content_type"],"properties":{"filename":{"type":"string"},"content":{"type":"string","description":"base64-encoded bytes"},"content_type":{"type":"string"}}}}}}}},"FeatureFlag":{"type":"object","description":"A feature flag. `enabled` is SQLite's 0/1 integer surrogate for a boolean.","required":["id","tenant_id","key","enabled","rollout_percent","created_at","updated_at"],"properties":{"id":{"type":"string"},"tenant_id":{"type":"string"},"key":{"type":"string","pattern":"^[a-z0-9][a-z0-9._-]{0,119}$"},"description":{"type":["string","null"]},"enabled":{"type":"integer","enum":[0,1]},"rollout_percent":{"type":"integer","minimum":0,"maximum":100},"created_at":{"type":"string"},"updated_at":{"type":"string"},"deleted_at":{"type":["string","null"]}}},"CreateFlagInput":{"type":"object","required":["key"],"properties":{"key":{"type":"string","pattern":"^[a-z0-9][a-z0-9._-]{0,119}$","description":"Lowercase alphanumerics plus `.`, `_`, `-`. Immutable once created."},"description":{"type":"string","maxLength":500},"enabled":{"type":"boolean"},"rollout_percent":{"type":"integer","minimum":0,"maximum":100}}},"UpdateFlagInput":{"type":"object","description":"At least one field is required. `key` is immutable.","minProperties":1,"properties":{"description":{"type":["string","null"],"maxLength":500},"enabled":{"type":"boolean"},"rollout_percent":{"type":"integer","minimum":0,"maximum":100}}},"FlagEvaluation":{"type":"object","description":"Result of evaluating one flag for one user. Bucketing is a stable SHA-256 over `tenant:key:user_id`, so raising `rollout_percent` only ever adds users.","required":["enabled","reason"],"properties":{"key":{"type":"string","description":"Present on the single-flag route; absent in the bulk map."},"enabled":{"type":"boolean"},"reason":{"type":"string","enum":["disabled","rollout_full","rollout_hash","rollout_out","not_found"]}}},"BulkEvaluateInput":{"type":"object","required":["keys"],"properties":{"user_id":{"type":"string","minLength":1,"maxLength":120},"keys":{"type":"array","minItems":1,"maxItems":100,"items":{"type":"string"}}}},"WaitlistEntry":{"type":"object","required":["id","tenant_id","email","status","created_at","updated_at"],"properties":{"id":{"type":"string"},"tenant_id":{"type":"string"},"email":{"type":"string","format":"email"},"name":{"type":["string","null"]},"referrer":{"type":["string","null"]},"status":{"type":"string","enum":["waiting","invited","rejected"]},"invited_at":{"type":["string","null"]},"created_at":{"type":"string"},"updated_at":{"type":"string"},"deleted_at":{"type":["string","null"]}}},"AddWaitlistEntryInput":{"type":"object","required":["email"],"properties":{"email":{"type":"string","format":"email","maxLength":254},"name":{"type":"string","minLength":1,"maxLength":120},"referrer":{"type":"string","minLength":1,"maxLength":120}}},"UpdateWaitlistEntryInput":{"type":"object","description":"At least one field required. `status` accepts `waiting` or `rejected` only — use `POST /waitlist/{id}/invite` to invite, so the `waitlist.invited` notifier event actually fires.","minProperties":1,"properties":{"status":{"type":"string","enum":["waiting","rejected"]},"name":{"type":"string","minLength":1,"maxLength":120}}},"WaitlistCreated":{"type":"object","required":["id","created_at"],"properties":{"id":{"type":"string"},"created_at":{"type":"string"}}},"ChangelogEntry":{"type":"object","required":["id","tenant_id","slug","title","body_markdown","created_at","updated_at"],"properties":{"id":{"type":"string"},"tenant_id":{"type":"string"},"slug":{"type":"string","pattern":"^[a-z0-9][a-z0-9-]{0,59}$"},"title":{"type":"string"},"body_markdown":{"type":"string"},"published_at":{"type":["string","null"]},"created_at":{"type":"string"},"updated_at":{"type":"string"},"deleted_at":{"type":["string","null"]}}},"CreateChangelogInput":{"type":"object","required":["slug","title","body_markdown"],"properties":{"slug":{"type":"string","pattern":"^[a-z0-9][a-z0-9-]{0,59}$"},"title":{"type":"string","minLength":1,"maxLength":200},"body_markdown":{"type":"string","minLength":1,"maxLength":100000},"publish":{"type":"boolean","description":"Publish immediately. Omitted/false creates a draft."}}},"UpdateChangelogInput":{"type":"object","description":"At least one field required. `published: true` on a draft stamps `published_at` and fires `changelog.published`; on an already-published entry it leaves the original timestamp intact. `published: false` reverts to draft.","minProperties":1,"properties":{"slug":{"type":"string","pattern":"^[a-z0-9][a-z0-9-]{0,59}$"},"title":{"type":"string","minLength":1,"maxLength":200},"body_markdown":{"type":"string","minLength":1,"maxLength":100000},"published":{"type":"boolean"}}},"PublicProjectRef":{"type":"object","required":["slug","name"],"properties":{"slug":{"type":"string"},"name":{"type":"string"}}},"PublicChangelogEntry":{"type":"object","required":["id","slug","title","body_markdown","published_at","created_at","updated_at"],"properties":{"id":{"type":"string"},"slug":{"type":"string"},"title":{"type":"string"},"body_markdown":{"type":"string"},"published_at":{"type":"string"},"created_at":{"type":"string"},"updated_at":{"type":"string"}}},"ContentType":{"type":"string","enum":["article","news","faq","howto"],"description":"Storage value. `howto` is shown as \"Guide\" in Plurism's own UI; the stored value and the schema.org `HowTo` type it emits are unchanged."},"FaqStructured":{"type":"object","required":["items"],"properties":{"items":{"type":"array","minItems":1,"maxItems":100,"items":{"type":"object","required":["question","answer_markdown"],"properties":{"question":{"type":"string","minLength":1,"maxLength":300},"answer_markdown":{"type":"string","minLength":1,"maxLength":20000}}}}}},"HowtoStructured":{"type":"object","required":["steps"],"properties":{"intro_markdown":{"type":"string","maxLength":20000},"steps":{"type":"array","minItems":1,"maxItems":100,"items":{"type":"object","required":["name","text_markdown"],"properties":{"name":{"type":"string","minLength":1,"maxLength":200},"text_markdown":{"type":"string","minLength":1,"maxLength":20000},"image_url":{"type":"string","format":"uri"}}}}}},"ContentEntry":{"type":"object","description":"`tags` and `structured` are stored as JSON **strings** on this authenticated surface; the public read surface decodes `tags` to an array.","required":["id","tenant_id","type","slug","title","created_at","updated_at"],"properties":{"id":{"type":"string"},"tenant_id":{"type":"string"},"type":{"$ref":"#/components/schemas/ContentType"},"slug":{"type":"string","pattern":"^[a-z0-9][a-z0-9-]{0,59}$"},"title":{"type":"string"},"excerpt":{"type":["string","null"]},"cover_image_url":{"type":["string","null"]},"author_name":{"type":["string","null"]},"author_url":{"type":["string","null"]},"author_type":{"type":["string","null"],"enum":["person","organization",null],"description":"schema.org author kind. NULL reads as `person`."},"tags":{"type":["string","null"],"description":"JSON array text, e.g. `[\"a\",\"b\"]`."},"body_markdown":{"type":["string","null"],"description":"article/news prose body."},"structured":{"type":["string","null"],"description":"JSON text for faq/howto bodies."},"published_at":{"type":["string","null"]},"created_at":{"type":"string"},"updated_at":{"type":"string","description":"SYSTEM row-modification stamp — bumps on any write."},"content_edited_at":{"type":["string","null"],"description":"Editorial last-edited stamp. This, not `updated_at`, is what a freshness signal should use."},"deleted_at":{"type":["string","null"]}}},"CreateContentInput":{"description":"Discriminated on `type`. `article`/`news` require `body_markdown`; `faq`/`howto` require `structured` and must omit `body_markdown`.","oneOf":[{"type":"object","title":"article","required":["type","slug","title","body_markdown"],"properties":{"slug":{"type":"string","pattern":"^[a-z0-9][a-z0-9-]{0,59}$"},"title":{"type":"string","minLength":1,"maxLength":200},"excerpt":{"type":"string","maxLength":500},"cover_image_url":{"type":"string","format":"uri"},"author_name":{"type":"string","maxLength":200},"author_url":{"type":"string","format":"uri"},"author_type":{"type":"string","enum":["person","organization"]},"tags":{"type":"array","maxItems":12,"items":{"type":"string","maxLength":40}},"publish":{"type":"boolean"},"published_at":{"type":"string","description":"Import-only. ISO-8601 with an explicit timezone, not in the future. Lets a migrating blog keep its real publish dates. Cannot be combined with `publish: false`."},"type":{"const":"article"},"body_markdown":{"type":"string","minLength":1,"maxLength":100000}}},{"type":"object","title":"news","required":["type","slug","title","body_markdown"],"properties":{"slug":{"type":"string","pattern":"^[a-z0-9][a-z0-9-]{0,59}$"},"title":{"type":"string","minLength":1,"maxLength":200},"excerpt":{"type":"string","maxLength":500},"cover_image_url":{"type":"string","format":"uri"},"author_name":{"type":"string","maxLength":200},"author_url":{"type":"string","format":"uri"},"author_type":{"type":"string","enum":["person","organization"]},"tags":{"type":"array","maxItems":12,"items":{"type":"string","maxLength":40}},"publish":{"type":"boolean"},"published_at":{"type":"string","description":"Import-only. ISO-8601 with an explicit timezone, not in the future. Lets a migrating blog keep its real publish dates. Cannot be combined with `publish: false`."},"type":{"const":"news"},"body_markdown":{"type":"string","minLength":1,"maxLength":100000}}},{"type":"object","title":"faq","required":["type","slug","title","structured"],"properties":{"slug":{"type":"string","pattern":"^[a-z0-9][a-z0-9-]{0,59}$"},"title":{"type":"string","minLength":1,"maxLength":200},"excerpt":{"type":"string","maxLength":500},"cover_image_url":{"type":"string","format":"uri"},"author_name":{"type":"string","maxLength":200},"author_url":{"type":"string","format":"uri"},"author_type":{"type":"string","enum":["person","organization"]},"tags":{"type":"array","maxItems":12,"items":{"type":"string","maxLength":40}},"publish":{"type":"boolean"},"published_at":{"type":"string","description":"Import-only. ISO-8601 with an explicit timezone, not in the future. Lets a migrating blog keep its real publish dates. Cannot be combined with `publish: false`."},"type":{"const":"faq"},"structured":{"$ref":"#/components/schemas/FaqStructured"}}},{"type":"object","title":"howto","required":["type","slug","title","structured"],"properties":{"slug":{"type":"string","pattern":"^[a-z0-9][a-z0-9-]{0,59}$"},"title":{"type":"string","minLength":1,"maxLength":200},"excerpt":{"type":"string","maxLength":500},"cover_image_url":{"type":"string","format":"uri"},"author_name":{"type":"string","maxLength":200},"author_url":{"type":"string","format":"uri"},"author_type":{"type":"string","enum":["person","organization"]},"tags":{"type":"array","maxItems":12,"items":{"type":"string","maxLength":40}},"publish":{"type":"boolean"},"published_at":{"type":"string","description":"Import-only. ISO-8601 with an explicit timezone, not in the future. Lets a migrating blog keep its real publish dates. Cannot be combined with `publish: false`."},"type":{"const":"howto"},"structured":{"$ref":"#/components/schemas/HowtoStructured"}}}]},"UpdateContentInput":{"type":"object","description":"At least one field required. `type` is immutable. `published_at` is NOT accepted — publish state is server-stamped via `published`, so the `content.published` event can't be bypassed.","minProperties":1,"properties":{"slug":{"type":"string","pattern":"^[a-z0-9][a-z0-9-]{0,59}$"},"title":{"type":"string","minLength":1,"maxLength":200},"excerpt":{"type":"string","maxLength":500},"cover_image_url":{"type":["string","null"],"format":"uri"},"author_name":{"type":["string","null"],"maxLength":200},"author_url":{"type":["string","null"],"format":"uri"},"author_type":{"type":["string","null"],"enum":["person","organization",null]},"tags":{"type":["array","null"],"maxItems":12,"items":{"type":"string","maxLength":40}},"body_markdown":{"type":["string","null"],"maxLength":100000},"structured":{"oneOf":[{"$ref":"#/components/schemas/FaqStructured"},{"$ref":"#/components/schemas/HowtoStructured"},{"type":"null"}]},"published":{"type":"boolean"}}},"PublicContentListEntry":{"type":"object","description":"List projection — no body. `tags` is decoded to an array here.","required":["id","type","slug","title","published_at","created_at","updated_at"],"properties":{"id":{"type":"string"},"type":{"$ref":"#/components/schemas/ContentType"},"slug":{"type":"string"},"title":{"type":"string"},"excerpt":{"type":["string","null"]},"cover_image_url":{"type":["string","null"]},"author_name":{"type":["string","null"]},"author_url":{"type":["string","null"]},"author_type":{"type":["string","null"]},"tags":{"type":"array","items":{"type":"string"}},"published_at":{"type":"string"},"created_at":{"type":"string"},"updated_at":{"type":"string"}}},"PublicContentEntry":{"allOf":[{"$ref":"#/components/schemas/PublicContentListEntry"},{"type":"object","properties":{"body_markdown":{"type":["string","null"]},"structured":{"type":["string","null"],"description":"Raw JSON text."},"html":{"type":"string","description":"Rendered body. Present when the renderer runs."},"json_ld":{"type":"object","description":"schema.org JSON-LD object for programmatic use."},"json_ld_script":{"type":"string","description":"The same JSON-LD, pre-escaped for direct drop-in to a `<script type=\"application/ld+json\">` tag. Use this rather than `JSON.stringify(json_ld)`, which allows `</script>` injection."}}}]},"FeedbackEntry":{"type":"object","required":["id","tenant_id","rating","source","created_at"],"properties":{"id":{"type":"string"},"tenant_id":{"type":"string"},"rating":{"type":"integer","minimum":0,"maximum":10},"comment":{"type":["string","null"]},"reporter_email":{"type":["string","null"]},"reporter_name":{"type":["string","null"]},"metadata":{"type":["string","null"],"description":"JSON object text."},"source":{"type":"string","enum":["user","auto-capture"]},"created_at":{"type":"string"},"deleted_at":{"type":["string","null"]}}},"CreateFeedbackInput":{"type":"object","required":["rating"],"properties":{"rating":{"type":"integer","minimum":0,"maximum":10,"description":"NPS scale. 9–10 promoter, 7–8 passive, 0–6 detractor."},"comment":{"type":"string","minLength":1,"maxLength":5000},"reporter_email":{"type":"string","format":"email","maxLength":254},"reporter_name":{"type":"string","minLength":1,"maxLength":120},"metadata":{"type":"object","additionalProperties":true,"description":"Free-form JSON. Not accepted on the public widget route."}}},"FeedbackStats":{"type":"object","required":["count","average","nps","promoters","passives","detractors"],"properties":{"count":{"type":"integer"},"average":{"type":"number"},"nps":{"type":["integer","null"],"description":"(promoters − detractors) as a percentage of respondents, −100..100. `null` when there are no rows."},"promoters":{"type":"integer"},"passives":{"type":"integer"},"detractors":{"type":"integer"}}},"FileEntry":{"type":"object","required":["id","tenant_id","r2_key","filename","content_type","size_bytes","created_at"],"properties":{"id":{"type":"string"},"tenant_id":{"type":"string"},"r2_key":{"type":"string"},"filename":{"type":"string"},"content_type":{"type":"string"},"size_bytes":{"type":"integer"},"metadata":{"type":["string","null"]},"created_at":{"type":"string"},"deleted_at":{"type":["string","null"]}}},"Notifier":{"type":"object","required":["id","tenant_id","type","name","config","enabled","created_at","updated_at"],"properties":{"id":{"type":"string"},"tenant_id":{"type":"string"},"type":{"type":"string","enum":["email","discord","http"]},"name":{"type":"string"},"config":{"type":"string","description":"JSON text; shape depends on `type` — see `NotifierConfig`."},"enabled":{"type":"integer","enum":[0,1]},"created_at":{"type":"string"},"updated_at":{"type":"string"},"deleted_at":{"type":["string","null"]}}},"NotifierConfig":{"description":"Discriminated on the notifier's `type`.","oneOf":[{"type":"object","title":"email","required":["to"],"properties":{"to":{"type":"string","format":"email"}}},{"type":"object","title":"discord","required":["webhook_url"],"properties":{"webhook_url":{"type":"string","format":"uri"}},"description":"Plurism always sends `allowed_mentions: {parse: []}`, so user-supplied text can never @-ping a server."},{"type":"object","title":"http","required":["url"],"properties":{"url":{"type":"string","format":"uri"},"secret":{"type":"string"}},"description":"Outbound URL is SSRF-guarded — private/link-local targets are rejected."}]},"CreateNotifierInput":{"type":"object","required":["type","name","config"],"properties":{"type":{"type":"string","enum":["email","discord","http"]},"name":{"type":"string","minLength":1,"maxLength":120},"config":{"$ref":"#/components/schemas/NotifierConfig"}}},"UpdateNotifierInput":{"type":"object","description":"At least one field required.","minProperties":1,"properties":{"name":{"type":"string","minLength":1,"maxLength":120},"config":{"$ref":"#/components/schemas/NotifierConfig"},"enabled":{"type":"boolean"}}},"NotificationEvent":{"type":"string","description":"Events a rule can subscribe to.","enum":["support.thread.created","support.thread.replied","support.thread.status_changed","support.thread.tag_added","support.thread.tag_removed","support.thread.metadata_updated","support.thread.deleted","support.thread.restored","support.reporter.merged","waitlist.entry_added","waitlist.invited","changelog.published","content.published","content.updated","content.unpublished"]},"NotifierRule":{"type":"object","required":["id","tenant_id","event","notifier_id","enabled","created_at","updated_at"],"properties":{"id":{"type":"string"},"tenant_id":{"type":"string"},"event":{"$ref":"#/components/schemas/NotificationEvent"},"notifier_id":{"type":"string"},"template_id":{"type":["string","null"]},"enabled":{"type":"integer","enum":[0,1]},"created_at":{"type":"string"},"updated_at":{"type":"string"},"deleted_at":{"type":["string","null"]}}},"CreateNotifierRuleInput":{"type":"object","required":["event","notifier_id"],"properties":{"event":{"$ref":"#/components/schemas/NotificationEvent"},"notifier_id":{"type":"string","minLength":1,"maxLength":64},"template_id":{"type":["string","null"],"maxLength":64}}},"UpdateNotifierRuleInput":{"type":"object","description":"At least one field required.","minProperties":1,"properties":{"enabled":{"type":"boolean"},"template_id":{"type":["string","null"],"maxLength":64}}},"EmailTemplate":{"type":"object","required":["id","tenant_id","name","subject","body_text","created_at","updated_at"],"properties":{"id":{"type":"string"},"tenant_id":{"type":"string"},"name":{"type":"string"},"subject":{"type":"string"},"body_text":{"type":"string"},"created_at":{"type":"string"},"updated_at":{"type":"string"},"deleted_at":{"type":["string","null"]}}},"CreateEmailTemplateInput":{"type":"object","required":["name","subject","body_text"],"properties":{"name":{"type":"string","minLength":1,"maxLength":120},"subject":{"type":"string","minLength":1,"maxLength":200},"body_text":{"type":"string","minLength":1,"maxLength":10000}}},"UpdateEmailTemplateInput":{"type":"object","description":"At least one field required.","minProperties":1,"properties":{"name":{"type":"string","minLength":1,"maxLength":120},"subject":{"type":"string","minLength":1,"maxLength":200},"body_text":{"type":"string","minLength":1,"maxLength":10000}}},"DeliveryLog":{"type":"object","required":["id","tenant_id","event","notifier_type","target","status","created_at"],"properties":{"id":{"type":"string"},"tenant_id":{"type":"string"},"event":{"type":"string"},"notifier_id":{"type":["string","null"]},"notifier_type":{"type":"string","enum":["email","discord","http"]},"target":{"type":"string"},"status":{"type":"string","enum":["sent","failed","skipped"]},"error":{"type":["string","null"]},"duration_ms":{"type":["integer","null"]},"created_at":{"type":"string"}}},"WebhookReceiver":{"type":"object","description":"An inbound endpoint. Third parties POST to `/webhooks/receive/{project_slug}/{slug}`.","required":["id","tenant_id","slug","forwarding_url","enabled","created_at","updated_at"],"properties":{"id":{"type":"string"},"tenant_id":{"type":"string"},"slug":{"type":"string","pattern":"^[a-z0-9][a-z0-9-]{0,59}$"},"signing_secret":{"type":["string","null"],"description":"Write-only in practice — used to verify inbound signatures."},"signature_header":{"type":["string","null"]},"forwarding_url":{"type":"string","format":"uri"},"enabled":{"type":"integer","enum":[0,1]},"created_at":{"type":"string"},"updated_at":{"type":"string"},"deleted_at":{"type":["string","null"]}}},"CreateWebhookReceiverInput":{"type":"object","required":["slug","forwarding_url"],"properties":{"slug":{"type":"string","pattern":"^[a-z0-9][a-z0-9-]{0,59}$"},"forwarding_url":{"type":"string","format":"uri","description":"Must be `https://`. SSRF-guarded — private, loopback and link-local targets are rejected."},"signing_secret":{"type":"string","minLength":8,"maxLength":256},"signature_header":{"type":"string","minLength":1,"maxLength":120}}},"UpdateWebhookReceiverInput":{"type":"object","description":"At least one field required.","minProperties":1,"properties":{"slug":{"type":"string","pattern":"^[a-z0-9][a-z0-9-]{0,59}$"},"forwarding_url":{"type":"string","format":"uri"},"signing_secret":{"type":["string","null"],"minLength":8,"maxLength":256},"signature_header":{"type":["string","null"],"maxLength":120},"enabled":{"type":"boolean"}}},"WebhookEvent":{"type":"object","required":["id","tenant_id","receiver_id","body","signature_valid","status","attempt_count","received_at"],"properties":{"id":{"type":"string"},"tenant_id":{"type":"string"},"receiver_id":{"type":"string"},"headers":{"type":["string","null"],"description":"JSON text of the captured inbound headers."},"body":{"type":"string","description":"Raw inbound body, stored verbatim for replay."},"signature_valid":{"type":"integer","enum":[0,1]},"status":{"type":"string","enum":["pending","delivering","delivered","failed","permanent_failure"]},"attempt_count":{"type":"integer"},"next_attempt_at":{"type":["string","null"]},"received_at":{"type":"string"}}},"WebhookDelivery":{"type":"object","required":["id","tenant_id","event_id","attempted_at","duration_ms"],"properties":{"id":{"type":"string"},"tenant_id":{"type":"string"},"event_id":{"type":"string"},"attempted_at":{"type":"string"},"http_status":{"type":["integer","null"]},"error":{"type":["string","null"]},"duration_ms":{"type":"integer"}}},"WebhookEventDetail":{"type":"object","required":["event","deliveries"],"properties":{"event":{"$ref":"#/components/schemas/WebhookEvent"},"deliveries":{"type":"array","items":{"$ref":"#/components/schemas/WebhookDelivery"}}}},"PlanLimits":{"type":"object","description":"Ceilings for the org's plan. Imported by both the pricing page and the enforcement middleware, so the two cannot drift.","required":["projects","maxSeats","opsPerMonth","r2Bytes","notifierPerMonth","emailPerMonth","analyticsEventsPerMonth","sendingDomains"],"properties":{"projects":{"type":"integer"},"maxSeats":{"type":"integer","description":"Tracked, not enforced at launch."},"opsPerMonth":{"type":"integer"},"r2Bytes":{"type":"integer"},"notifierPerMonth":{"type":"integer"},"emailPerMonth":{"type":"integer"},"analyticsEventsPerMonth":{"type":"integer"},"sendingDomains":{"type":"integer"}}},"UsageSnapshot":{"type":"object","required":["plan","limits","usage"],"properties":{"plan":{"type":"string","enum":["free","pro","business"]},"limits":{"$ref":"#/components/schemas/PlanLimits"},"usage":{"type":"object","required":["ops","notifier","email","filesBytes","projects","seats"],"properties":{"ops":{"type":"integer"},"notifier":{"type":"integer"},"email":{"type":"integer"},"filesBytes":{"type":"integer"},"projects":{"type":"integer"},"seats":{"type":"integer"}}}}},"SendEmailInput":{"type":"object","required":["from","to","subject"],"properties":{"from":{"type":"string","description":"Must be on a sending domain this project has verified. Accepts a bare address; RFC 5322 display-name form is not currently parsed."},"to":{"type":"string","format":"email"},"subject":{"type":"string"},"html":{"type":"string"},"text":{"type":"string"},"reply_to":{"type":"string","format":"email"},"idempotency_key":{"type":"string","description":"At-most-once key. May also be sent as the `X-Idempotency-Key` header. The claim is taken BEFORE metering, so a replay never re-bills the quota."}}},"SendEmailAccepted":{"type":"object","required":["id","status"],"properties":{"id":{"type":"string"},"message_id":{"type":["string","null"]},"status":{"type":"string","description":"Provider-reported send status."}}},"DnsRecord":{"type":"object","required":["type","name","value"],"properties":{"type":{"type":"string","enum":["TXT","CNAME"]},"name":{"type":"string"},"value":{"type":"string"}}},"EmailDomain":{"type":"object","required":["id","domain","status","records"],"properties":{"id":{"type":"string"},"domain":{"type":"string"},"status":{"type":"string"},"records":{"type":"array","items":{"$ref":"#/components/schemas/DnsRecord"},"description":"Publish all of these: one `plurism-verify=` ownership TXT plus one DKIM CNAME per token. Add them WITHOUT removing an existing MX or another provider's DKIM."},"ownershipVerifiedAt":{"type":["string","null"]},"dkimVerifiedAt":{"type":["string","null"]}}},"CreateEmailDomainInput":{"type":"object","required":["domain"],"properties":{"domain":{"type":"string"}}},"AnalyticsEventInput":{"type":"object","description":"Every field is whitelisted and length-capped server-side. `occurred_at` is stamped by the server (a client clock would let future-dated events evade pruning) and `country` is read from the edge, not the body — neither is accepted here.","properties":{"type":{"type":"string","maxLength":32,"default":"custom"},"name":{"type":"string","maxLength":128},"anonymous_id":{"type":"string","maxLength":64},"user_ref":{"type":"string","maxLength":128},"path":{"type":"string","maxLength":1024},"referrer_host":{"type":"string","maxLength":256},"utm_source":{"type":"string","maxLength":128},"utm_medium":{"type":"string","maxLength":128},"utm_campaign":{"type":"string","maxLength":128},"props":{"type":"object","additionalProperties":true,"description":"Serialised and capped at 1024 bytes. Dropped (not an error) if over."}}},"AnalyticsIngestInput":{"type":"object","required":["key"],"properties":{"key":{"type":"string","description":"Publishable key (`pa_…`) carrying `analytics:ingest`."},"events":{"type":"array","maxItems":50,"items":{"$ref":"#/components/schemas/AnalyticsEventInput"},"description":"Events beyond the first 50 are dropped, not rejected."}}},"ElementsWaitlistInput":{"type":"object","required":["key","email"],"properties":{"key":{"type":"string","description":"Publishable key (`pa_…`) carrying `waitlist:ingest`."},"email":{"type":"string","format":"email","maxLength":254},"name":{"type":"string","minLength":1,"maxLength":120},"referrer":{"type":"string","minLength":1,"maxLength":120},"_hp":{"type":"string","description":"Honeypot. Leave empty; the widget never fills it."}}},"ElementsFeedbackInput":{"type":"object","required":["key","rating"],"properties":{"key":{"type":"string","description":"Publishable key (`pa_…`) carrying `feedback:ingest`."},"rating":{"type":"integer","minimum":0,"maximum":10},"comment":{"type":"string","minLength":1,"maxLength":5000},"reporter_email":{"type":"string","format":"email","maxLength":254},"reporter_name":{"type":"string","minLength":1,"maxLength":120},"_hp":{"type":"string","description":"Honeypot. Leave empty; the widget never fills it."}}},"ElementsCreated":{"type":"object","required":["id","created_at"],"properties":{"id":{"type":"string"},"created_at":{"type":"string"}}},"HealthStatus":{"type":"object","required":["status","environment","timestamp"],"properties":{"status":{"type":"string","enum":["ok"]},"environment":{"type":"string"},"timestamp":{"type":"string","format":"date-time"},"cron_last_tick":{"type":["string","null"],"description":"Last scheduled-trigger heartbeat. `null` if unreadable."},"cron_stale":{"type":["boolean","null"],"description":"True when the heartbeat is over 5 minutes old — webhook retries and pruning have silently stopped. Treat as an incident even though HTTP serving is fine."}}},"ReadinessStatus":{"type":"object","required":["status","environment","timestamp","total_duration_ms","checks"],"properties":{"status":{"type":"string","enum":["ok","degraded"]},"environment":{"type":"string"},"timestamp":{"type":"string","format":"date-time"},"total_duration_ms":{"type":"integer"},"checks":{"type":"object","additionalProperties":{"type":"object","required":["ok","duration_ms"],"properties":{"ok":{"type":"boolean"},"detail":{"type":"string"},"duration_ms":{"type":"integer"}}},"description":"Keyed by dependency: `db_metadata`, `kv_cache`, `r2_files`."}}},"PublishableKey":{"type":"object","required":["id","name","key_type","key_prefix","scopes"],"properties":{"id":{"type":"string"},"name":{"type":"string"},"key_type":{"type":"string","enum":["secret","publishable"]},"key_prefix":{"type":"string"},"key_last4":{"type":["string","null"]},"scopes":{"type":["array","null"],"items":{"type":"string"}},"allowed_origins":{"type":["array","null"],"items":{"type":"string"}},"created_at":{"type":"string"},"last_used_at":{"type":["string","null"]},"revoked_at":{"type":["string","null"]}}},"MintedKey":{"allOf":[{"$ref":"#/components/schemas/PublishableKey"},{"type":"object","required":["key"],"properties":{"key":{"type":"string","description":"The plaintext key, returned ONCE. Only its hash is stored; it cannot be read back."}}}]},"MintKeyInput":{"type":"object","required":["name","allowed_origins"],"properties":{"name":{"type":"string","maxLength":120},"allowed_origins":{"type":"array","minItems":1,"maxItems":20,"items":{"type":"string","format":"uri"},"description":"Exact https origins, e.g. `https://app.example.com`. No path, no trailing slash — those never match a browser's Origin header."},"scopes":{"type":"array","items":{"type":"string","enum":["analytics:ingest","waitlist:ingest","feedback:ingest"]},"default":["analytics:ingest"],"description":"Publishable keys carry write-only ingest scopes only. Anything else is 403."},"key_type":{"type":"string","enum":["publishable"],"description":"Optional. Only `publishable` is accepted — asking for `secret` is refused rather than silently downgraded."}}},"AnalyticsTrafficPoint":{"type":"object","required":["date","pageviews","visitors"],"properties":{"date":{"type":"string","format":"date"},"pageviews":{"type":"integer"},"visitors":{"type":"integer","description":"Distinct visitors on that UTC day."}}},"AnalyticsTrafficSummary":{"type":"object","required":["pageviews","visitors","visits","views_per_visit","bounce_rate","avg_duration_seconds","current_visitors"],"properties":{"pageviews":{"type":"integer"},"visitors":{"type":"integer","description":"SUM of daily uniques over the window, NOT distinct visitors over the window. Someone who visits on three days counts three times. Storage is day-bucketed, so the distinct figure is not recoverable from it."},"visits":{"type":"integer","description":"Sessions."},"views_per_visit":{"type":"number"},"bounce_rate":{"type":"number","description":"0..1."},"avg_duration_seconds":{"type":"integer"},"current_visitors":{"type":"integer","description":"Distinct visitors in the last 5 minutes. Read from raw events, not the rollup, so this one figure is live rather than lagging."}}},"AnalyticsTraffic":{"type":"object","required":["range","since","series","summary"],"properties":{"range":{"type":"string","enum":["7d","30d","90d"],"description":"Echoed back so the caller knows which window it got."},"since":{"type":"string","format":"date","description":"Start of the window, `YYYY-MM-DD`. Covers [since 00:00:00 UTC, now]."},"series":{"type":"array","items":{"$ref":"#/components/schemas/AnalyticsTrafficPoint"}},"summary":{"$ref":"#/components/schemas/AnalyticsTrafficSummary"}}},"AnalyticsDimensionRow":{"type":"object","required":["name","count"],"properties":{"name":{"type":"string"},"count":{"type":"integer"}}},"AnalyticsAcquisition":{"type":"object","required":["range","since","channels","sources","pages","countries"],"properties":{"range":{"type":"string","enum":["7d","30d","90d"],"description":"Echoed back so the caller knows which window it got."},"since":{"type":"string","format":"date","description":"Start of the window, `YYYY-MM-DD`. Covers [since 00:00:00 UTC, now]."},"channels":{"type":"array","items":{"$ref":"#/components/schemas/AnalyticsDimensionRow"},"description":"Top 20 by visitors."},"sources":{"type":"array","items":{"$ref":"#/components/schemas/AnalyticsDimensionRow"},"description":"Top 20 by visitors."},"pages":{"type":"array","items":{"$ref":"#/components/schemas/AnalyticsDimensionRow"},"description":"Top 20 by pageviews — a page's traffic is views, not people."},"countries":{"type":"array","items":{"$ref":"#/components/schemas/AnalyticsDimensionRow"},"description":"Top 20 by visitors."}}},"AnalyticsFunnel":{"type":"object","required":["range","since","activation_event","visit","signup","activation","paid","signup_rate","activation_rate"],"properties":{"range":{"type":"string","enum":["7d","30d","90d"],"description":"Echoed back so the caller knows which window it got."},"since":{"type":"string","format":"date","description":"Start of the window, `YYYY-MM-DD`. Covers [since 00:00:00 UTC, now]."},"activation_event":{"type":"string","description":"The custom event that counts as activation, or `Returned (2nd day)` when the project has not configured one."},"visit":{"type":"integer","description":"Distinct visitors with any event in the window."},"signup":{"type":"integer","description":"Of those, the ones resolving to a live person."},"activation":{"type":"integer"},"paid":{"type":"integer","description":"Counted by visitor rather than by person, which guarantees `paid <= signup`: a payment-only buyer with no pixel touch has no visitor row and would otherwise widen the funnel as it descends."},"signup_rate":{"type":"number","description":"0..1. Zero denominator yields 0, never NaN."},"activation_rate":{"type":"number","description":"0..1."}}},"AnalyticsRevenueChannel":{"type":"object","required":["channel","revenue_cents"],"properties":{"channel":{"type":"string"},"revenue_cents":{"type":"integer"}}},"AnalyticsRevenue":{"type":"object","required":["range","since","total_cents","currency","paying_persons","by_channel","unattributed_cents","unattributed_pct"],"properties":{"range":{"type":"string","enum":["7d","30d","90d"],"description":"Echoed back so the caller knows which window it got."},"since":{"type":"string","format":"date","description":"Start of the window, `YYYY-MM-DD`. Covers [since 00:00:00 UTC, now]."},"total_cents":{"type":"integer","description":"Floored at zero. The ledger itself is unclamped (a refund whose matching charge falls outside the window can net negative) but no surface shows negative revenue."},"currency":{"type":["string","null"],"description":"The DOMINANT currency, not the only one. Multi-currency projects are a known v1 limitation: `total_cents` sums across currencies."},"paying_persons":{"type":"integer","description":"Distinct people with an acquisition, collapsing merged aliases and excluding erased persons."},"by_channel":{"type":"array","items":{"$ref":"#/components/schemas/AnalyticsRevenueChannel"},"description":"Attributed, non-refund revenue only."},"unattributed_cents":{"type":"integer"},"unattributed_pct":{"type":"number","description":"0..1, the pixel-coverage gap over ACQUISITION revenue on both sides of the ratio. Renewals are excluded because under attribute-once they carry no channel by design, so counting them would make this drift upward over time even under perfect coverage."}}}}}}