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. Staff\nwith TOTP enabled complete a second step at /v1/admin/auth/2fa-challenge." - name: 'Admin — Blog' description: "\nStaff authoring for the public marketing blog: CRUD + AI drafting. Drafts are\nnever published automatically — a human flips status to published. Every\nmutation is audited." - 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. AI-assisted\ndrafting lives in the sibling KbGeneratorController." - 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: 'Admin — Two-factor authentication' description: "\nStaff-facing mirror of the tenant TwoFactorSettingsController. Lets a staff\nmember set up, confirm, regenerate recovery codes for and disable their own\nTOTP second factor from the backoffice account page. Every mutation requires\nthe current password (defence against a stolen staff token) and is recorded\nto admin_audit_logs.\n\nLayout:\n - GET /v1/admin/me/2fa → status\n - POST /v1/admin/me/2fa/setup → mint a fresh secret + otpauth URL\n - POST /v1/admin/me/2fa/confirm → verify first code, return recovery codes\n - POST /v1/admin/me/2fa/recovery-codes → regenerate recovery codes\n - POST /v1/admin/me/2fa/recovery-codes/revoke → revoke a single code\n - DELETE /v1/admin/me/2fa → disable 2FA entirely" - 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: 'Birthday greetings' description: "\nOwner-only automation: a once-a-year WhatsApp \"happy birthday\" to contacts\nwhose chosen date custom field falls on today, sent at a tenant-local hour.\nReads are open to every member; mutations sit behind the `owner` middleware.\nGated to Professional+ (it rides on a date custom field)." - name: 'Blog (public)' description: "\nUnauthenticated, read-only access to PUBLISHED blog posts — consumed by the\nAstro landing site at build time and by anyone. The `published` scope is the\nsingle gate: a draft is never returned by either endpoint. Rate-limited at\nthe route. Responses carry only public fields (see BlogPostPresenter)." - 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: Exports description: "\nLGPD/GDPR data portability — owner-only contact-level dump of every\nunique conversation in the workspace, with aggregated message stats.\nSister export to ConversationExportController: that one is one row\nper message, this one is one row per (phone instance, contact phone).\nUse this when an operator wants to load the contact list into a CRM\nor email tool." - 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: 'Marketing outreach' description: "\nProactive cold-outreach campaigns (the marketing add-on). Owner-only,\nplan-gated to Business. Distinct from Broadcast: cold sends to a sourced\nlead list, minute-paced inside weekday/time windows, with AI message\nvariations, a per-campaign cooldown, and an explicit ban-risk\nacknowledgment. See Documentation/MARKETING_OUTREACH_SPEC.md." - name: 'Marketing outreach (lead sources)' description: "\nBroad, free-text lead discovery by query + locality (Phase B). Owner-only,\nBusiness-gated, rate-limited (every search hits an external source). The\nself-hosted scraper answers by default and degrades to open-data sources\n(OSM/CNES) when blocked. Only candidates with a usable public phone are\nreturned + saved, deduped by phone per tenant.\nSee Documentation/MARKETING_OUTREACH_SPEC.md." - 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\nRouting picks the WhatsApp number in this order: the token's own\nnumber scope when it has one, then an explicit `phone_instance_id`\nin the request, then the workspace's first connected number. A\nscoped token can never send through another number. Creates or\nreopens a conversation for the contact and persists the outbound\nMessage row so the panel renders it just like any other reply." - name: Notifications description: "\nThe in-app notification centre behind the header bell. Notifications are\ninherently per-user (the authenticated user is the notifiable), so every\nquery here is scoped through the user relation — no tenant filter needed,\nno way to read someone else's.\n\nThe stored `data` JSON is generic: a `type` the panel maps to a localized\ntemplate, `params` to interpolate, and a `link` to open on click." - name: Onboarding description: "\nConversational setup assistant: the owner chats about their business,\nthe AI interviews them and proposes configuration cards (hours,\nwelcome message, labels, saved replies, …). Nothing is written until\nthe owner approves cards, which land on the separate apply endpoint —\nwith fixed per-category handlers, never free-text interpretation.\n\nOwner-only (it drives workspace-wide configuration), plan-gated to\nthe AI feature and rate-limited: every chat turn is a paid LLM call." - 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: Re-engagement description: "\nOwner-managed config for the two AI revenue automations: no-show recovery\n(rebook a missed appointment) and reactivation (win back a dormant past\ncustomer). Both ship OFF; the owner opts in and customizes the copy.\nGated to Professional+ (they extend the booking engine). Reads are open to\nevery member; mutations are owner-only (route middleware)." - 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 — public booking page' description: "\nOwner toggle for the public self-service booking page (/book/{slug}).\nSome workspaces only take bookings through staff or the AI in chat and\ndon't want a shareable public link at all. Turning it off makes every\npublic booking endpoint answer 404 for the slug (in-flight payment\nconfirmations keep working — they resolve by Checkout session id).\nOwner-only (route group)." - 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: 'WhatsApp templates' description: '' - 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-faq-suggestions/{suggestion}/approve': post: summary: '' operationId: postV1AiFaqSuggestionsSuggestionApprove description: '' parameters: [] responses: { } tags: - 'AI configuration' requestBody: required: false 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' security: [] parameters: - in: path name: suggestion description: '' example: '564' required: true schema: type: string '/v1/ai-faq-suggestions/{suggestion}/dismiss': post: summary: '' operationId: postV1AiFaqSuggestionsSuggestionDismiss description: '' parameters: [] responses: { } tags: - 'AI configuration' security: [] parameters: - in: path name: suggestion description: '' example: '564' required: true schema: type: string /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 phone_instance_id: type: integer description: 'Optional WhatsApp number to pin the token to. A scoped token only sees and sends through that number; omit for workspace-wide access.' example: 3 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/2fa-challenge: post: summary: '' operationId: postV1AdminAuth2faChallenge description: '' parameters: [] responses: { } tags: - 'Admin — Authentication' requestBody: required: false content: application/json: schema: type: object properties: code: type: string description: '' example: architecto nullable: true recovery_code: type: string description: '' example: architecto nullable: true security: [] /v1/admin/auth/logout: post: summary: '' operationId: postV1AdminAuthLogout description: '' parameters: [] responses: { } tags: - 'Admin — Authentication' security: [] /v1/admin/blog/posts: post: summary: '' operationId: postV1AdminBlogPosts description: '' parameters: [] responses: { } tags: - 'Admin — Blog' security: [] /v1/admin/blog/generate: post: summary: 'Draft a post from a topic with AI. Always saved as a draft for review.' operationId: draftAPostFromATopicWithAIAlwaysSavedAsADraftForReview description: '' parameters: [] responses: { } tags: - 'Admin — Blog' requestBody: required: true content: application/json: schema: type: object properties: topic: type: string description: 'Must not be greater than 300 characters.' example: b required: - topic security: [] /v1/admin/blog/topics: post: summary: 'Suggest fresh, non-duplicate blog topics for the author to pick from.' operationId: suggestFreshNonDuplicateBlogTopicsForTheAuthorToPickFrom description: '' parameters: [] responses: { } tags: - 'Admin — Blog' security: [] /v1/admin/blog/bulk: post: summary: "Bulk-generate a content calendar: the AI invents N distinct topics, writes\na post for each, schedules them across the chosen cadence and (optionally)\ngenerates every cover — all in the background. The author reviews before\neach publish date." operationId: bulkGenerateAContentCalendarTheAIInventsNDistinctTopicsWritesAPostForEachSchedulesThemAcrossTheChosenCadenceAndoptionallyGeneratesEveryCoverAllInTheBackgroundTheAuthorReviewsBeforeEachPublishDate description: '' parameters: [] responses: { } tags: - 'Admin — Blog' requestBody: required: true content: application/json: schema: type: object properties: count: type: integer description: 'Must be at least 1.' example: 16 cadence: type: string description: '' example: architecto start_date: type: string description: 'Must be a valid date. Must be a date after or equal to today.' example: '2052-08-13' guidance: type: string description: 'Must not be greater than 300 characters.' example: 'n' nullable: true generate_covers: type: boolean description: '' example: true required: - count - cadence - start_date security: [] /v1/admin/blog/covers/bulk: post: summary: 'Queue cover generation for every post missing one — no per-post clicks.' operationId: queueCoverGenerationForEveryPostMissingOneNoPerPostClicks description: '' parameters: [] responses: { } tags: - 'Admin — Blog' security: [] '/v1/admin/blog/posts/{post}': put: summary: '' operationId: putV1AdminBlogPostsPost description: '' parameters: [] responses: { } tags: - 'Admin — Blog' security: [] delete: summary: '' operationId: deleteV1AdminBlogPostsPost description: '' parameters: [] responses: { } tags: - 'Admin — Blog' security: [] parameters: - in: path name: post description: 'The post.' example: '564' required: true schema: type: string '/v1/admin/blog/posts/{post}/cover': post: summary: 'Generate an AI cover image for a post and attach it.' operationId: generateAnAICoverImageForAPostAndAttachIt description: '' parameters: [] responses: { } tags: - 'Admin — Blog' security: [] delete: summary: 'Detach the cover image from a post.' operationId: detachTheCoverImageFromAPost description: '' parameters: [] responses: { } tags: - 'Admin — Blog' security: [] parameters: - in: path name: post description: 'The post.' example: '564' required: true schema: type: string '/v1/admin/blog/posts/{post}/translations': post: summary: 'Queue an AI (re)translation — one locale, or all when none is given.' operationId: queueAnAIretranslationOneLocaleOrAllWhenNoneIsGiven description: '' parameters: [] responses: { } tags: - 'Admin — Blog' requestBody: required: false content: application/json: schema: type: object properties: locale: type: string description: 'Must not be greater than 12 characters.' example: sr_BA nullable: true security: [] parameters: - in: path name: post description: 'The post.' example: '564' required: true schema: type: string /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/generate: post: summary: 'Generate a draft article for one screen and persist it.' operationId: generateADraftArticleForOneScreenAndPersistIt description: '' parameters: [] responses: { } tags: - 'Admin — Knowledge base' requestBody: required: true content: application/json: schema: type: object properties: screen: type: string description: '' example: architecto required: - screen security: [] /v1/admin/kb/images: post: summary: '' operationId: postV1AdminKbImages description: '' parameters: [] responses: { } tags: - 'Admin — Knowledge base' security: [] '/v1/admin/kb/articles/{article}/translations/{locale}': put: summary: '' operationId: putV1AdminKbArticlesArticleTranslationsLocale description: '' parameters: [] responses: { } tags: - 'Admin — Knowledge base' requestBody: required: true content: application/json: schema: type: object properties: title: type: string description: 'Must not be greater than 200 characters.' example: b body: type: string description: 'Must not be greater than 20000 characters.' example: 'n' required: - title - body security: [] parameters: - in: path name: article description: 'The article.' example: '564' required: true schema: type: string - in: path name: locale description: '' example: sr_BA required: true schema: type: string '/v1/admin/kb/articles/{article}/translations/{locale}/regenerate': post: summary: '' operationId: postV1AdminKbArticlesArticleTranslationsLocaleRegenerate description: '' parameters: [] responses: { } tags: - 'Admin — Knowledge base' security: [] parameters: - in: path name: article description: 'The article.' example: '564' required: true schema: type: string - in: path name: locale description: '' example: sr_BA required: true schema: type: string '/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}/phone-instances/{phoneInstance}/clear-warmup': post: summary: "Release one of the workspace's numbers from its warm-up window. For when\nthe WhatsApp account is established and only its Plixa connection is new —\nthe proactive-send ban guard is then unnecessary. Records who lifted it." operationId: releaseOneOfTheWorkspacesNumbersFromItsWarmUpWindowForWhenTheWhatsAppAccountIsEstablishedAndOnlyItsPlixaConnectionIsNewTheProactiveSendBanGuardIsThenUnnecessaryRecordsWhoLiftedIt description: '' parameters: [] responses: { } tags: - 'Admin — Tenants' parameters: - in: path name: tenant_id description: 'The ID of the tenant.' example: 16 required: true schema: type: integer - in: path name: phoneInstance description: '' example: 564 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/admin/tenants/{id}': delete: summary: "Permanently delete a workspace and ALL its data. Super-admin only (the\nroute is `admin.super`-gated). Irreversible: cancels Stripe, frees the\nEvolution numbers, and cascade-deletes every tenant row. The caller must\necho the workspace slug back as `confirm` — a deliberate fat-finger guard." operationId: permanentlyDeleteAWorkspaceAndALLItsDataSuperAdminOnlytheRouteIsadminsuperGatedIrreversibleCancelsStripeFreesTheEvolutionNumbersAndCascadeDeletesEveryTenantRowTheCallerMustEchoTheWorkspaceSlugBackAsconfirmADeliberateFatFingerGuard description: '' parameters: [] responses: { } tags: - 'Admin — Tenants' requestBody: required: true content: application/json: schema: type: object properties: confirm: type: string description: '' example: architecto required: - confirm parameters: - in: path name: id description: 'The ID of the tenant.' example: 16 required: true schema: type: integer /v1/admin/me/2fa/setup: post: summary: '' operationId: postV1AdminMe2faSetup description: '' parameters: [] responses: { } tags: - 'Admin — Two-factor authentication' requestBody: required: true content: application/json: schema: type: object properties: password: type: string description: '' example: '|]|{+-' required: - password security: [] /v1/admin/me/2fa/confirm: post: summary: '' operationId: postV1AdminMe2faConfirm description: '' parameters: [] responses: { } tags: - 'Admin — Two-factor authentication' requestBody: required: true content: application/json: schema: type: object properties: code: type: string description: '' example: architecto required: - code security: [] /v1/admin/me/2fa/recovery-codes: post: summary: '' operationId: postV1AdminMe2faRecoveryCodes description: '' parameters: [] responses: { } tags: - 'Admin — Two-factor authentication' requestBody: required: true content: application/json: schema: type: object properties: password: type: string description: '' example: '|]|{+-' required: - password security: [] /v1/admin/me/2fa/recovery-codes/revoke: post: summary: '' operationId: postV1AdminMe2faRecoveryCodesRevoke description: '' parameters: [] responses: { } tags: - 'Admin — Two-factor authentication' requestBody: required: true content: application/json: schema: type: object properties: password: type: string description: '' example: '|]|{+-' code: type: string description: 'Must be at least 4 characters. Must not be greater than 64 characters.' example: v required: - password - code security: [] /v1/admin/me/2fa: delete: summary: '' operationId: deleteV1AdminMe2fa description: '' parameters: [] responses: { } tags: - 'Admin — Two-factor authentication' requestBody: required: true content: application/json: schema: type: object properties: password: type: string description: '' example: '|]|{+-' required: - password security: [] /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 signup_source: type: string description: 'Must contain only letters, numbers, dashes and underscores. Must not be greater than 40 characters.' example: i 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/auth/email/resend: post: summary: '' operationId: postV1AuthEmailResend description: '' parameters: [] responses: { } tags: - Authentication security: [] /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/birthday-config: put: summary: 'Update the birthday greeting configuration' operationId: updateTheBirthdayGreetingConfiguration description: "Owner-only. Enabling requires a date custom field to be selected — a\ngreeting with no birthday source would never fire." parameters: [] responses: { } tags: - 'Birthday greetings' requestBody: required: true content: application/json: schema: type: object properties: enabled: type: boolean description: 'Send greetings automatically.' example: true field_definition_id: type: integer description: "The date custom field holding each contact's birthday." example: 12 nullable: true message: type: string description: 'Custom greeting; blank uses the localized Plixa default.' example: '🎉 Happy birthday, {first_name}!' nullable: true hour: type: integer description: 'Hour (0-23) in the workspace timezone to send.' example: 9 required: - enabled /v1/birthday-config/image: post: summary: 'Upload (or replace) the image sent alongside the greeting. Owner-only.' operationId: uploadorReplaceTheImageSentAlongsideTheGreetingOwnerOnly description: 'PNG/JPEG/WebP, ≤ 1 MB — same rules as the workspace logo (no SVG).' parameters: [] responses: { } tags: - 'Birthday greetings' requestBody: required: true content: multipart/form-data: schema: type: object properties: image: type: string format: binary description: 'Must be a file. Must not be greater than 1024 kilobytes.' required: - image delete: summary: 'Remove the greeting image. Owner-only.' operationId: removeTheGreetingImageOwnerOnly description: '' parameters: [] responses: { } tags: - 'Birthday greetings' /v1/birthday-config/test: post: summary: "Send a one-off test greeting to a phone number the owner enters, using\nthe saved message + image, so they see exactly what a customer gets." operationId: sendAOneOffTestGreetingToAPhoneNumberTheOwnerEntersUsingTheSavedMessage+ImageSoTheySeeExactlyWhatACustomerGets description: "Bypasses the date / opt-out / once-a-year guards (it's a preview)." parameters: [] responses: { } tags: - 'Birthday greetings' requestBody: required: true content: application/json: schema: type: object properties: phone: type: string description: 'WhatsApp number to receive the test.' example: '5511999999999' required: - phone '/v1/public/blog/posts/{slug}/events': post: summary: "Ingest an anonymous analytics beacon for a published post. Two event\ntypes: 'view' (recorded once per pageview on load) and 'ping' (a\nbest-effort engaged-time + scroll update sent when the reader leaves)." operationId: ingestAnAnonymousAnalyticsBeaconForAPublishedPostTwoEventTypesviewrecordedOncePerPageviewOnLoadAndpingaBestEffortEngagedTime+ScrollUpdateSentWhenTheReaderLeaves description: "Always answers 204 — fire-and-forget from the browser, nothing to leak.\nPrivacy: no cookie, no PII; the visitor is a daily-rotating salted hash." parameters: [] responses: { } tags: - 'Blog (public)' security: [] parameters: - in: path name: slug description: 'The slug of the post.' example: architecto required: true schema: type: string /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 (required unless whatsapp_template_id is set). @{{name}} and @{{first_name}} are substituted per recipient. Up to 1024 chars.' example: 'Hi @{{first_name}}! 50% off this Friday.' nullable: true whatsapp_template_id: type: integer description: 'Send an approved Cloud API template instead of free text (required to broadcast on a Cloud-API-only workspace).' example: 4 template_params: type: array description: 'One renderable value per template placeholder; @{{name}} / @{{first_name}} tokens resolve per recipient.' example: - architecto items: type: string 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: - magni 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-08-14' nullable: true apply_label_ids: type: array description: '' example: - 16 items: type: integer required: - name '/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: true 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/bulk: post: summary: 'Bulk-act on selected contacts' operationId: bulkActOnSelectedContacts description: "Owner-gated for `delete` (irreversible, same rule as the single-contact\ndelete); label + opt-out actions stay open to members. Max 100 ids per\ncall, tenant-scoped (the global scope bounds every query), idempotent." parameters: [] responses: { } tags: - Contacts requestBody: required: true content: application/json: schema: type: object properties: action: type: string description: 'One of delete, add_label, remove_label, opt_out, opt_in.' example: architecto ids: type: array description: 'Contact ids to act on (1-100).' example: - 16 items: type: integer label_id: type: integer description: 'Required for add_label / remove_label.' example: 16 nullable: true required: - action - ids '/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/contacts/import: post: summary: 'Import contacts from a CSV file' operationId: importContactsFromACSVFile description: '' parameters: [] responses: { } tags: - Contacts requestBody: required: true content: multipart/form-data: schema: type: object properties: file: type: string format: binary description: 'Must be a file. Must not be greater than 5120 kilobytes.' dry_run: type: boolean description: '' example: true nullable: true required: - file /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: post: summary: 'Start a conversation' operationId: startAConversation description: "Creates (or returns) the conversation for a phone number so an\nagent can message a customer proactively — the inbox previously\nonly showed threads the customer started. The new conversation is\nassigned to the requesting agent and marked as human-handled, so\nno automation (welcome / flow / AI) fires when the customer\nreplies. When a conversation for that number already exists on\nthe chosen WhatsApp connection it is returned as-is instead." parameters: [] responses: { } tags: - Conversations requestBody: required: true content: application/json: schema: type: object properties: phone: type: string description: 'Customer phone in international format, digits only or formatted.' example: '5511999998888' name: type: string description: 'Optional contact name.' example: 'Lucia Pereira' nullable: true phone_instance_id: type: integer description: 'Required when the workspace has more than one connected number.' example: 3 nullable: true required: - phone /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: '' operationId: postV1ConversationsConversationSnooze description: '' parameters: [] responses: { } tags: - Conversations requestBody: required: false content: application/json: schema: type: object properties: until: type: string description: 'Must be a valid date.' example: '2026-07-21T15:01:46' nullable: true security: [] parameters: - in: path name: conversation description: 'The conversation.' example: '564' required: true schema: type: string '/v1/conversations/{conversation}/read': post: summary: 'Mark a thread read — stamps `last_read_at` so its unread dot clears.' operationId: markAThreadReadStampslastReadAtSoItsUnreadDotClears description: "Called when an agent opens the thread (and as fresh messages land while\nit's open). Not audited: read state is high-frequency, low-signal." parameters: [] responses: { } tags: - Conversations security: [] parameters: - in: path name: conversation description: 'The conversation.' example: '564' required: true schema: type: string '/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}/messages/{message}/reactions': post: summary: 'React to a message with an emoji from the panel (WhatsApp "tapback").' operationId: reactToAMessageWithAnEmojiFromThePanelWhatsApptapback description: "Relays the reaction to WhatsApp via Evolution, then folds it onto the\ntarget message's `reactions` list under the workspace's own slot\n(`from_me: true`). An empty emoji clears our reaction.\n\nDeliberately does NOT engage human takeover: a reaction is a\nlightweight acknowledgement, not a reply, so it shouldn't silence the\nbot or claim the thread the way sending a message does." parameters: [] responses: { } tags: - Conversations requestBody: required: false content: application/json: schema: type: object properties: emoji: type: string description: "The emoji to react with; empty clears our reaction. 16 bytes\nfits any multi-codepoint emoji (skin-tone / ZWJ sequences)\nwithout allowing a whole message through this field. Must not be greater than 16 characters." example: bngzmiyvdljnikhw nullable: true security: [] parameters: - in: path name: conversation description: 'The conversation.' example: '564' required: true schema: type: string - in: path name: message description: 'The message.' example: 564 required: true schema: type: integer '/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/contacts/export: post: summary: 'Download the SELECTED contacts as CSV' operationId: downloadTheSELECTEDContactsAsCSV description: "Powers the contacts page's \"export selected\" bulk action: a row per\ncontact (the `contacts` table) for the posted ids, with labels joined.\nOwner-only, tenant-scoped." parameters: [] responses: { } tags: - Exports requestBody: required: true content: application/json: schema: type: object properties: ids: type: array description: 'Contact ids to export.' example: - 16 items: type: integer required: - ids /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: false 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: false 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/flows/{flow}/simulate': post: summary: 'Run one simulated turn of a flow' operationId: runOneSimulatedTurnOfAFlow description: "Send the resumable `state` from the previous turn (omit to start), the\ncustomer's latest `message`, and the visible text `history`. Optionally\nsend a `graph` to test the unsaved canvas; otherwise the flow's saved\ndraft (falling back to the published definition) is used." parameters: [] responses: { } tags: - Flows requestBody: required: false content: application/json: schema: type: object properties: graph: type: object description: 'The {nodes, edges} graph to test. Defaults to the saved draft.' example: [] properties: nodes: type: object description: '' example: null properties: { } edges: type: object description: '' example: null properties: { } state: type: object description: 'The resumable blob returned by the previous turn. Omit to start.' example: [] properties: { } nullable: true message: type: string description: "The customer's latest message. Omit on the first (start) turn. Max 2000 characters." example: architecto nullable: true history: type: array description: 'Full visible text transcript so far. Each entry: { role: "user"|"assistant", content: string }. Max 40 entries.' example: - architecto items: type: string contact_name: type: string description: 'The persona name the test customer goes by. Max 120 characters.' example: Maria nullable: true 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/marketing/campaigns: post: summary: 'Create a draft campaign. The ban-risk acknowledgment is mandatory.' operationId: createADraftCampaignTheBanRiskAcknowledgmentIsMandatory description: '' parameters: [] responses: { } tags: - 'Marketing outreach' requestBody: required: true content: application/json: schema: type: object properties: name: type: string description: 'Must not be greater than 100 characters.' example: b body: type: string description: 'Must not be greater than 1024 characters.' example: 'n' name_fallback: type: string description: 'Must not be greater than 40 characters.' example: g nullable: true phone_instance_id: type: integer description: '' example: 16 nullable: true pacing_min_seconds: type: integer description: '' example: 16 nullable: true pacing_max_seconds: type: integer description: '' example: 16 nullable: true daily_cap: type: integer description: 'Must be at least 1.' example: 22 nullable: true send_weekdays: type: array description: 'Must be between 0 and 6.' example: - 0 items: type: integer send_windows: type: array description: 'Must not have more than 6 items.' example: null items: type: object nullable: true properties: start: type: string description: 'This field is required when send_windows is present. Must be a valid date in the format H:i.' example: '15:01' end: type: string description: 'This field is required when send_windows is present. Must be a valid date in the format H:i.' example: '15:01' timezone: type: string description: 'Must be a valid time zone, such as Africa/Accra.' example: America/Bahia_Banderas nullable: true scheduled_at: type: string description: 'Must be a valid date. Must be a date after now.' example: '2052-08-14' nullable: true followup_enabled: type: boolean description: '' example: true followup_body: type: string description: 'Must not be greater than 1024 characters.' example: 'n' nullable: true followup_delay_hours: type: integer description: 'Must not be greater than 8760.' example: 7 nullable: true apply_label_ids: type: array description: '' example: - 16 items: type: integer risk_ack: type: boolean description: "Founder rule: outreach can never be created without the operator\nacknowledging the ban risk. Must be accepted." example: true required: - name - body - risk_ack '/v1/marketing/campaigns/{campaign}/recipients': post: summary: "Attach a lead list to a campaign. De-dupes within the payload and\nagainst already-attached rows, and drops opted-out contacts up front." operationId: attachALeadListToACampaignDeDupesWithinThePayloadAndAgainstAlreadyAttachedRowsAndDropsOptedOutContactsUpFront description: '' parameters: [] responses: { } tags: - 'Marketing outreach' requestBody: required: true content: application/json: schema: type: object properties: recipients: type: array description: 'Must have at least 1 items. Must not have more than 1000 items.' example: - [] items: type: object properties: phone: type: string description: 'Must match the regex /^\+?\d{8,18}$/.' example: '642559314232682282' name: type: string description: 'Must not be greater than 120 characters.' example: u nullable: true locality: type: string description: 'Must not be greater than 120 characters.' example: w nullable: true source: type: string description: 'Must not be greater than 40 characters.' example: p nullable: true required: - phone required: - recipients delete: summary: "Remove every pending recipient at once — the quick way to swap the list\n(e.g. after duplicating). Sent recipients are untouched." operationId: removeEveryPendingRecipientAtOnceTheQuickWayToSwapTheListegAfterDuplicatingSentRecipientsAreUntouched description: '' parameters: [] responses: { } tags: - 'Marketing outreach' parameters: - in: path name: campaign description: 'The campaign.' example: '564' required: true schema: type: string '/v1/marketing/campaigns/{campaign}/recipients/{recipient}': delete: summary: "Remove a single pending recipient from a campaign. Only pending rows can\ngo — a sent recipient is immutable history. Editable statuses only." operationId: removeASinglePendingRecipientFromACampaignOnlyPendingRowsCanGoASentRecipientIsImmutableHistoryEditableStatusesOnly description: '' parameters: [] responses: { } tags: - 'Marketing outreach' parameters: - in: path name: campaign description: 'The campaign.' example: '564' required: true schema: type: string - in: path name: recipient description: 'The recipient.' example: '564' required: true schema: type: string '/v1/marketing/campaigns/{campaign}/generate-variations': post: summary: 'Generate AI wording variations of the campaign body (preview + store).' operationId: generateAIWordingVariationsOfTheCampaignBodypreview+Store description: '' parameters: [] responses: { } tags: - 'Marketing outreach' parameters: - in: path name: campaign description: 'The campaign.' example: '564' required: true schema: type: string '/v1/marketing/campaigns/{campaign}/media': post: summary: "Upload one image to a campaign (owner-only, Business plan). Only allowed\nwhile the campaign hasn't started — the message content is then locked." operationId: uploadOneImageToACampaignownerOnlyBusinessPlanOnlyAllowedWhileTheCampaignHasntStartedTheMessageContentIsThenLocked description: '' parameters: [] responses: { } tags: - 'Marketing outreach' parameters: - in: path name: campaign description: 'The campaign.' example: '564' required: true schema: type: string '/v1/marketing/campaigns/{campaign}/media/{media}': delete: summary: 'Remove one image from a campaign.' operationId: removeOneImageFromACampaign description: '' parameters: [] responses: { } tags: - 'Marketing outreach' parameters: - in: path name: campaign description: 'The campaign.' example: '564' required: true schema: type: string - in: path name: media description: '' example: '564' required: true schema: type: string '/v1/marketing/campaigns/{campaign}/cancel': post: summary: 'Cancel a campaign. Already-sent recipients stay sent.' operationId: cancelACampaignAlreadySentRecipientsStaySent description: '' parameters: [] responses: { } tags: - 'Marketing outreach' parameters: - in: path name: campaign description: 'The campaign.' example: '564' required: true schema: type: string '/v1/marketing/campaigns/{campaign}/start': post: summary: "Start (or schedule) a campaign: validates the sender, schedule,\nrecipients, risk acknowledgment, and the cross-campaign cooldown, then\nqueues the dispatcher." operationId: startorScheduleACampaignValidatesTheSenderScheduleRecipientsRiskAcknowledgmentAndTheCrossCampaignCooldownThenQueuesTheDispatcher description: '' parameters: [] responses: { } tags: - 'Marketing outreach' parameters: - in: path name: campaign description: 'The campaign.' example: '564' required: true schema: type: string '/v1/marketing/campaigns/{campaign}/schedule': post: summary: "Schedule a campaign to start automatically at a future time — the answer\nto \"I can't start now (cooldown), let it run at the next opening\". Runs the\nsame readiness checks as start() minus warm-up (the number runs it later),\ndefaults the time to the active cross-campaign cooldown, and never lets it\nland before that cooldown lifts. plixa:run-scheduled-campaigns starts it." operationId: scheduleACampaignToStartAutomaticallyAtAFutureTimeTheAnswerToICantStartNowcooldownLetItRunAtTheNextOpeningRunsTheSameReadinessChecksAsStartMinusWarmUptheNumberRunsItLaterDefaultsTheTimeToTheActiveCrossCampaignCooldownAndNeverLetsItLandBeforeThatCooldownLiftsPlixarunScheduledCampaignsStartsIt description: '' parameters: [] responses: { } tags: - 'Marketing outreach' requestBody: required: false content: application/json: schema: type: object properties: scheduled_at: type: string description: 'Must be a valid date.' example: '2026-07-21T15:01:47' nullable: true parameters: - in: path name: campaign description: 'The campaign.' example: '564' required: true schema: type: string '/v1/marketing/campaigns/{campaign}/pause': post: summary: 'Pause a running campaign. Queued sends re-check status and go quiet.' operationId: pauseARunningCampaignQueuedSendsReCheckStatusAndGoQuiet description: '' parameters: [] responses: { } tags: - 'Marketing outreach' parameters: - in: path name: campaign description: 'The campaign.' example: '564' required: true schema: type: string '/v1/marketing/campaigns/{campaign}/resume': post: summary: 'Resume a paused campaign — re-queues the remaining pending recipients.' operationId: resumeAPausedCampaignReQueuesTheRemainingPendingRecipients description: '' parameters: [] responses: { } tags: - 'Marketing outreach' parameters: - in: path name: campaign description: 'The campaign.' example: '564' required: true schema: type: string '/v1/marketing/campaigns/{id}': patch: summary: "Edit a campaign that hasn't finished. Pacing and schedule stay tunable\nwhile paused; the message, sender and fallback word lock the moment a\nsingle recipient has been sent to (that send log is immutable history) —\nduplicate the campaign to change them." operationId: editACampaignThatHasntFinishedPacingAndScheduleStayTunableWhilePausedTheMessageSenderAndFallbackWordLockTheMomentASingleRecipientHasBeenSentTothatSendLogIsImmutableHistoryDuplicateTheCampaignToChangeThem description: '' parameters: [] responses: { } tags: - 'Marketing outreach' requestBody: required: false content: application/json: schema: type: object properties: name: type: string description: 'Must not be greater than 100 characters.' example: b body: type: string description: 'Must not be greater than 1024 characters.' example: 'n' name_fallback: type: string description: 'Must not be greater than 40 characters.' example: g nullable: true phone_instance_id: type: integer description: '' example: 16 nullable: true pacing_min_seconds: type: integer description: '' example: 16 pacing_max_seconds: type: integer description: '' example: 16 daily_cap: type: integer description: 'Must be at least 1.' example: 22 send_weekdays: type: array description: 'Must be between 0 and 6.' example: - 0 items: type: integer send_windows: type: array description: 'Must have at least 1 items. Must not have more than 6 items.' example: null items: type: object properties: start: type: string description: 'This field is required when send_windows is present. Must be a valid date in the format H:i.' example: '15:01' end: type: string description: 'This field is required when send_windows is present. Must be a valid date in the format H:i.' example: '15:01' timezone: type: string description: 'Must be a valid time zone, such as Africa/Accra.' example: America/Bahia_Banderas nullable: true media_mode: type: string description: '' example: all enum: - all - random followup_enabled: type: boolean description: '' example: true followup_body: type: string description: 'Must not be greater than 1024 characters.' example: m nullable: true followup_delay_hours: type: integer description: 'Must not be greater than 8760.' example: 15 nullable: true apply_label_ids: type: array description: '' example: - 16 items: type: integer parameters: - in: path name: id description: 'The ID of the campaign.' example: architecto required: true schema: type: string '/v1/marketing/campaigns/{campaign}/duplicate': post: summary: "Clone a campaign into a fresh draft — message, settings and (by default)\nthe lead list — so a finished or canceled campaign can be relaunched with\na tweaked message without retyping. The copy starts as a draft with reset\ncounters; the ban-risk acknowledgment carries over to the operator who\ncloned it so it stays one-click startable." operationId: cloneACampaignIntoAFreshDraftMessageSettingsAndbyDefaultTheLeadListSoAFinishedOrCanceledCampaignCanBeRelaunchedWithATweakedMessageWithoutRetypingTheCopyStartsAsADraftWithResetCountersTheBanRiskAcknowledgmentCarriesOverToTheOperatorWhoClonedItSoItStaysOneClickStartable description: '' parameters: [] responses: { } tags: - 'Marketing outreach' requestBody: required: false content: application/json: schema: type: object properties: name: type: string description: 'Must not be greater than 100 characters.' example: b nullable: true copy_recipients: type: boolean description: '' example: false nullable: true parameters: - in: path name: campaign description: 'The campaign.' example: '564' required: true schema: type: string /v1/marketing/leads/bulk-delete: post: summary: "Delete saved leads in bulk — a set of ids, or everything matching the\ncurrent filter when `all` is set. Tenant-scoped." operationId: deleteSavedLeadsInBulkASetOfIdsOrEverythingMatchingTheCurrentFilterWhenallIsSetTenantScoped description: '' parameters: [] responses: { } tags: - 'Marketing outreach (lead sources)' requestBody: required: false content: application/json: schema: type: object properties: ids: type: array description: '' example: - 16 items: type: integer all: type: boolean description: '' example: true nullable: true q: type: string description: 'Must not be greater than 120 characters.' example: 'n' nullable: true '/v1/marketing/leads/{id}': delete: summary: 'Delete a saved lead from the workspace bank.' operationId: deleteASavedLeadFromTheWorkspaceBank description: '' parameters: [] responses: { } tags: - 'Marketing outreach (lead sources)' parameters: - in: path name: id description: 'The ID of the lead.' example: architecto required: true schema: type: string /v1/marketing/lead-search: post: summary: 'Search a source and persist the resulting leads.' operationId: searchASourceAndPersistTheResultingLeads description: '' parameters: [] responses: { } tags: - 'Marketing outreach (lead sources)' requestBody: required: true content: application/json: schema: type: object properties: query: type: string description: 'Must not be greater than 80 characters.' example: b locality: type: string description: 'Must not be greater than 120 characters.' example: 'n' limit: type: integer description: 'Must be at least 1. Must not be greater than 200.' example: 7 nullable: true source: type: string description: '' example: architecto nullable: true required: - query - locality /v1/marketing/leads/verify-whatsapp: post: summary: "Verify, via Evolution, whether leads' numbers have a WhatsApp account, and\ncache the verdict on each lead (`is_whatsapp`). A landline that registered\nWhatsApp Business reports true — number format can't be trusted, so we\nactually ask. Needs a connected number." operationId: verifyViaEvolutionWhetherLeadsNumbersHaveAWhatsAppAccountAndCacheTheVerdictOnEachLeadisWhatsappALandlineThatRegisteredWhatsAppBusinessReportsTrueNumberFormatCantBeTrustedSoWeActuallyAskNeedsAConnectedNumber description: "Two modes: pass `ids` to check a specific set (returns the updated leads\nso the panel refreshes inline), or `all` to sweep the WHOLE bank — every\nnot-yet-checked lead matching the optional `q` filter, batched in chunks\nand capped per request (returns a summary; repeat clicks drain the rest)." parameters: [] responses: { } tags: - 'Marketing outreach (lead sources)' requestBody: required: false content: application/json: schema: type: object properties: ids: type: array description: '' example: - 16 items: type: integer all: type: boolean description: '' example: true nullable: true q: type: string description: 'Must not be greater than 120 characters.' example: 'n' nullable: true /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 phone_instance_id: type: integer description: "Which WhatsApp number sends the message, on multi-number\nworkspaces. A token scoped to a number always sends through\nthat number; the param exists so unscoped tokens can pick,\nand it 403s when it contradicts the bearer's scope. Must be at least 1." example: 85 nullable: true required: - phone - body security: [] /v1/notifications/read-all: post: summary: '' operationId: postV1NotificationsReadAll description: '' parameters: [] responses: { } tags: - Notifications security: [] '/v1/notifications/{id}/read': post: summary: '' operationId: postV1NotificationsIdRead description: '' parameters: [] responses: { } tags: - Notifications security: [] parameters: - in: path name: id description: 'The ID of the notification.' example: architecto required: true schema: type: string /v1/ai/onboarding/chat: post: summary: 'Run one setup-interview turn' operationId: runOneSetupInterviewTurn description: '' parameters: [] responses: { } tags: - Onboarding 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 required: - history /v1/ai/onboarding/apply: post: summary: 'Apply approved setup suggestions' operationId: applyApprovedSetupSuggestions description: "Deterministic per-category handlers; each item resolves to its own\nstatus (`applied`, `skipped_existing`, `skipped_plan`, `failed`)\nso one odd card never blocks the rest of the batch." parameters: [] responses: { } tags: - Onboarding requestBody: required: false content: application/json: schema: type: object properties: business_summary: type: string description: "The approved business summary; saved as the AI's business description. Max 4000 chars." example: architecto nullable: true suggestions: type: array description: 'Approved cards. Each entry: { category: string, content: object }.' example: - architecto items: type: string /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/cloud-api: post: summary: 'Connect a number via the official Cloud API (owner-only, plan-gated).' operationId: connectANumberViaTheOfficialCloudAPIownerOnlyPlanGated description: '' parameters: [] responses: { } tags: - 'Phone instances' requestBody: required: true content: application/json: schema: type: object properties: phone_number_id: type: string description: 'The Cloud API phone number id.' example: '109876543210' waba_id: type: string description: 'The WhatsApp Business Account id.' example: '220011002200' access_token: type: string description: 'A system-user access token for the WABA.' example: architecto app_secret: type: string description: 'The Meta app secret (used to verify inbound webhook signatures).' example: architecto app_id: type: string description: 'The Meta app id — optional, only needed for media-header templates.' example: architecto nullable: true required: - phone_number_id - waba_id - access_token - app_secret '/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: integer '/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: integer '/v1/phone-instances/{phone_instance}/resync-webhook': post: summary: "Re-register this number's Evolution webhook with the current event set\n(e.g. MESSAGES_UPDATE for delivered/read receipts) without a reconnect." operationId: reRegisterThisNumbersEvolutionWebhookWithTheCurrentEventSetegMESSAGESUPDATEForDeliveredreadReceiptsWithoutAReconnect description: "The one-click fix when a long-connected number predates a newly-added\nevent and so isn't receiving it — no QR re-scan needed." parameters: [] responses: { } tags: - 'Phone instances' security: [] parameters: - in: path name: phone_instance description: '' example: 564 required: true schema: type: integer '/v1/phone-instances/{id}': delete: summary: 'Remove an abandoned pairing' operationId: removeAnAbandonedPairing description: "Deletes a number still waiting for its first pair (`pending_qr`) — the\n\"clicked add number, never scanned the QR\" case — tearing down the\nEvolution instance (best-effort) and dropping the row so the plan slot\nfrees. Owner-only. A number that has connected may carry conversation\nhistory (which cascades on delete), so it is NOT removable here." parameters: [] responses: { } tags: - 'Phone instances' parameters: - in: path name: id description: 'The ID of the phone instance.' example: 16 required: true schema: type: integer /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/reengagement-config: put: summary: '' operationId: putV1ReengagementConfig description: '' parameters: [] responses: { } tags: - Re-engagement requestBody: required: true content: application/json: schema: type: object properties: noshow_recovery_enabled: type: boolean description: '' example: true noshow_recovery_delay_minutes: type: integer description: 'Must be at least 0. Must not be greater than 1440.' example: 1 noshow_recovery_message: type: string description: 'Must not be greater than 1000 characters.' example: 'n' nullable: true reactivation_enabled: type: boolean description: '' example: false reactivation_after_days: type: integer description: 'Must be at least 7. Must not be greater than 3650.' example: 7 reactivation_cooldown_days: type: integer description: 'Must be at least 7. Must not be greater than 3650.' example: 16 reactivation_message: type: string description: 'Must not be greater than 1000 characters.' example: m nullable: true review_request_enabled: type: boolean description: '' example: false review_request_url: type: string description: 'Must be a valid URL. Must not be greater than 2048 characters.' example: 'https://www.gulgowski.com/nihil-accusantium-harum-mollitia-modi-deserunt' nullable: true review_request_message: type: string description: 'Must not be greater than 1000 characters.' example: w nullable: true review_request_delay_minutes: type: integer description: 'Must be at least 0. Must not be greater than 10080.' example: 22 required: - noshow_recovery_enabled - reactivation_enabled security: [] /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-07-21T15:01:47' 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-07-21T15:01:47' 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-07-21' nullable: true start_time: type: string description: 'Must be a valid date in the format H:i.' example: '15:01' end_time: type: string description: 'Must be a valid date in the format H:i.' example: '15:01' required: - kind - start_time - end_time required: - timezone parameters: - in: path name: user description: '' example: '564' required: true schema: type: string /v1/scheduling/public-booking-config: put: summary: 'Enable or disable the public booking page' operationId: enableOrDisableThePublicBookingPage description: '' parameters: [] responses: { } tags: - 'Scheduling — public booking page' requestBody: required: true content: application/json: schema: type: object properties: enabled: type: boolean description: 'Whether /book/{slug} accepts visitors.' example: false required: - enabled /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-07-21' 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/whatsapp-templates: post: summary: 'Create a template and submit it to Meta for review (owner-only).' operationId: createATemplateAndSubmitItToMetaForReviewownerOnly description: '' parameters: [] responses: { } tags: - 'WhatsApp templates' requestBody: required: true content: application/json: schema: type: object properties: phone_instance_id: type: integer description: 'The Cloud API number (WABA) to create the template on.' example: 4 name: type: string description: 'Lowercase snake_case name, unique per number+language.' example: promo_offer language: type: string description: 'Meta language code.' example: en_US category: type: string description: 'MARKETING, UTILITY or AUTHENTICATION.' example: MARKETING purpose: type: string description: 'Bind the template to a proactive automation: noshow_recovery, reactivation, review_request, birthday or booking_confirmation. Omit for a general/broadcast template.' example: noshow_recovery nullable: true param_mapping: type: array description: 'Required with purpose: one renderable {token} string per body placeholder (e.g. ["{first_name}", "{booking_link}"]).' example: - architecto items: type: string example_params: type: array description: 'Required when the body has placeholders: one sample value per @{{n}} (Meta needs example.body_text to approve it).' example: - architecto items: type: string body: type: string description: 'Body text with positional @{{1}} placeholders.' example: 'Hi @{{1}}, book your visit: @{{2}}' header_text: type: string description: 'Optional static text header (max 60 chars).' example: architecto nullable: true header_format: type: string description: 'Optional IMAGE|VIDEO|DOCUMENT for a media header (use with header_handle from /header-sample).' example: architecto nullable: true header_handle: type: string description: 'The upload handle from POST /whatsapp-templates/header-sample (required with a media header_format).' example: architecto nullable: true header_param_mapping: type: string description: "A TEXT header may carry a single {{1}} variable (purpose templates\nonly): the token that fills it + a sample value for Meta. Must not be greater than 255 characters." example: 'y' nullable: true header_example: type: string description: 'Must not be greater than 255 characters.' example: v nullable: true footer_text: type: string description: 'Optional footer text (max 60 chars).' example: architecto nullable: true buttons: type: array description: 'Optional static buttons. Each: { type: QUICK_REPLY|URL|PHONE_NUMBER, text, url (for URL), phone_number (for PHONE_NUMBER) }.' example: - [] items: type: object properties: type: type: string description: 'This field is required when buttons is present.' example: null text: type: string description: 'This field is required when buttons is present. Must not be greater than 25 characters.' example: l url: type: string description: "A URL may carry a trailing {{1}} (dynamic button), which fails the\nstrict `url` rule, so validate the scheme and let Meta check the rest. This field is required when buttons.*.type is URL. Must match the regex /^https?:\\/\\/\\S+$/i. Must not be greater than 2000 characters." example: 'http://www.dickens.com/deserunt-aut-ab-provident-perspiciatis-quo-omnis-nostrum' nullable: true phone_number: type: string description: "A call button's number must be international (E.164-ish); accept\ncommon formatting chars here and normalise to bare digits below. This field is required when buttons.*.type is PHONE_NUMBER. Must match the regex /^\\+?[0-9\\s().\\-]{8,22}$/. Must not be greater than 24 characters." example: uwpwlvqwrsitcpsc nullable: true param_mapping: type: string description: 'Dynamic URL button: the token that fills {{1}} + a full sample URL. Must not be greater than 255 characters.' example: q nullable: true example: type: string description: 'Must not be greater than 2000 characters.' example: l nullable: true required: - phone_instance_id - name - language - category - body /v1/whatsapp-templates/header-sample: post: summary: "Upload a sample media file for a media-header template and return the\nreusable handle (Meta's Resumable Upload API is app-scoped, so the number\nmust carry its App ID). Owner-only." operationId: uploadASampleMediaFileForAMediaHeaderTemplateAndReturnTheReusableHandleMetasResumableUploadAPIIsAppScopedSoTheNumberMustCarryItsAppIDOwnerOnly description: '' parameters: [] responses: { } tags: - 'WhatsApp templates' requestBody: required: true content: multipart/form-data: schema: type: object properties: phone_instance_id: type: integer description: 'The Cloud API number.' example: 4 file: type: string format: binary description: 'The sample image/video/document for the header.' required: - phone_instance_id - file /v1/whatsapp-templates/sync: post: summary: "Reconcile local template statuses with Meta (approvals are async). Pulls\nevery template list for the workspace's Cloud API numbers and updates the\nmatching local rows by name+language." operationId: reconcileLocalTemplateStatusesWithMetaapprovalsAreAsyncPullsEveryTemplateListForTheWorkspacesCloudAPINumbersAndUpdatesTheMatchingLocalRowsByName+language description: '' parameters: [] responses: { } tags: - 'WhatsApp templates' '/v1/whatsapp-templates/{template}': put: summary: "Edit a template and resubmit it to Meta for review (owner-only) — the way\nto fix a REJECTED template or amend an APPROVED one. Meta treats the name\nand language as immutable, so only the body / category / components change;\nthe template goes back to PENDING review." operationId: editATemplateAndResubmitItToMetaForReviewownerOnlyTheWayToFixAREJECTEDTemplateOrAmendAnAPPROVEDOneMetaTreatsTheNameAndLanguageAsImmutableSoOnlyTheBodyCategoryComponentsChangeTheTemplateGoesBackToPENDINGReview description: '' parameters: [] responses: { } tags: - 'WhatsApp templates' requestBody: required: true content: application/json: schema: type: object properties: category: type: string description: 'MARKETING, UTILITY or AUTHENTICATION.' example: UTILITY purpose: type: string description: 'Bind the template to a proactive automation (see store).' example: booking_confirmation nullable: true param_mapping: type: array description: 'Required with purpose: one {token} per placeholder.' example: - architecto items: type: string example_params: type: array description: 'Required when the body has placeholders: one sample per @{{n}}.' example: - architecto items: type: string body: type: string description: 'Body text with positional @{{1}} placeholders.' example: 'Hi @{{1}}, your visit is confirmed.' header_text: type: string description: 'Optional static text header (max 60 chars).' example: architecto nullable: true header_format: type: string description: 'Optional IMAGE|VIDEO|DOCUMENT for a media header (re-upload a sample via /header-sample to get header_handle).' example: architecto nullable: true header_handle: type: string description: 'The upload handle for a media header (required with a media header_format).' example: architecto nullable: true header_param_mapping: type: string description: "A TEXT header may carry a single {{1}} variable (purpose templates\nonly): the token that fills it + a sample value for Meta. Must not be greater than 255 characters." example: 'y' nullable: true header_example: type: string description: 'Must not be greater than 255 characters.' example: v nullable: true footer_text: type: string description: 'Optional footer text (max 60 chars).' example: architecto nullable: true buttons: type: array description: 'Optional static buttons (see store).' example: - [] items: type: object properties: type: type: string description: 'This field is required when buttons is present.' example: null text: type: string description: 'This field is required when buttons is present. Must not be greater than 25 characters.' example: l url: type: string description: "A URL may carry a trailing {{1}} (dynamic button), which fails the\nstrict `url` rule, so validate the scheme and let Meta check the rest. This field is required when buttons.*.type is URL. Must match the regex /^https?:\\/\\/\\S+$/i. Must not be greater than 2000 characters." example: 'http://www.dickens.com/deserunt-aut-ab-provident-perspiciatis-quo-omnis-nostrum' nullable: true phone_number: type: string description: "A call button's number must be international (E.164-ish); accept\ncommon formatting chars here and normalise to bare digits below. This field is required when buttons.*.type is PHONE_NUMBER. Must match the regex /^\\+?[0-9\\s().\\-]{8,22}$/. Must not be greater than 24 characters." example: uwpwlvqwrsitcpsc nullable: true param_mapping: type: string description: 'Dynamic URL button: the token that fills {{1}} + a full sample URL. Must not be greater than 255 characters.' example: q nullable: true example: type: string description: 'Must not be greater than 2000 characters.' example: l nullable: true required: - category - body delete: summary: 'Delete a template from Meta (best-effort) and locally (owner-only).' operationId: deleteATemplateFromMetabestEffortAndLocallyownerOnly description: '' parameters: [] responses: { } tags: - 'WhatsApp templates' parameters: - in: path name: template description: '' example: '564' required: true schema: type: string /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'