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'