openapi: 3.0.3 info: title: 'Plixa API' description: 'REST API for the Plixa SaaS — WhatsApp customer service for small and medium businesses.' version: 1.0.0 servers: - url: 'https://api.plixa.app' tags: - name: 'AI configuration' description: "\nOn-demand quick-reply suggestions inside the inbox. Calls the AI\nprovider once and returns 3 short, distinct candidates an operator\ncan click into the composer, edit and send. Plan-gated to the same\ntier that allows automated replies — and rate-limited so the\n\"click again\" pattern can't blow the token budget." - name: 'API tokens' description: "\nPersonal access tokens used by external integrations (the panel\nitself uses its own tokens minted at login under the `panel` name;\nthis controller never returns those). All token names are prefixed\nwith `api:` server-side so the listing can be filtered cleanly.\n\nOwner-gated AND plan-gated (api_access feature)." - name: Account description: "\nThe signed-in agent's own availability for taking NEW conversations.\nSelf-service: every member manages only their own state here. The\nowner's read-only view of the whole team lives on the dashboard." - name: 'Admin — Authentication' description: "\nSign-in for Plixa staff. Issues Sanctum tokens owned by the Admin model,\nresolved on subsequent requests by the AuthenticateAdmin middleware." - name: 'Admin — Email campaigns' description: "\nStaff-authored marketing blasts sent from the backoffice through the\nverified Resend sender (hello@plixa.app). Each send is recorded with\nper-recipient delivery status; every mutation lands in admin_audit_logs." - name: 'Admin — Knowledge base' description: "\nStaff CRUD over help articles. Reads bypass the published scope so drafts\nare editable; every mutation is recorded to admin_audit_logs." - name: 'Admin — Support' description: "\nShared staff canned responses (macros) used to speed up replies. Global to\nthe backoffice team, not tenant-scoped." - name: 'Admin — Tenants' description: "\nCross-tenant oversight for staff: list every workspace with its plan,\nusage and status, drill into one, and run the safe lifecycle actions\n(suspend / reactivate). Every mutation is recorded to admin_audit_logs." - name: Authentication description: '' - name: Auto-assignment description: "\nPer-workspace routing rules that pick an assignee for brand-new\nconversations. Listing is open to every member so agents can see\nwhy they got assigned; mutation is owner-only and plan-gated." - name: Billing description: '' - name: Broadcasts description: "\nMass-message campaigns. Owner-only writes, plan-gated to\nProfessional+. Reads (list + detail + recipient breakdown) are\nopen to every member so agents can see what's been sent and to\nwhom." - name: 'Business hours' description: "\nPer-workspace open/closed schedule. When enabled, inbound messages\noutside the configured window trigger an auto-reply instead of an\nAI reply. Available on every plan — closed-hours signalling is a\ntrust feature, not a paywalled extra." - name: CSAT description: "\nPost-resolution satisfaction survey. When a conversation is resolved\nthe bot asks the customer to rate it 1-5; a low score optionally asks\nfor a comment. Owner-managed, available on every plan." - name: Contacts description: "\nTenant-scoped contact directory. Reads are open to every member;\nwrites are owner-gated because notes + field values are visible\nacross the workspace and shouldn't be agent-mutable.\n\nCustom field READS are always permitted (so agents see what the\nowner configured); only writes to definitions are plan-gated." - name: 'Contacts (custom fields)' description: "\nPer-tenant schema for the custom fields operators attach to\nContact rows. Listing is open to every member (the inbox renders\nthe definitions inline next to the contact's values). Writes are\nowner-only AND plan-gated (Professional+)." - name: Conversations description: '' - name: 'Daily digest' description: "\nOwner-only toggle for the daily activity email. Off by default;\nflipping it on enrolls the workspace in tomorrow's 9am UTC run." - name: Endpoints description: '' - name: Flows description: "\nVisual conversation flows: the bot walks an inbound conversation\nthrough a graph of steps (greeting, menu, capture, route to a sector,\nhand off) before the AI or a human takes over. Owner-only — flows are\nworkspace automation, not per-agent settings.\n\nEditing saves a draft; the live graph only changes on Publish, so a\nhalf-finished edit never reaches a real customer." - name: Inbox description: "\nOn-demand AI summary of an inbox thread. When an agent takes over a\nlong conversation, this gives them a 3-6-sentence TL;DR so they\ndon't have to scroll through hundreds of messages to ground\nthemselves. Customer never sees the output — operator-facing only.\n\nPlan-gated to the same tier that allows automated AI replies. Cached\non the conversation row; the cache invalidates when a new message\nlands. A `?refresh=1` query param forces regeneration." - name: Labels description: "\nWorkspace-level tags every member can pin onto conversations. Listing\nis open to every member (the inbox needs to render badges); creating,\nrenaming, recoloring and deleting is owner-only — keeps the palette\ndisciplined." - name: 'Messages API' description: "\nExternal entry point for sending outbound WhatsApp messages through\nPlixa. Authenticates via Sanctum personal access token, with the\n`write` ability required. Plan-gated to api_access = true.\n\nLooks up the tenant's first connected phone instance and uses it\n(Plixa MVP only supports one number per workspace anyway). Creates\nor reopens a conversation for the contact and persists the outbound\nMessage row so the panel renders it just like any other reply." - name: Onboarding description: "\nComputes the \"getting started\" checklist the dashboard banner uses\nto nudge new workspaces through their first-run setup. State is\nderived from existing tables on the fly — no extra columns, no\nbackground jobs to keep in sync. The only persisted bit is whether\nthe owner explicitly dismissed the banner." - name: 'Payments (Stripe Connect)' description: "\nOwner-only. Connects each workspace's own Stripe account (Express) so\nbooking deposits land there, and surfaces the payments the workspace has\nreceived. No API keys: Stripe hosts the onboarding." - name: 'Phone instances' description: '' - name: 'Push notifications' description: "\nBrowser-based push subscriptions backed by the W3C Push API + VAPID.\nThe panel registers a service worker, asks the browser for a\nPushSubscription, and POSTs the endpoint + keys here. Notifications\nare then routed through the same Laravel queue as our emails." - name: 'SLA targets' description: "\nTwo optional service-level targets per workspace:\n\n - `first_response_seconds` — how long the customer should wait\n before someone in the workspace answers their first message.\n - `resolution_seconds` — how long a conversation should stay\n open before it gets closed.\n\nNULL on either means \"no target set\"; the inbox and reports\nskip the SLA decoration entirely in that case. Each target is\ncapped at 7 days (604800s) — anything longer is hardly an SLA." - name: 'Saved replies' description: "\nCanned messages every workspace member can pick from while composing\nin the inbox. Visible to every member (no per-user replies in MVP),\nbut only owners can create/edit/delete — keeps the dropdown curated." - name: 'Scheduling — appointments' description: "\nThe calendar feed plus staff actions: manual booking, reschedule,\ncancel, and marking a no-show. All tenant-scoped; open to every member." - name: 'Scheduling — base schedule' description: "\nThe workspace \"base\" weekly working hours: a template the owner defines\nonce and imports into each provider, instead of typing the same hours for\n30 people. Stored as JSON on the tenant; only the weekly open windows\n(no date overrides). Owner-only." - name: 'Scheduling — calendar sync' description: "\nPer-agent Google Calendar link. Each member connects their own Google\naccount: Plixa writes their appointments to it and reads their free/busy\nback so outside commitments block Plixa slots. Tokens are encrypted at\nrest and never returned to the client." - name: 'Scheduling — holidays' description: "\nOwner toggle: import the workspace country's public holidays as closures\nso the AI never books on them. Enabling (or re-saving) runs a sync now;\ndisabling drops future holiday closures. Owner-only (route group)." - name: 'Scheduling — provider availability' description: "\nA provider's bookable working hours: a timezone plus a flat list of\nrules (recurring weekday windows and date overrides, open or blocked).\nReadable by members; the full-replace update is owner-only." - name: 'Scheduling — reminders' description: "\nOwner setting: when the workspace sends appointment reminders, as a list of\n\"minutes before the appointment\". Null falls back to the system default; an\nempty list turns reminders off. A service can still override per-service.\nOwner-only (route group)." - name: 'Scheduling — resources' description: "\nShared, capacity-limited assets (rooms, equipment). A booking of a linked\nservice consumes one unit; the slot is full once capacity is reached.\nReadable by members; mutations owner-only." - name: 'Scheduling — saved filters' description: "\nPer-user saved calendar filters: which providers, services and statuses to\nshow on the agenda. Each person manages their own; one can be the default,\napplied automatically when they open the calendar. Every member (not just\nowners) keeps their own set." - name: 'Scheduling — services' description: "\nBookable services. Readable by every member (the calendar + booking\nflows need them); create/update/delete are owner-only." - name: 'Scheduling — waitlist' description: "\nSurfaces the appointment waitlist (built up by the AI when no slots are\nfree) so staff can see who's waiting for which service and clear stale\nentries. Tenant-scoped; open to every member. The auto-notify-on-cancel\nflow lives in NativeSchedulingProvider — this is the human-facing view." - name: 'Scheduling — workspace closures' description: "\nDays the whole workspace is closed. Adding one date blocks every\nprovider (and the AI) for that day, so the owner closes the business\nonce instead of editing each provider's schedule. Reads are open to\nmembers; create/delete are owner-only (route group)." - name: Sectors description: "\nBusiness departments (Sales, Support, Finance…) the owner defines so\nconversations can be routed to the right team. Listing is open to\nevery member (the inbox and the flow editor both need it); creating,\nrenaming and assigning agents is owner-only." - name: Security description: "\nWorkspace-wide security / access toggles:\n - `require_two_factor` — every member must enable 2FA before the\n panel grants access.\n - `agent_restricted_to_own_conversations` — agents see only their\n own + unassigned conversations in the inbox. Off by default\n (matches the helpdesk industry pattern of a shared inbox); on\n is the multi-team / compliance choice." - name: Support description: "\nThe customer side of in-app support: a workspace member opens a ticket\nand converses with Plixa staff. Auto-scoped to the caller's tenant by\nthe BelongsToTenant global scope on SupportTicket." - name: Team description: '' - name: 'Two-factor authentication' description: "\nEndpoints the user hits from /account to set up, confirm, regenerate\nrecovery codes for, and disable their own TOTP second factor. All\nmutations require the current password — defence in depth against\nstolen session tokens.\n\nLayout:\n - GET /v1/me/2fa → status (enabled, configured-but-not-confirmed)\n - POST /v1/me/2fa/setup → mint a fresh secret + otpauth URL\n - POST /v1/me/2fa/confirm → verify first code, return recovery codes\n - POST /v1/me/2fa/recovery-codes → regenerate recovery codes\n - DELETE /v1/me/2fa → disable 2FA entirely" - name: Waitlist description: '' - name: Webhooks description: '' - name: 'Webhooks (outbound)' description: "\nOwner-only management of customer-facing webhook endpoints. Plixa\nfires `message.{inbound,outbound}` and `conversation.{created,\nupdated,deleted}` to every active subscribed endpoint, signed with\nHMAC-SHA256 (X-Plixa-Signature).\n\nThe endpoint secret is shown ONCE at create time. Storage is\nencrypted, the API never echoes it back — operators who lose the\nsecret rotate the endpoint instead." - name: 'Welcome message' description: "\nOwner-defined greeting auto-sent the first time a contact reaches the\nworkspace. Unlike the AI configuration, this is available on every\nplan — no plan gate." - name: 'Workspace configuration' description: "\nTenant-wide settings every member is bound to:\n - timezone (IANA, e.g. America/Sao_Paulo). All scheduled features\n (business hours, digests, SLA windows, \"today\" rollups) anchor\n to THIS — never to the server fuso or the operator's browser.\n - default locale (en | pt-BR | es). Used as the fallback UI\n language for members who haven't set their own in /account, and\n as the language for transactional emails the workspace sends.\n\nOwner-only. Members see their personal locale override in /account." components: securitySchemes: default: type: http scheme: bearer description: 'Generate a token by calling POST /v1/auth/login or POST /v1/auth/register. Send it as Authorization: Bearer {token}.' security: - default: [] paths: /v1/ai/preview: post: summary: 'Preview an AI reply without sending anything' operationId: previewAnAIReplyWithoutSendingAnything description: "Runs the AI provider against the supplied business description and\neither a single message or a multi-turn conversation. Lets\noperators iterate on their prompt before flipping the toggle on.\n\nPlan-gated to Professional+ (same guard as production replies) so\nthe playground can't be used as a free LLM proxy. Additionally\nthrottled to 10 requests per minute per tenant." parameters: [] responses: { } tags: - 'AI configuration' requestBody: required: true content: application/json: schema: type: object properties: prompt: type: string description: 'The business description being tested. Max 4000 characters.' example: "We're a Brazilian bakery focused on sourdough..." message: type: string description: 'The customer message to react to. Required when `history` is not provided. Max 1000 characters.' example: 'Do you ship to Vila Mariana?' nullable: true history: type: array description: 'Multi-turn conversation. Each entry: { role: "user"|"assistant", content: string }. Must end with a `user` entry. Max 20 entries.' example: - architecto items: type: string nullable: true required: - prompt '/v1/conversations/{conversation}/ai-suggestions': post: summary: 'Generate three reply candidates for a conversation' operationId: generateThreeReplyCandidatesForAConversation description: '' parameters: [] responses: { } tags: - 'AI configuration' parameters: - in: path name: conversation description: 'Conversation id.' example: 42 required: true schema: type: integer /v1/ai-config: put: summary: 'Update the AI reply configuration' operationId: updateTheAIReplyConfiguration description: "Refuses the update with 403 + AI_PLAN_REQUIRED when the active plan\ndoesn't include AI replies (Starter)." parameters: [] responses: { } tags: - 'AI configuration' requestBody: required: true content: application/json: schema: type: object properties: prompt: type: string description: 'Plain-text description of the business. Max 4000 characters.' example: "We're a Brazilian bakery focused on sourdough..." enabled: type: boolean description: 'Turn AI replies on or off.' example: true required: - prompt - enabled /v1/ai-faqs: post: summary: '' operationId: postV1AiFaqs description: '' parameters: [] responses: { } tags: - 'AI configuration' requestBody: required: true content: application/json: schema: type: object properties: question: type: string description: 'Must not be greater than 280 characters.' example: b answer: type: string description: 'Must not be greater than 2000 characters.' example: 'n' required: - question - answer security: [] '/v1/ai-faqs/{faq}': put: summary: '' operationId: putV1AiFaqsFaq description: '' parameters: [] responses: { } tags: - 'AI configuration' requestBody: required: true content: application/json: schema: type: object properties: question: type: string description: 'Must not be greater than 280 characters.' example: b answer: type: string description: 'Must not be greater than 2000 characters.' example: 'n' required: - question - answer security: [] delete: summary: '' operationId: deleteV1AiFaqsFaq description: '' parameters: [] responses: { } tags: - 'AI configuration' security: [] parameters: - in: path name: faq description: '' example: '564' required: true schema: type: string /v1/ai-faqs/reorder: post: summary: 'Bulk reorder. Body shape: `{ "ids": [3, 1, 7, .' operationId: bulkReorderBodyShapeids317 description: "..] }`. Position\nis reassigned based on array order so the frontend can drag-\nand-drop without sending a full list of (id, position) pairs." parameters: [] responses: { } tags: - 'AI configuration' requestBody: required: false content: application/json: schema: type: object properties: ids: type: array description: '' example: - 16 items: type: integer security: [] /v1/ai/improve-prompt: post: summary: '' operationId: postV1AiImprovePrompt description: '' parameters: [] responses: { } tags: - 'AI configuration' requestBody: required: true content: application/json: schema: type: object properties: prompt: type: string description: 'Must not be greater than 4000 characters.' example: b required: - prompt security: [] /v1/api-tokens: post: summary: 'Create a new API token' operationId: createANewAPIToken description: "The plain-text token is returned ONCE in this response — the\npanel must surface it for the operator to copy. Subsequent\nfetches only carry the hashed prefix." parameters: [] responses: { } tags: - 'API tokens' requestBody: required: true content: application/json: schema: type: object properties: name: type: string description: 'Human label for the token. Up to 60 characters.' example: 'CRM integration' abilities: type: array description: 'Subset of {read, write}. At least one required.' example: - read - write items: type: string expires_in_days: type: integer description: 'Optional expiry in days from now. 1-365.' example: 90 nullable: true required: - name - abilities '/v1/api-tokens/{token}': delete: summary: 'Revoke an API token' operationId: revokeAnAPIToken description: '' parameters: [] responses: { } tags: - 'API tokens' parameters: - in: path name: token description: 'Token id.' example: 11 required: true schema: type: integer /v1/me: get: summary: 'Current user and workspace' operationId: currentUserAndWorkspace description: "Returns the authenticated user and the workspace (tenant) they belong to.\nUseful for the panel boot-up: a single call hydrates the session state." parameters: [] responses: 200: description: Authenticated content: application/json: schema: type: object example: data: user: id: 1 tenant_id: 1 name: 'Lucia Pereira' email: lucia@plixa.app role: owner tenant: id: 1 name: 'Lucia Studios' slug: lucia-studios-ab12cd country_code: BR timezone: America/Sao_Paulo locale: pt status: active meta: null errors: null properties: data: type: object properties: user: type: object properties: id: type: integer example: 1 tenant_id: type: integer example: 1 name: type: string example: 'Lucia Pereira' email: type: string example: lucia@plixa.app role: type: string example: owner tenant: type: object properties: id: type: integer example: 1 name: type: string example: 'Lucia Studios' slug: type: string example: lucia-studios-ab12cd country_code: type: string example: BR timezone: type: string example: America/Sao_Paulo locale: type: string example: pt status: type: string example: active meta: type: string example: null nullable: true errors: type: string example: null nullable: true 401: description: 'Not authenticated' content: application/json: schema: type: object example: data: null meta: null errors: - code: UNAUTHENTICATED message: 'Authentication required.' properties: data: type: string example: null nullable: true meta: type: string example: null nullable: true errors: type: array example: - code: UNAUTHENTICATED message: 'Authentication required.' items: type: object properties: code: type: string example: UNAUTHENTICATED message: type: string example: 'Authentication required.' tags: - Account patch: summary: "Update the current user's preferences" operationId: updateTheCurrentUsersPreferences description: "Only fields the operator can edit on themselves — name, locale.\nWorkspace-level settings (tenant.locale, plan, etc.) live on\nother endpoints." parameters: [] responses: { } tags: - Account requestBody: required: false content: application/json: schema: type: object properties: name: type: string description: 'Display name. Max 255 chars.' example: 'Lucia Pereira' locale: type: string description: 'UI language. One of `en`, `pt-BR`, or null to fall back to browser detection.' example: pt-BR nullable: true /v1/me/availability: put: summary: "Set whether I'm accepting new conversations" operationId: setWhetherImAcceptingNewConversations description: '' parameters: [] responses: { } tags: - Account requestBody: required: true content: application/json: schema: type: object properties: is_available: type: boolean description: '' example: false required: - is_available /v1/me/availability/heartbeat: post: summary: 'Presence heartbeat from the open panel' operationId: presenceHeartbeatFromTheOpenPanel description: '' parameters: [] responses: { } tags: - Account /v1/account/deletion: post: summary: 'Schedule the workspace for permanent deletion.' operationId: scheduleTheWorkspaceForPermanentDeletion description: '' parameters: [] responses: { } tags: - Account requestBody: required: true content: application/json: schema: type: object properties: password: type: string description: "The owner's current password." example: secret-password required: - password delete: summary: 'Cancel a scheduled deletion and restore the workspace.' operationId: cancelAScheduledDeletionAndRestoreTheWorkspace description: '' parameters: [] responses: { } tags: - Account /v1/admin/auth/login: post: summary: '' operationId: postV1AdminAuthLogin description: '' parameters: [] responses: { } tags: - 'Admin — Authentication' requestBody: required: true content: application/json: schema: type: object properties: email: type: string description: 'Must be a valid email address.' example: gbailey@example.net password: type: string description: '' example: '|]|{+-' required: - email - password security: [] /v1/admin/auth/logout: post: summary: '' operationId: postV1AdminAuthLogout description: '' parameters: [] responses: { } tags: - 'Admin — Authentication' security: [] /v1/admin/campaigns: post: summary: '' operationId: postV1AdminCampaigns description: '' parameters: [] responses: { } tags: - 'Admin — Email campaigns' security: [] /v1/admin/campaigns/test: post: summary: "Send a one-off test of the composed content to a single address so\nstaff can eyeball the rendered email before blasting the list. Sent\nsynchronously for immediate pass/fail feedback; not persisted." operationId: sendAOneOffTestOfTheComposedContentToASingleAddressSoStaffCanEyeballTheRenderedEmailBeforeBlastingTheListSentSynchronouslyForImmediatePassfailFeedbackNotPersisted description: '' parameters: [] responses: { } tags: - 'Admin — Email campaigns' security: [] '/v1/admin/campaigns/{id}': delete: summary: '' operationId: deleteV1AdminCampaignsId description: '' parameters: [] responses: { } tags: - 'Admin — Email campaigns' security: [] parameters: - in: path name: id description: 'The ID of the campaign.' example: architecto required: true schema: type: string /v1/admin/kb/articles: post: summary: '' operationId: postV1AdminKbArticles description: '' parameters: [] responses: { } tags: - 'Admin — Knowledge base' security: [] /v1/admin/kb/images: post: summary: '' operationId: postV1AdminKbImages description: '' parameters: [] responses: { } tags: - 'Admin — Knowledge base' security: [] '/v1/admin/kb/articles/{article}': put: summary: '' operationId: putV1AdminKbArticlesArticle description: '' parameters: [] responses: { } tags: - 'Admin — Knowledge base' security: [] delete: summary: '' operationId: deleteV1AdminKbArticlesArticle description: '' parameters: [] responses: { } tags: - 'Admin — Knowledge base' security: [] parameters: - in: path name: article description: 'The article.' example: '564' required: true schema: type: string /v1/admin/support/canned-replies: post: summary: '' operationId: postV1AdminSupportCannedReplies description: '' parameters: [] responses: { } tags: - 'Admin — Support' security: [] '/v1/admin/support/canned-replies/{cannedReply}': patch: summary: '' operationId: patchV1AdminSupportCannedRepliesCannedReply description: '' parameters: [] responses: { } tags: - 'Admin — Support' security: [] delete: summary: '' operationId: deleteV1AdminSupportCannedRepliesCannedReply description: '' parameters: [] responses: { } tags: - 'Admin — Support' security: [] parameters: - in: path name: cannedReply description: '' example: '564' required: true schema: type: string /v1/admin/support/tags: post: summary: '' operationId: postV1AdminSupportTags description: '' parameters: [] responses: { } tags: - 'Admin — Support' requestBody: required: true content: application/json: schema: type: object properties: name: type: string description: 'Must not be greater than 50 characters.' example: b required: - name security: [] '/v1/admin/support/tags/{id}': delete: summary: '' operationId: deleteV1AdminSupportTagsId description: '' parameters: [] responses: { } tags: - 'Admin — Support' security: [] parameters: - in: path name: id description: 'The ID of the tag.' example: architecto required: true schema: type: string '/v1/admin/support/tickets/{ticket}/tags': put: summary: '' operationId: putV1AdminSupportTicketsTicketTags description: '' parameters: [] responses: { } tags: - 'Admin — Support' requestBody: required: false content: application/json: schema: type: object properties: tag_ids: type: array description: 'The id of an existing record in the support_tags table.' example: - 16 items: type: integer security: [] parameters: - in: path name: ticket description: 'The ticket.' example: '564' required: true schema: type: string '/v1/admin/support/tickets/{ticket}/viewing': post: summary: 'Heartbeat for ticket presence; returns the other staff viewing it.' operationId: heartbeatForTicketPresenceReturnsTheOtherStaffViewingIt description: '' parameters: [] responses: { } tags: - 'Admin — Support' security: [] parameters: - in: path name: ticket description: 'The ticket.' example: '564' required: true schema: type: string '/v1/admin/support/tickets/{ticket}/priority': post: summary: '' operationId: postV1AdminSupportTicketsTicketPriority description: '' parameters: [] responses: { } tags: - 'Admin — Support' requestBody: required: true content: application/json: schema: type: object properties: priority: type: string description: '' example: architecto required: - priority security: [] parameters: - in: path name: ticket description: 'The ticket.' example: '564' required: true schema: type: string '/v1/admin/support/tickets/{ticket}/assign': post: summary: '' operationId: postV1AdminSupportTicketsTicketAssign description: '' parameters: [] responses: { } tags: - 'Admin — Support' requestBody: required: false content: application/json: schema: type: object properties: admin_id: type: integer description: 'The id of an existing record in the admins table.' example: 16 nullable: true security: [] parameters: - in: path name: ticket description: 'The ticket.' example: '564' required: true schema: type: string '/v1/admin/support/tickets/{ticket}/messages': post: summary: '' operationId: postV1AdminSupportTicketsTicketMessages description: '' parameters: [] responses: { } tags: - 'Admin — Support' security: [] parameters: - in: path name: ticket description: 'The ticket.' example: '564' required: true schema: type: string '/v1/admin/support/tickets/{ticket}/close': post: summary: '' operationId: postV1AdminSupportTicketsTicketClose description: '' parameters: [] responses: { } tags: - 'Admin — Support' security: [] parameters: - in: path name: ticket description: 'The ticket.' example: '564' required: true schema: type: string '/v1/admin/support/tickets/{ticket}/reopen': post: summary: '' operationId: postV1AdminSupportTicketsTicketReopen description: '' parameters: [] responses: { } tags: - 'Admin — Support' security: [] parameters: - in: path name: ticket description: 'The ticket.' example: '564' required: true schema: type: string '/v1/admin/support/tickets/{ticket}/status': post: summary: 'Move the ticket through its lifecycle (open / pending / solved / closed).' operationId: moveTheTicketThroughItsLifecycleopenPendingSolvedClosed description: '' parameters: [] responses: { } tags: - 'Admin — Support' requestBody: required: true content: application/json: schema: type: object properties: status: type: string description: '' example: architecto required: - status security: [] parameters: - in: path name: ticket description: 'The ticket.' example: '564' required: true schema: type: string '/v1/admin/tenants/{tenant_id}/suspend': post: summary: '' operationId: postV1AdminTenantsTenant_idSuspend description: '' parameters: [] responses: { } tags: - 'Admin — Tenants' security: [] parameters: - in: path name: tenant_id description: 'The ID of the tenant.' example: 16 required: true schema: type: integer '/v1/admin/tenants/{tenant_id}/reactivate': post: summary: '' operationId: postV1AdminTenantsTenant_idReactivate description: '' parameters: [] responses: { } tags: - 'Admin — Tenants' security: [] parameters: - in: path name: tenant_id description: 'The ID of the tenant.' example: 16 required: true schema: type: integer '/v1/admin/tenants/{tenant_id}/impersonate': post: summary: "Mint a short-lived tenant token so staff can step into a customer's\npanel to debug. Time-boxed to 30 minutes and recorded to\nadmin_audit_logs — this is a powerful, fully-traceable action." operationId: mintAShortLivedTenantTokenSoStaffCanStepIntoACustomersPanelToDebugTimeBoxedTo30MinutesAndRecordedToAdminAuditLogsThisIsAPowerfulFullyTraceableAction description: '' parameters: [] responses: { } tags: - 'Admin — Tenants' requestBody: required: false content: application/json: schema: type: object properties: user_id: type: integer description: '' example: 16 nullable: true security: [] parameters: - in: path name: tenant_id description: 'The ID of the tenant.' example: 16 required: true schema: type: integer '/v1/admin/tenants/{tenant_id}/license': post: summary: "Grant a manual, Plixa-issued license — independent of Stripe. Used to\ncomp design partners / beta testers a plan for a custom window\n(`expires_in_months`) or for life (omit it). Unlocks the plan's\nfeatures + seats immediately via Tenant::planKey()." operationId: grantAManualPlixaIssuedLicenseIndependentOfStripeUsedToCompDesignPartnersBetaTestersAPlanForACustomWindowexpiresInMonthsOrForLifeomitItUnlocksThePlansFeatures+SeatsImmediatelyViaTenantplanKey description: '' parameters: [] responses: { } tags: - 'Admin — Tenants' requestBody: required: true content: application/json: schema: type: object properties: plan_key: type: string description: '' example: architecto expires_in_months: type: integer description: 'Must be at least 1. Must not be greater than 120.' example: 22 nullable: true required: - plan_key security: [] delete: summary: 'Revoke a manual license grant — the tenant falls back to Stripe.' operationId: revokeAManualLicenseGrantTheTenantFallsBackToStripe description: '' parameters: [] responses: { } tags: - 'Admin — Tenants' security: [] parameters: - in: path name: tenant_id description: 'The ID of the tenant.' example: 16 required: true schema: type: integer /v1/auth/register: post: summary: 'Register a new workspace' operationId: registerANewWorkspace description: "Creates a Tenant (workspace) and the owner User in a single transaction,\nthen issues a Sanctum bearer token for the owner." parameters: [] responses: 201: description: 'Account created' content: application/json: schema: type: object example: data: user: id: 1 tenant_id: 1 name: 'Lucia Pereira' email: lucia@plixa.app role: owner tenant: id: 1 name: 'Lucia Studios' slug: lucia-studios-ab12cd country_code: BR timezone: America/Sao_Paulo locale: pt status: active token: 1|abcdef0123456789 meta: null errors: null properties: data: type: object properties: user: type: object properties: id: type: integer example: 1 tenant_id: type: integer example: 1 name: type: string example: 'Lucia Pereira' email: type: string example: lucia@plixa.app role: type: string example: owner tenant: type: object properties: id: type: integer example: 1 name: type: string example: 'Lucia Studios' slug: type: string example: lucia-studios-ab12cd country_code: type: string example: BR timezone: type: string example: America/Sao_Paulo locale: type: string example: pt status: type: string example: active token: type: string example: 1|abcdef0123456789 meta: type: string example: null nullable: true errors: type: string example: null nullable: true 422: description: 'Validation failure' content: application/json: schema: type: object example: data: null meta: null errors: - code: VALIDATION_FAILED message: 'The given data was invalid.' details: email: - 'The email has already been taken.' properties: data: type: string example: null nullable: true meta: type: string example: null nullable: true errors: type: array example: - code: VALIDATION_FAILED message: 'The given data was invalid.' details: email: - 'The email has already been taken.' items: type: object properties: code: type: string example: VALIDATION_FAILED message: type: string example: 'The given data was invalid.' details: type: object properties: email: type: array example: - 'The email has already been taken.' items: type: string tags: - Authentication requestBody: required: true content: application/json: schema: type: object properties: name: type: string description: 'Full name of the owner.' example: 'Lucia Pereira' email: type: string description: 'Owner email (lowercased server-side).' example: lucia@plixa.app password: type: string description: 'Minimum 8 characters.' example: super-secret-pw company_name: type: string description: 'Workspace display name.' example: 'Lucia Studios' country_code: type: string description: 'ISO 3166-1 alpha-2 country code.' example: BR tax_id: type: string description: 'Optional local tax ID (CPF/CNPJ/VAT/EIN).' example: architecto nullable: true timezone: type: string description: 'Optional IANA timezone. Defaults to UTC.' example: America/Sao_Paulo nullable: true locale: type: string description: 'Optional locale: en, pt or es. Defaults to en.' example: pt nullable: true password_confirmation: type: string description: 'Must match password.' example: super-secret-pw required: - name - email - password - company_name - country_code - password_confirmation security: [] /v1/auth/login: post: summary: 'Sign in' operationId: signIn description: "Returns a Sanctum bearer token on success. Email comparison is\ncase-insensitive. Wrong email and wrong password both return the same\n401 with code INVALID_CREDENTIALS — we don't leak user existence." parameters: [] responses: 200: description: 'Signed in' content: application/json: schema: type: object example: data: user: id: 1 tenant_id: 1 name: 'Lucia Pereira' email: lucia@plixa.app role: owner tenant: id: 1 name: 'Lucia Studios' slug: lucia-studios-ab12cd country_code: BR timezone: America/Sao_Paulo locale: pt status: active token: 1|abcdef0123456789 meta: null errors: null properties: data: type: object properties: user: type: object properties: id: type: integer example: 1 tenant_id: type: integer example: 1 name: type: string example: 'Lucia Pereira' email: type: string example: lucia@plixa.app role: type: string example: owner tenant: type: object properties: id: type: integer example: 1 name: type: string example: 'Lucia Studios' slug: type: string example: lucia-studios-ab12cd country_code: type: string example: BR timezone: type: string example: America/Sao_Paulo locale: type: string example: pt status: type: string example: active token: type: string example: 1|abcdef0123456789 meta: type: string example: null nullable: true errors: type: string example: null nullable: true 401: description: 'Invalid credentials' content: application/json: schema: type: object example: data: null meta: null errors: - code: INVALID_CREDENTIALS message: 'The email or password is incorrect.' properties: data: type: string example: null nullable: true meta: type: string example: null nullable: true errors: type: array example: - code: INVALID_CREDENTIALS message: 'The email or password is incorrect.' items: type: object properties: code: type: string example: INVALID_CREDENTIALS message: type: string example: 'The email or password is incorrect.' tags: - Authentication requestBody: required: true content: application/json: schema: type: object properties: email: type: string description: 'Account email.' example: lucia@plixa.app password: type: string description: 'Account password.' example: super-secret-pw required: - email - password security: [] /v1/auth/2fa-challenge: post: summary: 'Verify a TOTP or recovery code and complete the login.' operationId: verifyATOTPOrRecoveryCodeAndCompleteTheLogin description: '' parameters: [] responses: { } tags: - Authentication requestBody: required: false content: application/json: schema: type: object properties: code: type: string description: 'TOTP code from the authenticator app. Pass either this or recovery_code.' example: architecto nullable: true recovery_code: type: string description: 'One of the recovery codes generated at setup.' example: architecto nullable: true /v1/auth/logout: post: summary: 'Sign out' operationId: signOut description: "Revokes the token that authenticated this request. Other tokens on the\nsame account keep working (e.g. another device, another integration)." parameters: [] responses: 204: description: 'Signed out' content: text/plain: schema: type: string example: '' tags: - Authentication /v1/auto-assignment-rules: post: summary: 'Create a new auto-assignment rule.' operationId: createANewAutoAssignmentRule description: '' parameters: [] responses: { } tags: - Auto-assignment requestBody: required: true content: application/json: schema: type: object properties: priority: type: integer description: 'Lower priority runs first.' example: 100 condition_type: type: string description: 'One of always|keyword|label.' example: keyword condition_value: type: string description: 'Required when condition_type is keyword (phrase) or label (label id).' example: architecto action_type: type: string description: 'One of assign_user|round_robin|least_busy.' example: least_busy action_user_id: type: integer description: 'Required when action_type is assign_user.' example: 16 is_active: type: boolean description: 'Defaults to true.' example: false required: - priority - condition_type - action_type /v1/auto-assignment-rules/preview: post: summary: "Preview which rule (if any) would fire for a hypothetical inbound\nand who it would route to — without assigning anything or advancing\nthe round-robin cursor. Lets an owner verify rules before they act\non real customers." operationId: previewWhichRuleifAnyWouldFireForAHypotheticalInboundAndWhoItWouldRouteToWithoutAssigningAnythingOrAdvancingTheRoundRobinCursorLetsAnOwnerVerifyRulesBeforeTheyActOnRealCustomers description: '' parameters: [] responses: { } tags: - Auto-assignment requestBody: required: false content: application/json: schema: type: object properties: message: type: string description: 'A sample inbound message body to match keyword rules against.' example: 'I want a refund' nullable: true label_id: type: integer description: 'A label id to match label rules against.' example: 16 nullable: true '/v1/auto-assignment-rules/{rule}': put: summary: 'Update an existing auto-assignment rule.' operationId: updateAnExistingAutoAssignmentRule description: '' parameters: [] responses: { } tags: - Auto-assignment delete: summary: 'Delete an auto-assignment rule.' operationId: deleteAnAutoAssignmentRule description: '' parameters: [] responses: 204: description: '' content: application/json: schema: type: object example: { } properties: { } tags: - Auto-assignment parameters: - in: path name: rule description: '' example: '564' required: true schema: type: string /v1/billing/checkout: post: summary: 'Start a Stripe Checkout session' operationId: startAStripeCheckoutSession description: "Creates a Stripe Checkout Session for the authenticated user's workspace\nand returns the hosted URL the panel should redirect to. A 7-day free\ntrial is applied to the workspace's first subscription." parameters: [] responses: 201: description: 'Session created' content: application/json: schema: type: object example: data: url: 'https://checkout.stripe.com/c/pay/cs_test_abc123' plan: professional interval: month meta: null errors: null properties: data: type: object properties: url: type: string example: 'https://checkout.stripe.com/c/pay/cs_test_abc123' plan: type: string example: professional interval: type: string example: month meta: type: string example: null nullable: true errors: type: string example: null nullable: true 422: description: 'Invalid plan' content: application/json: schema: type: object example: data: null meta: null errors: - code: VALIDATION_FAILED message: 'The given data was invalid.' details: plan: - 'The selected plan is invalid.' properties: data: type: string example: null nullable: true meta: type: string example: null nullable: true errors: type: array example: - code: VALIDATION_FAILED message: 'The given data was invalid.' details: plan: - 'The selected plan is invalid.' items: type: object properties: code: type: string example: VALIDATION_FAILED message: type: string example: 'The given data was invalid.' details: type: object properties: plan: type: array example: - 'The selected plan is invalid.' items: type: string 500: description: 'Stripe configuration missing' content: application/json: schema: type: object example: data: null meta: null errors: - code: INTERNAL_ERROR message: 'This plan has no Stripe price configured yet.' properties: data: type: string example: null nullable: true meta: type: string example: null nullable: true errors: type: array example: - code: INTERNAL_ERROR message: 'This plan has no Stripe price configured yet.' items: type: object properties: code: type: string example: INTERNAL_ERROR message: type: string example: 'This plan has no Stripe price configured yet.' tags: - Billing requestBody: required: true content: application/json: schema: type: object properties: plan: type: string description: 'Plan key — one of "starter", "professional", "business".' example: professional interval: type: string description: 'Billing cadence — "month" (default) or "year". Annual is billed at 10x the monthly rate.' example: year required: - plan /v1/billing/portal: post: summary: 'Open the Stripe Customer Portal' operationId: openTheStripeCustomerPortal description: "Returns a one-shot URL that lets the customer update card, change\nplan, see invoices, or cancel — all without us writing UI for it.\nThe portal must be enabled in Stripe Dashboard → Settings → Billing\n→ Customer portal (test mode and live mode are separate)." parameters: [] responses: 200: description: 'Portal URL returned' content: application/json: schema: type: object example: data: url: 'https://billing.stripe.com/p/session/test_...' meta: null errors: null properties: data: type: object properties: url: type: string example: 'https://billing.stripe.com/p/session/test_...' meta: type: string example: null nullable: true errors: type: string example: null nullable: true 404: description: 'No Stripe customer yet (no subscription started)' content: application/json: schema: type: object example: data: null meta: null errors: - code: NOT_FOUND message: 'No active subscription to manage yet.' properties: data: type: string example: null nullable: true meta: type: string example: null nullable: true errors: type: array example: - code: NOT_FOUND message: 'No active subscription to manage yet.' items: type: object properties: code: type: string example: NOT_FOUND message: type: string example: 'No active subscription to manage yet.' tags: - Billing /v1/broadcasts: post: summary: 'Create and queue a broadcast.' operationId: createAndQueueABroadcast description: '' parameters: [] responses: { } tags: - Broadcasts requestBody: required: true content: application/json: schema: type: object properties: name: type: string description: 'Internal name for the broadcast. Up to 100 chars.' example: 'Black Friday 50% off' body: type: string description: 'Message body. @{{name}} and @{{first_name}} are substituted per recipient. Up to 1024 chars.' example: 'Hi @{{first_name}}! 50% off this Friday.' source: type: string description: 'One of `manual` (default) or `label`.' example: manual recipients: type: array description: 'Required when source = manual. Each entry: { phone, name? }. Max 1000.' example: - architecto items: type: string label_id: type: integer description: 'Required when source = label.' example: 16 scheduled_at: type: string description: 'Must be a valid date. Must be a date after now.' example: '2052-07-03' nullable: true required: - name - body '/v1/broadcasts/{broadcast}/cancel': post: summary: "Cancel a broadcast still in flight. Already-sent recipients\nstay sent; pending ones are marked failed by the worker on\nthe next check." operationId: cancelABroadcastStillInFlightAlreadySentRecipientsStaySentPendingOnesAreMarkedFailedByTheWorkerOnTheNextCheck description: '' parameters: [] responses: { } tags: - Broadcasts parameters: - in: path name: broadcast description: 'The broadcast.' example: '564' required: true schema: type: string /v1/business-hours-config: put: summary: 'Update the business hours configuration' operationId: updateTheBusinessHoursConfiguration description: '' parameters: [] responses: { } tags: - 'Business hours' requestBody: required: true content: application/json: schema: type: object properties: enabled: type: boolean description: 'Turn the closed-hours auto-reply on or off.' example: true message: type: string description: 'The auto-reply text sent outside business hours. Up to 500 characters.' example: "We're closed right now. We'll get back to you in the morning!" nullable: true hours: type: array description: 'Weekly schedule. Exactly 7 entries, weekday 0 (Sunday) to 6 (Saturday). Each entry has `open` and `close` in HH:MM and an `enabled` boolean.' example: - architecto items: type: string pause_sla: type: boolean description: '' example: false required: - enabled - hours /v1/csat-config: put: summary: 'Update the CSAT configuration' operationId: updateTheCSATConfiguration description: '' parameters: [] responses: { } tags: - CSAT requestBody: required: true content: application/json: schema: type: object properties: enabled: type: boolean description: 'Turn the survey on or off.' example: true question: type: string description: 'The rating prompt.' example: 'How was your support? Reply 1-5.' nullable: true follow_up_enabled: type: boolean description: 'Ask for a comment after a low score (<=3).' example: true follow_up_question: type: string description: 'The comment prompt.' example: 'What could we improve?' nullable: true thank_you: type: string description: 'Optional closing message after a rating.' example: 'Thanks for the feedback!' nullable: true visible_to_agents: type: boolean description: '' example: true required: - enabled /v1/contacts: post: summary: 'Create a contact (or return the existing one for the phone)' operationId: createAContactorReturnTheExistingOneForThePhone description: "Idempotent per (tenant, phone): manual creation from the panel —\ne.g. booking an appointment for a walk-in who hasn't messaged yet —\nmust not fail when that phone already wrote in. Open to every member\nbecause agents legitimately add customers while booking; only\nfield-value writes and deletion stay owner-gated." parameters: [] responses: { } tags: - Contacts requestBody: required: true content: application/json: schema: type: object properties: contact_phone: type: string description: "Plain digits, optional leading +. Stored without the + to\nmatch how Evolution returns numbers in @s.whatsapp.net. Must match the regex /^\\+?\\d{8,18}$/." example: '642559314232682282' contact_name: type: string description: 'Must not be greater than 120 characters.' example: u nullable: true required: - contact_phone '/v1/contacts/{contact}/labels/{label}': post: summary: 'Attach a label to the contact' operationId: attachALabelToTheContact description: "Idempotent: re-attaching is a no-op. Open to every workspace\nmember so agents can tag customers without owner approval." parameters: [] responses: { } tags: - Contacts security: [] delete: summary: 'Detach a label from the contact' operationId: detachALabelFromTheContact description: '' parameters: [] responses: { } tags: - Contacts security: [] parameters: - in: path name: contact description: 'The contact.' example: '564' required: true schema: type: string - in: path name: label description: 'The label.' example: '564' required: true schema: type: string '/v1/contacts/{id}': patch: summary: "Update a contact's editable fields" operationId: updateAContactsEditableFields description: '' parameters: [] responses: { } tags: - Contacts requestBody: required: false content: application/json: schema: type: object properties: contact_name: type: string description: 'Must not be greater than 120 characters.' example: b nullable: true notes: type: string description: 'Must not be greater than 4000 characters.' example: 'n' nullable: true delete: summary: 'Delete a contact' operationId: deleteAContact description: "Field values cascade with the contact row. Conversations stay\n(their `contact_id` becomes null via the FK on-delete rule);\nthe panel still shows them by phone." parameters: [] responses: { } tags: - Contacts parameters: - in: path name: id description: 'The ID of the contact.' example: architecto required: true schema: type: string '/v1/contacts/{contact}/custom-fields': put: summary: "Set / clear the contact's custom field values in one shot" operationId: setClearTheContactsCustomFieldValuesInOneShot description: "Body: `{ values: { [definition_id]: string|null } }`. Empty\nstrings get coerced to null. Unknown definition ids are\nsilently skipped — keeps the panel resilient to an out-of-\ndate list. Owner-only, plan-gated." parameters: [] responses: { } tags: - Contacts requestBody: required: false content: application/json: schema: type: object properties: values: type: array description: 'Must not be greater than 1000 characters.' example: - b items: type: string nullable: true parameters: - in: path name: contact description: 'The contact.' example: '564' required: true schema: type: string /v1/custom-field-definitions: post: summary: 'Create a new field definition' operationId: createANewFieldDefinition description: '' parameters: [] responses: { } tags: - 'Contacts (custom fields)' '/v1/custom-field-definitions/{definition}': put: summary: 'Update a field definition' operationId: updateAFieldDefinition description: '' parameters: [] responses: { } tags: - 'Contacts (custom fields)' delete: summary: 'Delete a field definition' operationId: deleteAFieldDefinition description: "Cascades: every value of this field across all contacts is\nwiped at the database level via the FK on-delete rule." parameters: [] responses: { } tags: - 'Contacts (custom fields)' parameters: - in: path name: definition description: '' example: '564' required: true schema: type: string /v1/custom-field-definitions/reorder: post: summary: 'Reorder field definitions' operationId: reorderFieldDefinitions description: "Same pattern as saved-replies reorder: ids in the desired\ntop-to-bottom order. Ids from other tenants get silently\ndropped at the SQL update layer." parameters: [] responses: { } tags: - 'Contacts (custom fields)' requestBody: required: false content: application/json: schema: type: object properties: ids: type: array description: '' example: - 16 items: type: integer /v1/conversations/bulk: post: summary: 'Apply an action to many conversations in one request' operationId: applyAnActionToManyConversationsInOneRequest description: "Cuts the number of round-trips for operators triaging the inbox.\nSupported actions: close, reopen, assign, delete, add_label,\nremove_label. Returns the number of rows actually affected\n(silently skips ids that don't belong to the tenant —\nwithoutGlobalScope is never applied here)." parameters: [] responses: { } tags: - Conversations requestBody: required: true content: application/json: schema: type: object properties: action: type: string description: 'One of `close`, `reopen`, `assign`, `delete`, `add_label`, `remove_label`.' example: close ids: type: array description: 'Conversation ids to act on. 1-100 per call.' example: - 12 - 17 - 19 items: type: integer assignee_id: type: integer description: 'Required when `action=assign`. Null to unassign.' example: 7 nullable: true label_id: type: integer description: 'Required when `action=add_label` or `remove_label`.' example: 3 nullable: true required: - action - ids '/v1/conversations/{id}': patch: summary: 'Update a conversation' operationId: updateAConversation description: "Edit the contact label shown for the conversation. WhatsApp doesn't\nalways send a pushName, so operators need to tag contacts manually." parameters: [] responses: { } tags: - Conversations requestBody: required: false content: application/json: schema: type: object properties: contact_name: type: string description: 'Friendly label for the contact. Pass empty string to clear.' example: 'Lucia Pereira' nullable: true notes: type: string description: 'Must not be greater than 4000 characters.' example: 'n' nullable: true assigned_user_id: type: integer description: '' example: 16 nullable: true delete: summary: 'Delete a conversation' operationId: deleteAConversation description: "Permanently removes a conversation and every message inside it\nfor the authenticated tenant. The contact on the customer's phone\nis unaffected — only Plixa's local copy goes. Useful while\nsandbox-testing." parameters: [] responses: 204: description: '' content: application/json: schema: type: object example: { } properties: { } tags: - Conversations parameters: - in: path name: id description: 'The ID of the conversation.' example: architecto required: true schema: type: string - in: path name: conversation description: 'Conversation id.' example: 42 required: true schema: type: integer '/v1/conversations/{conversation}/snooze': post: summary: 'Snooze a conversation' operationId: snoozeAConversation description: "Hides the conversation from the default inbox list until the\ngiven timestamp passes. The `plixa:wake-snoozed-conversations`\nscheduled command clears the stamp at the right moment and\nbroadcasts a `ConversationUpdated` so the inbox refreshes\nwithout a hard reload. Pass `until: null` to wake immediately." parameters: [] responses: { } tags: - Conversations requestBody: required: false content: application/json: schema: type: object properties: until: type: string description: 'Optional ISO-8601 timestamp. Null wakes immediately.' example: '2026-05-29T09:00:00Z' nullable: true parameters: - in: path name: conversation description: 'Conversation id.' example: 42 required: true schema: type: integer '/v1/conversations/{conversation_id}/messages': post: summary: '' operationId: postV1ConversationsConversation_idMessages description: '' parameters: [] responses: { } tags: - Conversations requestBody: required: true content: application/json: schema: type: object properties: body: type: string description: 'Must not be greater than 4096 characters.' example: b type: type: string description: '' example: text enum: - text - note quoted_message_id: type: integer description: "Optional: reference to a previous message in the same\nconversation. When the quoted row has an\n`evolution_message_id` we relay the WhatsApp-protocol\n`quoted` envelope so the customer's phone shows the\nreply attached to the original bubble. Quotes pointing\nat internal notes (which never reached WhatsApp) are\nsaved locally but skipped on the outbound payload." example: 16 nullable: true required: - body security: [] parameters: - in: path name: conversation_id description: 'The ID of the conversation.' example: architecto required: true schema: type: string '/v1/conversations/{conversation_id}/media-messages': post: summary: 'Send an outbound media message' operationId: sendAnOutboundMediaMessage description: "Accepts a multipart upload (image, video, audio, document) plus\nan optional caption. The file is forwarded to Evolution as\nbase64 — no S3 in MVP, no media URL we host. The resulting\nMessage row stores trimmed metadata so the inbox renders a\nthumbnail / link inline just like inbound media." parameters: [] responses: { } tags: - Conversations requestBody: required: true content: multipart/form-data: schema: type: object properties: file: type: string format: binary description: 'The file to send. Up to 16 MB.' caption: type: string description: 'Optional caption shown next to the media.' example: architecto nullable: true voice: type: boolean description: 'Send as a WhatsApp voice note (PTT) instead of a plain file.' example: true duration_seconds: type: integer description: 'Recorded length in seconds (used for voice notes).' example: 5 nullable: true waveform: type: array description: 'Must be at least 0. Must not be greater than 255.' example: - 7 items: type: number required: - file parameters: - in: path name: conversation_id description: 'The ID of the conversation.' example: architecto required: true schema: type: string - in: path name: conversation description: 'Conversation id.' example: 42 required: true schema: type: integer /v1/digest-config: put: summary: 'Update the daily digest configuration' operationId: updateTheDailyDigestConfiguration description: '' parameters: [] responses: { } tags: - 'Daily digest' requestBody: required: true content: application/json: schema: type: object properties: enabled: type: boolean description: 'Receive a summary email.' example: true frequency: type: string description: 'Cadence: `daily`, `weekly` (Mondays) or `monthly` (1st of the month). Defaults to `daily`.' example: weekly required: - enabled /v1/digest-config/test: post: summary: 'Send a one-off "test" digest to the current authenticated user.' operationId: sendAOneOfftestDigestToTheCurrentAuthenticatedUser description: "Uses the same stats payload + template as the scheduled job so\nthe operator gets a real preview of what tomorrow's email will\nlook like. Differences from the scheduled run:\n - Recipient is the CURRENT user, not the owner. (An owner\n testing from their phone might want it sent to a personal\n inbox via a copy of the account.)\n - Empty-activity workspaces still get the email — for a\n test, the operator wants the layout regardless." parameters: [] responses: { } tags: - 'Daily digest' /v1/ai/extract-business-hours: post: summary: '' operationId: postV1AiExtractBusinessHours description: '' parameters: [] responses: { } tags: - Endpoints requestBody: required: true content: application/json: schema: type: object properties: prompt: type: string description: 'Must not be greater than 4000 characters.' example: b required: - prompt security: [] /v1/flows: post: summary: 'Create a flow' operationId: createAFlow description: '' parameters: [] responses: { } tags: - Flows requestBody: required: true content: application/json: schema: type: object properties: name: type: string description: 'The flow name.' example: 'WhatsApp reception' draft_definition: type: object description: '' example: null properties: { } reprompt_after_minutes: type: integer description: 'Must be at least 0. Must not be greater than 1440.' example: 22 close_after_minutes: type: integer description: 'Must be at least 0. Must not be greater than 10080.' example: 7 reprompt_message: type: string description: 'Must not be greater than 500 characters.' example: z nullable: true close_conversation_on_timeout: type: boolean description: '' example: true required: - name '/v1/flows/{id}': put: summary: 'Save a flow draft' operationId: saveAFlowDraft description: 'Persists the working graph without affecting live conversations.' parameters: [] responses: { } tags: - Flows requestBody: required: false content: application/json: schema: type: object properties: name: type: string description: 'Must not be greater than 80 characters.' example: b draft_definition: type: object description: '' example: null properties: { } reprompt_after_minutes: type: integer description: 'Must be at least 0. Must not be greater than 1440.' example: 22 close_after_minutes: type: integer description: 'Must be at least 0. Must not be greater than 10080.' example: 7 reprompt_message: type: string description: 'Must not be greater than 500 characters.' example: z nullable: true close_conversation_on_timeout: type: boolean description: '' example: true delete: summary: 'Delete a flow' operationId: deleteAFlow description: '' parameters: [] responses: { } tags: - Flows parameters: - in: path name: id description: 'The ID of the flow.' example: architecto required: true schema: type: string '/v1/flows/{flow}/publish': post: summary: 'Publish a flow' operationId: publishAFlow description: 'Validates the draft graph and copies it into the live definition.' parameters: [] responses: { } tags: - Flows parameters: - in: path name: flow description: 'The flow.' example: '564' required: true schema: type: string '/v1/flows/{flow}/toggle': post: summary: 'Enable or disable a flow' operationId: enableOrDisableAFlow description: "Turning a flow on switches every other flow with the same trigger\noff, so only one reception flow runs at a time. A flow must be\npublished before it can be enabled." parameters: [] responses: { } tags: - Flows requestBody: required: true content: application/json: schema: type: object properties: enabled: type: boolean description: '' example: true required: - enabled parameters: - in: path name: flow description: 'The flow.' example: '564' required: true schema: type: string '/v1/flows/{flow}/ai-build': post: summary: 'Build / edit a flow draft via natural language' operationId: buildEditAFlowDraftViaNaturalLanguage description: '' parameters: [] responses: { } tags: - Flows requestBody: required: true content: application/json: schema: type: object properties: history: type: array description: 'Conversation so far, oldest-first. Each entry: { role: "user"|"assistant", content: string }. Must end with a `user` entry. Max 30 turns.' example: - architecto items: type: string graph: type: object description: "The current draft to edit. Defaults to the flow's saved draft_definition." example: [] properties: nodes: type: object description: '' example: null properties: { } edges: type: object description: '' example: null properties: { } required: - history parameters: - in: path name: flow description: 'The flow.' example: '564' required: true schema: type: string '/v1/conversations/{conversation}/summary': post: summary: 'Generate or fetch the cached AI summary for a conversation' operationId: generateOrFetchTheCachedAISummaryForAConversation description: '' parameters: - in: query name: refresh description: 'Pass `1` to force regeneration even when the cache is fresh.' example: 1 required: false schema: type: integer description: 'Pass `1` to force regeneration even when the cache is fresh.' example: 1 responses: { } tags: - Inbox parameters: - in: path name: conversation description: 'Conversation id.' example: 42 required: true schema: type: integer '/v1/conversations/{conversation}/labels/{label}': post: summary: 'Attach a label to a conversation' operationId: attachALabelToAConversation description: "Any workspace member can attach. Idempotent: re-attaching is a\n200 with the same payload, not a duplicate row." parameters: [] responses: { } tags: - Labels delete: summary: 'Detach a label from a conversation' operationId: detachALabelFromAConversation description: '' parameters: [] responses: { } tags: - Labels parameters: - in: path name: conversation description: 'The conversation.' example: '564' required: true schema: type: string - in: path name: label description: 'The label.' example: '564' required: true schema: type: string /v1/labels: post: summary: 'Create a label' operationId: createALabel description: Owner-only. parameters: [] responses: { } tags: - Labels '/v1/labels/{id}': put: summary: 'Update a label' operationId: updateALabel description: Owner-only. parameters: [] responses: { } tags: - Labels delete: summary: 'Delete a label' operationId: deleteALabel description: "Owner-only. Also detaches the label from every conversation it's\nattached to (cascade on conversation_label)." parameters: [] responses: { } tags: - Labels parameters: - in: path name: id description: 'The ID of the label.' example: architecto required: true schema: type: string /v1/messages/send: post: summary: '' operationId: postV1MessagesSend description: '' parameters: [] responses: { } tags: - 'Messages API' requestBody: required: true content: application/json: schema: type: object properties: phone: type: string description: "Plain digits, optionally a leading +. Stored without the +\nto match how Evolution returns numbers in @s.whatsapp.net. Must match the regex /^\\+?\\d{8,18}$/." example: '642559314232682282' body: type: string description: 'Must not be greater than 4096 characters.' example: u required: - phone - body security: [] /v1/onboarding/dismiss: post: summary: '' operationId: postV1OnboardingDismiss description: '' parameters: [] responses: { } tags: - Onboarding security: [] /v1/payments/connect: post: summary: 'Start (or resume) Stripe onboarding — returns a hosted URL to redirect to.' operationId: startorResumeStripeOnboardingReturnsAHostedURLToRedirectTo description: '' parameters: [] responses: { } tags: - 'Payments (Stripe Connect)' /v1/payments/dashboard-link: post: summary: "One-time login link to the workspace's Stripe Express dashboard." operationId: oneTimeLoginLinkToTheWorkspacesStripeExpressDashboard description: '' parameters: [] responses: { } tags: - 'Payments (Stripe Connect)' /v1/phone-instances: post: summary: 'Add a WhatsApp number' operationId: addAWhatsAppNumber description: "Owner-only. Provisions a brand-new Evolution instance for the\ntenant — up to the plan's `whatsapp_numbers` limit — and returns it\nin `pending_qr` state so the panel can immediately show the pairing\nQR. Rejected with PHONE_NUMBER_LIMIT_REACHED (409) once the plan's\nallowance is exhausted." parameters: [] responses: { } tags: - 'Phone instances' '/v1/phone-instances/{phone_instance}/disconnect': post: summary: 'Disconnect a phone instance' operationId: disconnectAPhoneInstance description: "Logs the WhatsApp session out of Evolution and flips the local\nstatus to `disconnected`. The next pairing will require a fresh\nQR scan. Owner-only — disconnecting silently mid-day would\ndisrupt the entire workspace." parameters: [] responses: { } tags: - 'Phone instances' parameters: - in: path name: phone_instance description: '' example: '564' required: true schema: type: string '/v1/phone-instances/{phone_instance}/restart': post: summary: 'Restart a phone instance' operationId: restartAPhoneInstance description: "Asks Evolution to bounce the connection without unpairing. Useful\nwhen the panel shows \"disconnected\" but the WhatsApp session is\nstill valid upstream and just needs a nudge. Flips the local\nstatus to `pending_qr` so the next QR poll picks up a fresh code." parameters: [] responses: { } tags: - 'Phone instances' parameters: - in: path name: phone_instance description: '' example: '564' required: true schema: type: string /v1/push/subscriptions: post: summary: 'Register or refresh a push subscription' operationId: registerOrRefreshAPushSubscription description: "Called by the panel right after the browser grants Notifications\npermission. Endpoint is unique per (user, browser) — the channel\nupserts so re-subscribing won't create duplicates." parameters: [] responses: { } tags: - 'Push notifications' requestBody: required: true content: application/json: schema: type: object properties: endpoint: type: string description: 'Must not be greater than 2048 characters.' example: b keys: type: object description: '' example: [] properties: p256dh: type: string description: '' example: architecto auth: type: string description: '' example: architecto required: - p256dh - auth content_encoding: type: string description: 'Must not be greater than 32 characters.' example: 'n' nullable: true required: - endpoint delete: summary: 'Remove a push subscription' operationId: removeAPushSubscription description: "Called when the user turns notifications off in the panel or when\nthe service worker reports the subscription has been revoked." parameters: [] responses: { } tags: - 'Push notifications' requestBody: required: true content: application/json: schema: type: object properties: endpoint: type: string description: 'Must not be greater than 2048 characters.' example: b required: - endpoint /v1/admin/push/subscriptions: post: summary: 'Register or refresh a push subscription' operationId: registerOrRefreshAPushSubscription description: "Called by the panel right after the browser grants Notifications\npermission. Endpoint is unique per (user, browser) — the channel\nupserts so re-subscribing won't create duplicates." parameters: [] responses: { } tags: - 'Push notifications' requestBody: required: true content: application/json: schema: type: object properties: endpoint: type: string description: 'Must not be greater than 2048 characters.' example: b keys: type: object description: '' example: [] properties: p256dh: type: string description: '' example: architecto auth: type: string description: '' example: architecto required: - p256dh - auth content_encoding: type: string description: 'Must not be greater than 32 characters.' example: 'n' nullable: true required: - endpoint delete: summary: 'Remove a push subscription' operationId: removeAPushSubscription description: "Called when the user turns notifications off in the panel or when\nthe service worker reports the subscription has been revoked." parameters: [] responses: { } tags: - 'Push notifications' requestBody: required: true content: application/json: schema: type: object properties: endpoint: type: string description: 'Must not be greater than 2048 characters.' example: b required: - endpoint /v1/sla-config: put: summary: 'Update the workspace SLA targets' operationId: updateTheWorkspaceSLATargets description: '' parameters: [] responses: { } tags: - 'SLA targets' requestBody: required: false content: application/json: schema: type: object properties: first_response_seconds: type: integer description: 'Seconds within which the first inbound burst should be answered. Null disables this target. Max 604800 (7 days).' example: 900 nullable: true resolution_seconds: type: integer description: 'Seconds within which a conversation should be closed. Null disables this target. Max 604800 (7 days).' example: 86400 nullable: true /v1/saved-replies: post: summary: 'Create a saved reply' operationId: createASavedReply description: Owner-only. parameters: [] responses: { } tags: - 'Saved replies' requestBody: required: true content: application/json: schema: type: object properties: label: type: string description: 'Short label shown in the picker. Max 80 chars.' example: Hours body: type: string description: 'The reply body. Max 2000 chars.' example: "We're open Monday to Saturday 9am-7pm." required: - label - body '/v1/saved-replies/{savedReply}': put: summary: 'Update a saved reply' operationId: updateASavedReply description: Owner-only. parameters: [] responses: { } tags: - 'Saved replies' requestBody: required: true content: application/json: schema: type: object properties: label: type: string description: 'Must not be greater than 80 characters.' example: b body: type: string description: 'Must not be greater than 2000 characters.' example: 'n' required: - label - body delete: summary: 'Delete a saved reply' operationId: deleteASavedReply description: Owner-only. parameters: [] responses: { } tags: - 'Saved replies' parameters: - in: path name: savedReply description: '' example: '564' required: true schema: type: string /v1/saved-replies/reorder: post: summary: 'Reorder saved replies' operationId: reorderSavedReplies description: "Accepts an array of saved-reply ids in the desired display order.\nIds missing from the array keep their current `position` so a\npartial drag (e.g., reordering 3 out of 30) doesn't trash the\nrest. Owner-only." parameters: [] responses: { } tags: - 'Saved replies' requestBody: required: true content: application/json: schema: type: object properties: ids: type: array description: 'Reply ids in the desired top-to-bottom order.' example: - 12 - 7 - 9 items: type: integer required: - ids /v1/scheduling/appointments: post: summary: 'Book an appointment manually (staff)' operationId: bookAnAppointmentManuallystaff description: '' parameters: [] responses: { } tags: - 'Scheduling — appointments' requestBody: required: true content: application/json: schema: type: object properties: service_id: type: integer description: '' example: 16 start: type: string description: 'Must be a valid date.' example: '2026-06-10T00:37:58' provider_id: type: integer description: '' example: 16 nullable: true contact_id: type: integer description: '' example: 16 nullable: true notes: type: string description: 'Must not be greater than 2000 characters.' example: 'n' nullable: true repeat: type: string description: '' example: null occurrences: type: integer description: 'Must be at least 1. Must not be greater than 52.' example: 7 required: - service_id - start '/v1/scheduling/appointments/{id}': patch: summary: "Edit an appointment's basic fields" operationId: editAnAppointmentsBasicFields description: "The booking form lets staff create an appointment without a contact\n(e.g. a slot reserved before the customer is on file). Those fields are\notherwise stuck once the appointment exists — there's no generic edit —\nso this patches them in afterwards. Only the keys actually sent are\ntouched, so the panel can update just the contact." parameters: [] responses: { } tags: - 'Scheduling — appointments' requestBody: required: false content: application/json: schema: type: object properties: contact_id: type: integer description: '' example: 16 nullable: true notes: type: string description: 'Must not be greater than 2000 characters.' example: 'n' nullable: true parameters: - in: path name: id description: 'The ID of the appointment.' example: 16 required: true schema: type: integer '/v1/scheduling/appointments/{appointment}/reschedule': post: summary: 'Reschedule an appointment' operationId: rescheduleAnAppointment description: '' parameters: [] responses: { } tags: - 'Scheduling — appointments' requestBody: required: true content: application/json: schema: type: object properties: start: type: string description: 'Must be a valid date.' example: '2026-06-10T00:37:58' required: - start parameters: - in: path name: appointment description: 'The appointment.' example: 564 required: true schema: type: integer '/v1/scheduling/appointments/{appointment}/cancel': post: summary: 'Cancel an appointment' operationId: cancelAnAppointment description: '' parameters: [] responses: { } tags: - 'Scheduling — appointments' requestBody: required: false content: application/json: schema: type: object properties: reason: type: string description: 'Must not be greater than 500 characters.' example: b nullable: true scope: type: string description: '' example: null parameters: - in: path name: appointment description: 'The appointment.' example: 564 required: true schema: type: integer '/v1/scheduling/appointments/{appointment}/no-show': post: summary: 'Mark an appointment as a no-show' operationId: markAnAppointmentAsANoShow description: '' parameters: [] responses: { } tags: - 'Scheduling — appointments' parameters: - in: path name: appointment description: 'The appointment.' example: 564 required: true schema: type: integer '/v1/scheduling/appointments/{appointment}/complete': post: summary: 'Mark an appointment as completed (the patient attended)' operationId: markAnAppointmentAsCompletedthePatientAttended description: '' parameters: [] responses: { } tags: - 'Scheduling — appointments' parameters: - in: path name: appointment description: 'The appointment.' example: 564 required: true schema: type: integer '/v1/scheduling/appointments/{appointment}/arrive': post: summary: 'Mark that the patient has arrived / checked in (waiting to be seen)' operationId: markThatThePatientHasArrivedCheckedInwaitingToBeSeen description: '' parameters: [] responses: { } tags: - 'Scheduling — appointments' parameters: - in: path name: appointment description: 'The appointment.' example: 564 required: true schema: type: integer '/v1/scheduling/appointments/{appointment}/payment-link': post: summary: "Create a Stripe Checkout link to collect an appointment's deposit /\nprepayment. Operational (owner + agent) so a front-desk agent can send\nthe link. Returns 422 when nothing is owed." operationId: createAStripeCheckoutLinkToCollectAnAppointmentsDepositPrepaymentOperationalowner+AgentSoAFrontDeskAgentCanSendTheLinkReturns422WhenNothingIsOwed description: '' parameters: [] responses: { } tags: - 'Scheduling — appointments' parameters: - in: path name: appointment description: 'The appointment.' example: 564 required: true schema: type: integer '/v1/scheduling/appointments/{appointment}/approve': post: summary: "Approve a pending (requested) booking → confirmed, and schedule its\nreminders. Owner only." operationId: approveAPendingrequestedBookingConfirmedAndScheduleItsRemindersOwnerOnly description: '' parameters: [] responses: { } tags: - 'Scheduling — appointments' parameters: - in: path name: appointment description: 'The appointment.' example: 564 required: true schema: type: integer '/v1/scheduling/appointments/{appointment}/decline': post: summary: 'Decline a pending (requested) booking → cancelled. Owner only.' operationId: declineAPendingrequestedBookingCancelledOwnerOnly description: '' parameters: [] responses: { } tags: - 'Scheduling — appointments' parameters: - in: path name: appointment description: 'The appointment.' example: 564 required: true schema: type: integer /v1/scheduling/default-schedule: put: summary: 'Replace the workspace base schedule (owner only)' operationId: replaceTheWorkspaceBaseScheduleownerOnly description: '' parameters: [] responses: { } tags: - 'Scheduling — base schedule' requestBody: required: true content: application/json: schema: type: object properties: timezone: type: string description: 'IANA timezone.' example: Asia/Yekaterinburg rules: type: array description: 'Weekly open windows ({ weekday, start_time, end_time }).' example: - architecto items: type: string required: - timezone - rules /v1/scheduling/calendar/google/disconnect: post: summary: "Disconnect the current user's Google Calendar." operationId: disconnectTheCurrentUsersGoogleCalendar description: '' parameters: [] responses: { } tags: - 'Scheduling — calendar sync' /v1/scheduling/holiday-config: put: summary: 'Enable or disable holiday blocking' operationId: enableOrDisableHolidayBlocking description: "Enabling imports holidays immediately; a network hiccup still saves the\ntoggle (synced=false) and the scheduled sync retries later." parameters: [] responses: { } tags: - 'Scheduling — holidays' requestBody: required: true content: application/json: schema: type: object properties: block_holidays: type: boolean description: 'Whether to block public holidays.' example: false required: - block_holidays '/v1/scheduling/providers/{user}/schedule': put: summary: "Replace a provider's schedule (owner only)" operationId: replaceAProvidersScheduleownerOnly description: 'Sends the full set of rules; the previous rules are replaced wholesale.' parameters: [] responses: { } tags: - 'Scheduling — provider availability' requestBody: required: true content: application/json: schema: type: object properties: timezone: type: string description: 'Must not be greater than 64 characters.' example: Asia/Yekaterinburg rules: type: array description: 'Must not have more than 200 items.' example: null items: type: object properties: kind: type: string description: '' example: architecto weekday: type: integer description: 'Must be at least 0. Must not be greater than 6.' example: 4 nullable: true date: type: string description: 'Must be a valid date in the format Y-m-d.' example: '2026-06-10' nullable: true start_time: type: string description: 'Must be a valid date in the format H:i.' example: '00:37' end_time: type: string description: 'Must be a valid date in the format H:i.' example: '00:37' required: - kind - start_time - end_time required: - timezone parameters: - in: path name: user description: '' example: '564' required: true schema: type: string /v1/scheduling/reminder-config: put: summary: "Set the workspace's reminder timing" operationId: setTheWorkspacesReminderTiming description: '' parameters: [] responses: { } tags: - 'Scheduling — reminders' requestBody: required: true content: application/json: schema: type: object properties: offsets: type: array description: 'Minutes before the appointment to remind (empty = off).' example: - 16 items: type: integer required: - offsets /v1/scheduling/resources: post: summary: 'Create a resource (owner only)' operationId: createAResourceownerOnly description: '' parameters: [] responses: { } tags: - 'Scheduling — resources' '/v1/scheduling/resources/{id}': put: summary: 'Update a resource (owner only)' operationId: updateAResourceownerOnly description: '' parameters: [] responses: { } tags: - 'Scheduling — resources' delete: summary: 'Delete a resource (owner only)' operationId: deleteAResourceownerOnly description: '' parameters: [] responses: { } tags: - 'Scheduling — resources' parameters: - in: path name: id description: 'The ID of the resource.' example: architecto required: true schema: type: string /v1/scheduling/calendar-filters: post: summary: 'Save a new filter' operationId: saveANewFilter description: '' parameters: [] responses: { } tags: - 'Scheduling — saved filters' '/v1/scheduling/calendar-filters/{filter}': put: summary: 'Update a saved filter' operationId: updateASavedFilter description: '' parameters: [] responses: { } tags: - 'Scheduling — saved filters' delete: summary: 'Delete a saved filter' operationId: deleteASavedFilter description: '' parameters: [] responses: { } tags: - 'Scheduling — saved filters' parameters: - in: path name: filter description: '' example: '564' required: true schema: type: string /v1/scheduling/services: post: summary: 'Create a service (owner only)' operationId: createAServiceownerOnly description: '' parameters: [] responses: { } tags: - 'Scheduling — services' '/v1/scheduling/services/{id}': put: summary: 'Update a service (owner only)' operationId: updateAServiceownerOnly description: '' parameters: [] responses: { } tags: - 'Scheduling — services' delete: summary: 'Delete a service (owner only)' operationId: deleteAServiceownerOnly description: '' parameters: [] responses: { } tags: - 'Scheduling — services' parameters: - in: path name: id description: 'The ID of the service.' example: architecto required: true schema: type: string '/v1/scheduling/waitlist/{entry_id}': delete: summary: 'Remove a waitlist entry' operationId: removeAWaitlistEntry description: '' parameters: [] responses: { } tags: - 'Scheduling — waitlist' parameters: - in: path name: entry_id description: 'The ID of the entry.' example: 16 required: true schema: type: integer /v1/scheduling/closures: post: summary: 'Close the workspace on a date (owner only)' operationId: closeTheWorkspaceOnADateownerOnly description: "Idempotent per (tenant, date): re-closing an already-closed day just\nreturns it. Manual source — a holiday sync owns its own rows." parameters: [] responses: { } tags: - 'Scheduling — workspace closures' requestBody: required: true content: application/json: schema: type: object properties: date: type: string description: 'Must be a valid date in the format Y-m-d.' example: '2026-06-10' reason: type: string description: 'Must not be greater than 255 characters.' example: b nullable: true required: - date '/v1/scheduling/closures/{id}': delete: summary: 'Reopen the workspace on a date (owner only)' operationId: reopenTheWorkspaceOnADateownerOnly description: '' parameters: [] responses: { } tags: - 'Scheduling — workspace closures' parameters: - in: path name: id description: 'The ID of the closure.' example: architecto required: true schema: type: string /v1/sectors: post: summary: 'Create a sector' operationId: createASector description: Owner-only. parameters: [] responses: { } tags: - Sectors '/v1/sectors/{id}': put: summary: 'Update a sector' operationId: updateASector description: 'Owner-only. Pass `user_ids` to replace the assigned agents.' parameters: [] responses: { } tags: - Sectors delete: summary: 'Delete a sector' operationId: deleteASector description: "Owner-only. Conversations tagged with this sector keep their\nhistory but lose the tag (sector_id is set to null)." parameters: [] responses: { } tags: - Sectors parameters: - in: path name: id description: 'The ID of the sector.' example: architecto required: true schema: type: string /v1/security-config: put: summary: 'Update the workspace security configuration' operationId: updateTheWorkspaceSecurityConfiguration description: "Toggling `require_two_factor` ON is gated on the owner having\ntheir own 2FA enabled — otherwise the owner could lock themselves\nout instantly. Turning it OFF has no such gate." parameters: [] responses: { } tags: - Security requestBody: required: false content: application/json: schema: type: object properties: require_two_factor: type: boolean description: 'When true, every member must enable 2FA before the panel grants access.' example: true agent_restricted_to_own_conversations: type: boolean description: 'When true, agents see only conversations assigned to themselves or unassigned. Owners always see all.' example: false /v1/support/tickets: post: summary: '' operationId: postV1SupportTickets description: '' parameters: [] responses: { } tags: - Support security: [] '/v1/support/tickets/{ticket_id}/messages': post: summary: '' operationId: postV1SupportTicketsTicket_idMessages description: '' parameters: [] responses: { } tags: - Support security: [] parameters: - in: path name: ticket_id description: 'The ID of the ticket.' example: 16 required: true schema: type: integer '/v1/support/tickets/{ticket_id}/close': post: summary: '' operationId: postV1SupportTicketsTicket_idClose description: '' parameters: [] responses: { } tags: - Support security: [] parameters: - in: path name: ticket_id description: 'The ID of the ticket.' example: 16 required: true schema: type: integer '/v1/support/tickets/{ticket_id}/reopen': post: summary: '' operationId: postV1SupportTicketsTicket_idReopen description: '' parameters: [] responses: { } tags: - Support security: [] parameters: - in: path name: ticket_id description: 'The ID of the ticket.' example: 16 required: true schema: type: integer '/v1/support/tickets/{ticket_id}/rating': post: summary: 'Customer satisfaction rating — only once the ticket is solved/closed.' operationId: customerSatisfactionRatingOnlyOnceTheTicketIsSolvedclosed description: '' parameters: [] responses: { } tags: - Support requestBody: required: true content: application/json: schema: type: object properties: rating: type: integer description: 'Must be at least 1. Must not be greater than 5.' example: 1 comment: type: string description: 'Must not be greater than 1000 characters.' example: 'n' nullable: true required: - rating security: [] parameters: - in: path name: ticket_id description: 'The ID of the ticket.' example: 16 required: true schema: type: integer /v1/team/invitations: post: summary: 'Create an invitation' operationId: createAnInvitation description: "Owner-only. Returns the freshly issued invitation including the\none-shot `accept_url` so the panel can render a copy-to-clipboard\nbutton. After the response the token is hidden — losing it means\nre-issuing the invite." parameters: [] responses: { } tags: - Team requestBody: required: true content: application/json: schema: type: object properties: email: type: string description: 'Person being invited.' example: agent@plixa.app role: type: string description: 'Optional role (defaults to `agent`).' example: agent nullable: true required: - email '/v1/team/invitations/{invitation}/resend': post: summary: 'Resend the invitation email' operationId: resendTheInvitationEmail description: "Owner-only. Queues the same InvitationCreated mailable that the\ninitial create() does — same accept_url, same expiry. Returns\nthe invitation with the link so the owner can also copy-paste\nit to another channel." parameters: [] responses: { } tags: - Team parameters: - in: path name: invitation description: 'Invitation id.' example: 14 required: true schema: type: integer '/v1/team/invitations/{id}': delete: summary: 'Cancel a pending invitation' operationId: cancelAPendingInvitation description: "Owner-only. Accepted invitations cannot be cancelled — remove the\nuser from the workspace instead via MemberController::destroy." parameters: [] responses: { } tags: - Team parameters: - in: path name: id description: 'The ID of the invitation.' example: architecto required: true schema: type: string - in: path name: invitation description: 'Invitation id.' example: 14 required: true schema: type: integer '/v1/team/members/{id}': delete: summary: 'Remove a member' operationId: removeAMember description: "Owner-only. Unassigns any conversations that were attached to the\nremoved user (their messages stay). Owners can't remove themselves\n— they should transfer ownership first (out of MVP scope)." parameters: [] responses: { } tags: - Team parameters: - in: path name: id description: 'The ID of the member.' example: architecto required: true schema: type: string - in: path name: member description: 'The user id.' example: 7 required: true schema: type: integer /v1/team/invitations/accept: post: summary: 'Accept an invitation' operationId: acceptAnInvitation description: "Public — creates the user, attaches them to the tenant, marks the\ninvitation accepted, and returns a Sanctum token so the panel can\nsign them in immediately." parameters: [] responses: { } tags: - Team requestBody: required: true content: application/json: schema: type: object properties: token: type: string description: 'Invitation token.' example: aBcD1234… name: type: string description: 'Display name.' example: 'Lucia Pereira' password: type: string description: 'Min 8 characters.' example: super-secret-pw required: - token - name - password security: [] /v1/me/2fa/setup: post: summary: '' operationId: postV1Me2faSetup description: '' parameters: [] responses: { } tags: - 'Two-factor authentication' requestBody: required: true content: application/json: schema: type: object properties: password: type: string description: '' example: '|]|{+-' required: - password security: [] /v1/me/2fa/confirm: post: summary: '' operationId: postV1Me2faConfirm description: '' parameters: [] responses: { } tags: - 'Two-factor authentication' requestBody: required: true content: application/json: schema: type: object properties: code: type: string description: '' example: architecto required: - code security: [] /v1/me/2fa/recovery-codes: post: summary: '' operationId: postV1Me2faRecoveryCodes description: '' parameters: [] responses: { } tags: - 'Two-factor authentication' requestBody: required: true content: application/json: schema: type: object properties: password: type: string description: '' example: '|]|{+-' required: - password security: [] /v1/me/2fa/recovery-codes/revoke: post: summary: 'Revoke a single recovery code' operationId: revokeASingleRecoveryCode description: "For when an operator suspects ONE code leaked (left a printout\nsomewhere, sent it in a Slack DM by mistake) but wants to keep\nthe others valid instead of regenerating the whole list.\n\nPassword-gated, audit-logged. Returns the count of remaining\ncodes — the panel uses it to surface \"you have N codes left\"." parameters: [] responses: { } tags: - 'Two-factor authentication' requestBody: required: true content: application/json: schema: type: object properties: password: type: string description: 'Current account password.' example: '********' code: type: string description: 'The recovery code to revoke.' example: a1b2c3d4e5 required: - password - code /v1/me/2fa: delete: summary: '' operationId: deleteV1Me2fa description: '' parameters: [] responses: { } tags: - 'Two-factor authentication' requestBody: required: true content: application/json: schema: type: object properties: password: type: string description: '' example: '|]|{+-' required: - password security: [] /v1/waitlist: post: summary: 'Join the public waitlist' operationId: joinThePublicWaitlist description: "Stores an email for early access. Plixa never sells this list. The\nlanding page (plixa.app) posts here from its hero and footer forms." parameters: [] responses: 201: description: Added content: application/json: schema: type: object example: data: id: 42 email: founder@plixa.app created_at: '2026-05-25T12:00:00+00:00' meta: null errors: null properties: data: type: object properties: id: type: integer example: 42 email: type: string example: founder@plixa.app created_at: type: string example: '2026-05-25T12:00:00+00:00' meta: type: string example: null nullable: true errors: type: string example: null nullable: true 409: description: 'Already on the list' content: application/json: schema: type: object example: data: null meta: null errors: - code: WAITLIST_EMAIL_ALREADY_REGISTERED message: 'This email is already on the waitlist.' properties: data: type: string example: null nullable: true meta: type: string example: null nullable: true errors: type: array example: - code: WAITLIST_EMAIL_ALREADY_REGISTERED message: 'This email is already on the waitlist.' items: type: object properties: code: type: string example: WAITLIST_EMAIL_ALREADY_REGISTERED message: type: string example: 'This email is already on the waitlist.' tags: - Waitlist requestBody: required: true content: application/json: schema: type: object properties: email: type: string description: 'Email to add to the waitlist. Lowercased server-side.' example: founder@plixa.app locale: type: string description: 'Optional browser locale (BCP-47).' example: en-US nullable: true referrer: type: string description: 'Optional URL that referred the visitor.' example: 'https://news.ycombinator.com/' nullable: true required: - email security: [] /v1/webhooks/stripe: post: summary: 'Handle a Stripe webhook call.' operationId: handleAStripeWebhookCall description: '' parameters: [] responses: { } tags: - Webhooks security: [] /v1/webhook-endpoints: post: summary: '' operationId: postV1WebhookEndpoints description: '' parameters: [] responses: { } tags: - 'Webhooks (outbound)' security: [] '/v1/webhook-endpoints/{endpoint}': put: summary: '' operationId: putV1WebhookEndpointsEndpoint description: '' parameters: [] responses: { } tags: - 'Webhooks (outbound)' security: [] delete: summary: '' operationId: deleteV1WebhookEndpointsEndpoint description: '' parameters: [] responses: { } tags: - 'Webhooks (outbound)' security: [] parameters: - in: path name: endpoint description: '' example: '564' required: true schema: type: string '/v1/webhook-endpoints/{endpoint}/test': post: summary: "Fire a synthetic `webhook.test` delivery so the operator can\nverify their receiver wiring without waiting for a real message." operationId: fireASyntheticwebhooktestDeliverySoTheOperatorCanVerifyTheirReceiverWiringWithoutWaitingForARealMessage description: '' parameters: [] responses: { } tags: - 'Webhooks (outbound)' security: [] parameters: - in: path name: endpoint description: '' example: '564' required: true schema: type: string '/v1/webhook-deliveries/{delivery}/retry': post: summary: 'Re-fire a previously-failed delivery' operationId: reFireAPreviouslyFailedDelivery description: "Resets the delivery back to `pending`, zeroes the attempts\ncounter so the existing exponential-backoff schedule starts\nfresh, and queues a new DeliverWebhookJob with the same\npayload. Useful after an operator fixes the receiver — they\ndon't have to wait for another organic event to confirm.\n\nRefuses to act on deliveries that aren't in a terminal\n`failed` state (no point retrying a pending one — the job\nalready has it)." parameters: [] responses: { } tags: - 'Webhooks (outbound)' parameters: - in: path name: delivery description: '' example: '564' required: true schema: type: string /v1/welcome-config: put: summary: 'Update the welcome message configuration' operationId: updateTheWelcomeMessageConfiguration description: '' parameters: [] responses: { } tags: - 'Welcome message' requestBody: required: false content: application/json: schema: type: object properties: message: type: string description: 'The greeting to send on first contact. Up to 1000 characters.' example: 'Hi! Thanks for reaching out — we usually reply within 15 minutes.' nullable: true enabled: type: boolean description: 'Turn the welcome message on or off.' example: true delay_seconds: type: integer description: 'Optional wait before the welcome fires. 0-300. Defaults to 0 (immediate).' example: 5 /v1/workspace-config: put: summary: 'Update the workspace timezone + default locale + currency' operationId: updateTheWorkspaceTimezone+DefaultLocale+Currency description: "Owner-only. The timezone field accepts only real IANA identifiers\n(validated against DateTimeZone::listIdentifiers) so a typo can't\nsilently corrupt every scheduled feature." parameters: [] responses: { } tags: - 'Workspace configuration' requestBody: required: true content: application/json: schema: type: object properties: timezone: type: string description: 'IANA timezone (e.g. America/Sao_Paulo).' example: Asia/Yekaterinburg locale: type: string description: 'One of `en`, `pt-BR`, or `es`.' example: sr_BA currency: type: string description: '' example: architecto required: - timezone - locale - currency /v1/workspace-config/logo: post: summary: 'Upload (or replace) the workspace logo' operationId: uploadorReplaceTheWorkspaceLogo description: "Owner-only. Stored on the shared public-image disk under a UUID key.\nPNG/JPEG/WebP, ≤ 1 MB — no SVG (XSS surface). Display size is enforced\nby the consumers (a fixed-height box), so any aspect ratio is fine." parameters: [] responses: { } tags: - 'Workspace configuration' requestBody: required: true content: multipart/form-data: schema: type: object properties: logo: type: string format: binary description: 'Must be a file. Must not be greater than 1024 kilobytes.' required: - logo delete: summary: 'Remove the workspace logo. Owner-only.' operationId: removeTheWorkspaceLogoOwnerOnly description: '' parameters: [] responses: { } tags: - 'Workspace configuration'