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