{"openapi":"3.1.0","info":{"title":"SendSeven","description":"Unified messaging API for WhatsApp, Telegram, SMS, and more","version":"20261008.1119"},"paths":{"/api/v1/messages":{"post":{"tags":["Messages"],"summary":"Send Message","description":"Send a new message.\n\nSupports 3 resolution modes:\n\n**Mode 1: conversation_id only** (reply to existing conversation)\n- Just provide `conversation_id` and `text`. The recipient is auto-resolved\n  from the conversation's contact method or last inbound message.\n\n**Mode 2: contact_method_id** (cleanest, resolves everything)\n- Provide `contact_method_id` and `text`. The `to`, `channel_id`, and `contact_id`\n  are all resolved from the contact method record automatically.\n\n**Mode 3: contact_id + optional to + channel_id** (explicit)\n- Provide `contact_id` (and optionally `to` and `channel_id`). The conversation is\n  auto-resolved from the contact. A contact_method is auto-created if needed.\n\nAll modes are backward compatible. Existing requests with `to` + `conversation_id`\ncontinue to work.\n\n**Required Scopes:** `messages:create`\n\n\n**409 `conversation_owned_elsewhere`:** for team members restricted by\nCustom Inboxes, when the contact is in an ongoing (non-email)\nconversation handled in an inbox they cannot see. Email starts a new\nthread instead.","operationId":"send_message_api_v1_messages_post","security":[{"HTTPBearer":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MessageCreate"},"examples":{"mode1_conversation_only":{"summary":"Mode 1: Reply to conversation (no 'to' needed)","description":"Send a message to an existing conversation. The recipient is auto-resolved from the conversation's contact method or last inbound message.","value":{"conversation_id":"b5d8e2a1-4c3f-4a8b-9d2e-1f6a7b8c9d0e","text":"Hello! Your order #12345 has been shipped.","message_type":"text"}},"mode2_contact_method":{"summary":"Mode 2: Send via contact_method_id (cleanest)","description":"Send a message using a contact method ID. Everything (to, channel_id, contact_id) is resolved automatically.","value":{"contact_method_id":"d4e5f6a7-8901-2345-6789-0abcdef12345","text":"Your appointment is confirmed for tomorrow.","message_type":"text"}},"mode3_contact_auto_resolve":{"summary":"Mode 3: Send via contact_id (auto-resolve)","description":"Send a message to a contact. The conversation and channel are auto-resolved. A contact_method is auto-created if needed.","value":{"contact_id":"c1d2e3f4-5678-9abc-def0-1234567890ab","to":"+491521558086","text":"Welcome! How can we help you today?","message_type":"text"}},"backward_compatible":{"summary":"Backward compatible: to + conversation_id","description":"Existing format with explicit recipient and conversation ID still works.","value":{"to":"+491521558086","conversation_id":"b5d8e2a1-4c3f-4a8b-9d2e-1f6a7b8c9d0e","text":"Hello! Your order #12345 has been shipped.","message_type":"text"}},"image_with_contact":{"summary":"Image message via contact auto-resolve","description":"Send an image to a contact with attachments and metadata.","value":{"to":"+491521558086","contact_id":"c1d2e3f4-5678-9abc-def0-1234567890ab","message_type":"image","text":"Here is your receipt.","attachments":["att-a1b2c3d4-5678-9abc-def0-1234567890ab"],"meta":{"order_id":"ORD-98765"}}}}}}},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MessageResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"get":{"tags":["Messages"],"summary":"List Messages","description":"List messages for a conversation, or tenant-wide by status/direction.\n\n**Conversation mode** (`conversation_id` given): unchanged. Cursor or page pagination.\n\n**Tenant-wide mode** (no `conversation_id`, but `status` and/or `direction` given):\nreturns messages across all conversations of the tenant, newest first\n(`created_at` desc), page/page_size pagination only. Example:\n`GET /messages?status=failed&direction=outbound&page_size=3` lists recent failed\noutbound messages. Limited to the last 30 days unless `created_after` is given.\nEach item is the normal message object (incl. `error_message`, `conversation_id`,\n`contact_id`, `channel_id`, `platform`); `contact_id`/`channel_id` fall back to the\nconversation's values when the message row has none.\n\nSupports two pagination modes in conversation mode:\n\nSupports two pagination modes:\n1. **Page-based (standard)**: Use `page` and `page_size` parameters\n2. **Cursor pagination (recommended for large datasets)**: Use `cursor` parameter for O(1)\n   performance regardless of page number. Returns `next_cursor` in response.\n\nFor tables with billions of rows, always use cursor pagination.\n\n**Required Scopes:** `messages:read`","operationId":"list_messages_api_v1_messages_get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"conversation_id","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Conversation UUID. Required unless a tenant-wide filter (`status` and/or `direction`) is given.","title":"Conversation Id"},"description":"Conversation UUID. Required unless a tenant-wide filter (`status` and/or `direction`) is given."},{"name":"status","in":"query","required":false,"schema":{"anyOf":[{"$ref":"#/components/schemas/MessageStatus-Input"},{"type":"null"}],"description":"Tenant-wide mode only: filter by delivery status (e.g. `failed`).","title":"Status"},"description":"Tenant-wide mode only: filter by delivery status (e.g. `failed`)."},{"name":"direction","in":"query","required":false,"schema":{"anyOf":[{"$ref":"#/components/schemas/MessageDirection-Input"},{"type":"null"}],"description":"Tenant-wide mode only: filter by direction (`inbound` / `outbound`).","title":"Direction"},"description":"Tenant-wide mode only: filter by direction (`inbound` / `outbound`)."},{"name":"created_after","in":"query","required":false,"schema":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"description":"Tenant-wide mode only: only messages created at/after this ISO-8601 timestamp. Defaults to the last 30 days.","title":"Created After"},"description":"Tenant-wide mode only: only messages created at/after this ISO-8601 timestamp. Defaults to the last 30 days."},{"name":"page","in":"query","required":false,"schema":{"type":"integer","minimum":1,"description":"Page number (1-indexed)","default":1,"title":"Page"},"description":"Page number (1-indexed)"},{"name":"page_size","in":"query","required":false,"schema":{"type":"integer","maximum":100,"minimum":1,"description":"Items per page","default":50,"title":"Page Size"},"description":"Items per page"},{"name":"cursor","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Cursor for efficient keyset pagination (overrides page)","title":"Cursor"},"description":"Cursor for efficient keyset pagination (overrides page)"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MessageListResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/messages/internal-notes":{"post":{"tags":["Messages"],"summary":"Create Internal Note","description":"Create an internal note on a conversation.\n\nInternal notes are visible only to agents and are **never** sent to\nthe customer. They appear in the conversation timeline styled\ndifferently from regular messages.\n\nSupports @mentions: pass `mentioned_user_ids` to notify other team\nmembers. Each mentioned user receives an in-app notification with a\ndeep link to the conversation.\n\nOptional `assign_to_user_id` also assigns the conversation. That needs\nthe same permission as the assign endpoint: `conversations:update`\n(403 otherwise, and the note is not created), the cross-agent reassign\nrule, and an active workspace member as assignee (400 otherwise).\n\n**Required Scopes:** `messages:create` (+ `conversations:update` when\n`assign_to_user_id` changes the assignee)","operationId":"create_internal_note_api_v1_messages_internal_notes_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/InternalNoteCreate"}}},"required":true},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MessageResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"HTTPBearer":[]}]}},"/api/v1/messages/{message_id}":{"get":{"tags":["Messages"],"summary":"Get Message","description":"Get a specific message by ID.\n\nIf this message is a reply to another message, the `quoted_message` field\nwill contain a preview of the original message.\n\n**Required Scopes:** `messages:read`","operationId":"get_message_api_v1_messages__message_id__get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"message_id","in":"path","required":true,"schema":{"type":"string","title":"Message Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MessageResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/messages/send/interactive":{"post":{"tags":["Messages"],"summary":"Send interactive message","description":"Send an interactive message with buttons or list menu.\n\nInteractive messages provide rich UI elements for user engagement.\nCurrently supported on **WhatsApp only**. Other platforms will return\nan error with guidance on supported alternatives.\n\n## Message Types\n\n### Buttons (`type: \"buttons\"`)\n- Maximum 3 reply buttons\n- Each button has an `id` (returned when clicked) and `title` (max 20 chars)\n- Great for simple choices like \"Yes/No\", \"Accept/Decline\"\n\n### List Menu (`type: \"list\"`)\n- Up to 10 sections with rows\n- Each row has `id`, `title` (max 24 chars), and optional `description`\n- Maximum 10 total rows across all sections\n- Great for menus, product listings, options\n\n### Request Contact Info (`type: \"request_contact_info\"`)\n- A single button asking the user to share their own WhatsApp phone number\n- Only `body` is used — the button label is fixed by WhatsApp and shown in\n  the recipient's app language, so `buttons`/`sections` are rejected\n- The tap does **not** produce a button-reply webhook: the number arrives\n  as a contacts message and is linked (or merged) onto the contact\n  automatically\n- Primarily for contacts who adopted a WhatsApp **username** — WhatsApp\n  withholds their phone number from every webhook until they share it, so\n  until then they are reachable on WhatsApp only, not SMS/RCS/email\n\n## Example Request (Buttons)\n```json\n{\n    \"channel_id\": \"uuid\",\n    \"contact_id\": \"uuid\",\n    \"type\": \"buttons\",\n    \"body\": \"Would you like to proceed with your order?\",\n    \"buttons\": [\n        {\"id\": \"confirm\", \"title\": \"Yes, confirm\"},\n        {\"id\": \"cancel\", \"title\": \"No, cancel\"},\n        {\"id\": \"help\", \"title\": \"Need help\"}\n    ]\n}\n```\n\n## Example Request (List)\n```json\n{\n    \"channel_id\": \"uuid\",\n    \"contact_id\": \"uuid\",\n    \"type\": \"list\",\n    \"body\": \"Please select a product category:\",\n    \"button_text\": \"View Categories\",\n    \"sections\": [\n        {\n            \"title\": \"Electronics\",\n            \"rows\": [\n                {\"id\": \"phones\", \"title\": \"Phones\", \"description\": \"Smartphones and accessories\"},\n                {\"id\": \"laptops\", \"title\": \"Laptops\", \"description\": \"Notebooks and tablets\"}\n            ]\n        }\n    ]\n}\n```\n\n**Required Scopes:** `messages:create`\n\n\n**409 `conversation_owned_elsewhere`:** for team members restricted by\nCustom Inboxes, when the contact is in an ongoing (non-email)\nconversation handled in an inbox they cannot see. Email starts a new\nthread instead.","operationId":"send_interactive_message_api_v1_messages_send_interactive_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/InteractiveMessageCreate"},"examples":{"reply_buttons":{"summary":"Reply buttons (max 3)","description":"Send a message with up to 3 reply buttons. User taps a button to respond.","value":{"channel_id":"a1b2c3d4-5678-9abc-def0-1234567890ab","contact_id":"c1d2e3f4-5678-9abc-def0-1234567890ab","type":"buttons","body":"Would you like to schedule a callback?","header":{"type":"text","text":"Support Request"},"footer":"Available Mon-Fri 9am-5pm","buttons":[{"id":"btn_yes","title":"Yes, schedule"},{"id":"btn_no","title":"No thanks"},{"id":"btn_chat","title":"Chat instead"}]}},"list_menu":{"summary":"List menu with sections","description":"Send a scrollable list menu organized into sections. User selects one option.","value":{"channel_id":"a1b2c3d4-5678-9abc-def0-1234567890ab","contact_id":"c1d2e3f4-5678-9abc-def0-1234567890ab","type":"list","body":"Choose a topic for your support request:","button_text":"Select Topic","sections":[{"title":"Account","rows":[{"id":"acct_password","title":"Reset Password","description":"Reset your account password"},{"id":"acct_settings","title":"Account Settings","description":"Update profile or preferences"}]},{"title":"Billing","rows":[{"id":"bill_invoice","title":"Invoice Question","description":"Ask about a specific invoice"},{"id":"bill_refund","title":"Request Refund","description":"Submit a refund request"}]}]}},"request_contact_info":{"summary":"Request the user's phone number","description":"Show a single fixed-label button asking the user to share their own WhatsApp phone number. The label is set and localized by WhatsApp — pass no buttons.","value":{"channel_id":"a1b2c3d4-5678-9abc-def0-1234567890ab","contact_id":"c1d2e3f4-5678-9abc-def0-1234567890ab","type":"request_contact_info","body":"To send you delivery updates by SMS, please share your phone number."}}}}},"required":true},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InteractiveMessageResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"HTTPBearer":[]}]}},"/api/v1/messages/{message_id}/react":{"post":{"tags":["Messages"],"summary":"Add reaction to message","description":"Add a reaction (emoji) to a message.\n\nSends a reaction to the specified message on the messaging platform.\nSupported on every channel whose capability matrix advertises reactions\n(WhatsApp, Telegram, Messenger, Instagram, Live Chat); see the channel\ncapabilities endpoint for the per-channel `reactions` block.\n\n## Important Notes\n- The message's channel must support reactions (see channel capabilities)\n- The message must have an external_id (platform message ID)\n- Common emojis work best (thumbs up, heart, laugh, etc.)\n- Telegram only accepts emojis from its fixed reaction set\n- You can only have one reaction per message per sender\n\n## Example Request\n```json\n{\n    \"emoji\": \"thumbs up\"\n}\n```\n\n**Required Scopes:** `messages:create`","operationId":"add_reaction_api_v1_messages__message_id__react_post","security":[{"HTTPBearer":[]}],"parameters":[{"name":"message_id","in":"path","required":true,"schema":{"type":"string","title":"Message Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReactionCreate"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReactionResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"tags":["Messages"],"summary":"Remove reaction from message","description":"Remove a reaction from a message.\n\nRemoves your reaction from the specified message on any channel that\nsupports reactions (WhatsApp, Telegram, Messenger, Instagram, Live Chat).\nThis is done by sending an empty emoji reaction.\n\n**Required Scopes:** `messages:create`","operationId":"remove_reaction_api_v1_messages__message_id__react_delete","security":[{"HTTPBearer":[]}],"parameters":[{"name":"message_id","in":"path","required":true,"schema":{"type":"string","title":"Message Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReactionResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/messages/{message_id}/read":{"post":{"tags":["Messages"],"summary":"Mark a received message as read on the channel","description":"Send a read receipt for an inbound message.\n\nShows the contact that their message was read (for example the blue ticks\non WhatsApp or \"Seen\" on Messenger and Instagram). No request body is\nneeded.\n\n## Important Notes\n- Only **inbound** messages (sent by the contact) can be marked as read\n  (`422 message_not_inbound` otherwise).\n- Not every channel supports read receipts — check\n  `GET /channels/sender-actions`. Unsupported channels return\n  `422 sender_action_not_supported` (for example Telegram, SMS, email).\n- This is **not a message**: nothing is billed, no message is created and\n  no webhook is sent.\n- Your inbox's own unread state is **not** changed by this call.\n\n## Errors\n`detail` is an object: `{\"code\", \"message\", \"channel_type\", \"action\"}`.\n- `404 message_not_found`\n- `422 sender_action_not_supported`, `message_not_inbound`, `channel_unavailable`\n- `429 sender_action_rate_limited` — about 30 calls per minute per\n  conversation; retry after the `Retry-After` seconds\n- `502 sender_action_failed` — the messaging platform rejected the request\n\nRepeats are absorbed: a second read receipt for the same message, or a\ntyping `on` while the indicator is still showing, returns 200 with\n`deduplicated: true` and nothing is sent again (typing `off` is never\ndeduplicated).\n\n**Required Scopes:** `messages:create`","operationId":"mark_message_read_on_channel_api_v1_messages__message_id__read_post","security":[{"HTTPBearer":[]}],"parameters":[{"name":"message_id","in":"path","required":true,"schema":{"type":"string","title":"Message Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SenderActionResponse"}}}},"404":{"description":"Message or conversation not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SenderActionErrorResponse"}}}},"422":{"description":"Not possible on this channel or message: `sender_action_not_supported`, `message_not_inbound`, `no_inbound_message`, `channel_unavailable` (or a request validation error).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SenderActionErrorResponse"}}}},"429":{"description":"Too many read/typing requests for this conversation (`sender_action_rate_limited`, about 30 per minute). Honour `Retry-After`.","headers":{"Retry-After":{"description":"Seconds to wait before retrying.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SenderActionErrorResponse"}}}},"502":{"description":"The messaging platform rejected the action (`sender_action_failed`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SenderActionErrorResponse"}}}}}}},"/api/v1/messages/send/contacts":{"post":{"tags":["Messages"],"summary":"Send contact card(s)","description":"Send one or more contact cards via WhatsApp.\n\nContact cards allow sharing contact information including names, phone numbers,\nemail addresses, physical addresses, and organization details.\nCurrently supported on **WhatsApp only**.\n\n## Contact Card Structure\n\nEach contact must have at minimum a `name.formatted_name`. Optionally include:\n- **phones**: List of phone numbers with type (CELL, WORK, HOME, etc.)\n- **emails**: List of email addresses with type\n- **addresses**: Physical addresses\n- **org**: Organization/company information\n- **urls**: Website URLs\n- **birthday**: Date of birth (YYYY-MM-DD)\n\n## Example Request\n```json\n{\n    \"channel_id\": \"uuid\",\n    \"contact_id\": \"uuid\",\n    \"contacts\": [\n        {\n            \"name\": {\n                \"formatted_name\": \"John Doe\",\n                \"first_name\": \"John\",\n                \"last_name\": \"Doe\"\n            },\n            \"phones\": [\n                {\"phone\": \"+1234567890\", \"type\": \"CELL\"}\n            ],\n            \"emails\": [\n                {\"email\": \"john@example.com\", \"type\": \"WORK\"}\n            ],\n            \"org\": {\n                \"company\": \"Acme Inc\",\n                \"title\": \"Software Engineer\"\n            }\n        }\n    ]\n}\n```\n\n**Required Scopes:** `messages:create`\n\n\n**409 `conversation_owned_elsewhere`:** for team members restricted by\nCustom Inboxes, when the contact is in an ongoing (non-email)\nconversation handled in an inbox they cannot see. Email starts a new\nthread instead.","operationId":"send_contacts_api_v1_messages_send_contacts_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SendContactsRequest"}}},"required":true},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SendContactsResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"HTTPBearer":[]}]}},"/api/v1/messages/send/sticker":{"post":{"tags":["Messages"],"summary":"Send sticker","description":"Send a sticker via WhatsApp.\n\nStickers are static or animated images in .webp format that can be sent\nto WhatsApp contacts. Currently supported on **WhatsApp only**.\n\n## Sticker Requirements\n- **Format**: .webp only\n- **Static stickers**: 512x512 pixels, max 100KB\n- **Animated stickers**: 512x512 pixels, max 500KB\n\n## Sending Methods\nYou can send a sticker using one of two methods:\n1. **sticker_id**: Media ID of a previously uploaded sticker (preferred)\n2. **sticker_url**: Direct URL to a .webp sticker file\n\n## Example Request (by ID)\n```json\n{\n    \"channel_id\": \"uuid\",\n    \"contact_id\": \"uuid\",\n    \"sticker_id\": \"1234567890123456\"\n}\n```\n\n## Example Request (by URL)\n```json\n{\n    \"channel_id\": \"uuid\",\n    \"contact_id\": \"uuid\",\n    \"sticker_url\": \"https://example.com/stickers/happy.webp\"\n}\n```\n\n**Required Scopes:** `messages:create`\n\n\n**409 `conversation_owned_elsewhere`:** for team members restricted by\nCustom Inboxes, when the contact is in an ongoing (non-email)\nconversation handled in an inbox they cannot see. Email starts a new\nthread instead.","operationId":"send_sticker_api_v1_messages_send_sticker_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SendStickerRequest"}}},"required":true},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SendStickerResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"HTTPBearer":[]}]}},"/api/v1/messages/send/carousel":{"post":{"tags":["Messages"],"summary":"Send carousel message","description":"Send a carousel (multi-card) message via WhatsApp or Messenger.\n\nThis endpoint supports both platforms with different approaches:\n\n## WhatsApp Carousels (Template-based)\n\nWhatsApp carousels require a pre-approved template in Meta Business Manager.\nEach card can have:\n- **Header**: Image or video\n- **Body**: Dynamic text parameters\n- **Buttons**: Up to 2 buttons per card (max 10 cards)\n\n### Example Request (WhatsApp)\n```json\n{\n    \"channel_id\": \"uuid\",\n    \"contact_id\": \"uuid\",\n    \"template_name\": \"product_catalog\",\n    \"language\": \"en\",\n    \"cards\": [\n        {\n            \"header\": {\"type\": \"image\", \"media_id\": \"123456789\"},\n            \"body_parameters\": [\n                {\"type\": \"text\", \"text\": \"Product A\"},\n                {\"type\": \"text\", \"text\": \"$99.99\"}\n            ],\n            \"buttons\": [\n                {\"type\": \"url\", \"index\": 0, \"parameters\": [{\"type\": \"text\", \"text\": \"product-a\"}]}\n            ]\n        }\n    ]\n}\n```\n\n## Messenger Carousels (Generic Template)\n\nMessenger carousels use the Generic Template format and don't require pre-approval.\nEach element can have:\n- **Title**: Card title (required)\n- **Subtitle**: Card description\n- **Image**: Card image URL\n- **Default Action**: Action when card is tapped\n- **Buttons**: Up to 3 buttons per element (max 10 elements)\n\n### Example Request (Messenger)\n```json\n{\n    \"channel_id\": \"uuid\",\n    \"contact_id\": \"uuid\",\n    \"elements\": [\n        {\n            \"title\": \"Product A\",\n            \"subtitle\": \"Great product for $99.99\",\n            \"image_url\": \"https://example.com/product-a.jpg\",\n            \"default_action\": {\n                \"type\": \"web_url\",\n                \"url\": \"https://example.com/products/a\"\n            },\n            \"buttons\": [\n                {\"type\": \"web_url\", \"url\": \"https://example.com/buy/a\", \"title\": \"Buy Now\"},\n                {\"type\": \"postback\", \"title\": \"More Info\", \"payload\": \"INFO_A\"}\n            ]\n        }\n    ]\n}\n```\n\n## Supported Platforms\n- **WhatsApp**: Use `template_name` + `cards`\n- **Messenger**: Use `elements`\n\n**Required Scopes:** `messages:create`\n\n\n**409 `conversation_owned_elsewhere`:** for team members restricted by\nCustom Inboxes, when the contact is in an ongoing (non-email)\nconversation handled in an inbox they cannot see. Email starts a new\nthread instead.","operationId":"send_carousel_api_v1_messages_send_carousel_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SendCarouselRequest"}}},"required":true},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SendCarouselResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"HTTPBearer":[]}]}},"/api/v1/conversations":{"post":{"tags":["Conversations"],"summary":"Create Conversation","description":"Create a new conversation\n\n**Required Scopes:** `conversations:create`","operationId":"create_conversation_api_v1_conversations_post","security":[{"HTTPBearer":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConversationCreate"}}}},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConversationResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"get":{"tags":["Conversations"],"summary":"List Conversations","description":"List all conversations for the tenant with pagination.\n\n**Required Scopes:** `conversations:read`\n\nRespects multi-agent mode setting:\n- Multi-agent mode: Shows all conversations\n- Single-agent mode: Shows only assigned + unassigned (agents only)\n\n**Response format:**\n```json\n{\n    \"items\": [...],\n    \"pagination\": {\n        \"total\": 150,\n        \"page\": 1,\n        \"page_size\": 20,\n        \"total_pages\": 8,\n        \"has_next\": true,\n        \"has_prev\": false\n    }\n}\n```\n\nQuery parameters:\n- page: Page number (1-indexed, default: 1)\n- page_size: Items per page (1-100, default: 20)\n- status: Filter by conversation status (open, closed)\n- assigned_to: Filter by assignment (\"me\", \"unassigned\", or specific user_id)\n- needs_reply: Filter for conversations needing a reply (true/false)\n- filter: Alias for common filters (\"unanswered\" = needs_reply=true)\n- contact_id: Filter by specific contact ID\n- search: Search by contact name, phone, email, or conversation subject;\n  plus message text (last 90 days, terms of 3+ characters) when the\n  `message_search` feature is enabled for the workspace\n- channel_type: Filter by channel type (whatsapp, telegram, sms, email, messenger, instagram, live_chat)\n- tag_id: Repeatable tag id; matches a tag on the conversation OR its\n  contact. Unknown / other-workspace ids -> 422\n- tag_mode: `any` (default) or `all` — how several `tag_id` values combine","operationId":"list_conversations_api_v1_conversations_get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"page","in":"query","required":false,"schema":{"type":"integer","minimum":1,"description":"Page number (1-indexed)","default":1,"title":"Page"},"description":"Page number (1-indexed)"},{"name":"page_size","in":"query","required":false,"schema":{"type":"integer","maximum":100,"minimum":1,"description":"Items per page","default":20,"title":"Page Size"},"description":"Items per page"},{"name":"status","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Filter by tab/status: 'open' (excludes snoozed), 'snoozed', 'closed'","title":"Status"},"description":"Filter by tab/status: 'open' (excludes snoozed), 'snoozed', 'closed'"},{"name":"assigned_to","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Filter by assignment: 'me', 'unassigned', 'me_and_unassigned', or user_id","title":"Assigned To"},"description":"Filter by assignment: 'me', 'unassigned', 'me_and_unassigned', or user_id"},{"name":"needs_reply","in":"query","required":false,"schema":{"anyOf":[{"type":"boolean"},{"type":"null"}],"description":"Filter for conversations needing a reply","title":"Needs Reply"},"description":"Filter for conversations needing a reply"},{"name":"filter","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Alias: 'unanswered' sets needs_reply=True","title":"Filter"},"description":"Alias: 'unanswered' sets needs_reply=True"},{"name":"contact_id","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Filter by contact ID","title":"Contact Id"},"description":"Filter by contact ID"},{"name":"search","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Search by contact name, phone, email, or subject. When the `message_search` feature is enabled for the workspace (see GET /tenants/features) and the term has at least 3 characters, message text from the last 90 days is searched too (OR).","title":"Search"},"description":"Search by contact name, phone, email, or subject. When the `message_search` feature is enabled for the workspace (see GET /tenants/features) and the term has at least 3 characters, message text from the last 90 days is searched too (OR)."},{"name":"inbox_id","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Filter to conversations in this custom inbox","title":"Inbox Id"},"description":"Filter to conversations in this custom inbox"},{"name":"channel_type","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Filter by channel type (e.g. whatsapp, telegram, sms, email, messenger, instagram, live_chat)","title":"Channel Type"},"description":"Filter by channel type (e.g. whatsapp, telegram, sms, email, messenger, instagram, live_chat)"},{"name":"tag_id","in":"query","required":false,"schema":{"anyOf":[{"type":"array","items":{"type":"string"}},{"type":"null"}],"description":"tags.id — filter to conversations carrying the tag on the conversation OR on its contact. Repeatable (?tag_id=a&tag_id=b, max 20); combine with `tag_mode`. Each id must belong to the workspace (422 if not). AND-ed with all other filters.","title":"Tag Id"},"description":"tags.id — filter to conversations carrying the tag on the conversation OR on its contact. Repeatable (?tag_id=a&tag_id=b, max 20); combine with `tag_mode`. Each id must belong to the workspace (422 if not). AND-ed with all other filters."},{"name":"tag_mode","in":"query","required":false,"schema":{"enum":["any","all"],"type":"string","description":"How several `tag_id` values combine: `any` (default) = at least one of the tags, `all` = every tag.","default":"any","title":"Tag Mode"},"description":"How several `tag_id` values combine: `any` (default) = at least one of the tags, `all` = every tag."}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConversationListResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/conversations/bulk-close":{"post":{"tags":["Conversations"],"summary":"Bulk Close Conversations","description":"Bulk close multiple conversations without triggering ticket summarization.\n\nThis endpoint is intended for closing spam, duplicates, or other conversations\nthat do not need AI summarization or knowledge base entries.\n\n**Required Scopes:** `conversations:update`\n\nArgs:\n    request_body: Request body containing conversation_ids and optional reason\n\nReturns:\n    BulkCloseResponse with counts of successes and failures","operationId":"bulk_close_conversations_api_v1_conversations_bulk_close_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BulkCloseRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BulkCloseResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"HTTPBearer":[]}]}},"/api/v1/conversations/analytics/trending-tags":{"get":{"tags":["Conversations"],"summary":"Get Trending Tags","description":"Get trending AI tags from recently closed conversations.\n\nAnalyzes the most recent closed conversations with AI summaries and returns\nthe most common tags/categories.\n\n**Required Scopes:** `conversations:read`","operationId":"get_trending_tags_api_v1_conversations_analytics_trending_tags_get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":500,"minimum":1,"description":"Number of recent closed conversations to analyze","default":100,"title":"Limit"},"description":"Number of recent closed conversations to analyze"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/conversations/search-similar":{"post":{"tags":["Conversations"],"summary":"Search Similar Conversations","description":"**Retired.** Always returns `410 Gone`.\n\nSimilar ticket search was retired in October 2026. Conversation summaries\nare still stored on each conversation; list them with\n`GET /api/v1/knowledge-base/tickets`.\n\n**Required Scopes:** `conversations:read`","operationId":"search_similar_conversations_api_v1_conversations_search_similar_post","responses":{"410":{"description":"Gone. Similar ticket search has been retired.","content":{"application/json":{"schema":{}}}}},"deprecated":true,"security":[{"HTTPBearer":[]}]}},"/api/v1/conversations/{conversation_id}":{"get":{"tags":["Conversations"],"summary":"Get Conversation","description":"Get a specific conversation\n\n**Required Scopes:** `conversations:read`","operationId":"get_conversation_api_v1_conversations__conversation_id__get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"conversation_id","in":"path","required":true,"schema":{"type":"string","title":"Conversation Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConversationResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"patch":{"tags":["Conversations"],"summary":"Update Conversation","description":"Update a conversation\n\n**Required Scopes:** `conversations:update`","operationId":"update_conversation_api_v1_conversations__conversation_id__patch","security":[{"HTTPBearer":[]}],"parameters":[{"name":"conversation_id","in":"path","required":true,"schema":{"type":"string","title":"Conversation Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConversationUpdate"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConversationResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/conversations/{conversation_id}/assign/{user_id}":{"post":{"tags":["Conversations"],"summary":"Assign Conversation","description":"Assign a conversation to a user\n\n**Required Scopes:** `conversations:update`\n\nCross-agent rules (when allow_messaging_other_agents_conversations is False):\n- Self-assign (\"Assign to me\"): Always allowed for any agent\n- Reassign to someone else: Only ADMIN/OWNER roles","operationId":"assign_conversation_api_v1_conversations__conversation_id__assign__user_id__post","security":[{"HTTPBearer":[]}],"parameters":[{"name":"conversation_id","in":"path","required":true,"schema":{"type":"string","title":"Conversation Id"}},{"name":"user_id","in":"path","required":true,"schema":{"type":"string","title":"User Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConversationResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/conversations/{conversation_id}/close":{"post":{"tags":["Conversations"],"summary":"Close Conversation","description":"Close a conversation and create a ticket\n\n**Required Scopes:** `conversations:update`\n\nCross-agent rules (when allow_messaging_other_agents_conversations is False):\n- Only ADMIN/OWNER can close a conversation assigned to another agent\n\nArgs:\n    conversation_id: ID of the conversation to close\n    request_body: Request body containing optional notes and summarize flag","operationId":"close_conversation_api_v1_conversations__conversation_id__close_post","security":[{"HTTPBearer":[]}],"parameters":[{"name":"conversation_id","in":"path","required":true,"schema":{"type":"string","title":"Conversation Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CloseConversationRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConversationResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/conversations/{conversation_id}/typing":{"post":{"tags":["Conversations"],"summary":"Show or clear a typing indicator","description":"Show (or clear) a typing indicator to the contact in a conversation.\n\nUse it while a reply is being prepared, e.g. by your own bot. The body is\noptional; an empty body means `{\"state\": \"on\"}`.\n\n## Channel behavior\n- **WhatsApp:** the indicator also marks the contact's latest message as\n  read (`implied_read: true`) and lasts up to 25 seconds or until you\n  reply. `state: \"off\"` is accepted but has no effect.\n- **Telegram:** lasts about 5 seconds; call again to keep it visible.\n- Check `GET /channels/sender-actions` for every channel's support and\n  timeout (`typing_ttl_seconds`). Unsupported channels return\n  `422 sender_action_not_supported`.\n\n`message_id` (optional) ties the indicator to a specific inbound message\nof this conversation; channels that need one use the latest inbound\nmessage when omitted (`422 no_inbound_message` if there is none).\n\nThis is **not a message**: nothing is billed, no message is created and\nno webhook is sent. Your inbox's unread state is not changed.\n\n## Errors\n`detail` is an object: `{\"code\", \"message\", \"channel_type\", \"action\"}`.\n- `404 conversation_not_found`, `message_not_found`\n- `422 sender_action_not_supported`, `no_inbound_message`,\n  `message_not_inbound`, `channel_unavailable`\n- `429 sender_action_rate_limited` — about 30 calls per minute per\n  conversation; retry after the `Retry-After` seconds\n- `502 sender_action_failed` — the messaging platform rejected the request\n\nRepeats are absorbed: a second read receipt for the same message, or a\ntyping `on` while the indicator is still showing, returns 200 with\n`deduplicated: true` and nothing is sent again (typing `off` is never\ndeduplicated).\n\n**Required Scopes:** `messages:create`","operationId":"send_typing_indicator_api_v1_conversations__conversation_id__typing_post","security":[{"HTTPBearer":[]}],"parameters":[{"name":"conversation_id","in":"path","required":true,"schema":{"type":"string","title":"Conversation Id"}}],"requestBody":{"content":{"application/json":{"schema":{"anyOf":[{"$ref":"#/components/schemas/TypingIndicatorRequest"},{"type":"null"}],"title":"Body"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SenderActionResponse"}}}},"404":{"description":"Message or conversation not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SenderActionErrorResponse"}}}},"422":{"description":"Not possible on this channel or message: `sender_action_not_supported`, `message_not_inbound`, `no_inbound_message`, `channel_unavailable` (or a request validation error).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SenderActionErrorResponse"}}}},"429":{"description":"Too many read/typing requests for this conversation (`sender_action_rate_limited`, about 30 per minute). Honour `Retry-After`.","headers":{"Retry-After":{"description":"Seconds to wait before retrying.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SenderActionErrorResponse"}}}},"502":{"description":"The messaging platform rejected the action (`sender_action_failed`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SenderActionErrorResponse"}}}}}}},"/api/v1/conversations/{conversation_id}/snooze":{"post":{"tags":["Conversations"],"summary":"Snooze Conversation","description":"Snooze (postpone) a conversation.\n\n**Required Scopes:** `conversations:update`\n\nSnooze is a DERIVED state: the conversation ``status`` stays ``open`` /\n``assigned`` — it simply drops off the **Open** tab and appears on the\n**Snoozed** tab until ``snoozed_until`` passes (query-time expiry, no\nbackground job). Once the timestamp passes it behaves like a normal open\nconversation again.\n\nBody:\n- ``snoozed_until``: timezone-aware ISO datetime; MUST be in the future.\n- ``reopen_on_message``: when true the inbound-message worker clears the\n  snooze (returns it to Open) when a customer message arrives.\n\nEmits the ``conversation.updated`` webhook event and a\n``conversation_updated`` WebSocket broadcast so the UI moves the\nconversation to the Snoozed tab in real time.\n\nCross-agent rules (when allow_messaging_other_agents_conversations is\nFalse): only ADMIN/OWNER may snooze a conversation assigned to another\nagent.","operationId":"snooze_conversation_api_v1_conversations__conversation_id__snooze_post","security":[{"HTTPBearer":[]}],"parameters":[{"name":"conversation_id","in":"path","required":true,"schema":{"type":"string","title":"Conversation Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SnoozeConversationRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConversationResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"tags":["Conversations"],"summary":"Unsnooze Conversation","description":"Clear a conversation's snooze (return it to the Open tab).\n\n**Required Scopes:** `conversations:update`\n\nSets ``snoozed_until`` and ``snooze_reopen_on_message`` back to NULL.\nStatus is untouched. Idempotent — unsnoozing an already-unsnoozed\nconversation succeeds and still emits ``conversation.updated`` so stale\nclient state converges.\n\nCross-agent rules (when allow_messaging_other_agents_conversations is\nFalse): only ADMIN/OWNER may unsnooze a conversation assigned to another\nagent.","operationId":"unsnooze_conversation_api_v1_conversations__conversation_id__snooze_delete","security":[{"HTTPBearer":[]}],"parameters":[{"name":"conversation_id","in":"path","required":true,"schema":{"type":"string","title":"Conversation Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConversationResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/conversations/{conversation_id}/summarize":{"post":{"tags":["Conversations"],"summary":"Summarize Conversation","description":"Manually trigger AI summary generation for a conversation.\n\n**Required Scopes:** `conversations:update` (+ plan feature `ai_features`)","operationId":"summarize_conversation_api_v1_conversations__conversation_id__summarize_post","security":[{"HTTPBearer":[]}],"parameters":[{"name":"conversation_id","in":"path","required":true,"schema":{"type":"string","title":"Conversation Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/conversations/{conversation_id}/transcript":{"post":{"tags":["Conversations"],"summary":"Create Transcript Export","description":"Start an async transcript export (PDF or ZIP) for a conversation.\n\n**Required Scopes:** `conversations:read`\n\nCreates a background job and returns it immediately with status ``queued``\n(HTTP 202). Poll ``GET /conversations/{conversation_id}/transcript/{job_id}``\nuntil ``status`` is ``completed`` (a ``download_url`` is then returned) or\n``failed``.\n\nDeduplication: a double-click / concurrent request is collapsed via a Redis\n``SET NX EX`` lock, and a reload / fresh-completed request reuses the\nexisting job (an in-flight QUEUED/PROCESSING job, or a COMPLETED one from the\nlast few minutes) instead of enqueuing a duplicate.","operationId":"create_transcript_export_api_v1_conversations__conversation_id__transcript_post","security":[{"HTTPBearer":[]}],"parameters":[{"name":"conversation_id","in":"path","required":true,"schema":{"type":"string","title":"Conversation Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TranscriptExportRequest"}}}},"responses":{"202":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TranscriptExportJobResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/conversations/{conversation_id}/transcript/{job_id}":{"get":{"tags":["Conversations"],"summary":"Get Transcript Export","description":"Poll the status of an async transcript export job.\n\n**Required Scopes:** `conversations:read`\n\nWhen ``status`` is ``completed`` the response includes the ``download_url``\n(and ``storage_path``) written by the worker; when ``failed`` see\n``error_message``.","operationId":"get_transcript_export_api_v1_conversations__conversation_id__transcript__job_id__get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"conversation_id","in":"path","required":true,"schema":{"type":"string","title":"Conversation Id"}},{"name":"job_id","in":"path","required":true,"schema":{"type":"string","title":"Job Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TranscriptExportJobResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/conversations/{conversation_id}/email-transcript":{"post":{"tags":["Conversations"],"summary":"Email Conversation Transcript","description":"Email the conversation transcript to the contact (or any address).\n\n**Required Scopes:** `conversations:update`\n\nOffered when an agent closes a conversation (especially dead live-chat\ntickets where the visitor left before reading the replies), but callable\nfor ANY conversation. Sends SYNCHRONOUSLY in-request: one branded HTML\nemail (tenant logo/name header, localized greeting, optional agent note,\nthe chat transcript, and the tenant's imprint/compliance footer) from the\nselected ``EmailMailbox``.\n\nErrors: 404 unknown conversation/mailbox (tenant-scoped), 400 invalid\nemail address / mailbox not sendable, 409 ``CONTACT_BLOCKED`` (contact or\nrecipient address blocked), 422 request validation, 502 the email\nprovider rejected the send.","operationId":"email_conversation_transcript_api_v1_conversations__conversation_id__email_transcript_post","security":[{"HTTPBearer":[]}],"parameters":[{"name":"conversation_id","in":"path","required":true,"schema":{"type":"string","title":"Conversation Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/EmailTranscriptRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/EmailTranscriptResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/conversations/{conversation_id}/summary":{"get":{"tags":["Conversations"],"summary":"Get Conversation Summary","description":"Full AI ticket summary for a single conversation, broken into the\nthree sections the contact-page timeline renders on expand:\n``user_intent``, ``summary``, ``resolution``.\n\nLazy-loaded by the FE when a ``ticket_summary`` timeline row is\nexpanded — the timeline list itself only carries a 140-char\n``summary_preview`` to keep the list payload small. Reads the text\ndirectly off the ``Conversation`` SHARD row (no KB-doc / RAG fetch).\n\nReturns 404 if the conversation does not exist in the caller's tenant\n(tenant-isolated — never leaks cross-tenant ids). A conversation that\nexists but was never summarized (or was reopened, which NULLs these\nfields) returns 200 with all three sections ``null``.\n\n**Required Scopes:** `knowledge_base:read`","operationId":"get_conversation_summary_api_v1_conversations__conversation_id__summary_get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"conversation_id","in":"path","required":true,"schema":{"type":"string","title":"Conversation Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConversationSummaryDetail"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/conversations/{conversation_id}/previous":{"get":{"tags":["Conversations"],"summary":"Get Previous Conversations","description":"Summarize a contact's previous conversations + advertise the merge target.\n\nRedesigned (June 2026) to power a single-line history summary plus the\nreopen-&-merge affordance, instead of a list of heavy per-conversation\ncards. Returns CHEAP aggregates over the SAME contact's prior conversations\n(excluding the current one):\n\n  * `total_previous` — COUNT of prior conversations\n  * `distinct_channel_count` — COUNT(DISTINCT channel_id)\n  * `latest_previous_at` — MAX(last_message_at)\n  * `merge_target_conversation_id` — the ONLY conversation this one may be\n    merged into: the contact's OVERALL most-recent prior conversation, and\n    ONLY when that immediately-previous conversation is on the SAME channel\n    (`channel_id`) as the current one. Null when the immediately-previous\n    conversation overall is on a different channel (even if an older\n    same-channel prior exists). Computed by the SAME server-side helper the\n    `POST /{id}/merge` endpoint enforces with, so the FE can never advertise\n    a target the backend would reject.\n  * `mergeable` — `merge_target_conversation_id is not None`\n\n`items[]` is RETAINED in the response for backwards-compatibility but is\nnow ALWAYS returned EMPTY: the expensive per-item enrichment (bulk message\ncounts, last-message previews, channel-type hydration) is no longer\ncomputed. The only former consumer (`ConversationHistoryBoundary.tsx`) is\nbeing redesigned to use the aggregates. To list actual messages on a\nchannel, use `GET /api/v1/contacts/{contact_id}/messages`. The `limit`\nquery param is retained for compatibility but ignored.\n\nAll aggregates and the merge-target lookup respect contact-history\nvisibility: non-admin agents only see conversations on channels reachable\nthrough their accessible Custom Inboxes. Admins/owners see everything.\n\n**Required Scopes:** `conversations:read`\n\n**Response format:**\n```json\n{ \"items\": [], \"total_previous\": 4, \"distinct_channel_count\": 2,\n  \"latest_previous_at\": \"2026-06-01T10:00:00Z\", \"mergeable\": true,\n  \"merge_target_conversation_id\": \"...\" }\n```","operationId":"get_previous_conversations_api_v1_conversations__conversation_id__previous_get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"conversation_id","in":"path","required":true,"schema":{"type":"string","title":"Conversation Id"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":10,"minimum":1,"description":"Deprecated/ignored — items[] is no longer populated","default":3,"title":"Limit"},"description":"Deprecated/ignored — items[] is no longer populated"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PreviousConversationsResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/conversations/{conversation_id}/reopen":{"post":{"tags":["Conversations"],"summary":"Reopen Conversation","description":"Reopen a closed conversation\n\n**Required Scopes:** `conversations:update`","operationId":"reopen_conversation_api_v1_conversations__conversation_id__reopen_post","security":[{"HTTPBearer":[]}],"parameters":[{"name":"conversation_id","in":"path","required":true,"schema":{"type":"string","title":"Conversation Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConversationResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/conversations/{conversation_id}/merge":{"post":{"tags":["Conversations"],"summary":"Merge Conversation","description":"Merge a source conversation into this one and reopen it.\n\nThe path conversation (`conversation_id`) SURVIVES and is reopened; the\n`source_conversation_id` from the body is merged in (all its messages,\nnotes, tags, attachments, etc. move over) and then permanently deleted.\nBoth conversations must belong to the SAME contact.\n\nReply routing adopts the source conversation's active channel so future\nreplies go to where the contact last reached us.\n\nServer-side merge gating: the path conversation MUST be the single valid\nmerge target for the source — i.e. the contact's OVERALL most-recent PRIOR\nconversation, and ONLY when that immediately-previous conversation is on the\nSAME channel (`channel_id`) as the source. If the immediately-previous\nconversation overall is on a DIFFERENT channel (even when an older\nsame-channel prior exists), there is NO valid target. This is the exact\ntarget advertised by `GET /{id}/previous` (`merge_target_conversation_id`),\ncomputed by the SAME helper, so the two can never diverge.\n\nRejections:\n  * `400` — source == target (cannot merge into itself)\n  * `404` — either conversation missing/not in tenant, or a channel not\n    visible to the caller (contact-history visibility)\n  * `409` — conversations belong to different contacts, OR the path\n    conversation is not the valid merge target / no mergeable prior\n    conversation exists on this channel\n\n**Required Scopes:** `conversations:update`","operationId":"merge_conversation_api_v1_conversations__conversation_id__merge_post","security":[{"HTTPBearer":[]}],"parameters":[{"name":"conversation_id","in":"path","required":true,"schema":{"type":"string","title":"Conversation Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConversationMergeRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConversationResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/conversations/{conversation_id}/switchable-channels":{"get":{"tags":["Conversations"],"summary":"Get Switchable Channels","description":"List channels available for switching from this conversation.\n\nReturns all active switchable channels (WhatsApp, Telegram, Messenger,\nInstagram) excluding the conversation's current channel. Used by the\nfrontend to render a channel picker before generating deeplinks.\n\nCustom Inboxes security hardening (May 2026): for non-admin callers the\nlist is intersected with the channels reachable through the caller's\naccessible Custom Inboxes. Admins/owners and tenants with zero inboxes\nare unaffected.\n\n**Required Scopes:** `conversations:read`","operationId":"get_switchable_channels_api_v1_conversations__conversation_id__switchable_channels_get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"conversation_id","in":"path","required":true,"schema":{"type":"string","title":"Conversation Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SwitchableChannelsResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/conversations/{conversation_id}/sender-options":{"get":{"tags":["Conversations"],"summary":"Get Conversation Sender Options","description":"List email senders the caller may use for replying to this conversation.\n\nReturns the user's accessible support-capable email connections, plus an\nACL-override entry when the conversation's default reply sender (the\nreceiving connection's ``outbound_provider``) is NOT already in that set.\nThe client pre-selects ``default_option_id`` and POSTs the chosen value\nas ``MessageCreate.sender_id`` on the outbound send. The server\nre-validates the choice on the send path.\n\nReturns ``{\"options\": [], \"default_option_id\": null,\n\"default_reply_sender_id\": null}`` for non-email conversations so clients\ncan rely on a stable response shape.\n\n**Required Scopes:** `conversations:read`","operationId":"get_conversation_sender_options_api_v1_conversations__conversation_id__sender_options_get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"conversation_id","in":"path","required":true,"schema":{"type":"string","title":"Conversation Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SenderOptionsResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/conversations/{conversation_id}/switch-channel":{"post":{"tags":["Conversations"],"summary":"Switch Channel","description":"Generate channel switch deeplinks for a conversation.\n\nAccepts a list of ``channel_ids`` to generate links for specific channels,\nor omit to generate for all active switchable channels.\n\n**Required Scopes:** `conversations:update`","operationId":"switch_channel_api_v1_conversations__conversation_id__switch_channel_post","security":[{"HTTPBearer":[]}],"parameters":[{"name":"conversation_id","in":"path","required":true,"schema":{"type":"string","title":"Conversation Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ChannelSwitchRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ChannelSwitchResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/conversations/initiate":{"post":{"tags":["Conversations"],"summary":"Initiate Conversation","description":"Initiate a new outbound conversation with a contact.\n\nThis endpoint allows agents to start new conversations from the UI.\nBehavior varies by channel:\n\n- **WhatsApp**: Checks 24h window. If closed, requires template message.\n- **Email**: Sends email with optional signature appended.\n- **Telegram/Messenger/Instagram**: Sends message directly.\n\n**Required Scopes:** `conversations:create`\n\n**409 `CONTACT_BLOCKED`:** the contact is blocked\n(`{\"error\": \"CONTACT_BLOCKED\", \"message\": ..., \"contact_id\": ...}`).\n\n**409 `conversation_owned_elsewhere`:** for team members restricted by\nCustom Inboxes, when the contact is in an ongoing (non-email)\nconversation handled in an inbox they cannot see. Email starts a new\nthread instead.\n\n**Request Body:**\n```json\n{\n    \"contact_id\": \"uuid\",\n    \"channel_id\": \"uuid\",\n    \"message_type\": \"text\" | \"template\" | \"email\",\n\n    // For text messages\n    \"content\": \"Hello...\",\n\n    // For WhatsApp templates\n    \"template\": {\n        \"name\": \"hello_world\",\n        \"language\": \"en\",\n        \"components\": [...]\n    },\n\n    // For email\n    \"email\": {\n        \"subject\": \"Re: Your inquiry\",\n        \"body_html\": \"<p>Hello...</p>\",\n        \"include_signature\": true,\n        \"cc\": [],\n        \"bcc\": []\n    }\n}\n```\n\n**Response:**\n```json\n{\n    \"conversation_id\": \"uuid\",\n    \"message_id\": \"uuid\",\n    \"channel_type\": \"whatsapp\",\n    \"status\": \"sent\" | \"queued\" | \"failed\" | \"template_required\",\n    \"requires_template\": false,\n    \"window_status\": {\n        \"in_window\": true,\n        \"remaining_seconds\": 3600\n    }\n}\n```","operationId":"initiate_conversation_api_v1_conversations_initiate_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/InitiateConversationRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InitiateConversationResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"HTTPBearer":[]}]}},"/api/v1/conversations/check-existing":{"post":{"tags":["Conversations"],"summary":"Check Existing Conversation","description":"Check for existing conversations without creating one.\n\nThis is Phase 1 of the two-phase conversation creation flow.\nUse this endpoint to check if there are existing open or closed\nconversations before deciding to create new or reopen existing.\n\n**No webhooks are fired by this endpoint.**\n\nAfter checking, call `/conversations/open-or-create` with:\n- `action=\"create_new\"` to create a new conversation\n- `action=\"reopen_existing\"` to reopen the closed conversation\n\n**Required Scopes:** `conversations:create`\n\n\n**409 `conversation_owned_elsewhere`:** for team members restricted by\nCustom Inboxes, when the contact is in an ongoing (non-email)\nconversation handled in an inbox they cannot see. Email starts a new\nthread instead.\n\n**Request Body:**\n```json\n{\n    \"contact_id\": \"uuid\",\n    \"channel_id\": \"uuid\"\n}\n```\n\n**Response:**\n```json\n{\n    \"has_existing_open\": true,\n    \"existing_open_conversation_id\": \"uuid\",\n    \"has_existing_closed\": false,\n    \"existing_closed_info\": null,\n    \"channel_type\": \"whatsapp\",\n    \"window_status\": {\n        \"in_window\": true,\n        \"requires_template\": false,\n        \"last_customer_message_at\": null,\n        \"window_end_at\": \"2026-01-17T10:00:00Z\"\n    }\n}\n```","operationId":"check_existing_conversation_api_v1_conversations_check_existing_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CheckExistingConversationRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CheckExistingConversationResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"HTTPBearer":[]}]}},"/api/v1/conversations/open-or-create":{"post":{"tags":["Conversations"],"summary":"Open Or Create Conversation","description":"Open an existing conversation or create a new one.\n\nThis is a simplified endpoint that does NOT send a message.\nUse this when you want to navigate to a conversation and let the user\ncompose messages using the normal chat UI.\n\n**Two-Phase API (Recommended):**\n1. First call `/conversations/check-existing` to check for existing conversations\n2. Then call this endpoint with `action` to control behavior:\n   - `action=\"create_new\"`: Always create new conversation (fires webhook)\n   - `action=\"reopen_existing\"`: Reopen closed conversation (fires conversation.reopened webhook)\n   - `action=\"auto\"` (default): Backwards compatible - may create without webhook if closed exists\n\nFor WhatsApp channels, the response includes window_status to indicate\nwhether templates are required (24h window closed).\n\n**Required Scopes:** `conversations:create`\n\n\n**409 `conversation_owned_elsewhere`:** for team members restricted by\nCustom Inboxes, when the contact is in an ongoing (non-email)\nconversation handled in an inbox they cannot see. Email starts a new\nthread instead.\n\n**Request Body:**\n```json\n{\n    \"contact_id\": \"uuid\",\n    \"channel_id\": \"uuid\",\n    \"action\": \"create_new\"\n}\n```\n\n**Response:**\n```json\n{\n    \"conversation_id\": \"uuid\",\n    \"is_new\": true,\n    \"is_reopened\": false,\n    \"channel_type\": \"whatsapp\",\n    \"window_status\": {\n        \"in_window\": true,\n        \"requires_template\": false,\n        \"last_customer_message_at\": \"2026-01-16T10:00:00Z\",\n        \"window_end_at\": \"2026-01-17T10:00:00Z\"\n    }\n}\n```","operationId":"open_or_create_conversation_api_v1_conversations_open_or_create_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/OpenOrCreateConversationRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OpenOrCreateConversationResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"HTTPBearer":[]}]}},"/api/v1/conversations/{conversation_id}/bot-session":{"get":{"tags":["Conversations"],"summary":"Get Bot Session Status","description":"Get current bot session status for a conversation.\n\n**Required Scopes:** `conversations:read`\n\nReturns:\n    - bot_active: Whether a bot is currently handling this conversation\n    - bot_session_id: The ID of the active bot session (if any)\n    - bot_name: The name of the bot handling the conversation (if any)\n    - bot_id: The ID of the bot (if any)","operationId":"get_bot_session_status_api_v1_conversations__conversation_id__bot_session_get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"conversation_id","in":"path","required":true,"schema":{"type":"string","title":"Conversation Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/conversations/{conversation_id}/bot-session/disable":{"post":{"tags":["Conversations"],"summary":"Disable Bot Session","description":"Manually disable bot for this conversation.\n\nEnds the active bot session with reason AGENT_DISABLED.\nUse this when an agent wants to take over the conversation manually\nwithout sending a message first.\n\n**Required Scopes:** `conversations:update`\n\nReturns:\n    - success: Whether the bot session was disabled\n    - message: Status message","operationId":"disable_bot_session_api_v1_conversations__conversation_id__bot_session_disable_post","security":[{"HTTPBearer":[]}],"parameters":[{"name":"conversation_id","in":"path","required":true,"schema":{"type":"string","title":"Conversation Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/conversations/{conversation_id}/available-bots":{"get":{"tags":["Conversations"],"summary":"Get Available Bots","description":"Get list of available bots that can handle this conversation.\n\nReturns all active bots for the tenant that could be enabled on this conversation.\n\n**Required Scopes:** `conversations:read`\n\nReturns:\n    - bots: List of available bots with id and name","operationId":"get_available_bots_api_v1_conversations__conversation_id__available_bots_get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"conversation_id","in":"path","required":true,"schema":{"type":"string","title":"Conversation Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/conversations/{conversation_id}/bot-session/enable":{"post":{"tags":["Conversations"],"summary":"Enable Bot Session","description":"Enable a bot for this conversation.\n\nIf bot_id is provided, enables that specific bot.\nIf bot_id is not provided, finds the best matching bot based on activation rules.\n\n**Required Scopes:** `conversations:update`\n\nArgs:\n    bot_id: Optional specific bot ID to enable\n\nReturns:\n    - success: Whether the bot session was enabled\n    - message: Status message\n    - bot_session_id: The ID of the new bot session (if created)\n    - bot_name: The name of the bot (if created)","operationId":"enable_bot_session_api_v1_conversations__conversation_id__bot_session_enable_post","security":[{"HTTPBearer":[]}],"parameters":[{"name":"conversation_id","in":"path","required":true,"schema":{"type":"string","title":"Conversation Id"}},{"name":"bot_id","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Bot Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/comments/messenger/resubscribe":{"post":{"tags":["Comments"],"summary":"Subscribe Facebook Messenger Page(s) to the comment webhook","description":"Fix a **Facebook Messenger** Page that the capability probe reported as\n`resubscribe_required` — i.e. the connection is otherwise fine, the Page is just\nnot subscribed to the `feed` webhook field that delivers post comments.\n\nUnlike Instagram (which needs a real permission the customer can only add by\nreconnecting), no permission is missing here: the stored Page token already\ncarries everything Facebook private replies need. So instead of sending the\ncustomer to reconnect, we do it for them — this POSTs the Page's\n`subscribed_apps` with the complete Messenger field list **plus `feed`** (a full\nreplace, never a merge; a Page that also backs a legacy page-backed Instagram\nchannel keeps `comments` on the union).\n\nPass `channel_id` to fix one Messenger channel, or omit it to resubscribe every\nnon-archived Messenger channel in the workspace. Tenant-scoped and idempotent.\nAfter it returns, re-read `GET /channels/comment-capabilities` for the fresh\nverdict.\n\n**Required Scopes:** `channels:update`","operationId":"resubscribe_messenger_comments_api_v1_comments_messenger_resubscribe_post","security":[{"HTTPBearer":[]}],"parameters":[{"name":"channel_id","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Restrict to one Facebook Messenger channel. Omit to resubscribe every non-archived Messenger channel in the workspace.","title":"Channel Id"},"description":"Restrict to one Facebook Messenger channel. Omit to resubscribe every non-archived Messenger channel in the workspace."}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MessengerResubscribeResponse"}}}},"403":{"description":"The `social_comments_beta` feature is not enabled for this workspace (`error_code`: `feature_not_enabled`)."},"404":{"description":"A `channel_id` was given but no matching non-archived Facebook Messenger channel exists in this workspace (`error_code`: `channel_not_found`). Instagram channels are never valid here — they need a customer reconnect, not a resubscribe."},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/comments/{comment_id}/private-reply":{"post":{"tags":["Comments"],"summary":"Send a private reply to a comment","description":"Send a **private reply**: a direct message to the person who left a public\ncomment on your Instagram post or Facebook Page post.\n\nPass Meta's own comment id in the path — the value your `comment.received`\nwebhook delivers. The comment does not have to be stored in your workspace; if\nit is not, supply `channel_id` in the body.\n\n### What a private reply may contain\n\nThe two platforms differ, and this endpoint enforces the difference rather\nthan letting a field be silently dropped:\n\n| Content | Instagram | Facebook Messenger |\n|---|---|---|\n| `text` | yes | yes |\n| `buttons` | yes (up to 3, `url` or `postback`) | yes (up to 3, `url` or `postback`) |\n| `quick_replies` | yes (up to 13) | yes (up to 13) |\n| `image_url` | **no** — 400 | yes |\n\nButtons and quick replies are supported on **both** platforms — verified on\nlive accounts: they render for the recipient and taps arrive back as inbound\nmessages.\n\nImages on Instagram are not accepted at all. On either platform, content the\nprivate reply cannot carry belongs in a **follow-up** message: once the\nrecipient answers, the standard 24-hour messaging window opens and the normal\nsend API accepts buttons and media.\n\n### Buttons\n\nEach button is one of two kinds:\n\n* **URL button** — `{\"type\": \"url\", \"title\": \"Shop now\", \"url\": \"https://…\"}`.\n  Opens the link. Absolute **https** only. Nothing comes back to you.\n* **Postback button** — `{\"type\": \"postback\", \"title\": \"Get the code\", …}`.\n  Sends a value back to you when tapped, arriving as `meta.button_id` on the\n  resulting inbound message. Either:\n  * `\"action\": \"start_flow\", \"flow_id\": \"…\"` — the tap **starts that flow** for\n    the person who tapped, or\n  * `\"payload\": \"your-own-value\"` — an opaque value you match yourself.\n\nA `start_flow` button may target any published flow **except** one whose\ntrigger is \"Comment received\": that trigger is reserved for the first message,\nand this comment's single allowed private reply was already spent on the\nmessage carrying the button.\n\n### Rules Meta enforces\n\n* **One private reply per comment, ever.** A second attempt returns `409`.\n* **7-day window** from when the comment was created. After that, `410`.\n* **Not available on ad comments** (Facebook). This is invisible until you\n  send, so it surfaces as `422`.\n\n### Billing\n\nA private reply is a real Instagram/Messenger direct message and is billed\nexactly like any other outbound message on that channel.\n\n**🔐 Required Scopes:** `messages:create`\n","operationId":"send_comment_private_reply_api_v1_comments__comment_id__private_reply_post","security":[{"HTTPBearer":[]}],"parameters":[{"name":"comment_id","in":"path","required":true,"schema":{"type":"string","description":"Meta's comment id (Instagram: numeric; Facebook: `{post_id}_{comment_id}`) — exactly as delivered by the webhook.","title":"Comment Id"},"description":"Meta's comment id (Instagram: numeric; Facebook: `{post_id}_{comment_id}`) — exactly as delivered by the webhook."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PrivateReplyRequest"}}}},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PrivateReplyResponse"}}}},"400":{"description":"The request is not valid for this platform — most commonly an image on an **Instagram** channel, which Meta does not document for private replies. `error_code` is `unsupported_content` or `channel_required`."},"403":{"description":"The `social_comments_beta` feature is not enabled for this workspace (`error_code`: `feature_not_enabled`)."},"404":{"description":"The channel does not exist, is inactive, or is not an Instagram / Facebook Messenger channel (`error_code`: `channel_not_found`)."},"409":{"description":"A private reply has **already** been sent for this comment. Meta allows exactly one per comment, forever — this is permanent, not retryable (`error_code`: `private_reply_already_sent`). Also returned when the commenter is a blocked contact (`detail.error`: `CONTACT_BLOCKED`); unblock the contact to reply privately."},"410":{"description":"The **7-day** private-reply window measured from the comment's creation has closed. Permanent (`error_code`: `private_reply_window_expired`)."},"422":{"description":"Two distinct causes, told apart by `error_code`:\n\n* `private_reply_not_eligible` — Meta refuses a private reply to this particular comment: it is on an **ad** (Facebook only), the comment or its post was deleted, or the author is another Page. Nothing in Meta's webhooks flags an ad comment, so this can only be discovered at send time.\n* `invalid_buttons` — one or more buttons are unusable. The body carries `errors`: a list of `{index, field, reason}` naming the offending button by position. Reasons include `must_be_absolute_https_url`, `too_long`, `unknown_flow` and `flow_trigger_not_startable_from_button`.\n\nBoth carry an `error_code`, which also distinguishes them from FastAPI's schema-validation 422."},"502":{"description":"Meta rejected or failed the send for another reason (`error_code`: `send_failed`). Safe to retry."}}}},"/api/v1/comments/{comment_id}/public-reply":{"post":{"tags":["Comments"],"summary":"Post a public reply under a comment","description":"Post a **public reply** visible under a comment on your Instagram post or\nFacebook Page post — the ordinary \"reply to this comment\" action.\n\nPass Meta's own comment id in the path. If the comment is not stored in your\nworkspace, supply `channel_id` in the body.\n\nUnlike a **private reply**, a public reply is comment-layer only: it does not\ncreate a conversation, a contact or a `messages` row, and it is **not billed**\nlike a direct message. It also does not consume the one-per-comment private-reply\nallowance — you can reply publicly and still send a private DM afterwards. There\nis no time window and no duplicate suppression (Meta permits several public\nreplies to one comment).\n\nText only in v1 — no buttons, quick replies or media.\n\n**Required Scopes:** `messages:create`","operationId":"send_comment_public_reply_api_v1_comments__comment_id__public_reply_post","security":[{"HTTPBearer":[]}],"parameters":[{"name":"comment_id","in":"path","required":true,"schema":{"type":"string","description":"Meta's comment id (Instagram: numeric; Facebook: `{post_id}_{comment_id}`) — exactly as delivered by the webhook.","title":"Comment Id"},"description":"Meta's comment id (Instagram: numeric; Facebook: `{post_id}_{comment_id}`) — exactly as delivered by the webhook."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicReplyRequest"}}}},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicReplyResponse"}}}},"400":{"description":"`channel_id` is required because the comment is not stored in this workspace (`error_code`: `channel_required`)."},"403":{"description":"The `social_comments_beta` feature is not enabled for this workspace (`error_code`: `feature_not_enabled`)."},"404":{"description":"The channel does not exist, is inactive, or is not an Instagram / Facebook Messenger channel (`error_code`: `channel_not_found`)."},"502":{"description":"Meta rejected or failed the reply (`error_code`: `send_failed`). Safe to retry — public replies have no one-per-comment limit."},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/comments/{comment_id}/reply":{"post":{"tags":["Comments"],"summary":"Post a public reply and/or send a private DM in one call","description":"Post a public reply **and/or** send a private DM to a comment in one call.\nSupply `public_text`, `private_text`, or both (at least one is required).\n\nThe private-DM fields (`buttons`, `quick_replies`, `image_url`) behave exactly\nas on `POST /{comment_id}/private-reply` and apply only to the DM.\n\n### Order and partial-failure contract\n\nThe public reply is posted **first**, then the private DM:\n\n* **Public reply fails** → `502`, and the private DM is *not* attempted\n  (nothing was sent).\n* **Public reply succeeds, private DM fails** → `200` with\n  `status: \"partial\"`: the public reply is already live and is returned in\n  `public`, while `private` is `null` and `private_error` /\n  `private_error_type` describe the DM failure. The public reply is never\n  rolled back.\n* **Private-only request (no `public_text`)** → the private-reply errors map to\n  their usual HTTP statuses (`409` already-sent, `410` window, `422`\n  not-eligible / invalid buttons, …), exactly like the standalone endpoint.\n* **Everything sent** → `200` with `status: \"sent\"`.\n\nBilling: the private DM is billed like any outbound message; the public reply is\nnot billed.\n\n**Required Scopes:** `messages:create`","operationId":"send_comment_and_private_reply_api_v1_comments__comment_id__reply_post","security":[{"HTTPBearer":[]}],"parameters":[{"name":"comment_id","in":"path","required":true,"schema":{"type":"string","description":"Meta's comment id (Instagram: numeric; Facebook: `{post_id}_{comment_id}`) — exactly as delivered by the webhook.","title":"Comment Id"},"description":"Meta's comment id (Instagram: numeric; Facebook: `{post_id}_{comment_id}`) — exactly as delivered by the webhook."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CommentAndPrivateReplyRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CommentAndPrivateReplyResponse"}}}},"400":{"description":"`error_code` `channel_required` (comment not stored, no `channel_id`) or `unsupported_content` (e.g. an image on Instagram)."},"403":{"description":"The `social_comments_beta` feature is not enabled (`error_code`: `feature_not_enabled`)."},"404":{"description":"Channel not found (`error_code`: `channel_not_found`)."},"409":{"description":"A private reply was already sent for this comment (`error_code`: `private_reply_already_sent`). Only raised for a **private-only** request — see the partial contract below."},"410":{"description":"The 7-day private-reply window has closed (`error_code`: `private_reply_window_expired`). Private-only request only."},"422":{"description":"`private_reply_not_eligible`, `invalid_buttons` or `invalid_quick_replies` on the private DM. Private-only request only, plus FastAPI schema-validation 422."},"502":{"description":"The **public** reply failed (`error_code`: `send_failed`); nothing was sent, safe to retry."}}}},"/api/v1/comments/posts":{"get":{"tags":["Comments"],"summary":"List posts that have comments","description":"The left-hand column of the Comments view: your Instagram media and Facebook\nPage posts, newest published first, each with its comment count and — the number\nthat matters — how many of those comments are still **unanswered**.\n\nBoth counts are read from the post row itself rather than counted per request.\nThey are maintained as comments arrive and recomputed whenever a comment is\ntriaged or aged out, which is what keeps this list cheap on a post that went\nviral.\n\nPass `channel_id`: the Comments view is channel-scoped, and it is also the\nindexed path. Use `has_unanswered=true` for a \"needs attention\" list.\n\n**🔐 Required Scopes:** `messages:read`\n","operationId":"list_social_posts_api_v1_comments_posts_get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"channel_id","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Only posts on this Instagram / Facebook Messenger channel. Omit for every channel in the workspace.","title":"Channel Id"},"description":"Only posts on this Instagram / Facebook Messenger channel. Omit for every channel in the workspace."},{"name":"has_unanswered","in":"query","required":false,"schema":{"anyOf":[{"type":"boolean"},{"type":"null"}],"description":"`true` → only posts with at least one unanswered comment; `false` → only fully cleared posts; omit for all.","title":"Has Unanswered"},"description":"`true` → only posts with at least one unanswered comment; `false` → only fully cleared posts; omit for all."},{"name":"page","in":"query","required":false,"schema":{"type":"integer","minimum":1,"description":"Page number (1-indexed)","default":1,"title":"Page"},"description":"Page number (1-indexed)"},{"name":"page_size","in":"query","required":false,"schema":{"type":"integer","maximum":100,"minimum":1,"description":"Items per page","default":20,"title":"Page Size"},"description":"Items per page"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SocialPostListResponse"}}}},"403":{"description":"The `social_comments_beta` feature is not enabled for this workspace (`error_code`: `feature_not_enabled`)."},"503":{"description":"Comment storage is not available for this workspace yet (`error_code`: `social_comments_unavailable`). Transient — this is what a shard that has not received the comment tables answers, rather than a 500."},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/comments/posts/sync":{"post":{"tags":["Comments"],"summary":"Pull recent posts from Meta into the Posts list","description":"Fetch the newest posts straight off the connected Instagram account / Facebook\nPage and upsert them into the Posts list — **including posts that have no\ncomments yet**, which is what ordinary comment ingestion never surfaces.\n\nPass `channel_id` to sync one channel, or omit it to sync every connected social\nchannel in the workspace. `limit` (default 25) is how many recent posts to pull\nper channel.\n\nReturns `{ \"synced\": N, \"created\": M }`: `synced` is how many posts were read\nand upserted, `created` is how many of those were new. A channel Meta\nrate-limits or rejects is skipped rather than failing the whole call, so a large\nworkspace still syncs the channels that are reachable.\n\nThe like/comment counts and captions on existing posts are refreshed by the same\npass.\n\n**🔐 Required Scopes:** `messages:read`\n","operationId":"sync_social_posts_api_v1_comments_posts_sync_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SyncPostsRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SyncPostsResponse"}}}},"403":{"description":"The `social_comments_beta` feature is not enabled for this workspace (`error_code`: `feature_not_enabled`)."},"404":{"description":"A `channel_id` was given but no such channel exists in this workspace (`error_code`: `channel_not_found`)."},"400":{"description":"The given `channel_id` is not an Instagram / Facebook Messenger channel (`error_code`: `not_a_social_channel`)."},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"HTTPBearer":[]}]}},"/api/v1/comments/posts/insights/summary":{"get":{"tags":["Comments"],"summary":"Aggregate insights totals across the workspace's posts","description":"Totals of the cached per-post insights across all of this workspace's\ncomment-synced posts, for two windows: the **last 30 days** (posts published in\nthat window) and **all time**.\n\nBecause per-post insights are Meta **lifetime** totals, the 30-day window\nselects WHICH posts are summed (by `posted_at`), it is NOT a per-day series — the\nnumbers are still lifetime totals for the posts in scope. Each metric sums only\nthe non-null cached values, so a metric a platform never exposes never distorts\nthe total; `post_count` is how many posts had cached insights and contributed.\n\nServed entirely from the cache — no live Graph call. Use the refresh endpoints to\nupdate the underlying numbers first.\n\n**Required Scopes:** `messages:read`","operationId":"get_posts_insights_summary_api_v1_comments_posts_insights_summary_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SocialPostInsightsSummaryResponse"}}}},"403":{"description":"The `social_comments_beta` feature is not enabled for this workspace (`error_code`: `feature_not_enabled`)."},"503":{"description":"Comment storage is not available for this workspace yet (`error_code`: `social_comments_unavailable`). Transient — this is what a shard that has not received the comment tables answers, rather than a 500."}},"security":[{"HTTPBearer":[]}]}},"/api/v1/comments/posts/insights/refresh":{"post":{"tags":["Comments"],"summary":"Force-refresh cached insights for the workspace's recent posts","description":"Force a LIVE Meta re-fetch for the workspace's most recent posts, overwrite the\nper-post cache columns, and return each refreshed post with its updated insights.\n\nBounded by `limit` (default 25, max 50, most-recent-first) so a large workspace\nnever triggers a Graph storm — call it per channel with `channel_id` to refresh a\nspecific account. A post whose page lacks insights access is refreshed to\n`authorized: false` + `insights_not_authorized` (persisted, never a 500); a\nnon-IG/FB post to `insights_unsupported_platform`.\n\n**Required Scopes:** `messages:read`","operationId":"refresh_posts_insights_bulk_api_v1_comments_posts_insights_refresh_post","security":[{"HTTPBearer":[]}],"parameters":[{"name":"channel_id","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Only refresh posts on this Instagram / Facebook channel. Omit to refresh the most recent posts across the workspace.","title":"Channel Id"},"description":"Only refresh posts on this Instagram / Facebook channel. Omit to refresh the most recent posts across the workspace."},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":50,"minimum":1,"description":"How many most-recent posts to refresh.","default":25,"title":"Limit"},"description":"How many most-recent posts to refresh."}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SocialPostInsightsBulkRefreshResponse"}}}},"403":{"description":"The `social_comments_beta` feature is not enabled for this workspace (`error_code`: `feature_not_enabled`)."},"503":{"description":"Comment storage is not available for this workspace yet (`error_code`: `social_comments_unavailable`). Transient — this is what a shard that has not received the comment tables answers, rather than a 500."},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/comments/posts/{post_id}/insights/refresh":{"post":{"tags":["Comments"],"summary":"Force-refresh cached insights for one post","description":"Force a LIVE Meta re-fetch for one comment-synced post, overwrite its cache\ncolumns (and `insights_refreshed_at`), and return the updated insights.\n\nDegrades softly and persists the degraded state: a page lacking insights access →\n`authorized: false` + `insights_not_authorized`; an object Meta carries no\ninsights for → `insights_unavailable`; a non-IG/FB channel →\n`insights_unsupported_platform`. Never a 500 for a Meta-side condition.\n\n**Required Scopes:** `messages:read`","operationId":"refresh_post_insights_single_api_v1_comments_posts__post_id__insights_refresh_post","security":[{"HTTPBearer":[]}],"parameters":[{"name":"post_id","in":"path","required":true,"schema":{"type":"string","description":"Our post id (from `GET /comments/posts`).","title":"Post Id"},"description":"Our post id (from `GET /comments/posts`)."}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SocialPostInsightsRefreshResponse"}}}},"403":{"description":"The `social_comments_beta` feature is not enabled for this workspace (`error_code`: `feature_not_enabled`)."},"503":{"description":"Comment storage is not available for this workspace yet (`error_code`: `social_comments_unavailable`). Transient — this is what a shard that has not received the comment tables answers, rather than a 500."},"404":{"description":"No such post in this workspace (`post_not_found`)."},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/comments/posts/{post_id}/comments":{"get":{"tags":["Comments"],"summary":"List the comments on one post","description":"The comment thread for one post, oldest first so it reads chronologically.\n\n`post_id` is **our** id, as returned by `GET /comments/posts` — not Meta's post\nid.\n\nEvery row carries what it takes to render it and to decide what you may do with\nit: the text, the author, the state, whether it is a threaded reply and to which\ncomment, when Meta's 7-day private-reply window closes, and whether a private\nreply is still possible **with the reason when it is not**. Most comments end up\nunavailable, and that is normal rather than an error — see\n`private_reply_unavailable_reason`.\n\nFilter with `state`, which is repeatable: `?state=unanswered`,\n`?state=replied&state=auto_replied`. Omit it for the \"All\" tab.\n\n**🔐 Required Scopes:** `messages:read`\n","operationId":"list_post_comments_api_v1_comments_posts__post_id__comments_get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"post_id","in":"path","required":true,"schema":{"type":"string","description":"Our social post id (not Meta's).","title":"Post Id"},"description":"Our social post id (not Meta's)."},{"name":"state","in":"query","required":false,"schema":{"anyOf":[{"type":"array","items":{"type":"string"}},{"type":"null"}],"description":"Repeatable state filter: `unanswered` (alias of `pending`), `pending`, `auto_replied`, `replied`, `handled`, `ignored`. Omit for all states.","title":"State"},"description":"Repeatable state filter: `unanswered` (alias of `pending`), `pending`, `auto_replied`, `replied`, `handled`, `ignored`. Omit for all states."},{"name":"include_owner","in":"query","required":false,"schema":{"type":"boolean","description":"Include comments made by the connected account itself. Off by default: Meta echoes our own public replies back as ordinary comments, and they are noise in a work queue.","default":false,"title":"Include Owner"},"description":"Include comments made by the connected account itself. Off by default: Meta echoes our own public replies back as ordinary comments, and they are noise in a work queue."},{"name":"include_reply","in":"query","required":false,"schema":{"type":"boolean","description":"Hydrate each answered comment's private-reply message into `private_reply`. One extra query per page; turn it off for bulk exports.","default":true,"title":"Include Reply"},"description":"Hydrate each answered comment's private-reply message into `private_reply`. One extra query per page; turn it off for bulk exports."},{"name":"sort_order","in":"query","required":false,"schema":{"type":"string","pattern":"^(asc|desc)$","description":"Order by `commented_at`. Defaults to `asc` — a thread reads forwards.","default":"asc","title":"Sort Order"},"description":"Order by `commented_at`. Defaults to `asc` — a thread reads forwards."},{"name":"page","in":"query","required":false,"schema":{"type":"integer","minimum":1,"description":"Page number (1-indexed)","default":1,"title":"Page"},"description":"Page number (1-indexed)"},{"name":"page_size","in":"query","required":false,"schema":{"type":"integer","maximum":100,"minimum":1,"description":"Items per page","default":50,"title":"Page Size"},"description":"Items per page"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SocialCommentListResponse"}}}},"403":{"description":"The `social_comments_beta` feature is not enabled for this workspace (`error_code`: `feature_not_enabled`)."},"503":{"description":"Comment storage is not available for this workspace yet (`error_code`: `social_comments_unavailable`). Transient — this is what a shard that has not received the comment tables answers, rather than a 500."},"404":{"description":"No such post in this workspace (`error_code`: `post_not_found`)."},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/comments":{"get":{"tags":["Comments"],"summary":"Comment inbox — comments across all posts","description":"Every comment in the workspace, newest first, filtered by state — the screen an\nagent actually works from. It exists so nobody has to guess which post has\nsomething waiting on it: `GET /comments?state=unanswered` is the whole queue.\n\nDefaults to `state=unanswered`, because that is the question this view answers.\nPass `state` explicitly (repeatable) for anything else, or `state=pending&\nstate=auto_replied&state=replied&state=handled&state=ignored` for everything.\n\nEach row embeds a compact `post` reference — permalink, caption, thumbnail — so\na comment is legible without knowing which post it came from, and links back to\nthe post on Instagram / Facebook.\n\n**🔐 Required Scopes:** `messages:read`\n","operationId":"list_comment_inbox_api_v1_comments_get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"channel_id","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Only comments on this channel's posts.","title":"Channel Id"},"description":"Only comments on this channel's posts."},{"name":"state","in":"query","required":false,"schema":{"anyOf":[{"type":"array","items":{"type":"string"}},{"type":"null"}],"description":"Repeatable state filter: `unanswered` (alias of `pending`), `pending`, `auto_replied`, `replied`, `handled`, `ignored`. **Defaults to `unanswered`.**","title":"State"},"description":"Repeatable state filter: `unanswered` (alias of `pending`), `pending`, `auto_replied`, `replied`, `handled`, `ignored`. **Defaults to `unanswered`.**"},{"name":"include_owner","in":"query","required":false,"schema":{"type":"boolean","description":"Include the connected account's own comments (our public replies).","default":false,"title":"Include Owner"},"description":"Include the connected account's own comments (our public replies)."},{"name":"include_reply","in":"query","required":false,"schema":{"type":"boolean","description":"Hydrate `private_reply` for answered comments.","default":true,"title":"Include Reply"},"description":"Hydrate `private_reply` for answered comments."},{"name":"include_post","in":"query","required":false,"schema":{"type":"boolean","description":"Embed each comment's `post` reference. One extra query per page; turn it off if you only need the comments themselves.","default":true,"title":"Include Post"},"description":"Embed each comment's `post` reference. One extra query per page; turn it off if you only need the comments themselves."},{"name":"sort_order","in":"query","required":false,"schema":{"type":"string","pattern":"^(asc|desc)$","description":"Order by `commented_at`. Defaults to `desc` (newest first); pass `asc` to work the queue oldest-first.","default":"desc","title":"Sort Order"},"description":"Order by `commented_at`. Defaults to `desc` (newest first); pass `asc` to work the queue oldest-first."},{"name":"page","in":"query","required":false,"schema":{"type":"integer","minimum":1,"description":"Page number (1-indexed)","default":1,"title":"Page"},"description":"Page number (1-indexed)"},{"name":"page_size","in":"query","required":false,"schema":{"type":"integer","maximum":100,"minimum":1,"description":"Items per page","default":50,"title":"Page Size"},"description":"Items per page"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SocialCommentListResponse"}}}},"403":{"description":"The `social_comments_beta` feature is not enabled for this workspace (`error_code`: `feature_not_enabled`)."},"503":{"description":"Comment storage is not available for this workspace yet (`error_code`: `social_comments_unavailable`). Transient — this is what a shard that has not received the comment tables answers, rather than a 500."},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/comments/{comment_id}":{"get":{"tags":["Comments"],"summary":"Get one comment","description":"One comment, with its post and its private reply resolved.\n\n`comment_id` is **Meta's** comment id — the same value\n`POST /comments/{comment_id}/private-reply` takes, the same value the\n`comment.received` webhook delivers, and the same value that appears on the\noutbound reply message as `meta.comment_id`. Our own comment id is accepted too.\n\nThat is what makes this endpoint the **message → comment** direction of the\nreply linkage: a conversation showing a private reply holds only\n`meta.comment_id`, and this turns it into the comment text, author, state and\npost permalink needed to render \"↩ replying to …\".\n\n**🔐 Required Scopes:** `messages:read`\n","operationId":"get_social_comment_api_v1_comments__comment_id__get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"comment_id","in":"path","required":true,"schema":{"type":"string","description":"Meta's comment id (Instagram: numeric; Facebook: `{post_id}_{comment_id}`), or our own comment id.","title":"Comment Id"},"description":"Meta's comment id (Instagram: numeric; Facebook: `{post_id}_{comment_id}`), or our own comment id."},{"name":"include_reply","in":"query","required":false,"schema":{"type":"boolean","description":"Hydrate the private-reply message into `private_reply`.","default":true,"title":"Include Reply"},"description":"Hydrate the private-reply message into `private_reply`."}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SocialCommentResponse"}}}},"403":{"description":"The `social_comments_beta` feature is not enabled for this workspace (`error_code`: `feature_not_enabled`)."},"503":{"description":"Comment storage is not available for this workspace yet (`error_code`: `social_comments_unavailable`). Transient — this is what a shard that has not received the comment tables answers, rather than a 500."},"404":{"description":"No such comment in this workspace (`error_code`: `comment_not_found`). A comment we never stored — one that predates the feature, or that retention has aged out — is indistinguishable from one that never existed."},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"patch":{"tags":["Comments"],"summary":"Mark a comment handled or ignored (or reopen it)","description":"Clear a comment from the unanswered queue **without** sending a private reply —\nfor the comment you answered publicly, answered on the phone, or simply decided\nto skip. This is what stops the unanswered badge from being a number nobody can\never drive to zero.\n\n* `handled` — dealt with, no private reply sent.\n* `ignored` — not actionable.\n* `pending` — reopen; undoes either of the above.\n\nThe post's comment counters are **recomputed** from the comment rows as part of\nthe same transaction, not nudged up or down, so repeating this call — a\ndouble-click, a retried request, flipping a comment back and forth — can never\nmake the badge drift.\n\n`replied` and `auto_replied` are not settable here: they assert that a private\nreply message exists, and only sending one may assert that.\n\n**🔐 Required Scopes:** `conversations:update, messages:create`\n","operationId":"triage_social_comment_api_v1_comments__comment_id__patch","security":[{"HTTPBearer":[]}],"parameters":[{"name":"comment_id","in":"path","required":true,"schema":{"type":"string","description":"Meta's comment id, or our own comment id.","title":"Comment Id"},"description":"Meta's comment id, or our own comment id."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SocialCommentTriageRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SocialCommentResponse"}}}},"403":{"description":"The `social_comments_beta` feature is not enabled for this workspace (`error_code`: `feature_not_enabled`)."},"503":{"description":"Comment storage is not available for this workspace yet (`error_code`: `social_comments_unavailable`). Transient — this is what a shard that has not received the comment tables answers, rather than a 500."},"404":{"description":"No such comment in this workspace (`error_code`: `comment_not_found`)."},"409":{"description":"The comment already has a private reply, so its state is no longer yours to set — Meta permits exactly one private reply per comment, forever, and calling such a comment unanswered would advertise an action that can never succeed (`error_code`: `state_not_allowed`)."},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/social-posts":{"post":{"tags":["Social Posts"],"summary":"Create, schedule or publish a social post","description":"Author a post and publish it to one or more **Instagram** / **Facebook Page**\nchannels. Each channel becomes a target that succeeds or fails independently.\n\nThree modes:\n\n* **Draft** — omit both `scheduled_at` and `publish_now`. Saved as `draft`.\n* **Schedule** — set `scheduled_at` (future, UTC). Saved as `scheduled`; the\n  scheduler tick publishes it.\n* **Publish now** — set `publish_now: true`. Publishes synchronously and returns\n  the result, including per-target status and `published_count` /\n  `failed_count` / `skipped_count`.\n\nFormats: `text` (Facebook only), `image`, `video`, `reel` (Instagram only).\nInstagram always requires media; `text` posts must not carry media. Every media\nitem is an attachment UUID (upload first via `POST /api/v1/attachments/*`).\n\n**Required Scopes:** `campaigns:create`","operationId":"create_social_post_api_v1_social_posts_post","security":[{"HTTPBearer":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ScheduledPostCreateRequest"}}}},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ScheduledPostResponse"}}}},"400":{"description":"Validation failed: `channel_inactive`, `channel_type_not_supported`, `format_not_supported`, `scheduled_at_in_past`."},"403":{"description":"`social_comments_beta` not enabled (`feature_not_enabled`)."},"404":{"description":"A `channel_id` or media attachment id was not found."},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"get":{"tags":["Social Posts"],"summary":"List authored social posts","description":"List authored posts for this workspace, newest first, with each post's\nper-target status. Filter by `status`, and/or by a `scheduled_at` calendar\nwindow with `scheduled_from` / `scheduled_to` (ISO 8601, UTC, both inclusive) —\nthe two combine. `scheduled_from` after `scheduled_to` is a 400.\n\n**Required Scopes:** `campaigns:read`","operationId":"list_social_posts_api_v1_social_posts_get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"status","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Filter by post status: `draft`, `scheduled`, `publishing`, `published`, `partially_failed`, `failed`, `cancelled`.","title":"Status"},"description":"Filter by post status: `draft`, `scheduled`, `publishing`, `published`, `partially_failed`, `failed`, `cancelled`."},{"name":"scheduled_from","in":"query","required":false,"schema":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"description":"Calendar lower bound (ISO 8601, UTC, inclusive). Only posts whose `scheduled_at` is at or after this instant are returned. Combinable with `status` and `scheduled_to`.","title":"Scheduled From"},"description":"Calendar lower bound (ISO 8601, UTC, inclusive). Only posts whose `scheduled_at` is at or after this instant are returned. Combinable with `status` and `scheduled_to`."},{"name":"scheduled_to","in":"query","required":false,"schema":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"description":"Calendar upper bound (ISO 8601, UTC, inclusive). Only posts whose `scheduled_at` is at or before this instant are returned.","title":"Scheduled To"},"description":"Calendar upper bound (ISO 8601, UTC, inclusive). Only posts whose `scheduled_at` is at or before this instant are returned."},{"name":"page","in":"query","required":false,"schema":{"type":"integer","minimum":1,"description":"Page number (1-indexed).","default":1,"title":"Page"},"description":"Page number (1-indexed)."},{"name":"page_size","in":"query","required":false,"schema":{"type":"integer","maximum":100,"minimum":1,"description":"Items per page.","default":20,"title":"Page Size"},"description":"Items per page."}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ScheduledPostListResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/social-posts/{post_id}":{"get":{"tags":["Social Posts"],"summary":"Get one authored social post","description":"Get one authored post with its per-target status.\n\n**Required Scopes:** `campaigns:read`","operationId":"get_social_post_api_v1_social_posts__post_id__get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"post_id","in":"path","required":true,"schema":{"type":"string","description":"The authored post id.","title":"Post Id"},"description":"The authored post id."}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ScheduledPostResponse"}}}},"404":{"description":"Post not found (`post_not_found`)."},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"patch":{"tags":["Social Posts"],"summary":"Edit or cancel a draft / scheduled post","description":"Edit a `draft` or `scheduled` post (caption / media / `scheduled_at` /\n`channel_ids`), or **cancel** it with `cancel: true`.\n\n* Editing is allowed only while the post is `draft` or `scheduled`.\n* Changing `media_attachment_ids` re-validates ownership and re-bumps retention.\n* Changing `channel_ids` reconciles targets: removed channels are dropped, added\n  channels get a new `pending` target.\n* `cancel: true` moves the post to `cancelled`; rejected once it is `publishing`\n  or `published`. Cannot be combined with edit fields.\n\n**Required Scopes:** `campaigns:update`","operationId":"update_social_post_api_v1_social_posts__post_id__patch","security":[{"HTTPBearer":[]}],"parameters":[{"name":"post_id","in":"path","required":true,"schema":{"type":"string","description":"The authored post id.","title":"Post Id"},"description":"The authored post id."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ScheduledPostUpdateRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ScheduledPostResponse"}}}},"400":{"description":"Edit fields on a non-editable post, cancel on a publishing/published post, or a validation error (`post_not_editable`, `post_not_cancellable`, `scheduled_at_in_past`, channel/format errors)."},"403":{"description":"`social_comments_beta` not enabled."},"404":{"description":"Post, channel, or attachment not found."},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/social-posts/capabilities/publish":{"get":{"tags":["Social Posts"],"summary":"Which channels can publish posts, and which formats","description":"Report, for every Instagram / Facebook Page channel in this workspace, whether\nit can publish authored posts and which formats it supports (`text`, `image`,\n`video`, `reel`). Fail-closed: unknown channel types report `can_publish: false`.\n\n`beta_enabled` reports the tenant's `social_comments_beta` enrolment separately.\n\n**Required Scopes:** `channels:read`","operationId":"get_post_capabilities_api_v1_social_posts_capabilities_publish_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PostCapabilitiesResponse"}}}}},"security":[{"HTTPBearer":[]}]}},"/api/v1/social-posts/insights/account":{"get":{"tags":["Social Posts"],"summary":"Live account / page insights for one connected channel","description":"Fetch account-level (Instagram) or page-level (Facebook) insights LIVE for one\nconnected channel. Nothing is stored — this proxies Meta's `/insights` on each\ncall. If the page was connected before insights access was granted, the response\nis `authorized: false` with `error_code: insights_not_authorized` (a 200, not a\n500) so the UI can prompt a reconnect.\n\n**Required Scopes:** `campaigns:read`","operationId":"get_account_insights_api_v1_social_posts_insights_account_get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"channel_id","in":"query","required":true,"schema":{"type":"string","description":"The IG / FB Page channel to read.","title":"Channel Id"},"description":"The IG / FB Page channel to read."}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AccountInsightsResponse"}}}},"400":{"description":"Channel is not IG/FB, or its config is incomplete."},"403":{"description":"`social_comments_beta` not enabled."},"404":{"description":"Channel not found in this workspace."},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/social-posts/{post_id}/insights":{"get":{"tags":["Social Posts"],"summary":"Cached insights for one authored post's published targets","description":"Return insights for every PUBLISHED target of one authored post, served from a\nper-target cache. Each target carries its own `last_refreshed_at`; the top-level\n`last_refreshed_at` is the OLDEST of those (the staleness of the stalest number).\n\nNumbers are NOT re-fetched on every load. A target that has never been cached\n(`last_refreshed_at: null`) is populated LIVE once, on this first read, and\npersisted so the next load is served from cache. Use\n`POST /{post_id}/insights/refresh` to force a fresh read of all targets.\n\nA target whose page lacks insights access degrades to `authorized: false` +\n`error_code: insights_not_authorized`; a target whose channel was deleted\ndegrades to `insights_unavailable`; a target that never published (no\n`meta_post_id`) is omitted.\n\n**Required Scopes:** `campaigns:read`","operationId":"get_post_insights_api_v1_social_posts__post_id__insights_get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"post_id","in":"path","required":true,"schema":{"type":"string","description":"The authored post id.","title":"Post Id"},"description":"The authored post id."}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PostInsightsResponse"}}}},"403":{"description":"`social_comments_beta` not enabled."},"404":{"description":"Post not found (`post_not_found`)."},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/social-posts/{post_id}/insights/refresh":{"post":{"tags":["Social Posts"],"summary":"Force-refresh cached insights for one authored post's targets","description":"Force a LIVE re-fetch of Meta insights for every PUBLISHED target of one authored\npost, UPSERT the per-target cache, and return the freshly-cached\n`PostInsightsResponse`. Every readable target's `last_refreshed_at` is stamped to\nnow; the top-level `last_refreshed_at` is the oldest of the returned targets.\n\nDegrade behavior matches `GET /{post_id}/insights` (unauthorized page →\n`insights_not_authorized`, deleted channel → `insights_unavailable`, and the\ndegraded state is persisted too). A target that never published is omitted.\n\n**Required Scopes:** `campaigns:read`","operationId":"refresh_post_insights_api_v1_social_posts__post_id__insights_refresh_post","security":[{"HTTPBearer":[]}],"parameters":[{"name":"post_id","in":"path","required":true,"schema":{"type":"string","description":"The authored post id.","title":"Post Id"},"description":"The authored post id."}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PostInsightsResponse"}}}},"403":{"description":"`social_comments_beta` not enabled."},"404":{"description":"Post not found (`post_not_found`)."},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/channels/{channel_id}/rcs/capabilities":{"get":{"tags":["Channels"],"summary":"Check Rcs Capabilities","description":"Check whether one number can receive RCS from this agent.\n\n**Required Scopes:** `channels:read`\n\nAlso requires the tenant's `rcs_enabled` feature flag (403 otherwise).\n\nReturns **200 with `reachable: false`** for every kind of unreachable —\nthis is an answer, not an error. Google answers 404 whether the device has\nno RCS, has RCS switched off, sits on a carrier the agent is not launched\non, or belongs to a tester who has not accepted the invite, and it cannot\ntell those apart, so neither can we.\n\nSide effect: a reachable probe promotes a `pending` tester on this channel\nto `accepted` and stamps `accepted_at`. It is the only acceptance signal\nGoogle provides.","operationId":"check_rcs_capabilities_api_v1_channels__channel_id__rcs_capabilities_get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"channel_id","in":"path","required":true,"schema":{"type":"string","description":"RCS channel id","title":"Channel Id"},"description":"RCS channel id"},{"name":"msisdn","in":"query","required":true,"schema":{"type":"string","minLength":4,"maxLength":32,"description":"Number to probe","title":"Msisdn"},"description":"Number to probe"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RcsCapabilityResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/channels/health":{"get":{"tags":["Channels"],"summary":"Get Channel Health","description":"Get health status of all channel integrations.\n\nReturns the connection status of all active channel integrations,\nhighlighting any that have errors that need attention.\n\n**Required Scopes:** `channels:read`\n\nCurrently supports:\n- Email integrations (Gmail, SMTP/IMAP, etc.)\n- Messaging channels (Messenger / Instagram) whose access token was\n  invalidated (Graph API code=190) and need reconnecting. These appear\n  with `channel_type` = the messaging type (\"messenger\", \"instagram\", ...),\n  `connection_status` = \"disconnected\", `last_error` = the\n  disconnection_reason (\"token_invalidated\"), and `last_error_at` = when\n  the channel was disconnected.\n\nFuture support planned for:\n- WhatsApp Business API connections\n- Telegram bot connections","operationId":"get_channel_health_api_v1_channels_health_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ChannelHealthResponse"}}}}},"security":[{"HTTPBearer":[]}]}},"/api/v1/channels/types":{"get":{"tags":["Channels"],"summary":"Get Channel Types","description":"Get available channel types.\n\nReturns a list of available (implemented) channel types,\ncoming soon channel types, and detailed info for all types.\n\nThis endpoint is public (no auth required) as it's used during\nchannel setup and onboarding flows.","operationId":"get_channel_types_api_v1_channels_types_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ChannelTypesResponse"}}}}}}},"/api/v1/channels":{"post":{"tags":["Channels"],"summary":"Create Channel","description":"Connect a new messaging channel\n\n**Required Scopes:** `channels:create`\n\nRCS channels cannot be created here: SendSeven sets up and manages RCS\nagents (403 `rcs_managed_by_sendseven`).","operationId":"create_channel_api_v1_channels_post","security":[{"HTTPBearer":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ChannelCreate"}}}},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ChannelResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"get":{"tags":["Channels"],"summary":"List Channels","description":"List all active (non-archived) channels for the tenant.\n\nAny authenticated team member can list channels (used for app initialization).\n\nSecurity model (inverted May 2026):\n    Non-admin/non-owner callers ALWAYS see only the channels reachable\n    through their accessible Custom Inboxes -- regardless of the\n    ``for_compose`` query param. Previously this filter was opt-in via\n    ``for_compose=true``, which meant a restricted user could drop the\n    param and see every channel in the tenant (UX leak / info disclosure\n    -- they still couldn't send, but channel names appeared in any\n    listing/picker). The filter is now mandatory for non-admins.\n\n    ``for_compose`` is still accepted (and ignored for the security path)\n    for API back-compat. Admins/owners always see every channel.\n\n    If ``accessible_channel_ids_for_user`` returns ``None`` (zero-inbox\n    tenant fallback for the Custom Inboxes feature), no extra filter is\n    applied -- preserving pre-feature behaviour for tenants that have\n    never configured inboxes.\n\n**🔐 Required Scopes:** `channels:read`\n","operationId":"list_channels_api_v1_channels_get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"for_compose","in":"query","required":false,"schema":{"type":"boolean","default":false,"title":"For Compose"}},{"name":"channel_type","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Optional: filter results to a single channel_type (e.g. 'whatsapp')","title":"Channel Type"},"description":"Optional: filter results to a single channel_type (e.g. 'whatsapp')"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ChannelResponse"},"title":"Response List Channels Api V1 Channels Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/channels/comment-capabilities":{"get":{"tags":["Channels"],"summary":"Get Comment Capabilities","description":"Can these channels receive comments? (\"Comments -> Private Reply\" beta)\n\n**Required Scopes:** `channels:read`\n\nReports, for every non-archived Instagram and Facebook Messenger channel,\nwhether comment webhooks would actually arrive today — so the UI can show a\n\"reconnect to enable comments\" prompt instead of offering a feature that\nwould silently never fire.\n\n- **Instagram** needs the `instagram_business_manage_comments` permission,\n  which is fixed at connect time and can only be added by reconnecting.\n- **Facebook Messenger** needs the Page subscribed to the `feed` webhook\n  field. This is read LIVE from Facebook on every call, because the\n  subscription can change without any change to the channel.\n\n**Fail-closed:** `can_receive_comments` is `true` only when positively\nverified. A channel connected before we recorded granted permissions\nreports `false` / `scope_granted_unknown`, never an assumed `true`.\n\n`beta_enabled` reports the tenant's feature-flag enrolment separately, so\n\"not in the beta\" and \"in the beta but needs a reconnect\" are\ndistinguishable. Read-only — nothing is subscribed or changed.","operationId":"get_comment_capabilities_api_v1_channels_comment_capabilities_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ChannelCommentCapabilitiesResponse"}}}}},"security":[{"HTTPBearer":[]}]}},"/api/v1/channels/sender-actions":{"get":{"tags":["Channels"],"summary":"List read-receipt and typing-indicator support per channel type","description":"Which channel types support read receipts and typing indicators.\n\nReturns one item for **every** channel type (unsupported ones report\n`false`), so you can decide before calling\n`POST /messages/{message_id}/read` or\n`POST /conversations/{conversation_id}/typing`.\n\n- `read`: the contact can be shown that their message was read.\n- `typing`: the contact can be shown a typing indicator.\n- `typing_off`: the indicator can be cleared explicitly (otherwise\n  `state: \"off\"` is a no-op and the indicator ends on its own).\n- `typing_ttl_seconds`: how long one typing call is shown; `null` means\n  until cleared or until you reply.\n- `typing_requires_message`: the platform ties typing to a received\n  message (the latest inbound message is used when you pass none).\n\nFor a single connected channel, the same block is returned as\n`sender_actions` on `GET /channels/{channel_id}/capabilities`.\n\n**Required Scopes:** `channels:read`","operationId":"list_sender_action_support_api_v1_channels_sender_actions_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SenderActionSupportListResponse"}}}}},"security":[{"HTTPBearer":[]}]}},"/api/v1/channels/{channel_id}":{"get":{"tags":["Channels"],"summary":"Get Channel","description":"Get a specific channel\n\n**Required Scopes:** `channels:read`","operationId":"get_channel_api_v1_channels__channel_id__get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"channel_id","in":"path","required":true,"schema":{"type":"string","title":"Channel Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ChannelResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"put":{"tags":["Channels"],"summary":"Update Channel","description":"Update a channel configuration\n\n**Required Scopes:** `channels:update`\n\nOn an RCS channel only `name` and `is_active` may change here; an\n`identifier` or `config` change answers 403 `rcs_managed_by_sendseven`\n(SendSeven manages RCS agents and their credentials).","operationId":"update_channel_api_v1_channels__channel_id__put","security":[{"HTTPBearer":[]}],"parameters":[{"name":"channel_id","in":"path","required":true,"schema":{"type":"string","title":"Channel Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ChannelUpdate"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ChannelResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"tags":["Channels"],"summary":"Delete Channel","description":"Archive (soft-delete) a channel.\n\nThis preserves the channel record for billing history and audit purposes while:\n- Marking it as archived (hidden from normal listings)\n- Clearing sensitive credentials from config\n- Recording who archived it and when\n\nFor WhatsApp channels, the phone number is also deregistered from Meta.\nFor Messenger/Instagram channels, the Facebook Page is unsubscribed from\nour app so the asset can be cleanly re-connected later.\n\n**Required Scopes:** `channels:delete`\n\n**Returns:**\n- `success`: Boolean indicating if archive succeeded\n- `message`: Status message\n- `channel_id`: The archived channel's ID","operationId":"delete_channel_api_v1_channels__channel_id__delete","security":[{"HTTPBearer":[]}],"parameters":[{"name":"channel_id","in":"path","required":true,"schema":{"type":"string","title":"Channel Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ArchiveResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/channels/{channel_id}/capabilities":{"get":{"tags":["Channels"],"summary":"Get Channel Capabilities","description":"Get the feature capabilities matrix for a specific channel.\n\nReturns what message types and interactive elements are supported\nby the channel's messaging platform.\n\n**Required Scopes:** `channels:read`\n\n**Feature Categories:**\n- **Basic message types:** text, images, videos, audio, voice (PTT), documents, stickers\n- **Rich content:** location, contacts\n- **Engagement:** reactions\n- **Interactive elements:** interactive_buttons, interactive_lists, carousels, quick_replies\n- **Other:** templates (pre-approved message templates), proxy (proxy/passthrough support)\n\n**Response format:**\n- Boolean fields indicate simple support (true/false)\n- Object fields (e.g., interactive_buttons) include limits like max_buttons","operationId":"get_channel_capabilities_api_v1_channels__channel_id__capabilities_get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"channel_id","in":"path","required":true,"schema":{"type":"string","title":"Channel Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ChannelCapabilities"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/channels/{channel_id}/comment-capability":{"get":{"tags":["Channels"],"summary":"Get Channel Comment Capability","description":"Can THIS channel receive comments? (\"Comments -> Private Reply\" beta)\n\n**Required Scopes:** `channels:read`\n\nSingle-channel form of `GET /channels/comment-capabilities` — same\nfail-closed contract and the same `reason` codes. A channel of a type that\ncan never do comments returns `comments_supported: false` with reason\n`channel_type_not_supported` rather than an error.\n\nRead-only: nothing is subscribed, changed or repaired.","operationId":"get_channel_comment_capability_api_v1_channels__channel_id__comment_capability_get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"channel_id","in":"path","required":true,"schema":{"type":"string","title":"Channel Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ChannelCommentCapability"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/channels/{channel_id}/file-support":{"get":{"tags":["Channels"],"summary":"Get Channel File Support","description":"Get the supported file formats and size limits for a specific channel.\n\nReturns which file types (extensions) and categories (image, video, audio, document)\nare supported by the channel, along with maximum file size per category.\n\nUseful for client-side validation before uploading/sending attachments.\n\n**Required Scopes:** `channels:read`","operationId":"get_channel_file_support_api_v1_channels__channel_id__file_support_get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"channel_id","in":"path","required":true,"schema":{"type":"string","title":"Channel Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/channels/{channel_id}/refresh-webhook":{"post":{"tags":["Channels"],"summary":"Refresh Channel Webhook","description":"Re-register the webhook for a channel.\n\nThis is useful when:\n- Webhook URL needs to be updated\n- Webhook registration failed initially\n- You want to verify the webhook is correctly set\n\nWebhooks are always registered to point to production (api.sendseven.com)\nto ensure proper message delivery and staging forwarding.\n\n**Required Scopes:** `channels:update`\n\n**Supported channels:** Telegram (other channels have webhooks configured externally)","operationId":"refresh_channel_webhook_api_v1_channels__channel_id__refresh_webhook_post","security":[{"HTTPBearer":[]}],"parameters":[{"name":"channel_id","in":"path","required":true,"schema":{"type":"string","title":"Channel Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookRefreshResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/channels/{channel_id}/profile":{"get":{"tags":["Channels"],"summary":"Get Channel Profile","description":"Fetch channel profile from platform API (WhatsApp or Telegram).\n\nThis endpoint fetches the current profile data directly from the\nmessaging platform's API. No data is stored locally.\n\n**Required Scopes:** `channels:read`\n\n**Supported Channels:**\n- **WhatsApp:** Returns business profile (about, address, description, email,\n  profile picture, websites, vertical)\n- **Telegram:** Returns bot profile (name, username, description,\n  short description, commands, permissions)\n\n**Response:**\n- `channel_id`: The channel's ID\n- `channel_type`: \"whatsapp\" or \"telegram\"\n- `channel_name`: The channel's display name\n- `whatsapp`: WhatsApp-specific profile data (if applicable)\n- `telegram`: Telegram-specific profile data (if applicable)","operationId":"get_channel_profile_api_v1_channels__channel_id__profile_get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"channel_id","in":"path","required":true,"schema":{"type":"string","title":"Channel Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ChannelProfileResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"put":{"tags":["Channels"],"summary":"Update Channel Profile","description":"Update channel profile on platform API.\n\nThis endpoint updates the profile data directly on the messaging\nplatform's API. Only provided fields are updated.\n\n**Required Scopes:** `channels:update`\n\n**Supported Channels:**\n\n**WhatsApp Profile Fields:**\n- `about`: Short description (max 139 chars, shown in profile)\n- `address`: Business address (max 256 chars)\n- `description`: Full description (max 512 chars)\n- `email`: Business contact email\n- `websites`: List of website URLs (max 2)\n- `vertical`: Business category (UNDEFINED, OTHER, AUTO, BEAUTY, APPAREL,\n  EDU, ENTERTAIN, EVENT_PLAN, FINANCE, GROCERY, GOVT, HOTEL, HEALTH,\n  NONPROFIT, PROF_SERVICES, RETAIL, TRAVEL, RESTAURANT, NOT_A_BIZ)\n\n**Telegram Profile Fields:**\n- `name`: Bot display name (max 64 chars)\n- `description`: Full description shown on profile page (max 512 chars)\n- `short_description`: Short description shown in chat list (max 120 chars)\n- `commands`: List of bot commands, each with `command` and `description`\n\n**Returns:**\n- `success`: Boolean indicating if update succeeded\n- `message`: Success or error message\n- `updated_fields`: List of fields that were updated\n- `errors`: List of errors if some fields failed (partial success)","operationId":"update_channel_profile_api_v1_channels__channel_id__profile_put","security":[{"HTTPBearer":[]}],"parameters":[{"name":"channel_id","in":"path","required":true,"schema":{"type":"string","title":"Channel Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ChannelProfileUpdateRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ChannelProfileUpdateResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/channels/{channel_id}/profile/photo":{"post":{"tags":["Channels"],"summary":"Upload Channel Profile Photo","description":"Upload a profile photo for a WhatsApp channel.\n\nThis endpoint uploads a new profile picture directly to the WhatsApp\nBusiness Account via Meta's API.\n\n**Required Scopes:** `channels:update`\n\n**Supported Channels:** WhatsApp only (Telegram uses @BotFather for profile photos)\n\n**Image Requirements:**\n- Format: JPEG or PNG\n- Aspect ratio: Square (1:1) recommended\n- Size: 192x192 to 640x640 pixels\n- Maximum file size: 5MB\n\n**Request:**\n- Content-Type: multipart/form-data\n- Field name: `file`\n\n**Returns:**\n- `success`: Boolean indicating if upload succeeded\n- `message`: Success or error message\n- `profile_picture_url`: New profile picture URL if successful","operationId":"upload_channel_profile_photo_api_v1_channels__channel_id__profile_photo_post","security":[{"HTTPBearer":[]}],"parameters":[{"name":"channel_id","in":"path","required":true,"schema":{"type":"string","title":"Channel Id"}}],"requestBody":{"required":true,"content":{"multipart/form-data":{"schema":{"$ref":"#/components/schemas/Body_upload_channel_profile_photo_api_v1_channels__channel_id__profile_photo_post"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProfilePhotoUploadResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/channels/{channel_id}/whatsapp/username":{"get":{"tags":["Channels"],"summary":"Get Whatsapp Username","description":"Get the WhatsApp business username claimed by this channel's phone number.\n\nA username lets customers reach the business through `wa.me/<username>`\nwithout knowing its phone number. It belongs to the PHONE NUMBER, so each\nWhatsApp channel has at most one.\n\n**Required Scopes:** `channels:read`\n\n**Response:**\n- `username`: the current handle, or `null` when none is claimed\n- `status`: `approved` (live) or `reserved` (accepted, still propagating)\n- `wa_me_url`: short link to share with customers\n- `suggestions`: available handles suggested by WhatsApp (best-effort — an\n  empty list simply means WhatsApp returned none)","operationId":"get_whatsapp_username_api_v1_channels__channel_id__whatsapp_username_get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"channel_id","in":"path","required":true,"schema":{"type":"string","title":"Channel Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WhatsAppUsernameResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"post":{"tags":["Channels"],"summary":"Set Whatsapp Username","description":"Claim or change the WhatsApp business username for this channel's number.\n\n**Required Scopes:** `channels:write`\n\n**Format:** 3-35 characters of lowercase letters, numbers, periods and\nunderscores; at least one letter; no leading/trailing period, no `..`, no\n`www` prefix, no domain-like suffix. Periods and underscores are distinct.\n\n**Transfers:** if the handle is already held by another phone number in the\nsame Meta business portfolio, WhatsApp answers with error `147005`. Repeat\nthe request with `force_transfer: true` to move it to this number.\n\n**Status:** `approved` means the username is live immediately; `reserved`\nmeans WhatsApp accepted it and will confirm through the\n`business_username_updates` webhook shortly.","operationId":"set_whatsapp_username_api_v1_channels__channel_id__whatsapp_username_post","security":[{"HTTPBearer":[]}],"parameters":[{"name":"channel_id","in":"path","required":true,"schema":{"type":"string","title":"Channel Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WhatsAppUsernameUpdateRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WhatsAppUsernameUpdateResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"tags":["Channels"],"summary":"Delete Whatsapp Username","description":"Release the WhatsApp business username held by this channel's number.\n\n**Required Scopes:** `channels:write`\n\nWhatsApp does not return a released handle to the pool immediately, so\nsomeone else may not be able to claim it right away — and neither can you.\nTreat this as destructive.","operationId":"delete_whatsapp_username_api_v1_channels__channel_id__whatsapp_username_delete","security":[{"HTTPBearer":[]}],"parameters":[{"name":"channel_id","in":"path","required":true,"schema":{"type":"string","title":"Channel Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WhatsAppUsernameDeleteResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/channels/{channel_id}/instagram/media":{"get":{"tags":["Channels"],"summary":"List Instagram Media","description":"List media (posts, reels, carousels) for an Instagram channel.\n\nUsed by the ``comment_received`` flow-trigger config UI to let the user\npick specific posts to monitor for comments.\n\n**Required Scopes:** ``channels:read``\n\n**Pagination:**\n- Cursor-based via Meta's ``after`` parameter.\n- ``limit`` defaults to 25, max 50 (Meta enforces an upper bound around this).\n- Response includes ``pagination.next_cursor`` (null on the last page).\n\n**Errors:**\n- 404 — channel not found in this tenant.\n- 400 — channel exists but is not Instagram, or config is missing required fields.\n- 401 — Meta access token is expired / revoked (``error_code: \"token_expired\"``).\n- 429 — Meta rate limit exceeded.\n- 502 — Meta returned an unexpected error (full payload logged server-side, not exposed).","operationId":"list_instagram_media_api_v1_channels__channel_id__instagram_media_get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"channel_id","in":"path","required":true,"schema":{"type":"string","title":"Channel Id"}},{"name":"after","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Meta Graph API pagination cursor (opaque).","title":"After"},"description":"Meta Graph API pagination cursor (opaque)."},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":50,"minimum":1,"description":"Number of items per page (1-50).","default":25,"title":"Limit"},"description":"Number of items per page (1-50)."}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InstagramMediaListResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/channels/{channel_id}/facebook/posts":{"get":{"tags":["Channels"],"summary":"List Facebook Page Posts","description":"List published posts for a Facebook Page (Messenger) channel.\n\nUsed by the comment auto-reply rules UI and the ``comment_received``\nflow-trigger config to let the user pick specific posts to watch.\n\nReads ``/{page_id}/published_posts`` rather than ``/feed``: ``feed``\nalso returns posts made BY OTHERS on the Page and scheduled/unpublished\ndrafts, none of which a customer can meaningfully pick as \"a post of mine\nto auto-reply on\".\n\n**Required Scopes:** ``channels:read``\n\n**Pagination:**\n- Cursor-based via Meta's ``after`` parameter.\n- ``limit`` defaults to 25, max 50.\n- Response includes ``pagination.next_cursor`` (null on the last page).\n\n**Errors:**\n- 404 — channel not found in this tenant.\n- 400 — channel exists but is not a Messenger channel, or config is missing required fields.\n- 401 — Meta access token is expired / revoked (``error_code: \"token_expired\"``).\n- 429 — Meta rate limit exceeded.\n- 502 — Meta returned an unexpected error (full payload logged server-side, not exposed).","operationId":"list_facebook_page_posts_api_v1_channels__channel_id__facebook_posts_get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"channel_id","in":"path","required":true,"schema":{"type":"string","title":"Channel Id"}},{"name":"after","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Meta Graph API pagination cursor (opaque).","title":"After"},"description":"Meta Graph API pagination cursor (opaque)."},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":50,"minimum":1,"description":"Number of items per page (1-50).","default":25,"title":"Limit"},"description":"Number of items per page (1-50)."}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FacebookPagePostListResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/whatsapp-catalog/{channel_id}/status":{"get":{"tags":["WhatsApp Catalogue"],"summary":"Get Catalogue Status","description":"Catalogue status for a WhatsApp channel.\n\nUngated so the frontend can read ``beta_enabled`` to decide whether to show\nthe catalogue UI.\n\n**Required Scopes:** `channels:read`","operationId":"get_catalogue_status_api_v1_whatsapp_catalog__channel_id__status_get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"channel_id","in":"path","required":true,"schema":{"type":"string","title":"Channel Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CatalogueStatusResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/whatsapp-catalog/connect":{"post":{"tags":["WhatsApp Catalogue"],"summary":"Connect Catalogue","description":"Connect a Meta product catalogue to a WhatsApp channel.\n\nConsumes the catalogue token/code from the FBLB dialog, resolves the\ncatalogue id from the token's ``catalog_management`` granular scope, and\npersists the (encrypted) token + catalogue id on the channel.\n\n**Required Scopes:** `channels:update`","operationId":"connect_catalogue_api_v1_whatsapp_catalog_connect_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CatalogueConnectRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CatalogueConnectResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"HTTPBearer":[]}]}},"/api/v1/whatsapp-catalog/{channel_id}/sync":{"post":{"tags":["WhatsApp Catalogue"],"summary":"Sync Catalogue","description":"Re-sync products from Meta into our store.\n\n**Required Scopes:** `channels:update`","operationId":"sync_catalogue_api_v1_whatsapp_catalog__channel_id__sync_post","security":[{"HTTPBearer":[]}],"parameters":[{"name":"channel_id","in":"path","required":true,"schema":{"type":"string","title":"Channel Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CatalogueSyncResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/whatsapp-catalog/{channel_id}/products":{"get":{"tags":["WhatsApp Catalogue"],"summary":"List Catalogue Products","description":"List / search a channel's catalogue products (poll-on-open freshness).\n\n**Required Scopes:** `channels:read`","operationId":"list_catalogue_products_api_v1_whatsapp_catalog__channel_id__products_get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"channel_id","in":"path","required":true,"schema":{"type":"string","title":"Channel Id"}},{"name":"search","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Search"}},{"name":"page","in":"query","required":false,"schema":{"type":"integer","minimum":1,"default":1,"title":"Page"}},{"name":"page_size","in":"query","required":false,"schema":{"type":"integer","maximum":100,"minimum":1,"default":25,"title":"Page Size"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CatalogueProductListResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/whatsapp-catalog/{channel_id}":{"delete":{"tags":["WhatsApp Catalogue"],"summary":"Disconnect Catalogue","description":"Disconnect the catalogue: clear the id + token and delete mirrored products.\n\n**Required Scopes:** `channels:delete`","operationId":"disconnect_catalogue_api_v1_whatsapp_catalog__channel_id__delete","security":[{"HTTPBearer":[]}],"parameters":[{"name":"channel_id","in":"path","required":true,"schema":{"type":"string","title":"Channel Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CatalogueDisconnectResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/whatsapp-catalog/send-product":{"post":{"tags":["WhatsApp Catalogue"],"summary":"Send Single Product","description":"Send a Single Product Message into a conversation.\n\nFail-closed on Coexistence numbers (409). Enqueues onto the messages-out\ntopic for delivery.\n\n**Required Scopes:** `messages:create`","operationId":"send_single_product_api_v1_whatsapp_catalog_send_product_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SendSingleProductRequest"}}},"required":true},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MessageResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"HTTPBearer":[]}]}},"/api/v1/whatsapp-catalog/send-product-list":{"post":{"tags":["WhatsApp Catalogue"],"summary":"Send Product List","description":"Send a Multi-Product Message into a conversation.\n\nFail-closed on Coexistence numbers (409).\n\n**Required Scopes:** `messages:create`","operationId":"send_product_list_api_v1_whatsapp_catalog_send_product_list_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SendMultiProductRequest"}}},"required":true},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MessageResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"HTTPBearer":[]}]}},"/api/v1/contacts/bulk-actions/preview":{"post":{"tags":["Contact Bulk Actions"],"summary":"Preview Bulk Action","description":"Preview a contact bulk action (mass edit) — no changes are made.\n\n**Required Scopes:** `contacts:update`\n\nValidates the selection and every action against the tenant (tags/list/\ncustom field existence + value/type checks, `set_field` allowlist),\ncomputes how many contacts the selection currently matches and returns a\nsigned `confirmation_token`. Submit the IDENTICAL `selection` + `actions`\ntogether with that token to `/contacts/bulk-actions/execute` within\n`expires_in_seconds` (~10 minutes).\n\nSelection modes:\n- `{\"mode\": \"ids\", \"ids\": [...]}` — explicit contact ids (max 10000).\n- `{\"mode\": \"filter\", \"filter\": {...}}` — same params as `GET /contacts`\n  (`search`, `include_live_chat`, `custom_field_id/value/value_to/op`,\n  `list_id` + `list_subscription_status`/`list_subscription_channel`,\n  `is_blocked`); `{}` targets ALL contacts.\n\nAction types (executed in order per contact): `set_field`,\n`set_custom_field`, `add_tags`, `remove_tags`, `add_to_list`,\n`set_blocked` (`{\"type\": \"set_blocked\", \"blocked\": true|false}` — blocks or\nunblocks the contact and all of its identifiers on every channel; can be\nmixed with the other actions).","operationId":"preview_bulk_action_api_v1_contacts_bulk_actions_preview_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BulkActionPreviewRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BulkActionPreviewResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"HTTPBearer":[]}]}},"/api/v1/contacts/bulk-actions/execute":{"post":{"tags":["Contact Bulk Actions"],"summary":"Execute Bulk Action","description":"Execute a previously previewed contact bulk action (202 Accepted).\n\n**Required Scopes:** `contacts:update`\n\nRequires the `confirmation_token` from `/contacts/bulk-actions/preview`\ntogether with the IDENTICAL `selection` + `actions`. Errors:\n\n- **400** `{\"detail\": {\"code\": \"TOKEN_INVALID\" | \"TOKEN_EXPIRED\", ...}}` —\n  malformed/forged/expired token, or token minted for another tenant/user.\n- **409** `{\"detail\": {\"code\": \"PAYLOAD_MISMATCH\", ...}}` — the submitted\n  selection/actions differ from what was previewed.\n- **409** `{\"detail\": {\"code\": \"AFFECTED_COUNT_DRIFT\", \"affected_count\": N,\n  ...}}` — the match count changed since preview (any difference when the\n  previewed count was < 100, otherwise more than ±2%). Re-preview and\n  confirm again with the new count.\n\nOn success the run is recorded as a `BulkActionLog` audit row and executed\nas a chunked background job; poll `poll_url` until `status` is\n`completed`/`failed`. `affected_count` on the poll response is the running\ntotal of actually-changed contacts.","operationId":"execute_bulk_action_api_v1_contacts_bulk_actions_execute_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BulkActionExecuteRequest"}}},"required":true},"responses":{"202":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BulkActionExecuteResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"HTTPBearer":[]}]}},"/api/v1/contacts/bulk-actions":{"get":{"tags":["Contact Bulk Actions"],"summary":"List Bulk Actions","description":"List this tenant's contact bulk action runs, newest first (audit history).\n\n**Required Scopes:** `contacts:update`\n\nReturns the standard `{\"items\": [...], \"pagination\": {...}}` envelope;\neach item has the same shape as `GET /contacts/bulk-actions/{id}`.","operationId":"list_bulk_actions_api_v1_contacts_bulk_actions_get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"page","in":"query","required":false,"schema":{"type":"integer","minimum":1,"description":"Page number (1-indexed)","default":1,"title":"Page"},"description":"Page number (1-indexed)"},{"name":"page_size","in":"query","required":false,"schema":{"type":"integer","maximum":100,"minimum":1,"description":"Items per page (max 100)","default":20,"title":"Page Size"},"description":"Items per page (max 100)"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/contacts/bulk-actions/{bulk_action_id}":{"get":{"tags":["Contact Bulk Actions"],"summary":"Get Bulk Action","description":"Poll one contact bulk action run (status + audit detail).\n\n**Required Scopes:** `contacts:update`\n\n`status`: `pending` → `processing` → `completed` | `failed`.\n`affected_count` is the running total of actually-changed contacts\n(committed per chunk) and the final total once completed. `error` carries\nthe failure reason (`failed`) or a truncated summary of individually\nskipped contacts (`completed`).","operationId":"get_bulk_action_api_v1_contacts_bulk_actions__bulk_action_id__get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"bulk_action_id","in":"path","required":true,"schema":{"type":"string","title":"Bulk Action Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BulkActionStatusResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/contacts":{"post":{"tags":["Contacts"],"summary":"Create Contact","description":"Create a new contact.\n\nHonors the tenant's `duplicate_contact_mode` setting (or the optional\n`duplicate_mode` body field for a per-request override):\n\n- `auto_merge` (default): if a contact already exists with the same phone,\n  email, or platform identifier, return that contact (oldest wins as\n  primary). Incoming new methods/fields are merged into it.\n- `allow_duplicates`: always create a new contact. The response includes a\n  `potential_duplicates` list with any contacts sharing identifiers.\n- `dont_allow_duplicates`: returns HTTP 409 with a `duplicates` array if\n  a duplicate is detected; nothing is created.\n\n`custom_fields` (optional): typed custom field values keyed by custom\nfield definition ID (preferred) or field `key`. Validated BEFORE anything\nis written (422 per-field errors, see PATCH /contacts/{id}); written in the\nsame transaction as the contact. In `auto_merge`, when an existing contact\nis matched, the given values OVERWRITE its values only if the caller also\nholds `contacts:update`; otherwise they only fill fields that are empty\n(fields that already have a value are skipped). null/empty entries are\nignored on create.\n\n**Required Scopes:** `contacts:create`","operationId":"create_contact_api_v1_contacts_post","security":[{"HTTPBearer":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ContactCreate"}}}},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ContactResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"get":{"tags":["Contacts"],"summary":"List Contacts","description":"List contacts with pagination, search, server-side sorting, custom-field\nand list-subscription filtering.\n\n**Required Scopes:** `contacts:read`\n\nReturns a paginated list of contacts matching the search criteria.\nEach contact includes tags and custom fields.\n\n**Sorting** (full-dataset, applied in SQL before pagination):\n- `sort_by`: `created_at` (default), `name`, `email`, `custom_field`. Unknown\n  values return 422.\n- `sort_dir`: `asc` or `desc` (default `desc`).\n- `sort_by=custom_field` requires `custom_field_id`. Contacts without a value\n  for the field still appear (LEFT JOIN); NULLs sort first on asc, last on desc.\n\n**Custom-field filter** (combinable with `search`):\nThe match strategy is selected SERVER-SIDE from the field's `field_type`\n(looked up from `custom_field_definitions`), NOT from a client-trusted\noperator. `custom_field_op` may narrow/override within what the type allows.\n\n- `custom_field_id` alone → contacts that have ANY value for that field.\n- **text** (and email/phone/url) → CONTAINS match (`value LIKE %v%`, with `%`\n  and `_` in the input escaped to literals). This is the default for these\n  types. (Behavior change: previously exact-match.)\n- **select** → EXACT match; the supplied value must be one of the field's\n  configured `options` (422 if not).\n- **number** → operator-based via `custom_field_op`:\n  `eq` (default), `gt`, `lt`, `gte`, `lte`, `between`. For `between`,\n  `custom_field_value` is the lower bound and `custom_field_value_to` the\n  (inclusive) upper bound; lower must be <= upper. Values must parse as\n  numbers. Comparisons are numeric (TEXT values are regex-guarded then cast\n  to DECIMAL, so non-numeric rows never match and never error).\n- **other types** (boolean/date/datetime/location/multiselect/collection) →\n  EXACT match (unchanged, backwards compatible).\n\n`custom_field_op` allowlist: `eq`, `contains`, `gt`, `lt`, `gte`, `lte`,\n`between`. Anything else is rejected by FastAPI (422). Numeric operators are\nonly valid for `number` fields and `contains` only for text-like fields;\nincompatible combinations return 422.\n\nThe `custom_field_id` always validates the field belongs to the tenant\n(422 if not).\n\n**List-subscription filter** (combinable with `search` and the custom-field\nfilter — all filters are AND-ed, and `total` always reflects the filtered\nset):\n\n- `list_id` → a `lists.id`; validated to belong to the tenant (422 if not).\n- `list_subscription_status` (default `subscribed` when `list_id` is set):\n  - `subscribed` | `pending` | `unsubscribed` | `bounced` | `complained` →\n    the contact has a subscription row with EXACTLY that status.\n  - `any` → the contact has a subscription row for the list in ANY status.\n  - `not_subscribed` → the contact has NO `subscribed` row for the list\n    (i.e. not currently subscribed; includes contacts with no row at all).\n- `list_subscription_channel` → optional narrowing on the subscription's\n  channel (`email`, `sms`, `whatsapp`, `telegram`, `messenger`,\n  `instagram`, `browser_push`). Subscription rows are PER CHANNEL, so this\n  answers \"subscribed to list X via WhatsApp\". Applied inside the\n  EXISTS/NOT EXISTS.\n- Supplying `list_subscription_status` or `list_subscription_channel`\n  WITHOUT `list_id` returns 422.\n\nContacts are matched with a correlated `EXISTS`/`NOT EXISTS`, so a contact\nsubscribed to the same list via several channels is returned ONCE.\nFor a DYNAMIC list (membership computed from segment conditions, no\nsubscription rows) the list's conditions are evaluated instead:\n`subscribed`/`any` = matches the conditions, `not_subscribed` = does not;\nthe row-level statuses (`pending`/`unsubscribed`/`bounced`/`complained`)\ncannot exist and match nothing.\n\n**Tag-membership filter** (combinable with `search` and every other filter —\nall AND-ed):\n\n- `tag_id` → a `tags.id`; validated to belong to the tenant (422 if not).\n  Restricts to contacts that carry that tag via the `contact_tags`\n  association.\n- `tag_id` is REPEATABLE (`?tag_id=a&tag_id=b`): several ids match contacts\n  carrying ANY of them (OR / at-least-one). Duplicate ids are ignored.\n- This filters by tag **id** (stable), never by tag name. It is a SEPARATE\n  parameter from `search` on purpose: `search` matches name/email/phone/IDs\n  only and never matches tags, so \"find contacts by tag\" uses `tag_id` and\n  is not conflated with name search. When both are given, the text `search`\n  is applied WITHIN the set of contacts carrying the tag(s).\n\n**Blocked filter** (`is_blocked`, combinable with every other filter):\n`true` → only blocked contacts, `false` → only unblocked contacts. \"Blocked\"\nmeans the contact's own blocked flag (`is_blocked` on the item). The same\nparameter is accepted by `POST /contacts/export` and the bulk-actions filter\nselection. Every item also carries `blocked_inbound_count` and\n`last_blocked_inbound_at` (inbound messages dropped because of the block).\n\n**Optional extras** (`include`, comma-separated; unknown tokens → 422):\n\n- `subscriptions` → each item gets `subscriptions: [{list_id, list_name,\n  channel_type, status}]` — only `subscribed`/`pending` rows, sorted by\n  list name then channel; `[]` when none.\n- `conversation_stats` → each item gets `conversation_count` (0 when none)\n  and `last_conversation_at` (latest conversation activity, null when none).\n- Without `include` these fields are `null`. Each extra costs one batched\n  query per page. `contact_methods` is always present on every item.\n\n**Response format:**\n```json\n{\n    \"items\": [...],\n    \"pagination\": {\n        \"total\": 150,\n        \"page\": 1,\n        \"page_size\": 20,\n        \"total_pages\": 8,\n        \"has_next\": true,\n        \"has_prev\": false\n    }\n}\n```","operationId":"list_contacts_api_v1_contacts_get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"page","in":"query","required":false,"schema":{"type":"integer","minimum":1,"description":"Page number (1-indexed)","default":1,"title":"Page"},"description":"Page number (1-indexed)"},{"name":"page_size","in":"query","required":false,"schema":{"type":"integer","maximum":250,"minimum":1,"description":"Items per page (max 250)","default":20,"title":"Page Size"},"description":"Items per page (max 250)"},{"name":"search","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Search by name, phone, email, or IDs","title":"Search"},"description":"Search by name, phone, email, or IDs"},{"name":"include_live_chat","in":"query","required":false,"schema":{"type":"boolean","description":"Include live chat contacts","default":false,"title":"Include Live Chat"},"description":"Include live chat contacts"},{"name":"sort_by","in":"query","required":false,"schema":{"enum":["created_at","name","email","custom_field"],"type":"string","description":"Field to sort by. 'custom_field' requires custom_field_id.","default":"created_at","title":"Sort By"},"description":"Field to sort by. 'custom_field' requires custom_field_id."},{"name":"sort_dir","in":"query","required":false,"schema":{"enum":["asc","desc"],"type":"string","description":"Sort direction","default":"desc","title":"Sort Dir"},"description":"Sort direction"},{"name":"custom_field_id","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"custom_field_definitions.id used for filtering and/or as the sort target when sort_by='custom_field'.","title":"Custom Field Id"},"description":"custom_field_definitions.id used for filtering and/or as the sort target when sort_by='custom_field'."},{"name":"custom_field_value","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Value to filter by. Requires custom_field_id. The match strategy is chosen SERVER-SIDE from the field's field_type (text=contains, select=exact, number=operator-based). If custom_field_id is set without this, filters to contacts that have ANY value for that field. For op='between' this is the lower bound.","title":"Custom Field Value"},"description":"Value to filter by. Requires custom_field_id. The match strategy is chosen SERVER-SIDE from the field's field_type (text=contains, select=exact, number=operator-based). If custom_field_id is set without this, filters to contacts that have ANY value for that field. For op='between' this is the lower bound."},{"name":"custom_field_value_to","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Upper bound for the custom-field filter. Only used when custom_field_op='between' (number fields only); ignored otherwise.","title":"Custom Field Value To"},"description":"Upper bound for the custom-field filter. Only used when custom_field_op='between' (number fields only); ignored otherwise."},{"name":"custom_field_op","in":"query","required":false,"schema":{"anyOf":[{"enum":["eq","contains","gt","lt","gte","lte","between"],"type":"string"},{"type":"null"}],"description":"Optional comparison operator for the custom-field filter. If omitted, defaults are derived from the field_type (text->contains, select->eq, number->eq, others->eq). Numeric operators (gt/lt/gte/lte/between) are only valid for number fields; 'contains' is only valid for text-like fields. Incompatible combinations return 422.","title":"Custom Field Op"},"description":"Optional comparison operator for the custom-field filter. If omitted, defaults are derived from the field_type (text->contains, select->eq, number->eq, others->eq). Numeric operators (gt/lt/gte/lte/between) are only valid for number fields; 'contains' is only valid for text-like fields. Incompatible combinations return 422."},{"name":"list_id","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"lists.id — filter to contacts by their subscription to that list. Must belong to the tenant (422 if not).","title":"List Id"},"description":"lists.id — filter to contacts by their subscription to that list. Must belong to the tenant (422 if not)."},{"name":"list_subscription_status","in":"query","required":false,"schema":{"anyOf":[{"enum":["subscribed","pending","unsubscribed","bounced","complained","any","not_subscribed"],"type":"string"},{"type":"null"}],"description":"Subscription status to match for list_id. Defaults to 'subscribed'. 'any' = a subscription row in ANY status; 'not_subscribed' = NOT currently subscribed (includes contacts with no row at all). Requires list_id (422).","title":"List Subscription Status"},"description":"Subscription status to match for list_id. Defaults to 'subscribed'. 'any' = a subscription row in ANY status; 'not_subscribed' = NOT currently subscribed (includes contacts with no row at all). Requires list_id (422)."},{"name":"list_subscription_channel","in":"query","required":false,"schema":{"anyOf":[{"$ref":"#/components/schemas/SubscriptionChannelType"},{"type":"null"}],"description":"Optionally narrow the list filter to one subscription channel (subscription rows are per channel). Requires list_id (422).","title":"List Subscription Channel"},"description":"Optionally narrow the list filter to one subscription channel (subscription rows are per channel). Requires list_id (422)."},{"name":"tag_id","in":"query","required":false,"schema":{"anyOf":[{"type":"array","items":{"type":"string"}},{"type":"null"}],"description":"tags.id — filter to contacts carrying that tag. Repeatable (?tag_id=a&tag_id=b) to match contacts carrying ANY of the given tags (OR). Filters by tag id, NOT tag name; this is distinct from `search` (which never matches tags). Each id must belong to the tenant (422 if not). Combinable with `search` and the other filters (all AND-ed).","title":"Tag Id"},"description":"tags.id — filter to contacts carrying that tag. Repeatable (?tag_id=a&tag_id=b) to match contacts carrying ANY of the given tags (OR). Filters by tag id, NOT tag name; this is distinct from `search` (which never matches tags). Each id must belong to the tenant (422 if not). Combinable with `search` and the other filters (all AND-ed)."},{"name":"is_blocked","in":"query","required":false,"schema":{"anyOf":[{"type":"boolean"},{"type":"null"}],"description":"Blocked-state filter: true = only blocked contacts, false = only unblocked contacts, omitted = all. Uses the contact's blocked flag. Combinable with every other filter.","title":"Is Blocked"},"description":"Blocked-state filter: true = only blocked contacts, false = only unblocked contacts, omitted = all. Uses the contact's blocked flag. Combinable with every other filter."},{"name":"include","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Comma-separated optional extras to embed in each item: `subscriptions` (active list subscriptions), `conversation_stats` (conversation_count + last_conversation_at). Unknown tokens return 422.","title":"Include"},"description":"Comma-separated optional extras to embed in each item: `subscriptions` (active list subscriptions), `conversation_stats` (conversation_count + last_conversation_at). Unknown tokens return 422."}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ContactListResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/contacts/bulk-delete":{"post":{"tags":["Contacts"],"summary":"Bulk Delete Contacts","description":"Bulk delete multiple contacts and all their related data.\n\nAccepts up to 100 contact IDs. Each contact is deleted individually\nusing the same full-cascade GDPR deletion as single delete (conversations,\nmessages, attachments, email transcripts, live-chat sessions and flow runs\nare all removed). Each contact commits independently, so one failure does\nnot roll back the others.\n\n**Required Scopes:** `contacts:delete`","operationId":"bulk_delete_contacts_api_v1_contacts_bulk_delete_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BulkDeleteContactsRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BulkDeleteContactsResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"HTTPBearer":[]}]}},"/api/v1/contacts/duplicates":{"get":{"tags":["Contacts"],"summary":"List Duplicate Groups","description":"List groups of contacts sharing the same identifier (potential duplicates).\n\n**Required Scopes:** `contacts:read`\n\nReturns groups by signal value. Each group contains 2+ contacts that\nshare the signal, sorted by `created_at` ascending (oldest first — the\ntypical primary candidate).","operationId":"list_duplicate_groups_api_v1_contacts_duplicates_get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"signals","in":"query","required":false,"schema":{"type":"string","description":"Comma-separated list of signals to use for duplicate detection. Allowed: phone, email, platform_id, name. 'name' is opt-in (false-positive prone).","default":"phone,email,platform_id","title":"Signals"},"description":"Comma-separated list of signals to use for duplicate detection. Allowed: phone, email, platform_id, name. 'name' is opt-in (false-positive prone)."},{"name":"page","in":"query","required":false,"schema":{"type":"integer","minimum":1,"default":1,"title":"Page"}},{"name":"page_size","in":"query","required":false,"schema":{"type":"integer","maximum":100,"minimum":1,"default":25,"title":"Page Size"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DuplicateGroupListResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/contacts/export":{"post":{"tags":["Contacts"],"summary":"Export Contacts","description":"Start an async contact export to CSV or XLSX (202 Accepted).\n\n**Required Scopes:** `contacts:export`\n\nAccepts the SAME filter parameters as ``GET /contacts`` (search +\ncustom-field filter + list-subscription filter) so a filtered contact list\ncan be exported exactly as seen on screen. With no filters, ALL contacts are\nexported.\n\nThe file's column headers are exactly what ``POST /email-contacts/import``\nauto-detects (including a leading ``contact_id`` column and one flattened\n``custom.<key>`` column per custom field), so the round-trip\n*export → edit in Excel → re-import* needs no manual column mapping.\n\nPoll ``poll_url`` until ``status == completed``, then fetch the file via\n``download_url`` (302 redirect to a short-lived signed URL).\n\nExports are recorded in the audit trail (they are a full-PII dump).","operationId":"export_contacts_api_v1_contacts_export_post","security":[{"HTTPBearer":[]}],"parameters":[{"name":"format","in":"query","required":false,"schema":{"enum":["csv","xlsx"],"type":"string","description":"Output file format","default":"csv","title":"Format"},"description":"Output file format"},{"name":"search","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Search by name, phone, email, or IDs (same as GET /contacts)","title":"Search"},"description":"Search by name, phone, email, or IDs (same as GET /contacts)"},{"name":"include_live_chat","in":"query","required":false,"schema":{"type":"boolean","description":"Include live chat contacts (same as GET /contacts)","default":false,"title":"Include Live Chat"},"description":"Include live chat contacts (same as GET /contacts)"},{"name":"custom_field_id","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"custom_field_definitions.id used for filtering (same as GET /contacts).","title":"Custom Field Id"},"description":"custom_field_definitions.id used for filtering (same as GET /contacts)."},{"name":"custom_field_value","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Custom-field filter value (same semantics as GET /contacts).","title":"Custom Field Value"},"description":"Custom-field filter value (same semantics as GET /contacts)."},{"name":"custom_field_value_to","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Upper bound for custom_field_op='between' (same as GET /contacts).","title":"Custom Field Value To"},"description":"Upper bound for custom_field_op='between' (same as GET /contacts)."},{"name":"custom_field_op","in":"query","required":false,"schema":{"anyOf":[{"enum":["eq","contains","gt","lt","gte","lte","between"],"type":"string"},{"type":"null"}],"description":"Custom-field comparison operator (same as GET /contacts).","title":"Custom Field Op"},"description":"Custom-field comparison operator (same as GET /contacts)."},{"name":"list_id","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"lists.id — filter by list subscription (same as GET /contacts).","title":"List Id"},"description":"lists.id — filter by list subscription (same as GET /contacts)."},{"name":"list_subscription_status","in":"query","required":false,"schema":{"anyOf":[{"enum":["subscribed","pending","unsubscribed","bounced","complained","any","not_subscribed"],"type":"string"},{"type":"null"}],"description":"Subscription status for list_id, default 'subscribed' (same semantics as GET /contacts). Requires list_id.","title":"List Subscription Status"},"description":"Subscription status for list_id, default 'subscribed' (same semantics as GET /contacts). Requires list_id."},{"name":"list_subscription_channel","in":"query","required":false,"schema":{"anyOf":[{"$ref":"#/components/schemas/SubscriptionChannelType"},{"type":"null"}],"description":"Narrow the list filter to one subscription channel (same as GET /contacts). Requires list_id.","title":"List Subscription Channel"},"description":"Narrow the list filter to one subscription channel (same as GET /contacts). Requires list_id."},{"name":"is_blocked","in":"query","required":false,"schema":{"anyOf":[{"type":"boolean"},{"type":"null"}],"description":"true = only blocked contacts, false = only unblocked (same as GET /contacts).","title":"Is Blocked"},"description":"true = only blocked contacts, false = only unblocked (same as GET /contacts)."}],"responses":{"202":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ContactExportStartResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/contacts/export/{job_id}/status":{"get":{"tags":["Contacts"],"summary":"Get Contact Export Status","description":"Poll the status/progress of an async contact export job.\n\n**Required Scopes:** `contacts:export`\n\nWhen ``status`` is ``completed``, ``download_url`` points at the download\nendpoint (302 to a short-lived signed URL). ``failed`` carries\n``error_message``.","operationId":"get_contact_export_status_api_v1_contacts_export__job_id__status_get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"job_id","in":"path","required":true,"schema":{"type":"string","title":"Job Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ContactExportStatusResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/contacts/export/{job_id}/download":{"get":{"tags":["Contacts"],"summary":"Download Contact Export","description":"Download a finished contact export (302 redirect to a signed URL).\n\n**Required Scopes:** `contacts:export`\n\nOnly the requesting tenant's own COMPLETED export jobs can be downloaded\n(404 unknown job, 403 foreign tenant, 409 not finished yet). A fresh\nshort-lived (60 min) signed Cloud Storage URL is minted on every call.\nWith ``?redirect=false`` the signed URL is returned as JSON\n``{\"download_url\": ..., \"filename\": ...}`` instead of a 302.","operationId":"download_contact_export_api_v1_contacts_export__job_id__download_get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"job_id","in":"path","required":true,"schema":{"type":"string","title":"Job Id"}},{"name":"redirect","in":"query","required":false,"schema":{"type":"boolean","description":"true (default): 302 redirect to the signed URL. false: return {download_url, filename} JSON instead — needed by browser SPAs whose auth is Bearer-header-only (a top-level navigation cannot carry the header, and an XHR cannot follow the 302 to Cloud Storage because the bucket has no CORS rule).","default":true,"title":"Redirect"},"description":"true (default): 302 redirect to the signed URL. false: return {download_url, filename} JSON instead — needed by browser SPAs whose auth is Bearer-header-only (a top-level navigation cannot carry the header, and an XHR cannot follow the 302 to Cloud Storage because the bucket has no CORS rule)."}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/contacts/{contact_id}/integrations":{"get":{"tags":["Contacts"],"summary":"Get Contact Integrations","description":"List the contact's external CRM / integration deep-links.\n\nAggregates links across every configured provider (HubSpot today; extensible\nto Salesforce/Shopify/etc.) so the frontend can render an \"Integrations\"\ncard linking straight to the contact's record in each system.\n\nReturns an empty ``items`` list (NOT 404) when the contact has no external\nlinks. 404 is only returned when the contact does not exist for this tenant.\n\n**Required Scopes:** `contacts:read`","operationId":"get_contact_integrations_api_v1_contacts__contact_id__integrations_get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"contact_id","in":"path","required":true,"schema":{"type":"string","title":"Contact Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ContactIntegrationsResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/contacts/{contact_id}":{"get":{"tags":["Contacts"],"summary":"Get Contact","description":"Get a specific contact with custom fields and tags.\n\n**Required Scopes:** `contacts:read`\n\nIf the requested ID is the UUID of a contact that has been merged into\nanother (i.e., it appears in `contact_aliases.old_contact_id`), the\nresponse returns the surviving contact, includes a `merged_from`\nobject describing the merge, and sets the `X-Merged-Into` response\nheader to the surviving contact's UUID.","operationId":"get_contact_api_v1_contacts__contact_id__get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"contact_id","in":"path","required":true,"schema":{"type":"string","title":"Contact Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ContactResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"put":{"tags":["Contacts"],"summary":"Update Contact","description":"Update a contact\n\n`custom_fields` (optional) MERGES typed custom field values, keyed by\ncustom field definition ID (preferred) or field `key`: only the given\nfields are touched, `null` / `\"\"` / `[]` clears one. Every entry is\nvalidated against the field type BEFORE anything is written; any failure\nreturns 422 with one structured error per field (`code`: unknown_field,\nduplicate_field, invalid_type, invalid_format, not_an_option,\nout_of_range, too_long, too_many_fields) and nothing is changed. When\n`custom_fields` is sent, the response includes the contact's\n`custom_fields` map.\n\nSetting `phone` or `email` to a value another contact already has follows\nthe workspace's duplicate contact setting: `allow_duplicates` saves it on\nthis contact too; `dont_allow_duplicates` returns HTTP 409\n(`detail: \"duplicate_contact\"`, with the other contacts' ids) and nothing\nis changed; `auto_merge` keeps this contact's current value and updates\nthe other fields. Contacts are never merged by an update; use\n`POST /contacts/{contact_id}/merge`.\n\n**Required Scopes:** `contacts:update`","operationId":"update_contact_api_v1_contacts__contact_id__put","security":[{"HTTPBearer":[]}],"parameters":[{"name":"contact_id","in":"path","required":true,"schema":{"type":"string","title":"Contact Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ContactUpdate"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ContactResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"patch":{"tags":["Contacts"],"summary":"Partial Update Contact","description":"Partial update a contact\n\n`custom_fields` (optional) MERGES typed custom field values, keyed by\ncustom field definition ID (preferred) or field `key`: only the given\nfields are touched, `null` / `\"\"` / `[]` clears one. Every entry is\nvalidated against the field type BEFORE anything is written; any failure\nreturns 422 with one structured error per field (`code`: unknown_field,\nduplicate_field, invalid_type, invalid_format, not_an_option,\nout_of_range, too_long, too_many_fields) and nothing is changed. When\n`custom_fields` is sent, the response includes the contact's\n`custom_fields` map.\n\nSetting `phone` or `email` to a value another contact already has follows\nthe workspace's duplicate contact setting: `allow_duplicates` saves it on\nthis contact too; `dont_allow_duplicates` returns HTTP 409\n(`detail: \"duplicate_contact\"`, with the other contacts' ids) and nothing\nis changed; `auto_merge` keeps this contact's current value and updates\nthe other fields. Contacts are never merged by an update; use\n`POST /contacts/{contact_id}/merge`.\n\n**Required Scopes:** `contacts:update`","operationId":"partial_update_contact_api_v1_contacts__contact_id__patch","security":[{"HTTPBearer":[]}],"parameters":[{"name":"contact_id","in":"path","required":true,"schema":{"type":"string","title":"Contact Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ContactUpdate"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ContactResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"tags":["Contacts"],"summary":"Delete Contact","description":"Delete a contact and all related data.\n\nPerforms GDPR-compliant deletion (full cascade):\n- All PII is removed (subscriptions, custom fields, tags, campaign data)\n- The contact's conversations AND their messages, attachments (incl. stored\n  files), reactions, email transcripts, live-chat sessions and flow runs are\n  permanently deleted — nothing connected to the customer is retained\n- Billing records are preserved for invoicing\n\n**Required Scopes:** `contacts:delete`","operationId":"delete_contact_api_v1_contacts__contact_id__delete","security":[{"HTTPBearer":[]}],"parameters":[{"name":"contact_id","in":"path","required":true,"schema":{"type":"string","title":"Contact Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ContactDeleteResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/contacts/{contact_id}/merge/preview":{"post":{"tags":["Contacts"],"summary":"Preview Merge Contacts","description":"Preview what merging contacts would do to running flows. Nothing is changed.\n\n**Required Scopes:** `contacts:update`\n\nTakes the same body as `POST /contacts/{contact_id}/merge` and picks the\nsurviving contact the same way (oldest unless `primary_override: true`).\n\nRunning flows of merged contacts keep running on the surviving contact. A\nflow run is only stopped when the surviving contact is already running the\nsame flow, or when several merged contacts run the same flow (the most\nrecently started one keeps running). The response lists how many runs\nwould be stopped per flow.\n\nReturns 404 if any contact does not exist in this workspace, 422 if the\nrequest names no contact to merge.","operationId":"preview_merge_contacts_api_v1_contacts__contact_id__merge_preview_post","security":[{"HTTPBearer":[]}],"parameters":[{"name":"contact_id","in":"path","required":true,"schema":{"type":"string","title":"Contact Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MergeContactsRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MergeContactsPreviewResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/contacts/{contact_id}/merge":{"post":{"tags":["Contacts"],"summary":"Merge Contacts","description":"Manually merge one or more contacts into the contact at `{contact_id}`.\n\n**Required Scopes:** `contacts:update`\n\nBy default the OLDEST contact in the set (`{contact_id}` plus the\n`secondary_ids`) is used as primary. Pass `primary_override: true` to\nforce `{contact_id}` as primary even if a secondary is older.\n\nAll conversations, messages, notes, custom field values, tags,\nsubscriptions, and contact methods are migrated to the primary. The\nsecondary contact rows are hard-deleted; their UUIDs continue to\nresolve to the primary via `contact_aliases`.","operationId":"merge_contacts_api_v1_contacts__contact_id__merge_post","security":[{"HTTPBearer":[]}],"parameters":[{"name":"contact_id","in":"path","required":true,"schema":{"type":"string","title":"Contact Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MergeContactsRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MergeContactsResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/contacts/{contact_id}/lists":{"get":{"tags":["Contacts"],"summary":"Get Contact Lists","description":"Get all list memberships/subscriptions for a contact.\n\n**Required Scopes:** `contacts:read`\n\nReturns all lists that the contact is subscribed to via the unified subscription system,\nincluding channel-specific information and subscription status.\n\nFor newsletter lists, the same contact can be subscribed via multiple channels,\nso there will be separate entries for each channel subscription.","operationId":"get_contact_lists_api_v1_contacts__contact_id__lists_get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"contact_id","in":"path","required":true,"schema":{"type":"string","title":"Contact Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ContactListMembershipsResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/contacts/{contact_id}/link-activity":{"get":{"tags":["Contacts"],"summary":"Get Contact Link Activity","description":"Get all link click activity for a specific contact.\n\nReturns all tracked links the contact has clicked, including:\n- Total click count\n- Unique links clicked\n- Device breakdown (desktop, mobile, tablet, other)\n- Recent click events with link details\n- Top 5 most clicked links by this contact\n\n**Required Scopes:** `contacts:read`\n\nArgs:\n    contact_id: The contact's UUID\n    limit: Maximum number of recent clicks to return (default: 50, max: 200)","operationId":"get_contact_link_activity_api_v1_contacts__contact_id__link_activity_get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"contact_id","in":"path","required":true,"schema":{"type":"string","title":"Contact Id"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","default":50,"title":"Limit"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ContactLinkActivityResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/contacts/{contact_id}/available-channels":{"get":{"tags":["Contacts"],"summary":"Get Contact Available Channels","description":"Get list of channels available to reach a contact.\n\nReturns all channels the tenant has configured that the contact\nhas identifiers for, along with channel-specific status:\n\n- **WhatsApp**: Includes window_status with 24h messaging window info\n- **Email/Telegram/Messenger/Instagram**: Shows as available or unavailable\n\nWhen `for_user_id` is not provided, the current user's internal ID is\nautomatically resolved from the auth token and used for mailbox access\nfiltering.\n\n**Required Scopes:** `contacts:read`\n\n**Response:**\n```json\n{\n    \"contact_id\": \"uuid\",\n    \"channels\": [\n        {\n            \"channel_id\": \"uuid\",\n            \"channel_type\": \"whatsapp\",\n            \"channel_name\": \"WhatsApp Business\",\n            \"identifier\": \"+1234567890\",\n            \"status\": \"available\" | \"template_required\" | \"unavailable\",\n            \"window_status\": {\n                \"in_window\": false,\n                \"requires_template\": true,\n                \"last_customer_message_at\": \"2026-01-14T10:00:00Z\",\n                \"window_end_at\": \"2026-01-15T10:00:00Z\"\n            }\n        },\n        {\n            \"channel_id\": \"uuid\",\n            \"channel_type\": \"email\",\n            \"channel_name\": \"Support Email\",\n            \"identifier\": \"john@example.com\",\n            \"status\": \"available\"\n        }\n    ]\n}\n```","operationId":"get_contact_available_channels_api_v1_contacts__contact_id__available_channels_get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"contact_id","in":"path","required":true,"schema":{"type":"string","title":"Contact Id"}},{"name":"for_user_id","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Filter email mailboxes by user access (internal user UUID). If not provided, automatically resolved from the authenticated user.","title":"For User Id"},"description":"Filter email mailboxes by user access (internal user UUID). If not provided, automatically resolved from the authenticated user."}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AvailableChannelsResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/contacts/{contact_id}/block":{"post":{"tags":["Contacts"],"summary":"Block Contact","description":"Block a contact across all channels.\n\nBlocks the contact and its identifiers. Incoming messages from any of the\ncontact's phone numbers, email addresses and channel ids are dropped (and\ncounted in ``blocked_inbound_count``) and outgoing sends are refused with\n409 ``CONTACT_BLOCKED`` while the contact is blocked. Verified and\nchannel-native identifiers (the channel account the contact messaged\nfrom, verified email addresses) stay blocked even if the contact is\ndeleted and later re-created; unverified, manually entered details are\ncovered only while the contact exists and is blocked. Idempotent.\n\nCustom-Inbox restricted members can only block contacts they can reach\n(otherwise 404).\n\nFires ``contact.updated`` when the contact was not already blocked.\n\n**Required Scopes:** `contacts:update`","operationId":"block_contact_api_v1_contacts__contact_id__block_post","security":[{"HTTPBearer":[]}],"parameters":[{"name":"contact_id","in":"path","required":true,"schema":{"type":"string","title":"Contact Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/contacts/{contact_id}/unblock":{"post":{"tags":["Contacts"],"summary":"Unblock Contact","description":"Unblock a contact across all channels.\n\nClears the contact block and removes the blocks on all of its identifiers\n(every channel), including its email addresses on the email sender block\nlist. Idempotent.\n\nCustom-Inbox restricted members can only unblock contacts they can reach\n(otherwise 404).\n\nFires ``contact.updated`` when the contact was blocked.\n\n**Required Scopes:** `contacts:update`","operationId":"unblock_contact_api_v1_contacts__contact_id__unblock_post","security":[{"HTTPBearer":[]}],"parameters":[{"name":"contact_id","in":"path","required":true,"schema":{"type":"string","title":"Contact Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/contacts/{contact_id}/avatar":{"post":{"tags":["Contacts"],"summary":"Upload Contact Avatar","description":"Upload a profile picture for a contact.\n\nThe image is downscaled to 256px and stored in our own bucket, then served\nfrom the permanent `/api/v1/public/avatar/...` proxy. A manually uploaded\navatar is marked `avatar_is_custom` and is NEVER overwritten by the\nchannel's own profile picture — remove it (DELETE this endpoint) to let\nplatform syncing resume.\n\n**Required Scopes:** `contacts:update`","operationId":"upload_contact_avatar_api_v1_contacts__contact_id__avatar_post","security":[{"HTTPBearer":[]}],"parameters":[{"name":"contact_id","in":"path","required":true,"schema":{"type":"string","title":"Contact Id"}}],"requestBody":{"required":true,"content":{"multipart/form-data":{"schema":{"$ref":"#/components/schemas/Body_upload_contact_avatar_api_v1_contacts__contact_id__avatar_post"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ContactAvatarResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"tags":["Contacts"],"summary":"Delete Contact Avatar","description":"Remove a contact's profile picture.\n\nClears the stored image and re-opens platform syncing: the next inbound\nmessage from this contact re-mirrors whatever picture the channel currently\nreports. Until then the UI shows the contact's initials.\n\n**Required Scopes:** `contacts:update`","operationId":"delete_contact_avatar_api_v1_contacts__contact_id__avatar_delete","security":[{"HTTPBearer":[]}],"parameters":[{"name":"contact_id","in":"path","required":true,"schema":{"type":"string","title":"Contact Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ContactAvatarResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/contacts/{contact_id}/methods":{"get":{"tags":["Contacts"],"summary":"List Contact Methods","description":"List all contact methods for a contact.\n\nReturns all external identifiers (phone numbers, emails, platform IDs)\nassociated with this contact, optionally filtered by method type.\n\nBy default, **technical** method types (``LIVE_CHAT`` — the per-device\nvisitor cookie UUID used for fast returning-visitor lookup and flow\nengine channel resolution) are EXCLUDED from the response so the\ncontact-detail UI doesn't show implementation-detail rows. Pass\n``include_technical=true`` to surface them. An explicit\n``method_type=live_chat`` filter overrides this and always returns the\nmatching rows.\n\n**Required Scopes:** `contacts:read`","operationId":"list_contact_methods_api_v1_contacts__contact_id__methods_get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"contact_id","in":"path","required":true,"schema":{"type":"string","title":"Contact Id"}},{"name":"method_type","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Filter by method type (phone, email, whatsapp_id, whatsapp_bsuid, telegram_id, messenger_id, instagram_id)","title":"Method Type"},"description":"Filter by method type (phone, email, whatsapp_id, whatsapp_bsuid, telegram_id, messenger_id, instagram_id)"},{"name":"include_technical","in":"query","required":false,"schema":{"type":"boolean","description":"Include technical/internal method types in the response. Defaults to false — LIVE_CHAT (per-device visitor cookie UUID) and any future 'technical' types are hidden from the contact detail UI by default. Set true to include them (e.g. for admin tooling or debugging).","default":false,"title":"Include Technical"},"description":"Include technical/internal method types in the response. Defaults to false — LIVE_CHAT (per-device visitor cookie UUID) and any future 'technical' types are hidden from the contact detail UI by default. Set true to include them (e.g. for admin tooling or debugging)."}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ContactMethodResponse"},"title":"Response List Contact Methods Api V1 Contacts  Contact Id  Methods Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"post":{"tags":["Contacts"],"summary":"Create Contact Method","description":"Add a new contact method to a contact.\n\nCreates a new external identifier (phone, email, platform ID) linked to\nthis contact. For page-scoped types (messenger_id, instagram_id),\na channel_id is required.\n\nIf the proposed method's `(method_type, value, channel_id)` already\nbelongs to a *different* contact, this endpoint NEVER silently merges.\nInstead it returns HTTP 409 with a machine-readable\n`detail: \"duplicate_contact_method\"` body so the frontend can prompt the\nuser to either merge the two contacts or open the other contact.\n\nThis is true REGARDLESS of the tenant's `duplicate_contact_mode` default\n(including `auto_merge`): an interactive method-add must never swallow an\nunrelated contact. To actually merge, the client must make an explicit,\nuser-confirmed call to `POST /contacts/{contact_id}/merge` (which carries\nits own `contacts:update` scope check and uses oldest-wins semantics).\n\nThe optional per-request `duplicate_mode` body field still tunes the\nresponse shape on conflict, but none of the modes auto-merge here:\n\n- `allow_duplicates`: returns the existing method on the OTHER contact\n  unchanged (the unique constraint prevents duplicate ownership).\n- `dont_allow_duplicates` / `auto_merge` (default) / omitted: returns the\n  409 `duplicate_contact_method` conflict described above.\n\nAdding a method that already exists on the SAME contact is idempotent.\n\n`verified: true` marks the identifier as verified (`verified_source`\n`api`); on an existing method of this contact it upgrades it. Not allowed\nfor OAuth apps (403 `verification_not_allowed`).\n\n**Required Scopes:** `contacts:update`","operationId":"create_contact_method_api_v1_contacts__contact_id__methods_post","security":[{"HTTPBearer":[]}],"parameters":[{"name":"contact_id","in":"path","required":true,"schema":{"type":"string","title":"Contact Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ContactMethodCreate"}}}},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ContactMethodResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/contacts/{contact_id}/methods/{method_id}":{"put":{"tags":["Contacts"],"summary":"Update Contact Method","description":"Update a contact method (value, display_name, is_primary, verified).\n\n`verified: true` marks the identifier as verified (`verified_source`\n`api`), `false` clears it; omit it to keep the current state. Changing\n`value` without `verified: true` resets the method to unverified (a new\nvalue is a new claim). Changing `verified` is not allowed for OAuth apps\n(403 `verification_not_allowed`).\n\n**Required Scopes:** `contacts:update`","operationId":"update_contact_method_api_v1_contacts__contact_id__methods__method_id__put","security":[{"HTTPBearer":[]}],"parameters":[{"name":"contact_id","in":"path","required":true,"schema":{"type":"string","title":"Contact Id"}},{"name":"method_id","in":"path","required":true,"schema":{"type":"string","title":"Method Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ContactMethodUpdate"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ContactMethodResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"tags":["Contacts"],"summary":"Delete Contact Method","description":"Delete a contact method.\n\nIf the deleted method was primary, the next oldest method of the same type\nis automatically promoted. Denormalized fields (phone, email) on the contact\nare updated accordingly.\n\n**Required Scopes:** `contacts:update`","operationId":"delete_contact_method_api_v1_contacts__contact_id__methods__method_id__delete","security":[{"HTTPBearer":[]}],"parameters":[{"name":"contact_id","in":"path","required":true,"schema":{"type":"string","title":"Contact Id"}},{"name":"method_id","in":"path","required":true,"schema":{"type":"string","title":"Method Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/contacts/{contact_id}/methods/{method_id}/verification":{"patch":{"tags":["Contacts"],"summary":"Update Contact Method Verification","description":"Mark a contact method as verified or unverified.\n\nVerified identifiers are trusted by AI agents and by automatic duplicate\nhandling; unverified ones are treated as claims. Sets\n`verified_source` to `manual` and records who changed it. Marking a\nmethod verified also confirms any pending \"typed by a live chat visitor\"\nrecord for that identifier. Unmarking keeps that history.\n\nNot available to OAuth apps or embedded views (403\n`verification_not_allowed`).\n\n**Required Scopes:** `contacts:update`","operationId":"update_contact_method_verification_api_v1_contacts__contact_id__methods__method_id__verification_patch","security":[{"HTTPBearer":[]}],"parameters":[{"name":"contact_id","in":"path","required":true,"schema":{"type":"string","title":"Contact Id"}},{"name":"method_id","in":"path","required":true,"schema":{"type":"string","title":"Method Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ContactMethodVerificationUpdate"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ContactMethodResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/contacts/{contact_id}/history":{"get":{"tags":["Contacts"],"summary":"List Contact History","description":"Unified timeline of conversations, flow runs, flow-sent messages,\nscheduled sends, all messages, and AI ticket summaries for a single\ncontact.\n\nPowers the contact-detail \"History\" card (Activity + Messages view\nmodes). Returns a single keyset-paginated, time-descending stream\nmerged from multiple SHARD tables — see\n:class:`app.services.contact_history_service.ContactHistoryService`\nfor the per-source query strategy and batch lookups.\n\nEvent types:\n\n* ``conversation`` — one row per conversation (Activity mode).\n* ``flow_run``     — one row per flow run (Activity mode).\n* ``flow_message`` — flow-attributed messages only (legacy).\n* ``message``      — every message for the contact regardless of\n  flow attribution (Messages mode).\n* ``scheduler``    — pending or fired scheduled sends (reminders,\n  recurring sends, segment-blast materializations).\n* ``ticket_summary`` — AI conversation summaries persisted as editable\n  KB documents (``knowledge_base_documents`` filtered by the indexed\n  ``contact_id`` column, ``document_type='ticket_summary'``). The\n  ``document_id`` deep-links to the KB editor / raw GET-PUT endpoints.\n\n**Required Scopes:** `contacts:read` AND `flows:read` AND `conversations:read` AND `scheduler:read` AND `knowledge_base:read`","operationId":"list_contact_history_api_v1_contacts__contact_id__history_get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"contact_id","in":"path","required":true,"schema":{"type":"string","title":"Contact Id"}},{"name":"types","in":"query","required":false,"schema":{"anyOf":[{"type":"array","items":{"type":"string"}},{"type":"null"}],"description":"Filter by event_type. Repeatable. Allowed values: `conversation`, `flow_run`, `flow_message`, `message`, `scheduler`, `ticket_summary`. Omit to include all known types (current FE behavior). Use `message` for the flat all-messages stream that powers the Messages view mode; use `scheduler` for pending/fired scheduled sends; use `ticket_summary` for the contact's AI conversation summaries (editable KB documents).","title":"Types"},"description":"Filter by event_type. Repeatable. Allowed values: `conversation`, `flow_run`, `flow_message`, `message`, `scheduler`, `ticket_summary`. Omit to include all known types (current FE behavior). Use `message` for the flat all-messages stream that powers the Messages view mode; use `scheduler` for pending/fired scheduled sends; use `ticket_summary` for the contact's AI conversation summaries (editable KB documents)."},{"name":"date_from","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"ISO8601 lower bound on each source's sort column.","title":"Date From"},"description":"ISO8601 lower bound on each source's sort column."},{"name":"date_to","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"ISO8601 upper bound on each source's sort column.","title":"Date To"},"description":"ISO8601 upper bound on each source's sort column."},{"name":"q","in":"query","required":false,"schema":{"anyOf":[{"type":"string","minLength":0,"maxLength":200},{"type":"null"}],"description":"Substring filter (LIKE %q%). Ignored when shorter than 2 chars. Matches conversation.subject + last message text, flow.name, and message.text depending on the event_type.","title":"Q"},"description":"Substring filter (LIKE %q%). Ignored when shorter than 2 chars. Matches conversation.subject + last message text, flow.name, and message.text depending on the event_type."},{"name":"cursor","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Opaque cursor from a previous page's `next_cursor`.","title":"Cursor"},"description":"Opaque cursor from a previous page's `next_cursor`."},{"name":"page_size","in":"query","required":false,"schema":{"type":"integer","maximum":100,"minimum":1,"description":"Page size (default 50, max 100).","default":50,"title":"Page Size"},"description":"Page size (default 50, max 100)."}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ContactHistoryResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/contacts/{contact_id}/attachments":{"get":{"tags":["Contacts"],"summary":"List Contact Attachments","description":"Keyset-paginated, newest-first list of ALL media exchanged with a contact\nacross every one of their conversations.\n\nPowers the WhatsApp-profile-style media gallery on the contact-detail page.\nCovers every media category — images, video, audio, voice notes, AND\ndocuments/files (nothing is filtered out). Each item carries a short-lived\n(60 min) signed VIEW url into the private media bucket plus, when available,\na signed thumbnail url — never raw bucket urls.\n\nSources: a message's primary attachment, interactive/template header\nmedia, and email file attachments (`source=\"email\"`, one item per file,\n`message_id` = the email's message). Inline email images (signature logos,\n`cid:` parts) are never listed; emails received/sent before email files\nwere linked to generic attachments are not shown. Every item carries\n`source` (`message` | `email`).\n\nPagination is keyed on MESSAGES, ordered DESC by `(created_at,\nmessage_id)`: `page_size` bounds the messages per page, so `len(items)`\nmay exceed `page_size` when an email carries several files (all files of\none email always arrive on the same page). `next_cursor` is the opaque\ncursor of the last returned message when `has_more` is true.\n\nVisibility & tenancy: every query is filtered by `tenant_id`; the contact\nmust belong to the tenant (404 otherwise).\n\n**Required Scopes:** `contacts:read` AND `messages:read`","operationId":"list_contact_attachments_api_v1_contacts__contact_id__attachments_get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"contact_id","in":"path","required":true,"schema":{"type":"string","title":"Contact Id"}},{"name":"cursor","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Opaque cursor from a previous page's `next_cursor`","title":"Cursor"},"description":"Opaque cursor from a previous page's `next_cursor`"},{"name":"page_size","in":"query","required":false,"schema":{"type":"integer","maximum":100,"minimum":1,"description":"Page size (default 30, max 100)","default":30,"title":"Page Size"},"description":"Page size (default 30, max 100)"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ContactAttachmentsResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/contacts/{contact_id}/messages":{"get":{"tags":["Contacts"],"summary":"List Contact Channel Messages","description":"Keyset-paginated stream of a contact's messages on a SINGLE channel,\ndrawn from PRIOR conversations only.\n\nReturns this contact's messages on `channel_id` from conversations OTHER\nthan `before_conversation_id` whose parent conversation was created BEFORE\nthe `before_conversation_id` conversation (i.e. earlier threads on the same\nchannel). Newest-first, keyset-paginated on `(created_at, id)`.\n\nReuses the standard `MessageResponse` item shape (each item carries its own\n`conversation_id`) so the frontend renders these with the existing message\nbubble — no new bubble type is needed.\n\nVisibility & tenancy: the contact must belong to the tenant (404 otherwise);\n`before_conversation_id` must belong to the tenant AND this contact AND this\n`channel_id` (404 otherwise); and the requested `channel_id` must be visible\nto the caller via contact-history visibility (404 otherwise).\n\n**Required Scopes:** `contacts:read` AND `messages:read`","operationId":"list_contact_channel_messages_api_v1_contacts__contact_id__messages_get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"contact_id","in":"path","required":true,"schema":{"type":"string","title":"Contact Id"}},{"name":"channel_id","in":"query","required":true,"schema":{"type":"string","description":"Channel to scope the timeline to","title":"Channel Id"},"description":"Channel to scope the timeline to"},{"name":"before_conversation_id","in":"query","required":true,"schema":{"type":"string","description":"The currently-open conversation; only PRIOR conversations are included","title":"Before Conversation Id"},"description":"The currently-open conversation; only PRIOR conversations are included"},{"name":"cursor","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Opaque cursor from a previous page's `next_cursor`","title":"Cursor"},"description":"Opaque cursor from a previous page's `next_cursor`"},{"name":"page_size","in":"query","required":false,"schema":{"type":"integer","maximum":50,"minimum":1,"description":"Page size (default 5, max 50)","default":5,"title":"Page Size"},"description":"Page size (default 5, max 50)"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ContactChannelTimelineResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/conversations/{conversation_id}/contact-merge-suggestions":{"get":{"tags":["Contacts"],"summary":"List Conversation Contact Merge Suggestions","description":"List pending contact merge suggestions for a conversation's contact.\n\n**Required Scopes:** `conversations:read`\n\nA suggestion is created when a live chat visitor enters an email address\nor phone number (or the website passes a contact ID) that matches an\nexisting contact. The conversation is never linked to that contact\nautomatically: confirm the suggestion to merge the visitor into the\nexisting contact, or dismiss it to keep them separate.\n\n`matched_value` is what the visitor entered and is not verified.\n\nSuggestions are listed on both sides: in the visitor's conversations and in\nevery conversation of the matched contact. `perspective` says which side\nthis conversation's contact is on (`source` = the visitor, `target` = the\nmatched contact). Confirming or dismissing from either side resolves the\nsuggestion for both.\n\nOn the matched contact's side, `conversation_id` and `source_contact` are\nnull when you cannot open the visitor's conversation (for example because\nof your inbox access). You can still confirm or dismiss the suggestion.","operationId":"list_conversation_contact_merge_suggestions_api_v1_conversations__conversation_id__contact_merge_suggestions_get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"conversation_id","in":"path","required":true,"schema":{"type":"string","title":"Conversation Id"}},{"name":"page","in":"query","required":false,"schema":{"type":"integer","minimum":1,"description":"Page number (1-indexed)","default":1,"title":"Page"},"description":"Page number (1-indexed)"},{"name":"page_size","in":"query","required":false,"schema":{"type":"integer","maximum":100,"minimum":1,"description":"Items per page","default":50,"title":"Page Size"},"description":"Items per page"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ContactMergeSuggestionListResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/contact-merge-suggestions/{suggestion_id}/confirm":{"post":{"tags":["Contacts"],"summary":"Confirm Contact Merge Suggestion","description":"Confirm a contact merge suggestion: merge the visitor contact into the matched contact.\n\n**Required Scopes:** `contacts:update`\n\nThe request body must be `{\"confirmed\": true}`; anything else returns 422.\nThe unverified contact (`source_contact`) is merged into the existing\ncontact (`target_contact`), exactly like `POST /contacts/{contact_id}/merge`:\nconversations, messages, notes, tags and contact methods move to the\nsurviving contact. This cannot be undone.\n\nReturns 404 when the suggestion does not exist and 409 with\n`error_code: suggestion_not_pending` when it was already confirmed or\ndismissed. `conversation_id` and `source_contact` are null in the response\nwhen you cannot open the visitor's conversation.","operationId":"confirm_contact_merge_suggestion_api_v1_contact_merge_suggestions__suggestion_id__confirm_post","security":[{"HTTPBearer":[]}],"parameters":[{"name":"suggestion_id","in":"path","required":true,"schema":{"type":"string","title":"Suggestion Id"}}],"requestBody":{"content":{"application/json":{"schema":{"anyOf":[{"$ref":"#/components/schemas/ContactMergeSuggestionConfirmRequest"},{"type":"null"}],"title":"Body"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ContactMergeSuggestionResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/contact-merge-suggestions/{suggestion_id}/dismiss":{"post":{"tags":["Contacts"],"summary":"Dismiss Contact Merge Suggestion","description":"Dismiss a contact merge suggestion and keep both contacts separate.\n\n**Required Scopes:** `contacts:update`\n\nA dismissed pair is not suggested again. Returns 404 when the suggestion\ndoes not exist and 409 with `error_code: suggestion_not_pending` when it\nwas already confirmed or dismissed. `conversation_id` and `source_contact`\nare null in the response when you cannot open the visitor's conversation.","operationId":"dismiss_contact_merge_suggestion_api_v1_contact_merge_suggestions__suggestion_id__dismiss_post","security":[{"HTTPBearer":[]}],"parameters":[{"name":"suggestion_id","in":"path","required":true,"schema":{"type":"string","title":"Suggestion Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ContactMergeSuggestionResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/tenants":{"post":{"tags":["Tenants"],"summary":"Create Tenant","description":"Create a new tenant (organization) and start trial","operationId":"create_tenant_api_v1_tenants_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TenantCreate"}}},"required":true},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TenantResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"HTTPBearer":[]}]}},"/api/v1/tenants/me":{"get":{"tags":["Tenants"],"summary":"Get My Tenants","description":"Get current user's tenants","operationId":"get_my_tenants_api_v1_tenants_me_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"items":{"$ref":"#/components/schemas/TenantResponse"},"type":"array","title":"Response Get My Tenants Api V1 Tenants Me Get"}}}}},"security":[{"HTTPBearer":[]}]}},"/api/v1/tenants/features":{"get":{"tags":["Tenants"],"summary":"Get Tenant Features","description":"Get feature availability for the current tenant.\n\nReturns which UI and API features are enabled based on the\nbilling account's package type (Standard vs API Only).\n\nThis endpoint is designed for frontend feature gating.\nAll authenticated users can access this endpoint.","operationId":"get_tenant_features_api_v1_tenants_features_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FeatureAvailabilityResponse"}}}}},"security":[{"HTTPBearer":[]}]}},"/api/v1/tenants/me/retention":{"get":{"tags":["Tenants"],"summary":"Get My Retention","description":"Get the current data retention policy and values for the caller's tenant.\n\nMessage retention: the stored ``message_retention_days`` (0 = unlimited),\nthe plan's presets (``allowed_options``), ``floor`` (30 on API Only, else\n90) and ``ceiling``, whether the plan is API-only, and the\n``effective_days`` the nightly purge enforces.\n\nInactive contacts: ``contact_retention_days`` (0 = never), the\n``contact_effective_days`` the purge uses (null = never) and the allowed\nrange ``contact_floor``..``contact_max`` (not limited by the plan). Both\ndescribe the ACTIVE rule. A stricter rule that is still in its 7-day wait\nis in ``contact_retention_pending_days`` and applies at\n``contact_retention_pending_effective_at`` (ISO-8601 UTC).\n``can_tighten_contact_retention`` says whether the caller may switch the\nrule on or shorten it (workspace Owner/Admin with ``contacts:delete``,\nnever an API token or connected app).\n\nMessages have the same 7-day wait: ``message_retention_days`` and\n``effective_days`` describe the ACTIVE rule, a stricter one still waiting\nis in ``message_retention_pending_days`` / ``message_retention_pending_effective_at``,\nand ``can_tighten_message_retention`` says whether the caller may switch\nmessage deletion on or shorten it (Owner/Admin with ``messages:delete``).\n\n**Required Scopes:** `settings:read`","operationId":"get_my_retention_api_v1_tenants_me_retention_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RetentionPolicyResponse"}}}}},"security":[{"HTTPBearer":[]}]},"put":{"tags":["Tenants"],"summary":"Update My Retention","description":"Set the retention periods (in days) for the caller's tenant.\n\nSend ``message_retention_days`` and/or ``contact_retention_days`` (at least\none, otherwise 422). Both values are validated before either is written.\n\n``message_retention_days`` must be one of the plan's presets\n(``allowed_options`` in GET): API Only 30, 60 or 90; unlimited plans 90,\n180, 365, 730, 1095, 1825, 3650 or 0 (never); a finite cap > 90 the ladder\nvalues below the cap plus the cap. Anything else is HTTP 400 (0 on a capped\nplan too). Re-sending the current value (active or waiting) is accepted\nas-is, so an older value outside the presets keeps working until changed.\n\nMessage retention is compared with the ACTIVE rule by the period chosen\n(0 = never = longest; below the plan floor counts as the floor), so a value is stricter when\nit is shorter than the active choice, whatever the plan cap. On a plan\nwith a cap, choosing any period (even the cap) while the rule is 0 is\nswitching deletion on. A stricter value (switching on, or\nshortening) needs a workspace Owner or Admin holding ``messages:delete``\nsigned in to the app (HTTP 403 otherwise), waits 7 days\n(``message_retention_pending_*``) and every workspace Owner is emailed.\nOff (0), longer, or the active value again applies immediately and\ncancels a waiting change.\n\n``contact_retention_days`` deletes contacts (and their conversations) with\nno message in either direction for that many days: 0 (never) or 90..3650,\non every plan. It is compared with the ACTIVE rule:\n\n* Stricter (switching on, or shortening): only a workspace Owner or Admin\n  holding ``contacts:delete`` may do it, never an API token or connected\n  app (HTTP 403). The new rule waits 7 days\n  (``contact_retention_pending_*`` in the response) and every workspace\n  Owner is emailed. Shortening a rule that is already waiting restarts\n  the 7 days; loosening it keeps the original date.\n* Off (0), longer, or the active value again (undo): applies immediately\n  and cancels any waiting change.\n\nReturns HTTP 400 with a clear message if a value is out of bounds.\n\n**Required Scopes:** `settings:update` (plus Owner/Admin and\n`messages:delete` to switch on or shorten message deletion, or\n`contacts:delete` for inactive-contact deletion)","operationId":"update_my_retention_api_v1_tenants_me_retention_put","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RetentionUpdate"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RetentionPolicyResponse"}}}},"400":{"description":"A value is out of bounds for the plan or the allowed range."},"403":{"description":"Switching on or shortening automatic message deletion requires a workspace owner or admin with `messages:delete`; switching on or shortening automatic deletion of inactive contacts requires one with `contacts:delete`. Both must be done signed in to the app: API tokens and connected apps get 403 for those changes. Switching a rule off or choosing a longer period is allowed. When both changes are refused, the message error is returned."},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"HTTPBearer":[]}]}},"/api/v1/tenants/me/retention/preview":{"post":{"tags":["Tenants"],"summary":"Preview My Retention","description":"Preview what saving the proposed retention values would delete.\n\nSame body as ``PUT /tenants/me/retention`` (an omitted field keeps its\ncurrent value) and the same validation (400 on an invalid value). Counts\nthe messages, inactive contacts and conversations the nightly purge would\ndelete under the proposed values. Read-only: nothing is saved or deleted.\n``counts_capped`` = true means the counts stopped at a limit and are lower\nbounds. ``contact_deletion_starts_at`` (ISO-8601 UTC) is set when the\nproposed contact rule is stricter than the active one: it waits 7 days\nand starts deleting then (counts show what it deletes once it starts).\n``message_deletion_starts_at`` is the same for the proposed message rule.\n\nLimited to 6 calls per minute per workspace and one running preview per\nworkspace at a time (HTTP 429 otherwise). Results are cached for up to\n60 seconds.\n\n**Required Scopes:** `settings:update`","operationId":"preview_my_retention_api_v1_tenants_me_retention_preview_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RetentionUpdate"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RetentionPreviewResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"HTTPBearer":[]}]}},"/api/v1/tenants/{tenant_id}":{"get":{"tags":["Tenants"],"summary":"Get Tenant","description":"Get a specific tenant (must be a member)","operationId":"get_tenant_api_v1_tenants__tenant_id__get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"tenant_id","in":"path","required":true,"schema":{"type":"string","title":"Tenant Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TenantResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"put":{"tags":["Tenants"],"summary":"Update Tenant","description":"Update tenant information (requires settings:update scope).\n\nThe tenant is resolved from the `X-Tenant-ID` header and the caller's\nmembership + scope are enforced by `require_scope`, mirroring the sibling\nsettings PUTs (features / channel-priority / dashboard-settings). The\n`{tenant_id}` path segment is kept for URL compatibility but is shadowed by\nthe header-resolved value.\n\n**Required Scopes:** `settings:update`","operationId":"update_tenant_api_v1_tenants__tenant_id__put","security":[{"HTTPBearer":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TenantUpdate"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TenantResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"tags":["Tenants"],"summary":"Delete Tenant","description":"Deactivate a tenant (requires team:delete scope - OWNER only).\n\nDeactivation also releases the tenant's channels: every channel is archived,\nand WhatsApp numbers / Facebook Pages are unsubscribed and deregistered on\nMeta where applicable, so they can be connected again in another tenant.","operationId":"delete_tenant_api_v1_tenants__tenant_id__delete","security":[{"HTTPBearer":[]}],"parameters":[{"name":"tenant_id","in":"path","required":true,"schema":{"type":"string","title":"Tenant Id"}}],"responses":{"204":{"description":"Successful Response"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/tenants/{tenant_id}/leave":{"post":{"tags":["Tenants"],"summary":"Leave Tenant","description":"Leave a tenant the user is currently a member of.\n\nSelf-service equivalent of `DELETE /users/{user_id}` — no special\nscope required because the user is acting on themselves.\n\n**Validation:**\n- 404 if the user is not a member of this tenant.\n- 400 with `\"Cannot leave: you are the sole owner. ...\"` if the\n  user is the only OWNER of the tenant.\n- 400 with `\"Cannot leave: you are the only member. ...\"` if the\n  user is the only member.\n\n**Side effects on success:**\n- The TenantUser row is hard-deleted (mirrors the admin endpoint).\n- The user's `user_notification_settings` rows for this tenant are\n  deleted and the four Redis cache keys are cleared.\n\nReturns 204 No Content on success.","operationId":"leave_tenant_api_v1_tenants__tenant_id__leave_post","security":[{"HTTPBearer":[]}],"parameters":[{"name":"tenant_id","in":"path","required":true,"schema":{"type":"string","title":"Tenant Id"}}],"responses":{"204":{"description":"Successful Response"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/tenants/{tenant_id}/channel-priority":{"get":{"tags":["Tenants"],"summary":"Get Channel Priority","description":"Get channel priority for a tenant.\n\nChannel priority is used to resolve conflicts when a contact has multiple channels\navailable (e.g., both WhatsApp and Telegram). The priority list determines which\nchannel to use for sending campaigns.","operationId":"get_channel_priority_api_v1_tenants__tenant_id__channel_priority_get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"tenant_id","in":"path","required":true,"schema":{"type":"string","title":"Tenant Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Get Channel Priority Api V1 Tenants  Tenant Id  Channel Priority Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"put":{"tags":["Tenants"],"summary":"Update Channel Priority","description":"Update channel priority for a tenant (requires settings:update scope).\n\nChannel priority determines which channel to use when a contact has multiple\nchannels available. For example, if set to [\"whatsapp\", \"telegram\", \"sms\"],\ncampaigns will prefer WhatsApp, then Telegram, then SMS for contacts with\nmultiple channels.\n\nValid channel types: whatsapp, telegram, messenger, sms, instagram, viber\n\n**Required Scopes:** `settings:update`","operationId":"update_channel_priority_api_v1_tenants__tenant_id__channel_priority_put","security":[{"HTTPBearer":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ChannelPriorityUpdate"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Update Channel Priority Api V1 Tenants  Tenant Id  Channel Priority Put"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/company-profile":{"get":{"tags":["Company Profile"],"summary":"Get Company Profile","description":"Get tenant company profile and branding information\n\nReturns combined data:\n- Legal company information (for email compliance)\n- Branding settings (colors, logo)\n- Computed field: is_complete_for_email\n\n**Database:** PRIMARY (tenants table)\n\n**🔐 Required Scopes:** `settings:read`\n","operationId":"get_company_profile_api_v1_company_profile_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CompanyProfileResponse"}}}}},"security":[{"HTTPBearer":[]}]},"patch":{"tags":["Company Profile"],"summary":"Update Company Profile","description":"Update tenant company profile and branding (partial update)\n\nOnly updates provided fields. Unspecified fields remain unchanged.\n\n**Database:** PRIMARY (tenants table)\n\n**Example:**\n```json\n{\n    \"company_legal_name\": \"ACME Corporation\",\n    \"company_address_line1\": \"123 Main St\",\n    \"company_city\": \"San Francisco\",\n    \"company_state_province\": \"CA\",\n    \"company_postal_code\": \"94102\",\n    \"company_country\": \"United States\",\n    \"brand_colors\": {\n        \"primary\": \"#007bff\",\n        \"secondary\": \"#6c757d\",\n        \"accent\": \"#28a745\"\n    }\n}\n```\n\n**🔐 Required Scopes:** `settings:update`\n","operationId":"update_company_profile_api_v1_company_profile_patch","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CompanyProfileUpdate"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CompanyProfileResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"HTTPBearer":[]}]}},"/api/v1/users":{"get":{"tags":["Users"],"summary":"List Team Members","description":"List all users in the tenant with their RBAC roles.\n\nReturns a paginated list of team members with their roles and permissions.\nEach member also carries account-wide-usage access flags for the\n/settings/team access toggle: ``is_billing_account_owner`` (owner always has\nusage access → render the toggle checked+disabled) and\n``account_billing_read_granted`` (whether the member holds a direct\n``account:billing_read`` grant → checkbox state for non-owners). Both default\nFalse and are fail-closed.\n\n**Required Scopes:** `team:read`\n\n**Response format:**\n```json\n{\n    \"items\": [...],\n    \"pagination\": {\n        \"total\": 25,\n        \"page\": 1,\n        \"page_size\": 20,\n        \"total_pages\": 2,\n        \"has_next\": true,\n        \"has_prev\": false\n    }\n}\n```","operationId":"list_team_members_api_v1_users_get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"page","in":"query","required":false,"schema":{"type":"integer","minimum":1,"description":"Page number (1-indexed)","default":1,"title":"Page"},"description":"Page number (1-indexed)"},{"name":"page_size","in":"query","required":false,"schema":{"type":"integer","maximum":100,"minimum":1,"description":"Items per page","default":20,"title":"Page Size"},"description":"Items per page"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TenantUserListResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/users/{user_id}/account-billing-access":{"put":{"tags":["Users"],"summary":"Set Account Billing Access","description":"Grant or revoke account-wide usage access for a team member.\n\nIdempotently grants (``enabled=true``) or revokes (``enabled=false``) a\nDIRECT ``account:billing_read`` scope on the target user within the current\nworkspace. This is the backing endpoint for the \"Can view account-wide usage\"\ncheckbox on /settings/team.\n\nOWNER-ONLY: only the OWNER of the billing account that owns the current\nworkspace may toggle this — enforced by the same Part B owner-guard used on\nevery ``account:billing_read`` grant path\n(``_enforce_owner_for_account_billing_scope``). The billing account is always\nderived server-side from the authenticated tenant; never caller-supplied.\nFail-closed: a non-owner (or any missing row) gets 403.\n\n**Required Scopes:** `team:admin` (AND billing-account ownership)","operationId":"set_account_billing_access_api_v1_users__user_id__account_billing_access_put","security":[{"HTTPBearer":[]}],"parameters":[{"name":"user_id","in":"path","required":true,"schema":{"type":"string","title":"User Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AccountBillingAccessRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AccountBillingAccessResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/users/me":{"get":{"tags":["Users"],"summary":"Get Current User Profile","description":"Get current user's profile with optional tenant context.\n\nIf X-Tenant-ID header is provided, includes:\n- User's role in that tenant\n- Tenant's multi_agent_mode setting\n\n**Authentication:** Required (no specific scope needed)","operationId":"get_current_user_profile_api_v1_users_me_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CurrentUserResponse"}}}}},"security":[{"HTTPBearer":[]}]},"delete":{"tags":["Users"],"summary":"Delete my account","description":"Permanently delete the authenticated user's own account.\n\nThis is irreversible. It removes the login identity (you will be signed\nout everywhere and cannot sign in again), the profile (name, avatar,\nphone, preferences), API sessions and OAuth grants, and detaches the user\nfrom every workspace they are a member of. Workspaces, billing accounts,\nconversations and contacts belong to the organisation and are **not**\ndeleted — only the person's membership is removed.\n\n**Refused with 409** while the user is the **sole owner** or the **sole\nmember** of any workspace (tenant) or billing account: promote another\nowner first, or delete the workspace / cancel the billing account. The\nresponse body lists the blocking workspaces / billing accounts as\n`{\"blockers\": [{\"type\", \"id\", \"name\", \"reason\"}]}` with `reason` one of\n`sole_owner`, `sole_member`, `platform_admin`.\n\nReturns `{\"success\": true, \"id\": \"<user_id>\"}`.\n\n**Authentication:** Required (no specific scope — the user acts on themselves)","operationId":"delete_my_account_api_v1_users_me_delete","responses":{"200":{"description":"Account permanently deleted","content":{"application/json":{"schema":{}}}},"404":{"description":"Current user not found"},"409":{"description":"Blocked: sole owner / sole member of a workspace or billing account"},"502":{"description":"Identity provider refused the deletion; nothing was deleted"}},"security":[{"HTTPBearer":[]}]},"patch":{"tags":["Users"],"summary":"Update Current User","description":"Update current user's profile (alias for /users/me/profile).\n\nThis endpoint accepts the same request body as /users/me/profile.\nBoth endpoints are equivalent and provided for API convenience.\n\nUpdatable fields:\n- `display_name`: User-editable name (takes precedence over Auth0 name in UI)\n- `chat_nickname`: Display name in chat\n- `live_chat_available`: Availability for live chat assignment\n- `personal_phone`: Personal phone for receiving SMS/WhatsApp notifications (user-global)\n- `preferred_language`: UI language code (en, de, fr, es, pt, it, ru, hr, tr, ar)\n- `auto_translate_enabled`: Enable automatic translation of incoming messages (user-global)\n- `shoutbox_hidden`: Hide/deactivate the team-chat Shoutbox widget across all devices (user-global)\n- `spoken_languages`: Comma-separated ISO 639-1 codes the user speaks, e.g. \"de,en\" (user-global; empty string clears)\n- `phone`: Agent's phone number for email signatures (per-tenant, requires X-Tenant-ID header)\n- `title`: Agent's job title for email signatures (per-tenant, requires X-Tenant-ID header)\n\n**Authentication:** Required (no specific scope needed)","operationId":"update_current_user_api_v1_users_me_patch","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateProfileRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CurrentUserResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"HTTPBearer":[]}]}},"/api/v1/users/me/profile":{"patch":{"tags":["Users"],"summary":"Update Current User Profile","description":"Update current user's profile.\n\nUpdatable fields:\n- `display_name`: User-editable name (takes precedence over Auth0 name in UI)\n- `chat_nickname`: Display name in chat\n- `live_chat_available`: Availability for live chat assignment\n- `personal_phone`: Personal phone for receiving SMS/WhatsApp notifications (user-global)\n- `preferred_language`: UI language code (en, de, fr, es, pt, it, ru, hr, tr, ar)\n- `auto_translate_enabled`: Enable automatic translation of incoming messages (user-global)\n- `shoutbox_hidden`: Hide/deactivate the team-chat Shoutbox widget across all devices (user-global)\n- `spoken_languages`: Comma-separated ISO 639-1 codes the user speaks, e.g. \"de,en\" (user-global; empty string clears)\n- `phone`: Agent's phone number for email signatures (per-tenant, requires X-Tenant-ID header)\n- `title`: Agent's job title for email signatures (per-tenant, requires X-Tenant-ID header)\n\n**Authentication:** Required (no specific scope needed)","operationId":"update_current_user_profile_api_v1_users_me_profile_patch","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateProfileRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CurrentUserResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"HTTPBearer":[]}]}},"/api/v1/users/me/tenants":{"get":{"tags":["Users"],"summary":"Get User Tenants","description":"Get all tenants the current user belongs to.\n\nReturns a list of tenants (role field will use RBAC primary role).\n\n**Authentication:** Required (no specific scope needed)","operationId":"get_user_tenants_api_v1_users_me_tenants_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"items":{"$ref":"#/components/schemas/UserTenantResponse"},"type":"array","title":"Response Get User Tenants Api V1 Users Me Tenants Get"}}}}},"security":[{"HTTPBearer":[]}]}},"/api/v1/users/me/avatar":{"post":{"tags":["Users"],"summary":"Upload User Avatar","description":"Upload a profile avatar image for the current user.\n\nReplaces any existing avatar. The uploaded image is stored in GCP Cloud\nStorage and served via a public proxy endpoint.\n\n**Constraints:**\n- File type: PNG, JPG, JPEG\n- Max size: 2MB\n\n**Authentication:** Required (no specific scope needed)","operationId":"upload_user_avatar_api_v1_users_me_avatar_post","requestBody":{"content":{"multipart/form-data":{"schema":{"$ref":"#/components/schemas/Body_upload_user_avatar_api_v1_users_me_avatar_post"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CurrentUserResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"HTTPBearer":[]}]},"delete":{"tags":["Users"],"summary":"Delete User Avatar","description":"Remove the current user's custom avatar.\n\nSets the user's profile picture to None.\n\n**Authentication:** Required (no specific scope needed)","operationId":"delete_user_avatar_api_v1_users_me_avatar_delete","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}},"security":[{"HTTPBearer":[]}]}},"/api/v1/users/invite":{"post":{"tags":["Users"],"summary":"Invite User","description":"Invite a user to join the tenant.\n\nCreates a user record with pending Auth0 ID. The user can then log in\nvia Auth0 using the invited email address.\n\nWhen ``role_id`` is supplied, the invited user is assigned that RBAC role.\nThis accepts BOTH system roles AND this tenant's own custom roles (the same\nids returned by ``GET /roles``). The inviter may only assign a role whose\nscopes they themselves possess, and only ``team:admin`` holders may assign\nsystem roles -- identical to the ``POST /permissions/users/{id}/roles`` path.\n\n**Required Scopes:** `team:create`\n\nNote: Only OWNER and ADMIN roles can invite users (enforced by scope).","operationId":"invite_user_api_v1_users_invite_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserInviteRequest"}}},"required":true},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserInviteResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"HTTPBearer":[]}]}},"/api/v1/users/{user_id}":{"get":{"tags":["Users"],"summary":"Get User","description":"Get user details by ID.\n\nUser must be a member of the current tenant.\n\n**Required Scopes:** `team:read`","operationId":"get_user_api_v1_users__user_id__get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"user_id","in":"path","required":true,"schema":{"type":"string","title":"User Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"put":{"tags":["Users"],"summary":"Update User","description":"Update user profile.\n\nUsers can only update their own profile.\n\n**Required Scopes:** `team:update`","operationId":"update_user_api_v1_users__user_id__put","security":[{"HTTPBearer":[]}],"parameters":[{"name":"user_id","in":"path","required":true,"schema":{"type":"string","title":"User Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserUpdateRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"tags":["Users"],"summary":"Remove User From Tenant","description":"Remove user from tenant.\n\nOnly OWNER and ADMIN roles can remove users.\nOnly OWNER can remove another OWNER.\nUsers cannot remove themselves.\n\n**Required Scopes:** `team:delete`","operationId":"remove_user_from_tenant_api_v1_users__user_id__delete","security":[{"HTTPBearer":[]}],"parameters":[{"name":"user_id","in":"path","required":true,"schema":{"type":"string","title":"User Id"}}],"responses":{"204":{"description":"Successful Response"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/users/{user_id}/role":{"patch":{"tags":["Users"],"summary":"Update User Role","description":"Update user's role in the tenant.\n\nOnly OWNER and ADMIN roles can update user roles.\nOnly OWNER can change another OWNER's role.\n\n**Required Scopes:** `team:update`","operationId":"update_user_role_api_v1_users__user_id__role_patch","security":[{"HTTPBearer":[]}],"parameters":[{"name":"user_id","in":"path","required":true,"schema":{"type":"string","title":"User Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserRoleUpdateRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserRoleUpdateResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/invitations":{"get":{"tags":["Invitations"],"summary":"Get tenant invitations","description":"Get all invitations for a tenant (requires ADMIN or OWNER)\n\n**🔐 Required Scopes:** `team:read`\n","operationId":"get_tenant_invitations_api_v1_invitations_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InvitationListResponse"}}}}},"security":[{"HTTPBearer":[]}]},"post":{"tags":["Invitations"],"summary":"Create invitation","description":"Invite a user to join a tenant (requires ADMIN or OWNER role)\n\n**🔐 Required Scopes:** `team:create`\n","operationId":"create_invitation_api_v1_invitations_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/InvitationCreate"}}},"required":true},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InvitationResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"HTTPBearer":[]}]}},"/api/v1/invitations/{token}/accept":{"post":{"tags":["Invitations"],"summary":"Accept invitation","description":"Accept invitation with security hardening (rate limited, email verified)","operationId":"accept_invitation_api_v1_invitations__token__accept_post","security":[{"HTTPBearer":[]}],"parameters":[{"name":"token","in":"path","required":true,"schema":{"type":"string","title":"Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InvitationAcceptResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/invitations/{token}/decline":{"post":{"tags":["Invitations"],"summary":"Decline invitation","description":"Decline an invitation to join a tenant","operationId":"decline_invitation_api_v1_invitations__token__decline_post","security":[{"HTTPBearer":[]}],"parameters":[{"name":"token","in":"path","required":true,"schema":{"type":"string","title":"Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InvitationDeclineResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/invitations/{invitation_id}/resend":{"post":{"tags":["Invitations"],"summary":"Resend invitation","description":"Resend an invitation with a new token and expiry (requires ADMIN or OWNER)\n\n**🔐 Required Scopes:** `team:create`\n","operationId":"resend_invitation_api_v1_invitations__invitation_id__resend_post","security":[{"HTTPBearer":[]}],"parameters":[{"name":"invitation_id","in":"path","required":true,"schema":{"type":"string","title":"Invitation Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InvitationResendResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/invitations/{invitation_id}":{"delete":{"tags":["Invitations"],"summary":"Delete (cancel) invitation","description":"Delete/cancel an invitation in any status (requires ADMIN or OWNER)\n\n**🔐 Required Scopes:** `team:create`\n","operationId":"delete_invitation_api_v1_invitations__invitation_id__delete","security":[{"HTTPBearer":[]}],"parameters":[{"name":"invitation_id","in":"path","required":true,"schema":{"type":"string","title":"Invitation Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeleteResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/attachments/upload":{"post":{"tags":["Attachments"],"summary":"Upload Attachment","description":"Upload a file attachment\n\nArgs:\n    file: The file to upload\n    message_id: Optional message ID to associate with\n    purpose: Optional. ``agent_test`` marks a Conversational-agent test\n        console upload: deleted after 24 h unless it gets linked to a message.\n\nReturns:\n    Attachment ID and metadata\n\nSecurity:\n    - Validates file extension against blocklist\n    - Validates file magic bytes match claimed MIME type\n    - Enforces file size limits\n\n**🔐 Required Scopes:** `messages:create`\n","operationId":"upload_attachment_api_v1_attachments_upload_post","security":[{"HTTPBearer":[]}],"parameters":[{"name":"message_id","in":"query","required":false,"schema":{"type":"string","title":"Message Id"}},{"name":"purpose","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Purpose"}}],"requestBody":{"required":true,"content":{"multipart/form-data":{"schema":{"$ref":"#/components/schemas/Body_upload_attachment_api_v1_attachments_upload_post"}}}},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AttachmentUploadResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/attachments/from-url":{"post":{"tags":["Attachments"],"summary":"Create Attachment From Url","description":"Fetch a public URL server-side and store it as an attachment.\n\nReturns the same shape as ``POST /upload`` so the response is\ninterchangeable. Caller can then pass the returned ``id`` /\n``attachment_id`` in ``MessageCreate.attachments``.\n\nPer-tenant URL dedup: if this tenant already has an attachment\nwhose ``source_url`` matches the requested ``url`` exactly, that\nrow is returned without re-fetching or re-uploading. To force a\nre-fetch, vary the URL (e.g. add ``?t=12345``).\n\nSecurity:\n    - ``http://`` / ``https://`` only — ``file://``, ``gopher://``,\n      ``data://`` etc. rejected.\n    - Hostname resolved up-front; rejected if any A/AAAA record\n      falls into loopback, RFC1918, link-local (incl. cloud\n      metadata 169.254.169.254), ULA, reserved, or multicast.\n    - Bytes validated against the same magic-byte sniffer as\n      ``/upload`` AND against a stricter MIME allowlist\n      (``URL_FETCH_ALLOWED_MIME_TYPES``).\n    - 50 MB hard cap; streaming cut-off enforced even if the\n      server lies in ``Content-Length``.\n\n**Required Scopes:** ``messages:create``","operationId":"create_attachment_from_url_api_v1_attachments_from_url_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AttachmentFromUrlRequest"}}},"required":true},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AttachmentUploadResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"HTTPBearer":[]}]}},"/api/v1/attachments/{attachment_id}":{"get":{"tags":["Attachments"],"summary":"Get Attachment","description":"Get attachment metadata\n\nArgs:\n    attachment_id: ID of the attachment\n\nReturns:\n    Attachment metadata including download URL\n\n**🔐 Required Scopes:** `messages:read`\n","operationId":"get_attachment_api_v1_attachments__attachment_id__get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"attachment_id","in":"path","required":true,"schema":{"type":"string","title":"Attachment Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/app__schemas__attachment_schema__AttachmentResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"tags":["Attachments"],"summary":"Delete Attachment","description":"Delete an attachment\n\nPermanently removes the ``attachments`` row **and** its stored blob. This\nis irreversible. Attachments are re-usable and carry no foreign key back\nto the messages that referenced them, so deleting one that a previously\nsent message still points at is allowed and expected — the message keeps\nits metadata, the media simply stops resolving.\n\nArgs:\n    attachment_id: ID of the attachment\n\nReturns:\n    ``204 No Content`` on success. ``404`` when the attachment does not\n    exist or belongs to another tenant. ``500`` when the row exists but\n    the delete itself failed (e.g. the storage backend errored) — in that\n    case the row is left intact so the call can be retried.\n\n**Required Scopes:** `messages:delete`","operationId":"delete_attachment_api_v1_attachments__attachment_id__delete","security":[{"HTTPBearer":[]}],"parameters":[{"name":"attachment_id","in":"path","required":true,"schema":{"type":"string","title":"Attachment Id"}}],"responses":{"204":{"description":"Successful Response"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/attachments/{attachment_id}/download":{"get":{"tags":["Attachments"],"summary":"Download Attachment","description":"Download an attachment file\n\nArgs:\n    attachment_id: ID of the attachment\n    disposition: Content-Disposition mode - \"inline\" (default, for viewing in\n        browser) or \"attachment\" (forces download dialog)\n\nReturns:\n    File stream\n\n**Required Scopes:** `messages:read`","operationId":"download_attachment_api_v1_attachments__attachment_id__download_get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"attachment_id","in":"path","required":true,"schema":{"type":"string","title":"Attachment Id"}},{"name":"disposition","in":"query","required":false,"schema":{"type":"string","default":"inline","title":"Disposition"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/attachments/{attachment_id}/url":{"get":{"tags":["Attachments"],"summary":"Get Attachment Url","description":"Get a signed URL for an attachment\n\nArgs:\n    attachment_id: ID of the attachment\n    expiration_minutes: URL expiration time in minutes\n\nReturns:\n    Dict with signed URL\n\n**🔐 Required Scopes:** `messages:read`\n","operationId":"get_attachment_url_api_v1_attachments__attachment_id__url_get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"attachment_id","in":"path","required":true,"schema":{"type":"string","title":"Attachment Id"}},{"name":"expiration_minutes","in":"query","required":false,"schema":{"type":"integer","default":60,"title":"Expiration Minutes"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AttachmentUrlResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/attachments/{attachment_id}/summary":{"get":{"tags":["Attachments"],"summary":"Get Attachment Summary","description":"Get the AI summary of an image or PDF attachment.\n\nReturns the summary a Conversational Agent produced while reading the file,\nor one requested via `POST /attachments/{attachment_id}/summary`. Status\n`pending` means it is still being generated, `failed` means generation\nfailed (a new `POST` may retry). The text is derived from a\ncustomer-supplied file: treat it as untrusted plain text.\n\nErrors: `403 feature_disabled` when AI features are off for the workspace;\n`404` when the attachment is unknown, not visible to you, or has no summary.\n\n**Required Scopes:** `messages:read`","operationId":"get_attachment_summary_api_v1_attachments__attachment_id__summary_get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"attachment_id","in":"path","required":true,"schema":{"type":"string","title":"Attachment Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AttachmentAISummary-Output"}}}},"403":{"description":"AI features are not enabled for this workspace (`feature_disabled`)"},"404":{"description":"Unknown / inaccessible attachment, or no summary yet (`summary_not_found`)"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"post":{"tags":["Attachments"],"summary":"Summarize Attachment","description":"Generate an AI summary of an inbound image or PDF attachment on demand.\n\nOnly for files a customer sent (inbound message), images up to 10 MB and\nPDFs up to 20 MB (the first 10 pages are read). Idempotent: if a summary\nalready exists it is returned with `200` and nothing is charged; if one is\nbeing generated you get `202` (poll `GET /attachments/{attachment_id}/summary`).\nA new summary returns `201` and charges AI credits from the workspace pool\n(image 2, PDF 3, see `credits_charged`). A failed generation returns `502`\n`summary_failed`, charges nothing and may be retried. Request body: none.\n\nErrors: `403 feature_disabled`, `404` unknown/inaccessible attachment,\n`422 attachment_not_summarizable`, `429` rate limit (30/min per principal,\n60/min per workspace), `503 rate_limit_unavailable`.\n\n**Required Scopes:** `conversations:update`","operationId":"summarize_attachment_api_v1_attachments__attachment_id__summary_post","security":[{"HTTPBearer":[]}],"parameters":[{"name":"attachment_id","in":"path","required":true,"schema":{"type":"string","title":"Attachment Id"}}],"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AttachmentSummaryActionResponse-Output"}}}},"200":{"description":"A summary already existed (no credits charged)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AttachmentSummaryActionResponse-Input"}}}},"202":{"description":"A summary is already being generated (poll GET)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AttachmentSummaryActionResponse-Input"}}}},"403":{"description":"AI features are not enabled for this workspace (`feature_disabled`)"},"404":{"description":"Unknown or inaccessible attachment"},"422":{"description":"Attachment type/size/direction not supported (`attachment_not_summarizable`)"},"429":{"description":"Rate limit exceeded (30/min per user or API key, 60/min per workspace)"},"503":{"description":"Rate limiter unavailable (`rate_limit_unavailable`), retry shortly"},"502":{"description":"Summary generation failed (`summary_failed`, no credits charged, retry allowed)"}}}},"/api/v1/conversations/{conversation_id}/notes":{"post":{"tags":["Notes"],"summary":"Create Note","description":"Create a note on a conversation.\n\n**Required Scopes:** `notes:create`\n\nNotes provide internal context for agents handling conversations.\nThey are not visible to customers.\n\nThe note is also shown in the conversation timeline as an internal note\n(the response's `message_id` is that timeline item). Internal notes are\nnever sent to the customer and never billed.","operationId":"create_note_api_v1_conversations__conversation_id__notes_post","security":[{"HTTPBearer":[]}],"parameters":[{"name":"conversation_id","in":"path","required":true,"schema":{"type":"string","title":"Conversation Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/NoteCreate"}}}},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/NoteResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"get":{"tags":["Notes"],"summary":"List Conversation Notes","description":"Get all notes for a conversation.\n\n**Required Scopes:** `notes:read`\n\n404 when the conversation does not exist in the workspace or is not\nvisible to the caller (custom inboxes).","operationId":"list_conversation_notes_api_v1_conversations__conversation_id__notes_get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"conversation_id","in":"path","required":true,"schema":{"type":"string","title":"Conversation Id"}},{"name":"page","in":"query","required":false,"schema":{"type":"integer","minimum":1,"description":"Page number (1-indexed)","default":1,"title":"Page"},"description":"Page number (1-indexed)"},{"name":"page_size","in":"query","required":false,"schema":{"type":"integer","maximum":100,"minimum":1,"description":"Items per page","default":20,"title":"Page Size"},"description":"Items per page"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/NoteListResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/notes/feed":{"get":{"tags":["Notes"],"summary":"Get Unified Notes Feed","description":"Unified notes feed: notes, internal notes from the timeline and closing\nnotes in one list, newest first.\n\n**Required Scopes:** `notes:read` (timeline-only internal notes,\n`source=internal_message`, are only included when the caller also holds\n`messages:read`)\n\n`page * page_size` must not exceed 1000 (422). For the contact scopes the\nnewest 500 conversations of the contact are searched.\n\nA note that is shown both in the notes panel and in the timeline appears\nonce. Items with `editable=false` (timeline-only internal notes, closing\nnotes) cannot be edited via `PUT /notes/{id}`.","operationId":"get_unified_notes_feed_api_v1_notes_feed_get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"conversation_id","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Conversation to show notes for (contact is derived from it)","title":"Conversation Id"},"description":"Conversation to show notes for (contact is derived from it)"},{"name":"contact_id","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Contact to show notes for (required if conversation_id is not given)","title":"Contact Id"},"description":"Contact to show notes for (required if conversation_id is not given)"},{"name":"scope","in":"query","required":false,"schema":{"enum":["conversation","contact_conversations","contact","all"],"type":"string","description":"`conversation` = notes of this conversation; `contact_conversations` = notes of all conversations of the contact; `contact` = contact-level notes; `all` = both","default":"conversation","title":"Scope"},"description":"`conversation` = notes of this conversation; `contact_conversations` = notes of all conversations of the contact; `contact` = contact-level notes; `all` = both"},{"name":"page","in":"query","required":false,"schema":{"type":"integer","minimum":1,"description":"Page number (1-indexed)","default":1,"title":"Page"},"description":"Page number (1-indexed)"},{"name":"page_size","in":"query","required":false,"schema":{"type":"integer","maximum":100,"minimum":1,"description":"Items per page","default":20,"title":"Page Size"},"description":"Items per page"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UnifiedNoteFeedResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/notes/{note_id}":{"put":{"tags":["Notes"],"summary":"Update Note","description":"Update a note.\n\n**Required Scopes:** `notes:update`\n\nOnly the note's author or a workspace owner/admin may edit it (403\notherwise). 404 when the note's conversation is not visible to the caller.","operationId":"update_note_api_v1_notes__note_id__put","security":[{"HTTPBearer":[]}],"parameters":[{"name":"note_id","in":"path","required":true,"schema":{"type":"string","title":"Note Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/NoteUpdate"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/NoteResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"tags":["Notes"],"summary":"Delete Note","description":"Delete a note.\n\n**Required Scopes:** `notes:delete`\n\nOnly the note's author or a workspace owner/admin may delete it (403\notherwise). 404 when the note's conversation is not visible to the caller.","operationId":"delete_note_api_v1_notes__note_id__delete","security":[{"HTTPBearer":[]}],"parameters":[{"name":"note_id","in":"path","required":true,"schema":{"type":"string","title":"Note Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SuccessResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/contacts/{contact_id}/notes":{"post":{"tags":["Notes"],"summary":"Create Contact Note","description":"Create a note on a contact.\n\n**Required Scopes:** `notes:create`\n\nContact notes persist across conversations and provide\nlong-term context about a customer.","operationId":"create_contact_note_api_v1_contacts__contact_id__notes_post","security":[{"HTTPBearer":[]}],"parameters":[{"name":"contact_id","in":"path","required":true,"schema":{"type":"string","title":"Contact Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/NoteCreate"}}}},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ContactNoteResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"get":{"tags":["Notes"],"summary":"List Contact Notes","description":"Get all notes for a contact.\n\n**Required Scopes:** `notes:read`","operationId":"list_contact_notes_api_v1_contacts__contact_id__notes_get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"contact_id","in":"path","required":true,"schema":{"type":"string","title":"Contact Id"}},{"name":"page","in":"query","required":false,"schema":{"type":"integer","minimum":1,"description":"Page number (1-indexed)","default":1,"title":"Page"},"description":"Page number (1-indexed)"},{"name":"page_size","in":"query","required":false,"schema":{"type":"integer","maximum":100,"minimum":1,"description":"Items per page","default":20,"title":"Page Size"},"description":"Items per page"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ContactNoteListResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/tags":{"post":{"tags":["Tags"],"summary":"Create Tag","description":"Create a new tag.\n\nTags are used to categorize contacts and conversations. Each tag must have\na unique name within the tenant.\n\n**Required Scopes:** `tags:create` (management scope: tags can grant custom-inbox access, so agents holding only contacts/conversations:update cannot create/rename/delete them)","operationId":"create_tag_api_v1_tags_post","security":[{"HTTPBearer":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TagCreate"}}}},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TagResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"get":{"tags":["Tags"],"summary":"List Tags","description":"List all tags for the tenant with pagination.\n\nReturns a paginated list of tags, optionally filtered by search term.\n\n**Required Scopes:** `contacts:read` OR `conversations:read`","operationId":"list_tags_api_v1_tags_get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"page","in":"query","required":false,"schema":{"type":"integer","minimum":1,"description":"Page number (1-indexed)","default":1,"title":"Page"},"description":"Page number (1-indexed)"},{"name":"page_size","in":"query","required":false,"schema":{"type":"integer","maximum":100,"minimum":1,"description":"Items per page","default":20,"title":"Page Size"},"description":"Items per page"},{"name":"search","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Search by tag name","title":"Search"},"description":"Search by tag name"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TagListResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/tags/{tag_id}":{"get":{"tags":["Tags"],"summary":"Get Tag","description":"Get a specific tag by ID.\n\n**Required Scopes:** `contacts:read` OR `conversations:read`","operationId":"get_tag_api_v1_tags__tag_id__get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"tag_id","in":"path","required":true,"schema":{"type":"string","title":"Tag Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TagResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"put":{"tags":["Tags"],"summary":"Update Tag","description":"Update a tag.\n\nAll fields are optional. Only provided fields will be updated.\n\n**Required Scopes:** `tags:update` (management scope: tags can grant custom-inbox access, so agents holding only contacts/conversations:update cannot create/rename/delete them)","operationId":"update_tag_api_v1_tags__tag_id__put","security":[{"HTTPBearer":[]}],"parameters":[{"name":"tag_id","in":"path","required":true,"schema":{"type":"string","title":"Tag Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TagUpdate"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TagResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"tags":["Tags"],"summary":"Delete Tag","description":"Delete a tag.\n\nThis will also remove the tag from all contacts and conversations.\n\n**Required Scopes:** `tags:delete` (management scope: tags can grant custom-inbox access, so agents holding only contacts/conversations:update cannot create/rename/delete them)","operationId":"delete_tag_api_v1_tags__tag_id__delete","security":[{"HTTPBearer":[]}],"parameters":[{"name":"tag_id","in":"path","required":true,"schema":{"type":"string","title":"Tag Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SuccessResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/conversations/{conversation_id}/tags/{tag_id}":{"post":{"tags":["Tags"],"summary":"Add Tag To Conversation","description":"Add a tag to a conversation.\n\nIf the tag is already assigned to the conversation, this operation is idempotent.\n\n**Required Scopes:** `conversations:update`","operationId":"add_tag_to_conversation_api_v1_conversations__conversation_id__tags__tag_id__post","security":[{"HTTPBearer":[]}],"parameters":[{"name":"conversation_id","in":"path","required":true,"schema":{"type":"string","title":"Conversation Id"}},{"name":"tag_id","in":"path","required":true,"schema":{"type":"string","title":"Tag Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TagAssignmentResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"tags":["Tags"],"summary":"Remove Tag From Conversation","description":"Remove a tag from a conversation.\n\n**Required Scopes:** `conversations:update`","operationId":"remove_tag_from_conversation_api_v1_conversations__conversation_id__tags__tag_id__delete","security":[{"HTTPBearer":[]}],"parameters":[{"name":"conversation_id","in":"path","required":true,"schema":{"type":"string","title":"Conversation Id"}},{"name":"tag_id","in":"path","required":true,"schema":{"type":"string","title":"Tag Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TagAssignmentResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/contacts/{contact_id}/tags/{tag_id}":{"post":{"tags":["Tags"],"summary":"Add Tag To Contact","description":"Add a tag to a contact.\n\nIf the tag is already assigned to the contact, this operation is idempotent.\n\n**Required Scopes:** `contacts:update`","operationId":"add_tag_to_contact_api_v1_contacts__contact_id__tags__tag_id__post","security":[{"HTTPBearer":[]}],"parameters":[{"name":"contact_id","in":"path","required":true,"schema":{"type":"string","title":"Contact Id"}},{"name":"tag_id","in":"path","required":true,"schema":{"type":"string","title":"Tag Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TagAssignmentResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"tags":["Tags"],"summary":"Remove Tag From Contact","description":"Remove a tag from a contact.\n\n**Required Scopes:** `contacts:update`","operationId":"remove_tag_from_contact_api_v1_contacts__contact_id__tags__tag_id__delete","security":[{"HTTPBearer":[]}],"parameters":[{"name":"contact_id","in":"path","required":true,"schema":{"type":"string","title":"Contact Id"}},{"name":"tag_id","in":"path","required":true,"schema":{"type":"string","title":"Tag Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TagAssignmentResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/custom-fields":{"post":{"tags":["Custom Fields"],"summary":"Create Custom Field","description":"Create a new custom field definition.\n\nCustom fields extend the contact model with tenant-specific attributes.\nOnce created, the field `key` and `field_type` cannot be changed.\n\n**Required Scopes:** `settings:update`\n\n**Field Types:**\n- `text`: Free-form text\n- `number`: Numeric values\n- `boolean`: True/false\n- `date`: Date only\n- `datetime`: Date and time\n- `select`: Single selection from options\n- `multiselect`: Multiple selections from options\n- `email`: Email address\n- `phone`: Phone number\n- `url`: Website URL\n- `location`: Latitude/longitude\n- `collection`: Array of values\n\n**Validation (optional):** `validation` holds value constraints, type-applicable\nkeys only — `number`: `min`/`max`; `text`/`email`/`phone`/`url`:\n`min_length`/`max_length`/`pattern`; `date`/`datetime`: `date_min`/`date_max`\n(`YYYY-MM-DD` or `today`). Other keys/types → 422 `invalid_validation`.\n\n**AI access:** `ai_access` is `none` (default: AI agents never see the field),\n`read`, `read_write` (agents may update any value) or `fill_if_empty`\n(agents may only fill an empty value).\n`location` / `collection` fields accept only `none` / `read` (422\n`invalid_ai_access`).","operationId":"create_custom_field_api_v1_custom_fields_post","security":[{"HTTPBearer":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CustomFieldDefinitionCreate"}}}},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CustomFieldDefinitionResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"get":{"tags":["Custom Fields"],"summary":"List Custom Fields","description":"List all custom field definitions for the tenant.\n\nReturns a paginated list of custom field definitions ordered by display_order.\n\nCustom field definitions describe contact attributes, so reading them is\ngated by `contacts:read` (NOT `settings:read`). This lets contact-facing\nroles (e.g. Support Agent) render the custom-fields card without being\ngranted broad tenant-settings read access. Write operations remain gated by\n`settings:update` / `settings:admin`.\n\n**Required Scopes:** `contacts:read`","operationId":"list_custom_fields_api_v1_custom_fields_get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"page","in":"query","required":false,"schema":{"type":"integer","minimum":1,"description":"Page number (1-indexed)","default":1,"title":"Page"},"description":"Page number (1-indexed)"},{"name":"page_size","in":"query","required":false,"schema":{"type":"integer","maximum":100,"minimum":1,"description":"Items per page","default":20,"title":"Page Size"},"description":"Items per page"},{"name":"active_only","in":"query","required":false,"schema":{"type":"boolean","description":"Only return active fields","default":true,"title":"Active Only"},"description":"Only return active fields"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PaginatedResponse_CustomFieldDefinitionResponse_"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/custom-fields/{field_id}":{"get":{"tags":["Custom Fields"],"summary":"Get Custom Field","description":"Get a specific custom field definition.\n\nGated by `contacts:read` (custom fields describe contact attributes), NOT\n`settings:read`. Write operations remain gated by `settings:update` /\n`settings:admin`.\n\n**Required Scopes:** `contacts:read`","operationId":"get_custom_field_api_v1_custom_fields__field_id__get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"field_id","in":"path","required":true,"schema":{"type":"string","title":"Field Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CustomFieldDefinitionResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"put":{"tags":["Custom Fields"],"summary":"Update Custom Field","description":"Update a custom field definition.\n\nNote: The `key` and `field_type` cannot be changed after creation\nto preserve data integrity.\n\n`validation`: send an object to replace the value constraints, `null` to\nclear them, or omit it to keep them (see create for the allowed keys).\n\n`ai_hint` / `option_descriptions`: same semantics (object/string replaces,\n`null` clears, omitted keeps). `option_descriptions` keys must be options\nof the field after this update (422 `invalid_option_descriptions`). When\n`options` change and `option_descriptions` is omitted, descriptions of\nremoved options are dropped.\n\n`ai_access`: `none` / `read` / `read_write` / `fill_if_empty`; omit to keep,\n`null` resets to `none`. Writable modes on `location` / `collection` fields →\n422 `invalid_ai_access`.\n\n**Required Scopes:** `settings:update`","operationId":"update_custom_field_api_v1_custom_fields__field_id__put","security":[{"HTTPBearer":[]}],"parameters":[{"name":"field_id","in":"path","required":true,"schema":{"type":"string","title":"Field Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CustomFieldDefinitionUpdate"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CustomFieldDefinitionResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"tags":["Custom Fields"],"summary":"Delete Custom Field","description":"Delete (deactivate) a custom field definition.\n\nThe field is soft-deleted (marked inactive) to preserve existing data.\nInactive fields are not shown in the UI and cannot be assigned new values.\n\n**Required Scopes:** `settings:admin`","operationId":"delete_custom_field_api_v1_custom_fields__field_id__delete","security":[{"HTTPBearer":[]}],"parameters":[{"name":"field_id","in":"path","required":true,"schema":{"type":"string","title":"Field Id"}}],"responses":{"204":{"description":"Successful Response"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/contacts/{contact_id}/fields/{field_id}":{"post":{"tags":["Custom Fields"],"summary":"Set Contact Field Value","description":"Set a custom field value for a contact.\n\nThe value is validated against the field's type and options (if applicable).\nFor SELECT/MULTISELECT fields, values that don't match the defined options\nare rejected with 400 (matching is case-insensitive; the canonical option\nspelling is stored). If the field has `validation` constraints, a value\noutside them is rejected with 422 (`code: validation_failed`).\n\n**Required Scopes:** `contacts:update`","operationId":"set_contact_field_value_api_v1_contacts__contact_id__fields__field_id__post","security":[{"HTTPBearer":[]}],"parameters":[{"name":"contact_id","in":"path","required":true,"schema":{"type":"string","title":"Contact Id"}},{"name":"field_id","in":"path","required":true,"schema":{"type":"string","title":"Field Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SetFieldValueRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SuccessResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/contacts/{contact_id}/fields":{"get":{"tags":["Custom Fields"],"summary":"Get Contact Field Values","description":"Get all custom field values for a contact.\n\nReturns all custom fields with their current values for the specified contact.\n\n**Required Scopes:** `contacts:read`","operationId":"get_contact_field_values_api_v1_contacts__contact_id__fields_get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"contact_id","in":"path","required":true,"schema":{"type":"string","title":"Contact Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ContactFieldValuesResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/contact-tokens":{"post":{"tags":["Contact Tokens"],"summary":"Create Contact Token","description":"Create a contact token for the live chat widget\n\nMints a short-lived, opaque token (`s7ct_…`) for ONE contact that is\nredeemable only on `widget_id`. Pass it to the widget as\n`SendSevenConfig.contactToken`; the chat then starts bound to that contact\nand AI agents receive the exposed fields and `context` as verified data.\n\nContact resolution (first hit wins): `contact.external_id` mapping →\n`contact_id` → a contact holding one of the `methods` you mark\n`verified: true` (claims never match) → created when `create_if_missing`\nis true (requires `contacts:create`).\n\nMethods (`email`, `phone`, `whatsapp`) are attached to the contact;\n`verified: true` stores them as verified, `false` as an unverified claim\n(an already-verified method is never downgraded). A method held by a\ndifferent contact is a conflict: `on_conflict: \"error\"` (default) returns\n409 `identifier_conflict` and changes nothing; `on_conflict: \"merge\"`\nmerges those contacts into this one (only when your method is\n`verified: true` AND the other contact's copy is verified too; any other\nconflict is not attached and is listed in `skipped`). Merge requires\n`contacts:update`.\n\n`save` / `save_if_empty` write standard fields (`name`, `first_name`,\n`last_name`, `language`) or active custom fields to the contact now\n(`save_if_empty` only fills empty fields). `context` is shared with the\nchat only and never stored on the contact. Everything is one\ntransaction.\n\nLegacy shape: scalar `contact.email` / `contact.phone` and `expose` keep\nworking (`expose` is deprecated).\n\nThe token is returned **only in this response**.\n\nErrors: 403 `insufficient_scope`; 404 `widget_not_found` /\n`contact_not_found`; 409 `identifier_conflict` / `merge_failed`; 422\n`invalid_method_type`, `invalid_method_value`, `invalid_on_conflict`,\n`unknown_field`, `invalid_field_value`, `invalid_save`,\n`unknown_custom_field`, `unknown_standard_field`, `context_too_large`,\n`invalid_ttl`, `invalid_max_redemptions`, `invalid_contact`; 429\n`rate_limited` (120 per minute per credential and per workspace).\n\nHigh-privilege scope: not grantable to OAuth apps.\n\n**Required Scopes:** `contact_tokens:create`, `contacts:read` (+ `contacts:create` for `create_if_missing`, + `contacts:update` to change an existing contact: attach methods, link `external_id`, `save`, or `on_conflict: \"merge\"`)","operationId":"create_contact_token_api_v1_contact_tokens_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ContactTokenCreate"}}},"required":true},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ContactTokenResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"HTTPBearer":[]}]}},"/api/v1/contact-tokens/{token_id}":{"delete":{"tags":["Contact Tokens"],"summary":"Revoke Contact Token","description":"Revoke a contact token\n\nThe token can no longer be redeemed and every chat session context created\nfrom it stops being used immediately. Idempotent: revoking an already\nrevoked token succeeds.\n\n**Required Scopes:** `contact_tokens:create`","operationId":"revoke_contact_token_api_v1_contact_tokens__token_id__delete","security":[{"HTTPBearer":[]}],"parameters":[{"name":"token_id","in":"path","required":true,"schema":{"type":"string","title":"Token Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeleteResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/contacts/{contact_id}/contact-tokens/revoke":{"post":{"tags":["Contact Tokens"],"summary":"Revoke Contact Tokens For Contact","description":"Revoke all contact tokens of a contact\n\nUse it on logout or when a user's access changes. Every unrevoked token of\nthe contact is revoked and their chat session context stops being used.\n\n**Required Scopes:** `contact_tokens:create`","operationId":"revoke_contact_tokens_for_contact_api_v1_contacts__contact_id__contact_tokens_revoke_post","security":[{"HTTPBearer":[]}],"parameters":[{"name":"contact_id","in":"path","required":true,"schema":{"type":"string","title":"Contact Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ContactTokenRevokeResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/context-endpoints":{"get":{"tags":["Context Endpoints"],"summary":"List Context Endpoints","description":"List context endpoints (signing secrets are never included, only a 4-char hint)\n\n**Required Scopes:** `settings:read`","operationId":"list_context_endpoints_api_v1_context_endpoints_get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"page","in":"query","required":false,"schema":{"type":"integer","minimum":1,"default":1,"title":"Page"}},{"name":"page_size","in":"query","required":false,"schema":{"type":"integer","maximum":100,"minimum":1,"default":50,"title":"Page Size"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ContextEndpointListResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"post":{"tags":["Context Endpoints"],"summary":"Create Context Endpoint","description":"Create a context endpoint. The response contains the signing secret ONCE.\n\n`timeout_ms` is clamped to 200-3000, `ttl_seconds` to 0-3600. The URL must\nbe https and must not point to a private network.\n\n**Required Scopes:** `settings:update` (not available to OAuth apps)","operationId":"create_context_endpoint_api_v1_context_endpoints_post","security":[{"HTTPBearer":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ContextEndpointCreate"}}}},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ContextEndpointWithSecretResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/context-endpoints/{endpoint_id}":{"get":{"tags":["Context Endpoints"],"summary":"Get Context Endpoint","description":"Get one context endpoint (without its secret)\n\n**Required Scopes:** `settings:read`","operationId":"get_context_endpoint_api_v1_context_endpoints__endpoint_id__get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"endpoint_id","in":"path","required":true,"schema":{"type":"string","title":"Endpoint Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ContextEndpointResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"patch":{"tags":["Context Endpoints"],"summary":"Update Context Endpoint","description":"Update a context endpoint (partial; `response_map: null` clears the map)\n\nChanging `url` also requires `contacts:read`, is not available to OAuth\napps, and ROTATES the signing secret: the response then contains the new\n`secret` (shown once). Without a URL change no `secret` is returned.\n\n**Required Scopes:** `settings:update` (+ `contacts:read` when `url` changes)","operationId":"update_context_endpoint_api_v1_context_endpoints__endpoint_id__patch","security":[{"HTTPBearer":[]}],"parameters":[{"name":"endpoint_id","in":"path","required":true,"schema":{"type":"string","title":"Endpoint Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ContextEndpointUpdate"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"anyOf":[{"$ref":"#/components/schemas/ContextEndpointWithSecretResponse"},{"$ref":"#/components/schemas/ContextEndpointResponse"}],"title":"Response Update Context Endpoint Api V1 Context Endpoints  Endpoint Id  Patch"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"tags":["Context Endpoints"],"summary":"Delete Context Endpoint","description":"Delete a context endpoint. Agents using it stop calling it immediately.\n\n**Required Scopes:** `settings:update`","operationId":"delete_context_endpoint_api_v1_context_endpoints__endpoint_id__delete","security":[{"HTTPBearer":[]}],"parameters":[{"name":"endpoint_id","in":"path","required":true,"schema":{"type":"string","title":"Endpoint Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeleteResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/context-endpoints/{endpoint_id}/rotate-secret":{"post":{"tags":["Context Endpoints"],"summary":"Rotate Context Endpoint Secret","description":"Generate a new signing secret. The old secret stops working immediately.\n\n**Required Scopes:** `settings:update` (not available to OAuth apps)","operationId":"rotate_context_endpoint_secret_api_v1_context_endpoints__endpoint_id__rotate_secret_post","security":[{"HTTPBearer":[]}],"parameters":[{"name":"endpoint_id","in":"path","required":true,"schema":{"type":"string","title":"Endpoint Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ContextEndpointWithSecretResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/context-endpoints/{endpoint_id}/test":{"post":{"tags":["Context Endpoints"],"summary":"Test Context Endpoint","description":"Call the endpoint once (no cache) for a contact and return what the agent\nwould see after the response map, plus the request body that was sent.\nWorks on disabled endpoints too.\n\n**Required Scopes:** `settings:update` + `contacts:read`","operationId":"test_context_endpoint_api_v1_context_endpoints__endpoint_id__test_post","security":[{"HTTPBearer":[]}],"parameters":[{"name":"endpoint_id","in":"path","required":true,"schema":{"type":"string","title":"Endpoint Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ContextEndpointTestRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ContextEndpointTestResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/verifications":{"post":{"tags":["Contact Verification"],"summary":"Create Verification","description":"Start a contact verification\n\nSends a 6-digit one-time code (method `email`, `sms` or `whatsapp`) or\nopens a PIN check (method `pin`, nothing is sent). Pass exactly one of\n`contact_id` or `identifier`.\n\nThe code always goes to a contact method **on record**: with `contact_id`\nto that contact's matching method (verified first, then primary, then\nnewest); with an `identifier` that belongs to a contact, to that stored\nmethod. An identifier that is not on record receives the code itself\n(proof of possession) and is attached as a verified method on approval.\nThe response never contains the destination.\n\nCodes are valid for 10 minutes and allow 5 attempts. Each code sent is\nbilled as one message on its channel.\n\n**PIN self-unlock** (`purpose: \"pin_unlock\"`): when a contact's PIN is\nlocked until reset (3 lockouts within 24 hours), send a code to one of\nthe contact's **verified** email addresses or phone numbers. Use method\n`email`, `sms`, `whatsapp` or `pin_unlock_auto` (picks email, then SMS,\nthen WhatsApp). An `identifier` only selects the contact; the code is\nnever sent to it. Approving the code unlocks the PIN and fires\n`contact_pin.self_unlocked`; it does not verify identity. Errors: 409\n`pin_not_locked`, 409 `no_verified_method`.\n\nErrors: 404 `contact_not_found` / `conversation_not_found`; 422\n`invalid_request`, `invalid_identifier`, `method_identifier_mismatch`,\n`method_not_enabled`, `no_method_on_record`, `destination_not_allowed`\n(with `reason`: `country_not_allowed`, `not_mobile` or `invalid_number`;\nSMS codes go only to mobile numbers in your allowed countries within the\nEU/EEA, WhatsApp codes to numbers in your allowed countries);\n402 `insufficient_sms_balance`;\n409 `no_whatsapp_auth_template` / `channel_unavailable`; 429 `rate_limited`;\n502 `send_failed` (the claim is `failed`).\n\n**Required Scopes:** `verifications:create`","operationId":"create_verification_api_v1_verifications_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/VerificationCreate"}}},"required":true},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/VerificationResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"HTTPBearer":[]}]}},"/api/v1/verifications/{verification_id}/check":{"post":{"tags":["Contact Verification"],"summary":"Check Verification","description":"Check a verification code or PIN\n\nReturns `approved`, `pending` (wrong code, with fewer `attempts_remaining`),\n`expired` or `locked` (after the 5th wrong attempt). Checking is free.\nSend `code` for email/SMS/WhatsApp verifications and `pin` for PIN checks.\n\n**Required Scopes:** `verifications:create`","operationId":"check_verification_api_v1_verifications__verification_id__check_post","security":[{"HTTPBearer":[]}],"parameters":[{"name":"verification_id","in":"path","required":true,"schema":{"type":"string","title":"Verification Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/VerificationCheck"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/VerificationCheckResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/verifications/{verification_id}/resend":{"post":{"tags":["Contact Verification"],"summary":"Resend Verification","description":"Resend a new code for the same verification\n\nAllowed 60 seconds after the last send, at most 5 times. A resend issues\na new code (the old one stops working) and restarts the 10-minute\nvalidity.\n\nErrors: 429 `resend_too_soon` (with `retry_after`), 409\n`resend_limit_reached`, 409 `not_pending`, 429 `rate_limited`, 422\n`destination_not_allowed` (with `reason`, when the allowed countries\nchanged since the code was first sent).\n\n**Required Scopes:** `verifications:create`","operationId":"resend_verification_api_v1_verifications__verification_id__resend_post","security":[{"HTTPBearer":[]}],"parameters":[{"name":"verification_id","in":"path","required":true,"schema":{"type":"string","title":"Verification Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/VerificationResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/verifications/{verification_id}/cancel":{"post":{"tags":["Contact Verification"],"summary":"Cancel Verification","description":"Cancel a pending verification\n\n**Required Scopes:** `verifications:create`","operationId":"cancel_verification_api_v1_verifications__verification_id__cancel_post","security":[{"HTTPBearer":[]}],"parameters":[{"name":"verification_id","in":"path","required":true,"schema":{"type":"string","title":"Verification Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/VerificationResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/verifications/{verification_id}":{"get":{"tags":["Contact Verification"],"summary":"Get Verification","description":"Get a verification's status\n\n**Required Scopes:** `verifications:read`","operationId":"get_verification_api_v1_verifications__verification_id__get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"verification_id","in":"path","required":true,"schema":{"type":"string","title":"Verification Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/VerificationResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/verification-settings":{"get":{"tags":["Contact Verification"],"summary":"Get Verification Settings","description":"Get contact verification settings\n\n`enabled_methods` defaults to `[\"email\"]`. `allowed_countries` is the\neffective list of destination countries for SMS and WhatsApp codes (the\nworkspace country unless you set a list); `sms_country_cap` lists the\ncountries SMS codes can ever reach (EU/EEA) and `eu_countries` the EU\nmember states.\n\n**Required Scopes:** `settings:read`","operationId":"get_verification_settings_api_v1_verification_settings_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/VerificationSettingsResponse"}}}}},"security":[{"HTTPBearer":[]}]},"put":{"tags":["Contact Verification"],"summary":"Update Verification Settings","description":"Update contact verification settings\n\nFields you omit are kept. `email_sender` is `generic` or an active mailbox id; `sms_sender` is `generic` or\nan active channel id of that type. `allowed_countries` takes ISO 3166 alpha-2 codes (`null` resets to\nthe workspace country); codes outside the EU/EEA only apply to WhatsApp.\n`agent_new_number_daily_cap` (0-200, `null` = default 10, 0 = none) limits how many codes the AI\nagent may send per day to phone numbers that are not on file. Errors: 422\n`invalid_email_sender`, `invalid_channel`, `invalid_template`, `invalid_enabled_methods`,\n`invalid_allowed_countries`, `invalid_agent_new_number_daily_cap`.\n\n**Required Scopes:** `verification_settings:update`","operationId":"update_verification_settings_api_v1_verification_settings_put","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/VerificationSettingsUpdate"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/VerificationSettingsResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"HTTPBearer":[]}]}},"/api/v1/verification-settings/whatsapp-template":{"post":{"tags":["Contact Verification"],"summary":"Create Whatsapp Auth Template","description":"Create a WhatsApp authentication template for verification codes\n\nSubmits a compliant AUTHENTICATION template (copy-code button, security\nrecommendation, 10-minute expiry) to Meta for the WhatsApp channel and\nstores it in the settings. Meta reviews it; codes can be sent once it is\napproved.\n\n**Required Scopes:** `verification_settings:update`","operationId":"create_whatsapp_auth_template_api_v1_verification_settings_whatsapp_template_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WhatsAppAuthTemplateCreate"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WhatsAppAuthTemplateResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"HTTPBearer":[]}]}},"/api/v1/contacts/{contact_id}/pin":{"get":{"tags":["Contact Verification"],"summary":"Get Contact Pin Status","description":"Get a contact's PIN status (never the PIN itself)\n\nReturns `{is_set, locked, locked_until, escalated, set_at}`. After 5 wrong\nPINs in a row the PIN is locked for 1 hour (`locked_until`) and unlocks\nautomatically; the 3rd lock within 24 hours sets `escalated` and the PIN\nstays locked until it is reset.\n\n**Required Scopes:** `contact_pins:manage` or `contacts:read`","operationId":"get_contact_pin_status_api_v1_contacts__contact_id__pin_get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"contact_id","in":"path","required":true,"schema":{"type":"string","title":"Contact Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ContactPinStatus"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"put":{"tags":["Contact Verification"],"summary":"Set Contact Pin","description":"Set or replace a contact's identification PIN\n\n4 to 12 digits. Replacing a PIN also clears a lockout. Error: 422 `invalid_pin`.\n\n**Required Scopes:** `contact_pins:manage`","operationId":"set_contact_pin_api_v1_contacts__contact_id__pin_put","security":[{"HTTPBearer":[]}],"parameters":[{"name":"contact_id","in":"path","required":true,"schema":{"type":"string","title":"Contact Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ContactPinSet"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ContactPinActionResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"tags":["Contact Verification"],"summary":"Delete Contact Pin","description":"Remove a contact's PIN\n\nError: 404 `pin_not_set`.\n\n**Required Scopes:** `contact_pins:manage`","operationId":"delete_contact_pin_api_v1_contacts__contact_id__pin_delete","security":[{"HTTPBearer":[]}],"parameters":[{"name":"contact_id","in":"path","required":true,"schema":{"type":"string","title":"Contact Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ContactPinActionResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/contacts/{contact_id}/pin/reset-lockout":{"post":{"tags":["Contact Verification"],"summary":"Reset Contact Pin Lockout","description":"Reset a PIN lockout (temporary or escalated)\n\nClears the lock, the failed-attempt counter and the 24-hour lockout history.\n\nError: 404 `pin_not_set`.\n\n**Required Scopes:** `contact_pins:manage`","operationId":"reset_contact_pin_lockout_api_v1_contacts__contact_id__pin_reset_lockout_post","security":[{"HTTPBearer":[]}],"parameters":[{"name":"contact_id","in":"path","required":true,"schema":{"type":"string","title":"Contact Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ContactPinActionResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/conversations/{conversation_id}/contact-context":{"get":{"tags":["Conversations"],"summary":"Get Conversation Contact Context","description":"Get the contact context an AI agent sees for this conversation\n\nReturns `assurance`, the active contact-token `grant` (or null), `fields`\n(`key`, `name`, `value`, `source`, `verified`), `methods`\n(`type`, `value`, `verified`), `tags` (`name`, `verified`; ALL of the\ncontact's tags, always `verified: false`) and `subscriptions` (`list`, `channel_type`, `status`,\n`opt_in_method`, `verified`). `tags` / `subscriptions` are `[]` when unavailable.\n\n**Required Scopes:** `conversations:read` + `contacts:read`","operationId":"get_conversation_contact_context_api_v1_conversations__conversation_id__contact_context_get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"conversation_id","in":"path","required":true,"schema":{"type":"string","title":"Conversation Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/campaigns":{"post":{"tags":["Campaigns"],"summary":"Create Campaign","description":"Create a new campaign\n\nWith `scheduled_at` the campaign is created as `scheduled` and must pass\nthe same checks as `POST /campaigns/{id}/schedule`: a large WhatsApp\ncampaign on a number that is still warming up is refused with HTTP 409\n`warmup_acknowledgement_required` unless `acknowledge_warmup_warning` is\ntrue, and a split plan that runs too long is refused with HTTP 422\n`split_plan_too_long`. Nothing is created in either case.\n\n**Send speed and batches:** `send_rate_limit` (messages per minute,\n1-60000) and `send_batch_size` (recipients per batch, at least 1) are\noptional. Rejected values return HTTP 422 `{code, message}` with `code`\none of `send_rate_limit_out_of_range`, `send_batch_size_invalid`,\n`send_pacing_conflicts_with_split` (cannot be combined with\n`send_split_config`) or `send_pacing_unavailable` (batches are not\navailable for this account yet).\n\n**Audience:** send `list_ids` (saved lists) OR `audience_conditions`\n(Audience Builder tree, same format as a dynamic list's\n`segment_conditions`). The tree must be a top-level `AND` with at least\none positive list membership (`has_optin`, `value: true`, a static or\nnewsletter list of this workspace); the campaign's lists are then\nderived from those membership lists and `list_ids` is ignored. Rejected\ntrees return HTTP 422 `{code, message}`: `audience_list_membership_required`,\n`audience_membership_list_invalid`, `audience_conditions_invalid`.\n\n**Required Scopes:** `campaigns:create`","operationId":"create_campaign_api_v1_campaigns_post","security":[{"HTTPBearer":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CampaignCreate"}}}},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CampaignResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"get":{"tags":["Campaigns"],"summary":"List Campaigns","description":"List all campaigns for the tenant\n\n**Required Scopes:** `campaigns:read`","operationId":"list_campaigns_api_v1_campaigns_get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"campaign_status","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Campaign Status"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","default":50,"title":"Limit"}},{"name":"offset","in":"query","required":false,"schema":{"type":"integer","default":0,"title":"Offset"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/CampaignResponse"},"title":"Response List Campaigns Api V1 Campaigns Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/campaigns/variables":{"get":{"tags":["Campaigns"],"summary":"Get Personalization Variables","description":"Get available personalization variables for campaign messages.\n\nReturns a list of variable placeholders (e.g. ``{{contact.name}}``)\nthat can be used in campaign message text. At send time, these placeholders\nare automatically replaced with the recipient contact's data.\n\nIncludes the tenant's active custom field definitions as\n``{{contact.custom.<key>}}`` variables.\n\n**Required Scopes:** `campaigns:read`","operationId":"get_personalization_variables_api_v1_campaigns_variables_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"items":{"additionalProperties":{"type":"string"},"type":"object"},"type":"array","title":"Response Get Personalization Variables Api V1 Campaigns Variables Get"}}}}},"security":[{"HTTPBearer":[]}]}},"/api/v1/campaigns/channel-media-specs":{"get":{"tags":["Campaigns"],"summary":"Get Channel Media Specs","description":"Get per-channel media specifications for campaign attachments.\n\nReturns supported file types, size limits, and format constraints\nfor each messaging channel. Useful for the campaign wizard to show\nupload constraints before the user selects files.\n\n**🔐 Required Scopes:** `campaigns:read`\n","operationId":"get_channel_media_specs_api_v1_campaigns_channel_media_specs_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ChannelMediaSpecsResponse"}}}}},"security":[{"HTTPBearer":[]}]}},"/api/v1/campaigns/unsubscribe-counts":{"get":{"tags":["Campaigns"],"summary":"Get Campaign Unsubscribe Counts","description":"Batch opt-out counts for the messenger campaigns INDEX page.\n\nONE grouped query over the ``contact_subscription_events`` ledger for the\ngiven campaign ids (tenant-scoped). Per campaign: ``total`` opt-outs,\n``exact`` (link/manual — definite attribution) and ``estimated`` (keyword\nheuristic). Campaigns with no opt-outs are OMITTED — treat a missing id as 0.\n\n**Required Scopes:** `campaigns:read`","operationId":"get_campaign_unsubscribe_counts_api_v1_campaigns_unsubscribe_counts_get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"ids","in":"query","required":true,"schema":{"type":"string","description":"Comma-separated campaign ids (max 200)","title":"Ids"},"description":"Comma-separated campaign ids (max 200)"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CampaignUnsubscribeCountsResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/campaigns/{campaign_id}":{"get":{"tags":["Campaigns"],"summary":"Get Campaign","description":"Get a specific campaign\n\n**Required Scopes:** `campaigns:read`","operationId":"get_campaign_api_v1_campaigns__campaign_id__get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"campaign_id","in":"path","required":true,"schema":{"type":"string","title":"Campaign Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CampaignResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"put":{"tags":["Campaigns"],"summary":"Update Campaign","description":"Update a campaign\n\n`send_split_config` (WhatsApp only) sets split sending; explicit `null`\nclears it. Rejected with 422 `{code, message}`: `invalid_split_config`,\n`split_sending_requires_whatsapp`, `split_config_not_editable`. An\n`approved` campaign may change ONLY `send_split_config`,\n`send_rate_limit` and `send_batch_size`.\n\n`send_rate_limit` / `send_batch_size`: explicit `null` clears the value,\nomitting the key leaves it unchanged. A campaign that is `sending`,\n`throttled`, `paused` or `failed` may change ONLY these two fields (the\nnext messages and the next batch use the new values); switching them on\nfor a campaign that is already sending moves its unsent recipients into\nbatch 1 at the new speed. Rejected with 422 `{code, message}`:\n`send_rate_limit_out_of_range`, `send_batch_size_invalid`,\n`send_pacing_conflicts_with_split`, `send_pacing_unavailable`,\n`send_pacing_not_editable`.\n\n`audience_conditions` (Audience Builder tree): omit to leave unchanged,\nexplicit `null` clears it (the campaign keeps its current lists). While a\ntree is set, the campaign's lists are derived from its membership lists\nand any `list_ids` sent are ignored. Editable only while the campaign is\n`draft` or `scheduled`. Rejected with 422 `{code, message}`:\n`audience_list_membership_required`, `audience_membership_list_invalid`,\n`audience_conditions_invalid`.\n\n**Required Scopes:** `campaigns:update`","operationId":"update_campaign_api_v1_campaigns__campaign_id__put","security":[{"HTTPBearer":[]}],"parameters":[{"name":"campaign_id","in":"path","required":true,"schema":{"type":"string","title":"Campaign Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CampaignUpdate"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CampaignResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"tags":["Campaigns"],"summary":"Delete Campaign","description":"Delete a campaign (draft or scheduled only).\n\nA campaign that has already sent messages (for example contacts enrolled\nfrom HubSpot) cannot be deleted: HTTP 400. Sent emails of an email\ncampaign are kept for billing before the delete.\n\n**Required Scopes:** `campaigns:delete`","operationId":"delete_campaign_api_v1_campaigns__campaign_id__delete","security":[{"HTTPBearer":[]}],"parameters":[{"name":"campaign_id","in":"path","required":true,"schema":{"type":"string","title":"Campaign Id"}}],"responses":{"204":{"description":"Successful Response"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/campaigns/{campaign_id}/segments":{"post":{"tags":["Campaigns"],"summary":"Add Segment","description":"Add a segment filter to a campaign\n\n**Required Scopes:** `campaigns:update`","operationId":"add_segment_api_v1_campaigns__campaign_id__segments_post","security":[{"HTTPBearer":[]}],"parameters":[{"name":"campaign_id","in":"path","required":true,"schema":{"type":"string","title":"Campaign Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SegmentCreate"}}}},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SegmentResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/campaigns/preview-segment":{"post":{"tags":["Campaigns"],"summary":"Preview Segment","description":"Preview contacts that would be included in a segment (for UI testing)\n\n``limit`` (default 10) is capped at 100; a larger value is a 422.\nReturns contact names/phones/emails, so it needs `contacts:read` too.\n\n**Required Scopes:** `campaigns:read` AND `contacts:read`","operationId":"preview_segment_api_v1_campaigns_preview_segment_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SegmentPreviewRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Preview Segment Api V1 Campaigns Preview Segment Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"HTTPBearer":[]}]}},"/api/v1/campaigns/{campaign_id}/estimate-cost":{"get":{"tags":["Campaigns"],"summary":"Estimate Campaign Cost","description":"Estimate campaign cost and check balance before sending\n\n**Required Scopes:** `campaigns:read`\n\nReturns:\n    - total_cost_eur: RAW usage cost in EUR (before the included pool)\n    - recipient_count: Number of recipients\n    - breakdown: Cost breakdown by channel ({count, cost})\n    - warnings: List of warnings about variable costs\n    - can_send: Whether campaign can be sent based on balance\n    - balance_error: Error message if insufficient balance\n    - current_balance_eur: Current balance in EUR\n\nIncluded-pool fields (base_fee plans only; 0 / null on legacy\n`minimum_commit` plans or plans with no included pool -- the UI gates the\npool UI on `included_messages_total > 0` and otherwise headlines\n`total_cost_eur`):\n    - included_messages_total: effective pooled allowance this month (int)\n    - included_messages_used: pooled units already consumed this month (int)\n    - included_messages_remaining: max(0, total - used) (int)\n    - projected_sends: billable send units this campaign draws (int)\n    - sends_covered_by_pool: min(projected_sends, remaining) (int)\n    - sends_over_pool: max(0, projected_sends - remaining) (int)\n    - payable_overage_eur: EUR actually payable for THIS campaign after the\n      pool absorbs what it can (0.00 when fully covered; null when no pool\n      applies -- use total_cost_eur). Excludes Meta/WhatsApp per-conversation\n      template fees, which Meta bills separately.\n    - fully_covered: bool, true when sends_over_pool == 0\n    - pool_is_shared: bool, true when the pool is shared across the account\n      (human wording is authored on the frontend for i18n)\n\nRCS Newsletter (MAU) agents (only present when the campaign sends via one):\n    - rcs_newsletter: {recipient_count, sendseven_fee_eur: 0.0, billing: \"mau\"}.\n      These recipients carry NO per-message fee and draw NO pool units, so\n      they are excluded from total_cost_eur / breakdown / projected_sends\n      (recipient_count still includes them). Their only cost is the monthly\n      active user price from the RCS balance -- see `GET /rcs-estimate`.","operationId":"estimate_campaign_cost_api_v1_campaigns__campaign_id__estimate_cost_get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"campaign_id","in":"path","required":true,"schema":{"type":"string","title":"Campaign Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Estimate Campaign Cost Api V1 Campaigns  Campaign Id  Estimate Cost Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/campaigns/{campaign_id}/send":{"post":{"tags":["Campaigns"],"summary":"Send Campaign","description":"Send a campaign immediately\n\n**Required Scopes:** `campaigns:send`\n\nOptional body `{\"acknowledge_warmup_warning\": true}` (no body = false).\nA large WhatsApp campaign on a number that is still warming up is refused\nwith HTTP 409 `detail = {\"code\": \"warmup_acknowledgement_required\",\n\"message\": ..., \"warmup\": <warm-up assessment>}` unless acknowledged or\nsplit over days (`send_split_config`). Nothing is prepared or sent on 409.\nA split plan that would run too long is refused with HTTP 422\n`{\"code\": \"split_plan_too_long\", \"message\": ...}`.\n\nWith `send_split_config`, only day 1's batch is sent now; later batches\ngo out automatically (the response `split` block has `days_total`,\n`next_batch_at`, `scheduled_count`).\n\n**Response (200):** `{success, campaign_id, recipient_count,\npublished_count, dispatch, split, trial_recipient_cap, message}`. Messages\nare sent in the background after the response. `published_count` is the\nnumber of recipients queued to be sent now (all of them, or day 1 of a\nsplit plan). `dispatch` is `\"queued\"` when that number is above 0, and\n`\"pending\"` when nothing is due yet or queuing is retried automatically.\n\n**Send speed and batches:** with `send_rate_limit` and/or\n`send_batch_size` saved on the campaign, messages go out at the chosen\nspeed and, with a batch size, only the first batch is sent now. The\nresponse then also has `pacing: {paced, batch_number, armed, remaining,\nsend_rate_limit, send_batch_size}` (`armed` = recipients in batch 1,\n`remaining` = recipients waiting for later batches; `published_count` is\n0 because the messages are released gradually). After each batch the\ncampaign is `paused` with `throttle_reason = \"batch_complete\"`; send the\nnext one with `POST /campaigns/{id}/send-next-batch`. Invalid saved\nvalues are refused with HTTP 422 `{code, message}` (see\n`PUT /campaigns/{id}`).\n\nOnly campaigns with status `draft`, `approved` or `scheduled` can be sent.\n\n**Note:** Campaigns with status `in_review` or `rejected` cannot be sent\n(HTTP 400). A campaign that is already sending, throttled, completed,\npaused, failed or cancelled is refused with HTTP 409 -- re-posting here\nwould re-prepare the whole recipient list and send the campaign a second\ntime. Use `POST /campaigns/{id}/resume` for a paused or failed campaign.","operationId":"send_campaign_api_v1_campaigns__campaign_id__send_post","security":[{"HTTPBearer":[]}],"parameters":[{"name":"campaign_id","in":"path","required":true,"schema":{"type":"string","title":"Campaign Id"}}],"requestBody":{"content":{"application/json":{"schema":{"anyOf":[{"$ref":"#/components/schemas/app__schemas__campaign_schema__SendCampaignRequest"},{"type":"null"}],"title":"Send Request"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Send Campaign Api V1 Campaigns  Campaign Id  Send Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/campaigns/{campaign_id}/pause":{"post":{"tags":["Campaigns"],"summary":"Pause Campaign","description":"Pause a sending campaign\n\nAlso allowed while a split-sending campaign waits for its next daily batch\n(`throttled` with `throttle_reason = \"daily_batch_limit\"`). A paused\ncampaign never resumes on its own -- use resume.\n\n**Required Scopes:** `campaigns:send`","operationId":"pause_campaign_api_v1_campaigns__campaign_id__pause_post","security":[{"HTTPBearer":[]}],"parameters":[{"name":"campaign_id","in":"path","required":true,"schema":{"type":"string","title":"Campaign Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CampaignResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/campaigns/{campaign_id}/resume":{"post":{"tags":["Campaigns"],"summary":"Resume Campaign","description":"Resume a paused or failed campaign\n\nFor split-sending campaigns the remaining recipients are re-planned from\nnow: today's batch is sent, later batches continue on the following days.\n\nOptional body `{\"acknowledge_warmup_warning\": true}` (no body = false).\nA campaign the scheduler paused with `throttle_reason =\n\"warmup_acknowledgement_required\"` (its warm-up acknowledgement was\nmissing or cleared by an edit), and any campaign resumed before its\nrecipients were prepared, passes the same WhatsApp warm-up gate as\n`POST /send`: HTTP 409 `warmup_acknowledgement_required` unless\nacknowledged; nothing changes on 409. Such a campaign is then dispatched\nlike a fresh send (recipients prepared, day 1 of a split plan published).\nA campaign held because its audience grew since the acknowledgement\n(`warmup_assessment.hold.kind = \"audience_grew\"`) always needs\n`acknowledge_warmup_warning: true`. A successful resume clears\n`warmup_assessment.hold`.\n\nResume continues the recipients that are still pending. It does not\nre-send failed recipients; use `POST /campaigns/{id}/retry-failed` for\nthat. Sending continues in the background after the response.\n\nCampaigns sent in batches: a campaign paused after finishing a batch\n(`throttle_reason = \"batch_complete\"`) sends its next batch at the saved\nbatch size (the same as `POST /campaigns/{id}/send-next-batch` with an\nempty body). Any other pause continues the current batch at the saved\nsend speed. A campaign sent in batches that ended as `completed` or\n`failed` while recipients were still waiting is re-opened the same way.\nWhile sending at a set speed or in batches is briefly unavailable, a\ncampaign that uses it returns HTTP 422 `send_pacing_unavailable`.\n\n**Required Scopes:** `campaigns:send`","operationId":"resume_campaign_api_v1_campaigns__campaign_id__resume_post","security":[{"HTTPBearer":[]}],"parameters":[{"name":"campaign_id","in":"path","required":true,"schema":{"type":"string","title":"Campaign Id"}}],"requestBody":{"content":{"application/json":{"schema":{"anyOf":[{"$ref":"#/components/schemas/ResumeCampaignRequest"},{"type":"null"}],"title":"Resume Request"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CampaignResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/campaigns/{campaign_id}/send-next-batch":{"post":{"tags":["Campaigns"],"summary":"Send Next Batch","description":"Send the next batch of a campaign sent in batches\n\nOnly for a campaign that finished a batch: `status = \"paused\"` with\n`throttle_reason = \"batch_complete\"`, or one that ended as `completed` /\n`failed` while recipients were still waiting (it continues its unfinished\nbatch first, if any). Anything else returns HTTP 409\n`{\"code\": \"campaign_not_awaiting_batch\", \"message\": ...}` (also when two\nrequests race: only the first one sends a batch). HTTP 422\n`send_pacing_unavailable` while batch sending is briefly unavailable.\n\nBody (optional): `{\"batch_size\": 500}` sends 500 recipients now and saves\n500 as the campaign's batch size (1-1,000,000; anything else is HTTP 422\n`{\"code\": \"send_batch_size_invalid\", \"message\": ...}`); `{\"all_remaining\": true}` sends\neveryone who is still waiting. An empty body sends the next batch at the\nsaved batch size. Recipients who opted out, unsubscribed or were blocked\nsince the campaign was prepared are skipped (`skipped_ineligible`).\n\n**Response (200):** `{batch_number, armed, skipped_ineligible, remaining,\ncampaign}`. The batch is sent in the background at the campaign's send\nspeed; the campaign pauses again when the batch is done (or completes\nwhen nobody is left).\n\n**Required Scopes:** `campaigns:send`","operationId":"send_next_batch_api_v1_campaigns__campaign_id__send_next_batch_post","security":[{"HTTPBearer":[]}],"parameters":[{"name":"campaign_id","in":"path","required":true,"schema":{"type":"string","title":"Campaign Id"}}],"requestBody":{"content":{"application/json":{"schema":{"anyOf":[{"$ref":"#/components/schemas/CampaignSendNextBatchRequest"},{"type":"null"}],"title":"Batch Request"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SendNextBatchResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/campaigns/{campaign_id}/batches":{"get":{"tags":["Campaigns"],"summary":"List Campaign Batches","description":"Per-batch delivery counts of a campaign sent in batches\n\nOne item per batch: `{batch_number, total, pending, sent, delivered,\nread, failed, skipped}`, oldest batch first. `remaining` is the number of\nrecipients waiting for a later batch, `current_batch_number` the batch\nthe campaign is on and `send_batch_size` the saved batch size. A campaign\nthat is not sent in batches returns an empty list.\n\n**Required Scopes:** `campaigns:read`","operationId":"list_campaign_batches_api_v1_campaigns__campaign_id__batches_get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"campaign_id","in":"path","required":true,"schema":{"type":"string","title":"Campaign Id"}},{"name":"page","in":"query","required":false,"schema":{"type":"integer","minimum":1,"description":"Page number (1-indexed)","default":1,"title":"Page"},"description":"Page number (1-indexed)"},{"name":"page_size","in":"query","required":false,"schema":{"type":"integer","maximum":200,"minimum":1,"description":"Batches per page","default":50,"title":"Page Size"},"description":"Batches per page"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CampaignBatchListResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/campaigns/{campaign_id}/send-plan":{"get":{"tags":["Campaigns"],"summary":"Get Campaign Send Plan","description":"Send plan: speed, batches and estimated duration\n\nRead-only estimate for the campaign's audience at its saved send speed\nand batch size, or at the values given as query parameters (to preview a\nchange before saving it). `effective_rate_per_minute` is the requested\nspeed capped by what the campaign's channels can send, and\n`rate_limited_by` says which of the two decides (`\"campaign\"` or\n`\"channel\"`). `channels[]` lists each channel's recipients and maximum\nmessages per second. `eta_seconds` estimates the time to send everyone\nwho is left (time paused between batches not included);\n`eta_seconds_per_batch` the time per batch. `pacing_available = false`\nmeans send speed and batches are not available for this account yet.\n\nOut-of-range query values return HTTP 422 `{code, message}`\n(`send_rate_limit_out_of_range`, `send_batch_size_invalid`).\n\n**Required Scopes:** `campaigns:read`","operationId":"get_campaign_send_plan_api_v1_campaigns__campaign_id__send_plan_get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"campaign_id","in":"path","required":true,"schema":{"type":"string","title":"Campaign Id"}},{"name":"send_rate_limit","in":"query","required":false,"schema":{"anyOf":[{"type":"integer"},{"type":"null"}],"description":"Preview this send speed (messages per minute, 1-60000) instead of the saved one","title":"Send Rate Limit"},"description":"Preview this send speed (messages per minute, 1-60000) instead of the saved one"},{"name":"send_batch_size","in":"query","required":false,"schema":{"anyOf":[{"type":"integer"},{"type":"null"}],"description":"Preview this batch size (>= 1) instead of the saved one","title":"Send Batch Size"},"description":"Preview this batch size (>= 1) instead of the saved one"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SendPlanResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/campaigns/{campaign_id}/retry-failed":{"post":{"tags":["Campaigns"],"summary":"Retry Failed Recipients","description":"Retry failed recipients of a campaign\n\nResets FAILED recipients back to pending and re-queues them for sending.\nDesigned for \"top up, then retry\": an SMS campaign that was paused because\nthe prepaid SMS balance was used up can be re-driven with\n``{\"error_codes\": [\"insufficient_sms_balance\"]}`` once credit is available.\nSMS recipients the carrier rejected or did not deliver are re-driven with\n``{\"error_codes\": [\"sms_rejected_by_carrier\"]}`` / ``[\"sms_not_delivered\"]``\n(e.g. after the cause of an automatic pause was fixed).\nSMS recipients that failed because the SMS provider temporarily limited\nthe sending rate are re-driven with ``{\"error_codes\": [\"sms_rate_limited\"]}``\n-- lower the campaign's `send_rate_limit` or set a `send_batch_size`\nfirst (`PUT /campaigns/{id}`) so the retry does not hit the limit again.\nWith no body (or an empty list) every failed recipient is retried.\nUnknown codes return HTTP 422 listing the allowed codes.\n\nAllowed on campaigns in status `completed`, `failed` or `paused`. A\n`completed` or `failed` campaign returns to `sending` and any recipients\nstill pending are re-queued as well. A `paused` campaign STAYS paused: the\nfailed recipients are reset and go out together with the rest when the\ncampaign is resumed (`POST /campaigns/{id}/resume`). A retry never\nresumes a paused campaign.\n\n**Response:** `{\"success\": true, \"id\": \"<campaign_id>\", \"requeued\": <n>}`\nwhere `requeued` is the number of failed recipients reset. When the\ncampaign was paused and stays paused the body also carries\n`\"paused\": true` (the field is omitted otherwise).\n\n**Errors:**\n* HTTP 429 `{\"code\": \"retry_failed_cooldown\", \"message\", \"retry_after_seconds\"}`\n  (plus a `Retry-After` header) when failed recipients of this campaign\n  were retried less than 10 minutes ago.\n* HTTP 422 `{\"code\": \"retry_requires_send_speed\", \"message\"}` when the\n  campaign was paused because the SMS provider limited the sending rate\n  and has no send speed / batch size yet: set `send_rate_limit` or\n  `send_batch_size` with `PUT /campaigns/{id}` first.\n\nFor a split-sending campaign the reset recipients join the remaining\nplan, re-planned from now like `POST /resume`: only today's batch is sent\nimmediately, the rest follows on the next days.\n\nFor a campaign sent in batches the reset recipients join the current\nbatch and are sent at the campaign's send speed.\n\n**Required Scopes:** `campaigns:send`","operationId":"retry_failed_recipients_api_v1_campaigns__campaign_id__retry_failed_post","security":[{"HTTPBearer":[]}],"parameters":[{"name":"campaign_id","in":"path","required":true,"schema":{"type":"string","title":"Campaign Id"}}],"requestBody":{"content":{"application/json":{"schema":{"anyOf":[{"$ref":"#/components/schemas/RetryFailedRequest"},{"type":"null"}],"title":"Request"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Retry Failed Recipients Api V1 Campaigns  Campaign Id  Retry Failed Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/campaigns/{campaign_id}/cancel":{"post":{"tags":["Campaigns"],"summary":"Cancel Campaign","description":"Cancel a scheduled, sending or paused campaign\n\nRecipients that were not sent yet are not sent (for a campaign sent in\nbatches this includes everyone waiting for a later batch). Messages\nalready sent are not recalled.\n\n**Required Scopes:** `campaigns:send`","operationId":"cancel_campaign_api_v1_campaigns__campaign_id__cancel_post","security":[{"HTTPBearer":[]}],"parameters":[{"name":"campaign_id","in":"path","required":true,"schema":{"type":"string","title":"Campaign Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CampaignResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/campaigns/{campaign_id}/schedule":{"post":{"tags":["Campaigns"],"summary":"Schedule Campaign","description":"Schedule a campaign for future sending.\n\nAccepts a local datetime and IANA timezone string. The backend converts to UTC\nfor storage. The original timezone is preserved for display purposes.\n\nThe scheduled time must be at least 5 minutes in the future.\n\n`acknowledge_warmup_warning` (default false): same WhatsApp warm-up rule as\n`POST /send` -- 409 `warmup_acknowledgement_required` when required and not\ngiven. The acknowledgement is stored; the scheduled dispatch does not\nre-ask -- unless the audience, content, channels, template or split config\nis edited afterwards: that clears the acknowledgement and the dispatch then\nholds the campaign `paused` with `throttle_reason =\n\"warmup_acknowledgement_required\"` until `POST /resume` acknowledges.\nA split plan that would run too long: 422 `split_plan_too_long`.\n\n**Required Scopes:** `campaigns:update`","operationId":"schedule_campaign_api_v1_campaigns__campaign_id__schedule_post","security":[{"HTTPBearer":[]}],"parameters":[{"name":"campaign_id","in":"path","required":true,"schema":{"type":"string","title":"Campaign Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ScheduleCampaignRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CampaignResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/campaigns/{campaign_id}/cancel-schedule":{"post":{"tags":["Campaigns"],"summary":"Cancel Schedule","description":"Cancel a scheduled campaign and move it back to draft.\n\nThis clears the scheduled_at time and reverts the status to draft,\nallowing the user to edit and reschedule.\n\n**Required Scopes:** `campaigns:update`","operationId":"cancel_schedule_api_v1_campaigns__campaign_id__cancel_schedule_post","security":[{"HTTPBearer":[]}],"parameters":[{"name":"campaign_id","in":"path","required":true,"schema":{"type":"string","title":"Campaign Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CampaignResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/campaigns/{campaign_id}/statistics":{"get":{"tags":["Campaigns"],"summary":"Get Campaign Statistics","description":"Get campaign statistics (sent, delivered, failed, etc.)\n\n**Required Scopes:** `campaigns:read`","operationId":"get_campaign_statistics_api_v1_campaigns__campaign_id__statistics_get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"campaign_id","in":"path","required":true,"schema":{"type":"string","title":"Campaign Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Get Campaign Statistics Api V1 Campaigns  Campaign Id  Statistics Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/campaigns/{campaign_id}/link-performance":{"get":{"tags":["Campaigns"],"summary":"Get Campaign Link Performance","description":"Get aggregate link performance for a Reach (messenger) campaign.\n\nRolls per-recipient child rows up under their template via\n``COALESCE(parent_link_id, id)``. Each row shows total clicks, unique\nrecipients (per_recipient mode only), first/last click timestamps,\nand the template's short URL.\n\n**Required Scopes:** `campaigns:read`\n\nReturns:\n    ``{\"items\": [...]}`` — list of links sorted by clicks DESC. Empty\n    list if the campaign has no tracked links yet.","operationId":"get_campaign_link_performance_api_v1_campaigns__campaign_id__link_performance_get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"campaign_id","in":"path","required":true,"schema":{"type":"string","title":"Campaign Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Get Campaign Link Performance Api V1 Campaigns  Campaign Id  Link Performance Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/campaigns/{campaign_id}/unsubscribes":{"get":{"tags":["Campaigns"],"summary":"Get Campaign Unsubscribes","description":"List recipients who opted out attributed to THIS messenger campaign.\n\nCampaign-attributed opt-outs from the ``contact_subscription_events`` SHARD\nledger, split by source and confidence:\n\n* LINK opt-outs (``method='link'``, ``attribution='exact'``) — recipient used\n  this campaign's own unsubscribe surface; attribution is certain.\n* KEYWORD opt-outs (``method='keyword'``, ``attribution='heuristic'``) — a\n  \"stop\"-style keyword opt-out attributed to the last campaign on that channel\n  within ``window_hours``. Only keyword opt-outs the heuristic matched to THIS\n  campaign appear here.\n\n``method_counts`` and ``attribution_counts`` cover the full result set;\n``items`` is the requested page ordered by opt-out time DESC\n(``page``/``page_size``, default 25). The UI labels link=exact vs\nkeyword=approximate off each item's ``attribution``.\n\n**Required Scopes:** `campaigns:read`","operationId":"get_campaign_unsubscribes_api_v1_campaigns__campaign_id__unsubscribes_get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"campaign_id","in":"path","required":true,"schema":{"type":"string","title":"Campaign Id"}},{"name":"page","in":"query","required":false,"schema":{"type":"integer","minimum":1,"default":1,"title":"Page"}},{"name":"page_size","in":"query","required":false,"schema":{"type":"integer","maximum":100,"minimum":1,"default":25,"title":"Page Size"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/app__schemas__campaign_schema__CampaignUnsubscribesResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/campaigns/{campaign_id}/recipients":{"get":{"tags":["Campaigns"],"summary":"List Campaign Recipients","description":"List a campaign's recipients (messenger/newsletter) with delivery status.\n\nGroups: `successful` = sent/delivered/read, `failed` = failed (skipped\nexcluded), `all` = the union of both, `held` = waiting for a later batch\n(campaigns sent in batches; not part of `all`). Each item:\n`{id, contact_id, name, identifier, status, error_reason, sent_at,\ndelivered_at, failed_at}`.\n\n**Required Scopes:** `campaigns:read`","operationId":"list_campaign_recipients_api_v1_campaigns__campaign_id__recipients_get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"campaign_id","in":"path","required":true,"schema":{"type":"string","title":"Campaign Id"}},{"name":"status","in":"query","required":false,"schema":{"type":"string","description":"Recipient group: failed | successful | all | held","default":"all","title":"Status"},"description":"Recipient group: failed | successful | all | held"},{"name":"page","in":"query","required":false,"schema":{"type":"integer","minimum":1,"description":"Page number (1-indexed)","default":1,"title":"Page"},"description":"Page number (1-indexed)"},{"name":"page_size","in":"query","required":false,"schema":{"type":"integer","maximum":100,"minimum":1,"description":"Items per page","default":50,"title":"Page Size"},"description":"Items per page"},{"name":"search","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Search recipient handle, contact name or phone","title":"Search"},"description":"Search recipient handle, contact name or phone"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CampaignRecipientListResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/campaigns/{campaign_id}/recipients/{recipient_id}/retry":{"post":{"tags":["Campaigns"],"summary":"Retry Campaign Recipient","description":"Retry a single failed recipient of a messenger/newsletter campaign.\n\nCalls the retry execution in ``CampaignService.retry_single_recipient`` and\nreturns the updated recipient item so the UI can update the row inline.\n\n- Recipient not found -> 404\n- Recipient not retryable in its current state -> 409\n\n**Required Scopes:** `campaigns:send`","operationId":"retry_campaign_recipient_api_v1_campaigns__campaign_id__recipients__recipient_id__retry_post","security":[{"HTTPBearer":[]}],"parameters":[{"name":"campaign_id","in":"path","required":true,"schema":{"type":"string","title":"Campaign Id"}},{"name":"recipient_id","in":"path","required":true,"schema":{"type":"string","title":"Recipient Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CampaignRecipientItem"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/campaigns/{campaign_id}/recipients/{recipient_id}":{"delete":{"tags":["Campaigns"],"summary":"Delete Campaign Recipient","description":"Remove a single recipient from a messenger/newsletter campaign.\n\nHard-deletes the per-recipient row (tenant + campaign ownership validated),\nthen recomputes the campaign's denormalized counters and drops\n`total_recipients` by 1. A recipient who was already sent the message\n(sent / delivered / read) cannot be removed: HTTP 409.\n\n**Required Scopes:** `campaigns:update`","operationId":"delete_campaign_recipient_api_v1_campaigns__campaign_id__recipients__recipient_id__delete","security":[{"HTTPBearer":[]}],"parameters":[{"name":"campaign_id","in":"path","required":true,"schema":{"type":"string","title":"Campaign Id"}},{"name":"recipient_id","in":"path","required":true,"schema":{"type":"string","title":"Recipient Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeleteResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/campaigns/{campaign_id}/recipients/remove-failed":{"post":{"tags":["Campaigns"],"summary":"Remove Failed Campaign Recipients","description":"Bulk-remove all failed recipients from a messenger/newsletter campaign.\n\nHard-deletes every failed-group row for this campaign, recomputes the\ncampaign counters and drops `total_recipients` by the number removed. This\nis DISTINCT from `POST /{campaign_id}/retry-failed`, which re-queues failed\nrecipients instead of deleting them.\n\n**Required Scopes:** `campaigns:update`","operationId":"remove_failed_campaign_recipients_api_v1_campaigns__campaign_id__recipients_remove_failed_post","security":[{"HTTPBearer":[]}],"parameters":[{"name":"campaign_id","in":"path","required":true,"schema":{"type":"string","title":"Campaign Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RemoveFailedRecipientsResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/campaigns/{campaign_id}/recipients/{recipient_id}/unsubscribe":{"post":{"tags":["Campaigns"],"summary":"Unsubscribe Campaign Recipient","description":"Opt a messenger/newsletter recipient's contact out of this campaign's lists.\n\nThis is DISTINCT from `DELETE /{campaign_id}/recipients/{recipient_id}`\n(\"Remove\"), which only deletes the per-recipient row and does NOT change\nsubscription state. This endpoint resolves the recipient's contact, computes\nthe campaign's originating list(s) (`campaign_lists` ∩ the contact's\ncurrently-subscribed lists) and opts the contact out of all of them via the\nsubscription chokepoint, writing the opt-out ledger with campaign attribution\n(`method='manual'`).\n\n- Recipient (or its campaign) not found for this tenant -> 404\n- Recipient is an anonymous subscriber with no linked contact -> 422\n- Contact subscribed to none of the campaign's lists (or the campaign targets\n  no lists) -> 200 with an empty `unsubscribed_lists`\n- Contact already opted out of all of the campaign's lists -> 200 with\n  `already_unsubscribed: true`\n\n**Required Scopes:** `campaigns:update`","operationId":"unsubscribe_campaign_recipient_api_v1_campaigns__campaign_id__recipients__recipient_id__unsubscribe_post","security":[{"HTTPBearer":[]}],"parameters":[{"name":"campaign_id","in":"path","required":true,"schema":{"type":"string","title":"Campaign Id"}},{"name":"recipient_id","in":"path","required":true,"schema":{"type":"string","title":"Recipient Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RecipientUnsubscribeResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/media-gallery":{"get":{"tags":["Media Gallery"],"summary":"List Media Assets","description":"List gallery assets (excludes archived by default).\n\nReturns the unified ``{items, pagination}`` shape. Each item carries BOTH\nthe durable ``attachment_id`` and a fresh ``url``. Every filter —\n``archived``, ``favorites_only``, ``media_type``, ``tag`` and ``query``\nsearch — composes with AND and with pagination (e.g.\n``?archived=only&favorites_only=true`` = archived favorites).\n\n**Required Scopes:** `media_gallery:read`","operationId":"list_media_assets_api_v1_media_gallery_get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"query","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Match display_name or description","title":"Query"},"description":"Match display_name or description"},{"name":"tag","in":"query","required":false,"schema":{"anyOf":[{"type":"array","items":{"type":"string"}},{"type":"null"}],"description":"Filter by tag. Repeat the param for multi-tag AND (asset must carry every tag), e.g. ?tag=logo&tag=2024.","title":"Tag"},"description":"Filter by tag. Repeat the param for multi-tag AND (asset must carry every tag), e.g. ?tag=logo&tag=2024."},{"name":"media_type","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Filter by media type: image | video | document","title":"Media Type"},"description":"Filter by media type: image | video | document"},{"name":"favorites_only","in":"query","required":false,"schema":{"type":"boolean","description":"When true, return only assets marked as favorite.","default":false,"title":"Favorites Only"},"description":"When true, return only assets marked as favorite."},{"name":"archived","in":"query","required":false,"schema":{"$ref":"#/components/schemas/MediaArchivedFilter","description":"Archive-status view: 'active' (default) = non-archived only (unchanged behavior); 'only' = archived-only (the dedicated Archived tab, for recovering an accidentally-archived asset); 'all' = unified active + archived.","default":"active"},"description":"Archive-status view: 'active' (default) = non-archived only (unchanged behavior); 'only' = archived-only (the dedicated Archived tab, for recovering an accidentally-archived asset); 'all' = unified active + archived."},{"name":"page","in":"query","required":false,"schema":{"type":"integer","minimum":1,"default":1,"title":"Page"}},{"name":"page_size","in":"query","required":false,"schema":{"type":"integer","maximum":100,"minimum":1,"default":20,"title":"Page Size"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"post":{"tags":["Media Gallery"],"summary":"Create Media Asset","description":"Save an already-stored attachment into the gallery.\n\nRe-verifies the attachment belongs to the caller's tenant before insert\n(never trusts a client-supplied ``attachment_id``); derives\n``media_type``/``content_type``/``file_size`` from the attachment and\nvalidates the type/size caps.\n\n**Required Scopes:** `media_gallery:create`","operationId":"create_media_asset_api_v1_media_gallery_post","security":[{"HTTPBearer":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MediaGalleryCreateRequest"}}}},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MediaGalleryAssetResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/media-gallery/{asset_id}":{"patch":{"tags":["Media Gallery"],"summary":"Update Media Asset","description":"Update a gallery asset. Providing ``tags`` REPLACES the full tag set.\n\n**Required Scopes:** `media_gallery:update`","operationId":"update_media_asset_api_v1_media_gallery__asset_id__patch","security":[{"HTTPBearer":[]}],"parameters":[{"name":"asset_id","in":"path","required":true,"schema":{"type":"string","title":"Asset Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MediaGalleryUpdateRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MediaGalleryAssetResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"tags":["Media Gallery"],"summary":"Delete Media Asset","description":"Hard-delete a gallery asset (tags cascade). The underlying attachment is\nNOT deleted.\n\n**Required Scopes:** `media_gallery:delete`","operationId":"delete_media_asset_api_v1_media_gallery__asset_id__delete","security":[{"HTTPBearer":[]}],"parameters":[{"name":"asset_id","in":"path","required":true,"schema":{"type":"string","title":"Asset Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MediaGalleryDeleteResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/media-gallery/{asset_id}/favorite":{"post":{"tags":["Media Gallery"],"summary":"Favorite Media Asset","description":"Mark a gallery asset as a favorite (idempotent one-tap toggle).\n\nReturns the updated asset. Surface favorites via the list endpoint's\n``?favorites_only=true`` filter.\n\n**Required Scopes:** `media_gallery:update`","operationId":"favorite_media_asset_api_v1_media_gallery__asset_id__favorite_post","security":[{"HTTPBearer":[]}],"parameters":[{"name":"asset_id","in":"path","required":true,"schema":{"type":"string","title":"Asset Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MediaGalleryAssetResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"tags":["Media Gallery"],"summary":"Unfavorite Media Asset","description":"Remove a gallery asset's favorite mark (idempotent one-tap toggle).\n\nReturns the updated asset.\n\n**Required Scopes:** `media_gallery:update`","operationId":"unfavorite_media_asset_api_v1_media_gallery__asset_id__favorite_delete","security":[{"HTTPBearer":[]}],"parameters":[{"name":"asset_id","in":"path","required":true,"schema":{"type":"string","title":"Asset Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MediaGalleryAssetResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/media-gallery/{asset_id}/thumbnail":{"post":{"tags":["Media Gallery"],"summary":"Request Media Thumbnail","description":"Lazily generate a thumbnail for an asset that has none (generate-on-view).\n\nIdempotent and cheap: if the asset already has a thumbnail (or is a\ndocument) this just returns it. Otherwise it de-dupes concurrent callers\n(Redis claim) and enqueues a single background generation job, returning the\nasset immediately with its current ``url`` — the generated ``thumbnail_url``\nappears on the next list/refresh. Safe for the gallery/picker to call once\nper visible thumbnail-less card.\n\n**Required Scopes:** `media_gallery:read`","operationId":"request_media_thumbnail_api_v1_media_gallery__asset_id__thumbnail_post","security":[{"HTTPBearer":[]}],"parameters":[{"name":"asset_id","in":"path","required":true,"schema":{"type":"string","title":"Asset Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MediaGalleryAssetResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/scheduler/sends":{"post":{"tags":["Scheduler"],"summary":"Create Scheduled Send","description":"Create a scheduled send.\n\nThe send fires once at ``send_at`` (UTC) and transitions to ``completed``,\nunless ``recurrence_rule`` is set — in which case the row stays in\n``scheduled`` and ``next_run_at`` advances per the rule.\n\n**Required Scopes:** `scheduler:write`","operationId":"create_scheduled_send_api_v1_scheduler_sends_post","security":[{"HTTPBearer":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateScheduledSendRequest"}}}},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ScheduledSendResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"get":{"tags":["Scheduler"],"summary":"List Scheduled Sends","description":"List scheduled sends for the current tenant.\n\n**Required Scopes:** `scheduler:read`","operationId":"list_scheduled_sends_api_v1_scheduler_sends_get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"status","in":"query","required":false,"schema":{"anyOf":[{"$ref":"#/components/schemas/ScheduledSendStatus"},{"type":"null"}],"description":"Filter by status (scheduled, running, completed, failed, canceled)","title":"Status"},"description":"Filter by status (scheduled, running, completed, failed, canceled)"},{"name":"channel_type","in":"query","required":false,"schema":{"anyOf":[{"$ref":"#/components/schemas/ChannelType"},{"type":"null"}],"description":"Filter by channel type","title":"Channel Type"},"description":"Filter by channel type"},{"name":"contact_id","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Filter to scheduled sends for a specific contact","title":"Contact Id"},"description":"Filter to scheduled sends for a specific contact"},{"name":"page","in":"query","required":false,"schema":{"type":"integer","minimum":1,"description":"Page number (1-indexed)","default":1,"title":"Page"},"description":"Page number (1-indexed)"},{"name":"page_size","in":"query","required":false,"schema":{"type":"integer","maximum":100,"minimum":1,"description":"Items per page","default":50,"title":"Page Size"},"description":"Items per page"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ScheduledSendListResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/scheduler/sends/{send_id}":{"get":{"tags":["Scheduler"],"summary":"Get Scheduled Send","description":"Fetch a single scheduled send.\n\n**Required Scopes:** `scheduler:read`","operationId":"get_scheduled_send_api_v1_scheduler_sends__send_id__get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"send_id","in":"path","required":true,"schema":{"type":"string","title":"Send Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ScheduledSendResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"patch":{"tags":["Scheduler"],"summary":"Update Scheduled Send","description":"Update a scheduled send.\n\nEditable fields: ``payload``, ``send_at``, ``recurrence_rule``. Pass an\nempty string for ``recurrence_rule`` to clear it. Only allowed while the\nsend is in status ``scheduled`` — returns 409 for any other state.\n\nUpdating ``send_at`` also resets ``next_run_at`` to the same value so the\nworker picks up the new schedule on the next tick.\n\n**Required Scopes:** `scheduler:write`","operationId":"update_scheduled_send_api_v1_scheduler_sends__send_id__patch","security":[{"HTTPBearer":[]}],"parameters":[{"name":"send_id","in":"path","required":true,"schema":{"type":"string","title":"Send Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateScheduledSendRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ScheduledSendResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"tags":["Scheduler"],"summary":"Cancel Scheduled Send","description":"Cancel a scheduled send (soft-cancel — sets status to ``canceled``).\n\nIdempotent: cancelling an already-cancelled / completed / failed send\nreturns the same shape rather than 404. The row is preserved for audit.\n\n**Required Scopes:** `scheduler:write`","operationId":"cancel_scheduled_send_api_v1_scheduler_sends__send_id__delete","security":[{"HTTPBearer":[]}],"parameters":[{"name":"send_id","in":"path","required":true,"schema":{"type":"string","title":"Send Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/scheduler/rules":{"post":{"tags":["Scheduler"],"summary":"Create Scheduled Rule","description":"Create a scheduled rule.\n\nThree rule kinds are supported:\n\n  - ``birthday``: materializes one send per contact whose birthday falls\n    in the upcoming window. Requires ``time_of_day_local``; ``timezone``\n    defaults to per-contact-local when NULL. Must NOT set\n    ``recurrence_rule``.\n  - ``recurring_cron``: evaluates ``recurrence_rule`` (RRULE) against the\n    rule's ``segment_filter`` audience. ``recurrence_rule`` is required.\n  - ``segment_blast``: one-shot send to a segment at\n    ``next_evaluation_at``. ``recurrence_rule`` must be NULL.\n\n**Required Scopes:** `scheduler:write`","operationId":"create_scheduled_rule_api_v1_scheduler_rules_post","security":[{"HTTPBearer":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateScheduledRuleRequest"}}}},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ScheduledRuleResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"get":{"tags":["Scheduler"],"summary":"List Scheduled Rules","description":"List scheduled rules for the current tenant.\n\n**Required Scopes:** `scheduler:read`","operationId":"list_scheduled_rules_api_v1_scheduler_rules_get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"status","in":"query","required":false,"schema":{"anyOf":[{"$ref":"#/components/schemas/ScheduledRuleStatus"},{"type":"null"}],"description":"Filter by status (active, paused, archived)","title":"Status"},"description":"Filter by status (active, paused, archived)"},{"name":"kind","in":"query","required":false,"schema":{"anyOf":[{"$ref":"#/components/schemas/ScheduledRuleKind"},{"type":"null"}],"description":"Filter by rule kind (birthday, recurring_cron, segment_blast)","title":"Kind"},"description":"Filter by rule kind (birthday, recurring_cron, segment_blast)"},{"name":"page","in":"query","required":false,"schema":{"type":"integer","minimum":1,"description":"Page number (1-indexed)","default":1,"title":"Page"},"description":"Page number (1-indexed)"},{"name":"page_size","in":"query","required":false,"schema":{"type":"integer","maximum":100,"minimum":1,"description":"Items per page","default":50,"title":"Page Size"},"description":"Items per page"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ScheduledRuleListResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/scheduler/rules/{rule_id}":{"get":{"tags":["Scheduler"],"summary":"Get Scheduled Rule","description":"Fetch a single scheduled rule.\n\n**Required Scopes:** `scheduler:read`","operationId":"get_scheduled_rule_api_v1_scheduler_rules__rule_id__get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"rule_id","in":"path","required":true,"schema":{"type":"string","title":"Rule Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ScheduledRuleResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"patch":{"tags":["Scheduler"],"summary":"Update Scheduled Rule","description":"Update a scheduled rule.\n\nMutates only fields present in the body. ``status`` may only be set to\n``active`` or ``paused`` — use ``DELETE`` to archive. Pass an empty string\nfor ``recurrence_rule`` to clear it (turn a recurring rule into a one-shot\nvia ``next_evaluation_at``).\n\n**Required Scopes:** `scheduler:write`","operationId":"update_scheduled_rule_api_v1_scheduler_rules__rule_id__patch","security":[{"HTTPBearer":[]}],"parameters":[{"name":"rule_id","in":"path","required":true,"schema":{"type":"string","title":"Rule Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateScheduledRuleRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ScheduledRuleResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"tags":["Scheduler"],"summary":"Delete Scheduled Rule","description":"Soft-delete a scheduled rule (sets status to ``archived`` and clears\n``next_evaluation_at``). The row is preserved for audit + so already-\nmaterialized ScheduledSend rows keep a valid ``rule_id`` reference.\n\nIdempotent.\n\n**Required Scopes:** `scheduler:write`","operationId":"delete_scheduled_rule_api_v1_scheduler_rules__rule_id__delete","security":[{"HTTPBearer":[]}],"parameters":[{"name":"rule_id","in":"path","required":true,"schema":{"type":"string","title":"Rule Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/scheduler/rules/{rule_id}/pause":{"post":{"tags":["Scheduler"],"summary":"Pause Scheduled Rule","description":"Pause a scheduled rule. The evaluator skips paused rules entirely.\n\n**Required Scopes:** `scheduler:write`","operationId":"pause_scheduled_rule_api_v1_scheduler_rules__rule_id__pause_post","security":[{"HTTPBearer":[]}],"parameters":[{"name":"rule_id","in":"path","required":true,"schema":{"type":"string","title":"Rule Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ScheduledRuleResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/scheduler/rules/{rule_id}/resume":{"post":{"tags":["Scheduler"],"summary":"Resume Scheduled Rule","description":"Resume a paused rule. ``next_evaluation_at`` is set to now so the\nevaluator picks it up on the next tick.\n\n**Required Scopes:** `scheduler:write`","operationId":"resume_scheduled_rule_api_v1_scheduler_rules__rule_id__resume_post","security":[{"HTTPBearer":[]}],"parameters":[{"name":"rule_id","in":"path","required":true,"schema":{"type":"string","title":"Rule Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ScheduledRuleResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/scheduler/rules/{rule_id}/sends":{"get":{"tags":["Scheduler"],"summary":"List Sends For Rule","description":"List the ScheduledSends produced by this rule.\n\nUsed by the Rules tab drill-in: \"show me what this rule has actually\nmaterialized.\" Ordered by next_run_at descending (most recent first).\n\n**Required Scopes:** `scheduler:read`","operationId":"list_sends_for_rule_api_v1_scheduler_rules__rule_id__sends_get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"rule_id","in":"path","required":true,"schema":{"type":"string","title":"Rule Id"}},{"name":"page","in":"query","required":false,"schema":{"type":"integer","minimum":1,"description":"Page number (1-indexed)","default":1,"title":"Page"},"description":"Page number (1-indexed)"},{"name":"page_size","in":"query","required":false,"schema":{"type":"integer","maximum":100,"minimum":1,"description":"Items per page","default":50,"title":"Page Size"},"description":"Items per page"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ScheduledSendListResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/contacts/{contact_id}/scheduled-sends":{"get":{"tags":["Scheduler"],"summary":"List Scheduled Sends For Contact","description":"List scheduled sends for a specific contact.\n\nUsed by the contact-detail \"Reminders\" tab. When ``status`` is omitted the\nresponse defaults to active sends (``scheduled`` + ``running``) — the rows\na user expects to see. Pass an explicit status to inspect history.\n\n**Required Scopes:** `scheduler:read` AND `contacts:read`","operationId":"list_scheduled_sends_for_contact_api_v1_contacts__contact_id__scheduled_sends_get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"contact_id","in":"path","required":true,"schema":{"type":"string","title":"Contact Id"}},{"name":"status","in":"query","required":false,"schema":{"anyOf":[{"$ref":"#/components/schemas/ScheduledSendStatus"},{"type":"null"}],"description":"Filter by status. Defaults to active sends (scheduled, running) when omitted.","title":"Status"},"description":"Filter by status. Defaults to active sends (scheduled, running) when omitted."},{"name":"page","in":"query","required":false,"schema":{"type":"integer","minimum":1,"description":"Page number (1-indexed)","default":1,"title":"Page"},"description":"Page number (1-indexed)"},{"name":"page_size","in":"query","required":false,"schema":{"type":"integer","maximum":100,"minimum":1,"description":"Items per page","default":50,"title":"Page Size"},"description":"Items per page"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ScheduledSendListResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/whatsapp-flows/ai/estimate":{"post":{"tags":["WhatsApp Flows"],"summary":"Estimate Ai Generation","description":"AI credits a generation will cost — shown before the user confirms.\n\n**🔐 Required Scopes:** `whatsapp_flows:write`\n","operationId":"estimate_ai_generation_api_v1_whatsapp_flows_ai_estimate_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/EstimateRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/EstimateResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"HTTPBearer":[]}]}},"/api/v1/whatsapp-flows/ai/generate":{"post":{"tags":["WhatsApp Flows"],"summary":"Generate Flow","description":"Queue a generation from a description. Poll ``/jobs/{job_id}``. Does NOT save a flow.\n\n**🔐 Required Scopes:** `whatsapp_flows:write`\n","operationId":"generate_flow_api_v1_whatsapp_flows_ai_generate_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GenerateRequest"}}},"required":true},"responses":{"202":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/JobResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"HTTPBearer":[]}]}},"/api/v1/whatsapp-flows/ai/refine":{"post":{"tags":["WhatsApp Flows"],"summary":"Refine Flow","description":"Queue a rewrite of one screen (or one component); the rest of the doc stays frozen.\n\n**🔐 Required Scopes:** `whatsapp_flows:write`\n","operationId":"refine_flow_api_v1_whatsapp_flows_ai_refine_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RefineRequest"}}},"required":true},"responses":{"202":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/JobResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"HTTPBearer":[]}]}},"/api/v1/whatsapp-flows/ai/jobs/{job_id}":{"get":{"tags":["WhatsApp Flows"],"summary":"Get Ai Job","description":"Poll a generate/refine job (kept 24h). Another workspace's job is a 404.\n\n**🔐 Required Scopes:** `whatsapp_flows:write`\n","operationId":"get_ai_job_api_v1_whatsapp_flows_ai_jobs__job_id__get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"job_id","in":"path","required":true,"schema":{"type":"string","maxLength":36,"title":"Job Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/JobResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/whatsapp-flows/ai/jobs/{job_id}/cancel":{"post":{"tags":["WhatsApp Flows"],"summary":"Cancel Ai Job","description":"Cancel a queued/running generate/refine job (works from any pod).\n\nAn active job becomes ``failed`` with ``error.code = \"cancelled\"`` and is\nnever charged; no further AI calls or repair rounds start. Idempotent. A job\nthat already finished (or is committing its result) is returned unchanged —\na succeeded job stays succeeded. Another workspace's job is a 404.\n\n**Required Scopes:** `whatsapp_flows:write`","operationId":"cancel_ai_job_api_v1_whatsapp_flows_ai_jobs__job_id__cancel_post","security":[{"HTTPBearer":[]}],"parameters":[{"name":"job_id","in":"path","required":true,"schema":{"type":"string","maxLength":36,"title":"Job Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/JobResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/whatsapp-flows/templates":{"get":{"tags":["WhatsApp Flows"],"summary":"List Wa Flow Templates","description":"List SendSeven starter flow templates (envelopes without ``builder_doc``).\n\n**Required Scopes:** `whatsapp_flows:read`","operationId":"list_wa_flow_templates_api_v1_whatsapp_flows_templates_get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"lang","in":"query","required":false,"schema":{"anyOf":[{"type":"string","maxLength":16},{"type":"null"}],"description":"de | en (default: de)","title":"Lang"},"description":"de | en (default: de)"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/whatsapp-flows/templates/{key}":{"get":{"tags":["WhatsApp Flows"],"summary":"Get Wa Flow Template","description":"Full template envelope incl. ``builder_doc`` and ``placeholders``.\n\n**Required Scopes:** `whatsapp_flows:read`","operationId":"get_wa_flow_template_api_v1_whatsapp_flows_templates__key__get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"key","in":"path","required":true,"schema":{"type":"string","maxLength":64,"title":"Key"}},{"name":"lang","in":"query","required":false,"schema":{"anyOf":[{"type":"string","maxLength":16},{"type":"null"}],"title":"Lang"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/whatsapp-flows/publish-targets":{"get":{"tags":["WhatsApp Flows"],"summary":"List Publish Targets","description":"Active WhatsApp Cloud API numbers of the workspace with their WABA.\n\n``supports_dynamic`` = plan allows dynamic flows AND the number is not a\nWhatsApp Business App (Coexistence) number.\n\n**Required Scopes:** `whatsapp_flows:read`","operationId":"list_publish_targets_api_v1_whatsapp_flows_publish_targets_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}},"security":[{"HTTPBearer":[]}]}},"/api/v1/whatsapp-flows/publications/{publication_id}/publish":{"post":{"tags":["WhatsApp Flows"],"summary":"Publish Publication","description":"Publish the pushed Meta DRAFT (irreversible on Meta).\n\nThe pushed version is recompiled first; if the Flow JSON changed it is\nuploaded to the draft before publishing (422 ``version_recompile_invalid``\nif it no longer validates, 409 ``meta_validation_errors`` if Meta rejects\nthe new JSON).\n\n**Required Scopes:** `whatsapp_flows:write`","operationId":"publish_publication_api_v1_whatsapp_flows_publications__publication_id__publish_post","security":[{"HTTPBearer":[]}],"parameters":[{"name":"publication_id","in":"path","required":true,"schema":{"type":"string","maxLength":36,"title":"Publication Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/whatsapp-flows/publications/{publication_id}/deprecate":{"post":{"tags":["WhatsApp Flows"],"summary":"Deprecate Publication","description":"Deprecate the published Meta flow (it can no longer be sent).\n\n**Required Scopes:** `whatsapp_flows:write`","operationId":"deprecate_publication_api_v1_whatsapp_flows_publications__publication_id__deprecate_post","security":[{"HTTPBearer":[]}],"parameters":[{"name":"publication_id","in":"path","required":true,"schema":{"type":"string","maxLength":36,"title":"Publication Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/whatsapp-flows/publications/{publication_id}/refresh-preview":{"post":{"tags":["WhatsApp Flows"],"summary":"Refresh Publication Preview","description":"Fetch a fresh Meta web preview link (valid ~30 days).\n\n**Required Scopes:** `whatsapp_flows:write`","operationId":"refresh_publication_preview_api_v1_whatsapp_flows_publications__publication_id__refresh_preview_post","security":[{"HTTPBearer":[]}],"parameters":[{"name":"publication_id","in":"path","required":true,"schema":{"type":"string","maxLength":36,"title":"Publication Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/whatsapp-flows/custom-field-usage/{custom_field_id}":{"get":{"tags":["WhatsApp Flows"],"summary":"Get Custom Field Usage","description":"WhatsApp Flows that reference a contact custom field.\n\nScans the latest version and the published version builder doc of every\nnon-archived flow for ``options_source`` / ``mapping`` / ``prefill``\nreferences to ``custom_field_id``. ``published`` = the PUBLISHED version\nuses it. Used by the custom-field editor to warn before values change.\n\nResponse: ``{\"items\": [{definition_id, name, status, published, uses}], \"pagination\": {...}}``\n(all matches on one page).\n\n**Required Scopes:** `whatsapp_flows:read`","operationId":"get_custom_field_usage_api_v1_whatsapp_flows_custom_field_usage__custom_field_id__get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"custom_field_id","in":"path","required":true,"schema":{"type":"string","minLength":1,"maxLength":64,"title":"Custom Field Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/whatsapp-flows":{"get":{"tags":["WhatsApp Flows"],"summary":"List Wa Flows","description":"List the workspace's WhatsApp Flows.\n\nArchived flows are hidden unless ``status=archived`` (only archived),\n``status=all`` or ``include_archived=true``.\n\n**Required Scopes:** `whatsapp_flows:read`","operationId":"list_wa_flows_api_v1_whatsapp_flows_get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"page","in":"query","required":false,"schema":{"type":"integer","minimum":1,"default":1,"title":"Page"}},{"name":"page_size","in":"query","required":false,"schema":{"type":"integer","maximum":100,"minimum":1,"default":25,"title":"Page Size"}},{"name":"search","in":"query","required":false,"schema":{"anyOf":[{"type":"string","maxLength":200},{"type":"null"}],"title":"Search"}},{"name":"status","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"draft | published | deprecated | archived | active | all (default: not archived)","title":"Status"},"description":"draft | published | deprecated | archived | active | all (default: not archived)"},{"name":"kind","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"static | dynamic","title":"Kind"},"description":"static | dynamic"},{"name":"include_archived","in":"query","required":false,"schema":{"type":"boolean","description":"Also return archived flows (default: hidden)","default":false,"title":"Include Archived"},"description":"Also return archived flows (default: hidden)"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"post":{"tags":["WhatsApp Flows"],"summary":"Create Wa Flow","description":"Create a flow: blank, from a SendSeven template, or from a builder doc (AI / import).\n\n**Required Scopes:** `whatsapp_flows:write`","operationId":"create_wa_flow_api_v1_whatsapp_flows_post","security":[{"HTTPBearer":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateWaFlowRequest"}}}},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/whatsapp-flows/{definition_id}":{"get":{"tags":["WhatsApp Flows"],"summary":"Get Wa Flow","description":"Flow detail incl. the current draft version (with ``builder_doc``).\n\n**Required Scopes:** `whatsapp_flows:read`","operationId":"get_wa_flow_api_v1_whatsapp_flows__definition_id__get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"definition_id","in":"path","required":true,"schema":{"type":"string","maxLength":36,"title":"Definition Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"patch":{"tags":["WhatsApp Flows"],"summary":"Update Wa Flow","description":"Update metadata (name, description, categories) or archive / restore.\n\nThe builder doc is saved via ``POST /{id}/versions``.\n\n**Required Scopes:** `whatsapp_flows:write`","operationId":"update_wa_flow_api_v1_whatsapp_flows__definition_id__patch","security":[{"HTTPBearer":[]}],"parameters":[{"name":"definition_id","in":"path","required":true,"schema":{"type":"string","maxLength":36,"title":"Definition Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateWaFlowRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"tags":["WhatsApp Flows"],"summary":"Archive Wa Flow","description":"Archive (soft delete; same as ``PATCH {status: \"archived\"}``).\n\nSendSeven-side only: Meta flows are not touched. Archived flows are hidden\nfrom pickers but existing references (automation nodes, template FLOW\nbuttons, API sends by id) keep working.\n\n**Required Scopes:** `whatsapp_flows:write`","operationId":"archive_wa_flow_api_v1_whatsapp_flows__definition_id__delete","security":[{"HTTPBearer":[]}],"parameters":[{"name":"definition_id","in":"path","required":true,"schema":{"type":"string","maxLength":36,"title":"Definition Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/whatsapp-flows/{definition_id}/clone":{"post":{"tags":["WhatsApp Flows"],"summary":"Clone Wa Flow","description":"Copy the current version into a new flow (same or sibling workspace).\n\nCross-workspace copies are limited to the same billing account and need\n``whatsapp_flows:write`` in the target workspace (checked by the service).\nAPI keys may only copy into another workspace when the key itself holds\n``tenants:manage``; OAuth apps may only copy within the current workspace.\n\n**Required Scopes:** `whatsapp_flows:write`","operationId":"clone_wa_flow_api_v1_whatsapp_flows__definition_id__clone_post","security":[{"HTTPBearer":[]}],"parameters":[{"name":"definition_id","in":"path","required":true,"schema":{"type":"string","maxLength":36,"title":"Definition Id"}}],"requestBody":{"content":{"application/json":{"schema":{"anyOf":[{"$ref":"#/components/schemas/CloneWaFlowRequest"},{"type":"null"}],"title":"Body"}}}},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/whatsapp-flows/{definition_id}/versions":{"get":{"tags":["WhatsApp Flows"],"summary":"List Wa Flow Versions","description":"Version history, newest first.\n\n**Required Scopes:** `whatsapp_flows:read`","operationId":"list_wa_flow_versions_api_v1_whatsapp_flows__definition_id__versions_get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"definition_id","in":"path","required":true,"schema":{"type":"string","maxLength":36,"title":"Definition Id"}},{"name":"page","in":"query","required":false,"schema":{"type":"integer","minimum":1,"default":1,"title":"Page"}},{"name":"page_size","in":"query","required":false,"schema":{"type":"integer","maximum":100,"minimum":1,"default":25,"title":"Page Size"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"post":{"tags":["WhatsApp Flows"],"summary":"Create Wa Flow Version","description":"Save the draft as a new immutable version (validated + compiled).\n\nInvalid docs are still saved (``is_valid=false``); identical content\nreturns the current version with **200**. Dynamic docs need the Scale plan.\n\n**Required Scopes:** `whatsapp_flows:write`","operationId":"create_wa_flow_version_api_v1_whatsapp_flows__definition_id__versions_post","security":[{"HTTPBearer":[]}],"parameters":[{"name":"definition_id","in":"path","required":true,"schema":{"type":"string","maxLength":36,"title":"Definition Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateVersionRequest"}}}},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/whatsapp-flows/{definition_id}/versions/{version_id}":{"get":{"tags":["WhatsApp Flows"],"summary":"Get Wa Flow Version","description":"One immutable version incl. ``builder_doc`` and compiled Flow JSON.\n\n**Required Scopes:** `whatsapp_flows:read`","operationId":"get_wa_flow_version_api_v1_whatsapp_flows__definition_id__versions__version_id__get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"definition_id","in":"path","required":true,"schema":{"type":"string","maxLength":36,"title":"Definition Id"}},{"name":"version_id","in":"path","required":true,"schema":{"type":"string","maxLength":36,"title":"Version Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/whatsapp-flows/{definition_id}/validate":{"post":{"tags":["WhatsApp Flows"],"summary":"Validate Wa Flow","description":"Validate a builder doc against the workspace (nothing is saved).\n\n**Required Scopes:** `whatsapp_flows:read`","operationId":"validate_wa_flow_api_v1_whatsapp_flows__definition_id__validate_post","security":[{"HTTPBearer":[]}],"parameters":[{"name":"definition_id","in":"path","required":true,"schema":{"type":"string","maxLength":36,"title":"Definition Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidateRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/whatsapp-flows/{definition_id}/compile":{"post":{"tags":["WhatsApp Flows"],"summary":"Compile Wa Flow","description":"Compile an unsaved doc (or a stored version) to Meta Flow JSON (nothing is saved).\n\n**Required Scopes:** `whatsapp_flows:read`","operationId":"compile_wa_flow_api_v1_whatsapp_flows__definition_id__compile_post","security":[{"HTTPBearer":[]}],"parameters":[{"name":"definition_id","in":"path","required":true,"schema":{"type":"string","maxLength":36,"title":"Definition Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CompileRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/whatsapp-flows/{definition_id}/publications":{"get":{"tags":["WhatsApp Flows"],"summary":"List Wa Flow Publications","description":"One publication per WhatsApp Business Account the flow was pushed to.\n\n**Required Scopes:** `whatsapp_flows:read`","operationId":"list_wa_flow_publications_api_v1_whatsapp_flows__definition_id__publications_get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"definition_id","in":"path","required":true,"schema":{"type":"string","maxLength":36,"title":"Definition Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"post":{"tags":["WhatsApp Flows"],"summary":"Push Wa Flow","description":"Push a version to Meta as a DRAFT, one Meta flow per WhatsApp Business Account.\n\n``channel_ids`` are grouped by WABA. Per-WABA failures are returned in\n``errors`` when at least one WABA succeeded; otherwise the error is raised.\n\nThe version's builder doc is recompiled with the current compiler before\nupload (422 ``version_recompile_invalid`` with ``issues`` if it no longer\nvalidates). Imported flows are uploaded byte-for-byte as stored.\n\n**Required Scopes:** `whatsapp_flows:write`","operationId":"push_wa_flow_api_v1_whatsapp_flows__definition_id__publications_post","security":[{"HTTPBearer":[]}],"parameters":[{"name":"definition_id","in":"path","required":true,"schema":{"type":"string","maxLength":36,"title":"Definition Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PushRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/whatsapp-flows/{definition_id}/references":{"get":{"tags":["WhatsApp Flows"],"summary":"Get Wa Flow References","description":"Where this WhatsApp Flow is used (archive / publish-changes dialogs).\n\n* ``automations`` -- Send WhatsApp Flow nodes (and form-completed triggers,\n  ``node_id: \"trigger\"``) of this workspace's automations that reference it.\n* ``templates`` -- WhatsApp templates whose FLOW button opens one of its Meta\n  flows (current, still-live and superseded ids), ANY status (the retirement\n  check only counts APPROVED/PENDING -- ``template_refs_for_meta_flows``).\n* ``scheduled_sends_count`` -- scheduled sends never reference a flow\n  definition directly (they go through templates), so this is ``0``.\n\nArchiving never breaks these references; they keep working.\n\n**Required Scopes:** `whatsapp_flows:read`","operationId":"get_wa_flow_references_api_v1_whatsapp_flows__definition_id__references_get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"definition_id","in":"path","required":true,"schema":{"type":"string","maxLength":36,"title":"Definition Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/whatsapp-flows/{definition_id}/preview-data":{"post":{"tags":["WhatsApp Flows"],"summary":"Preview Wa Flow Data","description":"Entry-screen data a send would carry, per key with its source (nothing is sent).\n\nSame resolve + merge as a real send: ``initial_data`` (override) > saved\nchannel defaults > send-time bindings incl. default prefill > ``__example__``.\nWithout ``contact_id`` no contact data is resolved (examples only). Values\ncoming from the contact are returned verbatim, never template-rendered.\n\nResponse: ``{screen, data, sources: {<key>: \"prefill\"|\"binding\"|\"override\"|\"example\"}}``\n(saved channel defaults count as ``binding``).\n\n**Required Scopes:** `whatsapp_flows:read` AND `contacts:read`","operationId":"preview_wa_flow_data_api_v1_whatsapp_flows__definition_id__preview_data_post","security":[{"HTTPBearer":[]}],"parameters":[{"name":"definition_id","in":"path","required":true,"schema":{"type":"string","maxLength":36,"title":"Definition Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PreviewDataRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/whatsapp-flows/{definition_id}/send":{"post":{"tags":["WhatsApp Flows"],"summary":"Send Wa Flow","description":"Send a published flow to a contact (interactive inside 24h, or via an approved template).\n\n`initial_data` string values may use contact variables (`{{contact.first_name}}`,\n`{{contact.email}}`, `{{contact.custom.<key>}}`, ...); they are rendered for the\nrecipient before sending. Draft flows cannot be sent (`draft_send_not_supported`).\n\nCustom Inboxes: a contact-addressed send by a member restricted to specific\ninboxes returns 409 `conversation_owned_elsewhere` while the contact is in\nan ongoing conversation a colleague handles that the member cannot open.\n\n**Required Scopes:** `messages:create` AND `whatsapp_flows:read`","operationId":"send_wa_flow_api_v1_whatsapp_flows__definition_id__send_post","security":[{"HTTPBearer":[]}],"parameters":[{"name":"definition_id","in":"path","required":true,"schema":{"type":"string","maxLength":36,"title":"Definition Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SendRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/whatsapp-flows/{definition_id}/sessions":{"get":{"tags":["WhatsApp Flows"],"summary":"List Wa Flow Sessions","description":"Sessions (sends) of this flow with their answers, newest first.\n\nInputs marked ``sensitive`` in the flow are masked as ``••••``.\n\n**Required Scopes:** `whatsapp_flows:read`","operationId":"list_wa_flow_sessions_api_v1_whatsapp_flows__definition_id__sessions_get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"definition_id","in":"path","required":true,"schema":{"type":"string","maxLength":36,"title":"Definition Id"}},{"name":"page","in":"query","required":false,"schema":{"type":"integer","minimum":1,"default":1,"title":"Page"}},{"name":"page_size","in":"query","required":false,"schema":{"type":"integer","maximum":100,"minimum":1,"default":25,"title":"Page Size"}},{"name":"status","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Status"}},{"name":"contact_id","in":"query","required":false,"schema":{"anyOf":[{"type":"string","maxLength":36},{"type":"null"}],"title":"Contact Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/whatsapp-flows/{definition_id}/analytics":{"get":{"tags":["WhatsApp Flows"],"summary":"Wa Flow Analytics","description":"Funnel (sent → delivered → read → opened → completed / expired) and per-screen views.\n\nCounts are distinct sessions per event type in the range.\n\n**Required Scopes:** `whatsapp_flows:read`","operationId":"wa_flow_analytics_api_v1_whatsapp_flows__definition_id__analytics_get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"definition_id","in":"path","required":true,"schema":{"type":"string","maxLength":36,"title":"Definition Id"}},{"name":"date_from","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"ISO date/datetime (default: 30 days ago)","title":"Date From"},"description":"ISO date/datetime (default: 30 days ago)"},{"name":"date_to","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"ISO date/datetime, inclusive day (default: now)","title":"Date To"},"description":"ISO date/datetime, inclusive day (default: now)"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/whatsapp-flow-data-sources":{"get":{"tags":["WhatsApp Flows"],"summary":"List Wa Flow Data Sources","description":"List data sources for dynamic WhatsApp Flows (secrets are never returned).\n\n**Required Scopes:** `whatsapp_flows:read`","operationId":"list_wa_flow_data_sources_api_v1_whatsapp_flow_data_sources_get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"page","in":"query","required":false,"schema":{"type":"integer","minimum":1,"default":1,"title":"Page"}},{"name":"page_size","in":"query","required":false,"schema":{"type":"integer","maximum":100,"minimum":1,"default":25,"title":"Page Size"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"post":{"tags":["WhatsApp Flows"],"summary":"Create Wa Flow Data Source","description":"Create a data source. ``webhook`` needs the Scale plan; its URL is SSRF-checked.\n\nWithout a ``secret`` a signing secret is generated and returned ONCE as\n``signing_secret`` (it is never shown again).\n\n**Required Scopes:** `whatsapp_flows:write`","operationId":"create_wa_flow_data_source_api_v1_whatsapp_flow_data_sources_post","security":[{"HTTPBearer":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DataSourceCreate"}}}},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/whatsapp-flow-data-sources/{data_source_id}":{"get":{"tags":["WhatsApp Flows"],"summary":"Get Wa Flow Data Source","description":"One data source (secret and header values are never returned).\n\n**Required Scopes:** `whatsapp_flows:read`","operationId":"get_wa_flow_data_source_api_v1_whatsapp_flow_data_sources__data_source_id__get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"data_source_id","in":"path","required":true,"schema":{"type":"string","maxLength":36,"title":"Data Source Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"patch":{"tags":["WhatsApp Flows"],"summary":"Update Wa Flow Data Source","description":"Update a data source. ``secret`` is write-only; a header value of ``••••`` keeps the stored one.\n\n**Required Scopes:** `whatsapp_flows:write`","operationId":"update_wa_flow_data_source_api_v1_whatsapp_flow_data_sources__data_source_id__patch","security":[{"HTTPBearer":[]}],"parameters":[{"name":"data_source_id","in":"path","required":true,"schema":{"type":"string","maxLength":36,"title":"Data Source Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DataSourceUpdate"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"tags":["WhatsApp Flows"],"summary":"Delete Wa Flow Data Source","description":"Delete a data source. Flows bound to it fall back to their static data.\n\n**Required Scopes:** `whatsapp_flows:write`","operationId":"delete_wa_flow_data_source_api_v1_whatsapp_flow_data_sources__data_source_id__delete","security":[{"HTTPBearer":[]}],"parameters":[{"name":"data_source_id","in":"path","required":true,"schema":{"type":"string","maxLength":36,"title":"Data Source Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/whatsapp-flow-data-sources/{data_source_id}/rotate-secret":{"post":{"tags":["WhatsApp Flows"],"summary":"Rotate Wa Flow Data Source Secret","description":"Generate a new signing secret; returned ONCE as ``signing_secret``.\n\nThe replaced secret stays valid for 24h (``previous_secret_expires_at``): until\nthen each request carries two ``v1`` signatures (new + old) so the receiver can\nswitch over without dropped requests. Rotating again inside the window retires\nthe older secret immediately.\n\n**Required Scopes:** `whatsapp_flows:write`","operationId":"rotate_wa_flow_data_source_secret_api_v1_whatsapp_flow_data_sources__data_source_id__rotate_secret_post","security":[{"HTTPBearer":[]}],"parameters":[{"name":"data_source_id","in":"path","required":true,"schema":{"type":"string","maxLength":36,"title":"Data Source Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/channels/{channel_id}/whatsapp-flows/import":{"post":{"tags":["WhatsApp Flows"],"summary":"Import Wa Flow From Meta","description":"Import a flow built in Meta's Flow Builder (read-only, JSON-only).\n\n**Required Scopes:** `whatsapp_flows:write`","operationId":"import_wa_flow_from_meta_api_v1_channels__channel_id__whatsapp_flows_import_post","security":[{"HTTPBearer":[]}],"parameters":[{"name":"channel_id","in":"path","required":true,"schema":{"type":"string","maxLength":36,"title":"Channel Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ImportRequest"}}}},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/flows/secrets":{"get":{"tags":["Flows"],"summary":"List Flow Secrets","description":"List the tenant's flow secrets (names + masked previews only).\n\n**Required Scopes:** `flows:read`","operationId":"list_flow_secrets_api_v1_flows_secrets_get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"page","in":"query","required":false,"schema":{"type":"integer","minimum":1,"default":1,"title":"Page"}},{"name":"page_size","in":"query","required":false,"schema":{"type":"integer","maximum":500,"minimum":1,"default":100,"title":"Page Size"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FlowSecretListResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/flows/secrets/{name}":{"put":{"tags":["Flows"],"summary":"Upsert Flow Secret","description":"Create or rotate a flow secret. The value is write-only.\n\n**Required Scopes:** `flows:write`","operationId":"upsert_flow_secret_api_v1_flows_secrets__name__put","security":[{"HTTPBearer":[]}],"parameters":[{"name":"name","in":"path","required":true,"schema":{"type":"string","minLength":1,"maxLength":64,"description":"Secret name (upper-cased; ^[A-Z][A-Z0-9_]{0,63}$)","title":"Name"},"description":"Secret name (upper-cased; ^[A-Z][A-Z0-9_]{0,63}$)"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/FlowSecretUpsertRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FlowSecretEntry"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"tags":["Flows"],"summary":"Delete Flow Secret","description":"Delete a flow secret by name.\n\n**Required Scopes:** `flows:write`","operationId":"delete_flow_secret_api_v1_flows_secrets__name__delete","security":[{"HTTPBearer":[]}],"parameters":[{"name":"name","in":"path","required":true,"schema":{"type":"string","minLength":1,"maxLength":64,"description":"Secret name (upper-cased; ^[A-Z][A-Z0-9_]{0,63}$)","title":"Name"},"description":"Secret name (upper-cased; ^[A-Z][A-Z0-9_]{0,63}$)"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeleteResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/flows":{"post":{"tags":["Flows"],"summary":"Create Flow","description":"Create a new flow in ``draft`` status.\n\nThe ``definition`` JSON is validated structurally — see\n:class:`FlowDefinitionValidator`. Validation failures return\n``400 FLOW_DEFINITION_INVALID`` with the full error list.\n\n**Required Scopes:** `flows:write`","operationId":"create_flow_api_v1_flows_post","security":[{"HTTPBearer":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateFlowRequest"}}}},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/app__schemas__flow_schema__FlowResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"get":{"tags":["Flows"],"summary":"List Flows","description":"List flows for the current tenant.\n\n**Required Scopes:** `flows:read`","operationId":"list_flows_api_v1_flows_get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"status","in":"query","required":false,"schema":{"anyOf":[{"$ref":"#/components/schemas/FlowStatus"},{"type":"null"}],"description":"Filter by status (draft, published, paused, archived).","title":"Status"},"description":"Filter by status (draft, published, paused, archived)."},{"name":"name","in":"query","required":false,"schema":{"anyOf":[{"type":"string","maxLength":200},{"type":"null"}],"description":"Case-insensitive substring filter on flow name.","title":"Name"},"description":"Case-insensitive substring filter on flow name."},{"name":"manually_triggerable","in":"query","required":false,"schema":{"anyOf":[{"type":"boolean"},{"type":"null"}],"description":"When True, return only flows that operators can manually start from the picker (``flows.manually_triggerable = TRUE``). When omitted/None the flows index page returns all flows without silent filtering.","title":"Manually Triggerable"},"description":"When True, return only flows that operators can manually start from the picker (``flows.manually_triggerable = TRUE``). When omitted/None the flows index page returns all flows without silent filtering."},{"name":"page","in":"query","required":false,"schema":{"type":"integer","minimum":1,"default":1,"title":"Page"}},{"name":"page_size","in":"query","required":false,"schema":{"type":"integer","maximum":100,"minimum":1,"default":50,"title":"Page Size"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FlowListResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/flows/{flow_id}/duplicate":{"post":{"tags":["Flows"],"summary":"Duplicate Flow","description":"Create an independent copy of an existing flow as a fresh ``draft``.\n\nThe copy mirrors every behaviour-defining field on the source — full\n``definition`` JSON (trigger, schedule, nodes, edges, node configs,\nre-entry policy), ``description``, and the channel/tag/etc. config\nnested inside ``definition``. Node ids are regenerated and every\ninternal reference (edges, ``entry_node_id``, config-level\n``*_node_id`` keys) is remapped so the copy is fully self-consistent.\n\nResets on the copy:\n\n* ``status`` is ``draft`` regardless of source status — so nothing\n  fires until the user explicitly publishes the copy.\n* Fresh ``id`` / ``created_at`` / ``updated_at``.\n* No run history (``flow_runs`` rows are not copied).\n\nBody is optional. When ``name`` is omitted the default is\n``\"<source.name> (copy)\"``. Source flows in any status (draft,\npublished, paused, archived) can be duplicated.\n\nReturns 404 when the source flow doesn't exist on the caller's tenant.\n\n**Required Scopes:** `flows:write`","operationId":"duplicate_flow_api_v1_flows__flow_id__duplicate_post","security":[{"HTTPBearer":[]}],"parameters":[{"name":"flow_id","in":"path","required":true,"schema":{"type":"string","title":"Flow Id"}}],"requestBody":{"content":{"application/json":{"schema":{"anyOf":[{"$ref":"#/components/schemas/DuplicateFlowRequest"},{"type":"null"}],"title":"Body"}}}},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/app__schemas__flow_schema__FlowResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/flows/templates":{"get":{"tags":["Flows"],"summary":"List Flow Templates","description":"List available flow templates (Phase 3 — May 2026).\n\nTemplates ship pre-built flow definitions that exercise the new\ntriggers (incoming_message, conversation_opened) and nodes (choice,\nbranch, run_assistant, kb_query). The Flows Creator surfaces this\ngallery before opening an empty canvas.\n\n**Required Scopes:** `flows:read`\n\nReturns a list of template summaries (without the full definition).\nFetch one template's full definition via\n``GET /flows/templates/{template_id}``.","operationId":"list_flow_templates_api_v1_flows_templates_get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"language","in":"query","required":false,"schema":{"anyOf":[{"type":"string","maxLength":10},{"type":"null"}],"description":"ISO 639-1 language code (e.g. 'en', 'de', 'fr'). Currently advisory — copy is EN-only in this phase.","title":"Language"},"description":"ISO 639-1 language code (e.g. 'en', 'de', 'fr'). Currently advisory — copy is EN-only in this phase."}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/flows/templates/{template_id}":{"get":{"tags":["Flows"],"summary":"Get Flow Template","description":"Return one flow template including its full ``definition`` ready to\ndrop into ``POST /flows`` (the FE typically prefills the create form\nwith this).\n\n**Required Scopes:** `flows:read`","operationId":"get_flow_template_api_v1_flows_templates__template_id__get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"template_id","in":"path","required":true,"schema":{"type":"string","title":"Template Id"}},{"name":"language","in":"query","required":false,"schema":{"anyOf":[{"type":"string","maxLength":10},{"type":"null"}],"title":"Language"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/flows/variables":{"get":{"tags":["Flows"],"summary":"Get Flow Variables","description":"Return the typed variable catalog used by the Flow Builder's Branch\nnode \"match contact field / tag / list / custom field\" picker.\n\n**Required Scopes:** `flows:read`\n\nSections:\n\n* ``contact_fields``  — built-in Contact columns + tags/lists pseudo-fields.\n* ``custom_fields``   — tenant ``custom_field_definitions`` exposed as\n  ``contact.custom.<key>`` paths with a best-effort type hint.\n* ``tags``            — tenant tag names (path = tag name).\n* ``lists``           — tenant subscription lists (path = list UUID,\n  label = list name).\n* ``flow_variables``  — populated only when ``flow_id`` is supplied;\n  otherwise empty.","operationId":"get_flow_variables_api_v1_flows_variables_get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"flow_id","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Optional flow ID. When provided, the response also includes the flow's own ``flow.vars.<output_var>`` variables walked from its ``collect_input`` / ``kb_query`` / ``create_tracked_link`` nodes.","title":"Flow Id"},"description":"Optional flow ID. When provided, the response also includes the flow's own ``flow.vars.<output_var>`` variables walked from its ``collect_input`` / ``kb_query`` / ``create_tracked_link`` nodes."}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FlowVariablesResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/flows/reorder":{"patch":{"tags":["Flows"],"summary":"Reorder Flows","description":"Re-order flow trigger priorities for the current tenant.\n\nAccepts EITHER an ordered ``flow_ids`` list (server assigns\n``priority = position``, 0-based, so the first id wins) OR an explicit\n``items: [{flow_id, priority}]`` list. Exactly one shape must be supplied.\nLower ``priority`` = higher precedence (evaluated first when multiple flows\nmatch the same inbound event).\n\nAll ids are validated to belong to this tenant BEFORE any write; the whole\nbatch is persisted in a single transaction (fail-closed — if any id is not\nowned by the tenant, nothing is written and a 404 is returned).\n\n**Required Scopes:** `flows:write`\n\n**Database:** SHARD (``flows`` — tenant-filtered on every row).\n\nReturns the resulting ordering: ``{\"items\": [{flow_id, name, priority},\n...]}`` sorted by ``priority`` ASC.","operationId":"reorder_flows_api_v1_flows_reorder_patch","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReorderFlowsRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReorderFlowsResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"HTTPBearer":[]}]}},"/api/v1/flows/{flow_id}":{"get":{"tags":["Flows"],"summary":"Get Flow","description":"Fetch a single flow.\n\n``run_count`` is the number of ``flow_runs`` rows a permanent delete\n(``DELETE /flows/{id}/permanent``) would destroy — the UI uses it to\nwarn before that irreversible call.\n\n**Required Scopes:** `flows:read`","operationId":"get_flow_api_v1_flows__flow_id__get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"flow_id","in":"path","required":true,"schema":{"type":"string","title":"Flow Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/app__schemas__flow_schema__FlowResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"patch":{"tags":["Flows"],"summary":"Update Flow","description":"Update a flow.\n\nAll fields optional. When ``definition`` is provided the API re-runs\nsave-time structural validation. Editing a published flow is allowed —\nthe engine snapshots ``definition`` into\n``flow_runs.context.definition_snapshot`` at run-start so in-flight\nruns are isolated from the edit.\n\n**Required Scopes:** `flows:write`","operationId":"update_flow_api_v1_flows__flow_id__patch","security":[{"HTTPBearer":[]}],"parameters":[{"name":"flow_id","in":"path","required":true,"schema":{"type":"string","title":"Flow Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateFlowRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/app__schemas__flow_schema__FlowResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"tags":["Flows"],"summary":"Archive Flow","description":"Soft-archive a flow.\n\nSets ``status='archived'``. Idempotent — archiving an already-archived\nflow returns the same shape rather than 404. Existing in-flight runs\ncontinue.\n\n**Required Scopes:** `flows:write`","operationId":"archive_flow_api_v1_flows__flow_id__delete","security":[{"HTTPBearer":[]}],"parameters":[{"name":"flow_id","in":"path","required":true,"schema":{"type":"string","title":"Flow Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/flows/{flow_id}/validation":{"get":{"tags":["Flows"],"summary":"Get Flow Validation","description":"Return the publish-time validation snapshot for a flow.\n\nSame shape as ``FlowResponse.validation`` — exposed as a dedicated\nendpoint so the Flows editor can poll cheap validation passes\nafter local edits without re-fetching the whole flow row. The\nFlows editor uses this to gate the \"Activate\" button and surface\ninline ``{path, message}`` warnings on the offending nodes /\ntrigger.\n\n**Required Scopes:** `flows:read`","operationId":"get_flow_validation_api_v1_flows__flow_id__validation_get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"flow_id","in":"path","required":true,"schema":{"type":"string","title":"Flow Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FlowValidationStatus"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/flows/{flow_id}/publish":{"post":{"tags":["Flows"],"summary":"Publish Flow","description":"Publish a flow.\n\nRuns the **stricter** publish-time validation pass:\n\n- Every node reachable from ``entry_node_id``.\n- No orphan nodes.\n- Every condition has both ``yes_node_id`` + ``no_node_id`` wired.\n- No infinite cycles in synchronous-only paths (Delay/Send-bearing\n  cycles are allowed — they're the legitimate \"drip every day\" pattern).\n\nReturns ``400 FLOW_DEFINITION_INVALID`` with the full error list on\nfailure. Returns ``409`` when trying to publish an archived flow.\n\n**Required Scopes:** `flows:publish`","operationId":"publish_flow_api_v1_flows__flow_id__publish_post","security":[{"HTTPBearer":[]}],"parameters":[{"name":"flow_id","in":"path","required":true,"schema":{"type":"string","title":"Flow Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/app__schemas__flow_schema__FlowResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/flows/{flow_id}/pause":{"post":{"tags":["Flows"],"summary":"Pause Flow","description":"Pause a published flow.\n\nNew ``POST /flows/{id}/runs`` calls return ``409`` while paused\n(``FlowEngineService.start_run`` requires PUBLISHED). In-flight runs\ncontinue — operators want to drain.\n\nIdempotent on already-paused.\n\n**Required Scopes:** `flows:write`","operationId":"pause_flow_api_v1_flows__flow_id__pause_post","security":[{"HTTPBearer":[]}],"parameters":[{"name":"flow_id","in":"path","required":true,"schema":{"type":"string","title":"Flow Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/app__schemas__flow_schema__FlowResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/flows/{flow_id}/permanent":{"delete":{"tags":["Flows"],"summary":"Delete Flow Permanently","description":"Permanently delete a flow — irreversible, destroys run history.\n\nDistinct from ``DELETE /flows/{id}``, which soft-archives (reversible\nvia ``POST /flows/{id}/unarchive``). This endpoint removes the\n``flows`` row AND every child row: ``flow_runs``, ``flow_run_steps``,\n``flow_run_waits``. No FK cascades exist on the flow tables, so the\nservice deletes children explicitly inside one transaction. Keyword-bot\nwiring that pointed at the flow is detached (set to NULL) rather than\ndeleted. Conversation ``messages`` and ``tracked_links`` produced by\nthe flow are KEPT — they are real history and their flow attribution\nreaders are already best-effort.\n\nRefuses with ``409`` rather than corrupting state:\n\n* flow is ``published`` — pause or archive it first;\n* the flow has in-flight runs (``running``/``waiting``) — stop them or\n  let them finish. Flow-Test-Mode runs never block.\n\nThe ``detail`` string names which of the two it is and, for in-flight\nruns, how many. ``GET /flows/{id}`` (and the flows list) expose\n``run_count`` so the UI can warn how many runs a delete would destroy\nBEFORE calling this.\n\nReturns ``404`` when the flow does not exist for this tenant — this is\nNOT idempotent-on-missing like archive, because \"already gone\" and\n\"never yours\" must not be distinguishable across tenants.\n\n**Required Scopes:** `flows:write`","operationId":"delete_flow_permanently_api_v1_flows__flow_id__permanent_delete","security":[{"HTTPBearer":[]}],"parameters":[{"name":"flow_id","in":"path","required":true,"schema":{"type":"string","title":"Flow Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/flows/{flow_id}/unarchive":{"post":{"tags":["Flows"],"summary":"Unarchive Flow","description":"Restore a soft-archived flow to draft status.\n\nReturns 409 if the flow is not archived.\n\n**Required Scopes:** `flows:write`","operationId":"unarchive_flow_api_v1_flows__flow_id__unarchive_post","security":[{"HTTPBearer":[]}],"parameters":[{"name":"flow_id","in":"path","required":true,"schema":{"type":"string","title":"Flow Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/app__schemas__flow_schema__FlowResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/flows/{flow_id}/runs":{"post":{"tags":["Flows"],"summary":"Start Flow Run","description":"Start a flow run for a contact.\n\nHonors idempotency from EITHER the request body field\n``idempotency_key`` OR the HTTP header ``Idempotency-Key`` (body wins\non conflict). The engine's ``start_run`` returns the existing run on\ncollision with HTTP 200; new runs return HTTP 201.\n\nReturns ``409`` if the flow isn't published, ``404`` if the contact\ndoesn't exist on this tenant, ``409 FLOW_RUN_BLOCKED`` when the\nre-entry policy is ``block`` and an active run already exists.\n\n**Required Scopes:** `flows:write`","operationId":"start_flow_run_api_v1_flows__flow_id__runs_post","security":[{"HTTPBearer":[]}],"parameters":[{"name":"flow_id","in":"path","required":true,"schema":{"type":"string","title":"Flow Id"}},{"name":"Idempotency-Key","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Idempotency-Key"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/StartRunRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FlowRunResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"get":{"tags":["Flows"],"summary":"List Flow Runs","description":"List runs for a flow.\n\n**Required Scopes:** `flows:read`","operationId":"list_flow_runs_api_v1_flows__flow_id__runs_get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"flow_id","in":"path","required":true,"schema":{"type":"string","title":"Flow Id"}},{"name":"contact_id","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Filter to runs for this contact","title":"Contact Id"},"description":"Filter to runs for this contact"},{"name":"status","in":"query","required":false,"schema":{"anyOf":[{"$ref":"#/components/schemas/FlowRunStatus"},{"type":"null"}],"description":"Filter by run status (running, waiting, completed, failed, canceled).","title":"Status"},"description":"Filter by run status (running, waiting, completed, failed, canceled)."},{"name":"page","in":"query","required":false,"schema":{"type":"integer","minimum":1,"default":1,"title":"Page"}},{"name":"page_size","in":"query","required":false,"schema":{"type":"integer","maximum":100,"minimum":1,"default":50,"title":"Page Size"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FlowRunListResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/flows/{flow_id}/runs/{run_id}":{"get":{"tags":["Flows"],"summary":"Get Flow Run","description":"Fetch a single flow run.\n\n**Required Scopes:** `flows:read`","operationId":"get_flow_run_api_v1_flows__flow_id__runs__run_id__get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"flow_id","in":"path","required":true,"schema":{"type":"string","title":"Flow Id"}},{"name":"run_id","in":"path","required":true,"schema":{"type":"string","title":"Run Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FlowRunResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/flows/{flow_id}/runs/{run_id}/stop":{"post":{"tags":["Flows"],"summary":"Stop Flow Run","description":"Operator-initiated cancel of a flow run.\n\nMarks the run ``canceled``, deletes pending waits and cancels any\nflows it started and is waiting on (Start Flow node). Idempotent —\nalready-terminal runs return their current row unchanged.\n\n**Required Scopes:** `flows:write`","operationId":"stop_flow_run_api_v1_flows__flow_id__runs__run_id__stop_post","security":[{"HTTPBearer":[]}],"parameters":[{"name":"flow_id","in":"path","required":true,"schema":{"type":"string","title":"Flow Id"}},{"name":"run_id","in":"path","required":true,"schema":{"type":"string","title":"Run Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FlowRunResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/flows/runs/{run_id}/stop":{"post":{"tags":["Flows"],"summary":"Stop Flow Run By Run Id","description":"Operator-initiated cancel of a flow run, by ``run_id`` alone.\n\nConvenience companion to ``POST /flows/{flow_id}/runs/{run_id}/stop``\nfor callers (notably the Conversations UI) that have the run id from\nthe conversation's ``active_flow_run.run_id`` but no flow_id. Marks\nthe run ``canceled``, deletes pending waits and cancels any flows it\nstarted and is waiting on (Start Flow node). Idempotent —\nalready-terminal runs return their current row unchanged.\n\n**Required Scopes:** `flows:write`","operationId":"stop_flow_run_by_run_id_api_v1_flows_runs__run_id__stop_post","security":[{"HTTPBearer":[]}],"parameters":[{"name":"run_id","in":"path","required":true,"schema":{"type":"string","title":"Run Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FlowRunResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/flows/{flow_id}/runs/{run_id}/steps":{"get":{"tags":["Flows"],"summary":"List Flow Run Steps","description":"List the audit-trail rows for a single flow run, oldest first.\n\nEach row is enriched from the CURRENT ``flows.definition``:\n  * ``node_label`` — editor-visible label (``display_name`` →\n    snapshot ``label``/``name`` → derived).\n  * ``node_config_snapshot`` — a curated subset of the node's\n    current config (e.g. for ``collect_input``: ``validation``,\n    ``max_attempts``, ``on_timeout_node_id``, ``prompt``).\n    **NOT** a point-in-time snapshot of what the engine executed —\n    callers that need that fidelity should rely on ``input_snapshot``.\n  * ``branch_taken`` — best-effort from\n    ``output_snapshot.chosen_branch``.\n\n**Required Scopes:** `flows:read`","operationId":"list_flow_run_steps_api_v1_flows__flow_id__runs__run_id__steps_get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"flow_id","in":"path","required":true,"schema":{"type":"string","title":"Flow Id"}},{"name":"run_id","in":"path","required":true,"schema":{"type":"string","title":"Run Id"}},{"name":"page","in":"query","required":false,"schema":{"type":"integer","minimum":1,"default":1,"title":"Page"}},{"name":"page_size","in":"query","required":false,"schema":{"type":"integer","maximum":500,"minimum":1,"default":100,"title":"Page Size"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FlowRunStepListResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/flows/{flow_id}/observability/funnel":{"get":{"tags":["Flows"],"summary":"Get Observability Funnel","description":"Per-node funnel for Observability v2.\n\nReturns ``items`` in canvas order with ``count`` (distinct runs that\nsuccessfully executed the node) and ``drop_from_previous`` (fraction\nlost between consecutive nodes). The first row is always the trigger.\n\nWindow defaults to the last 30 days.\n\n**Required Scopes:** `flows:read`","operationId":"get_observability_funnel_api_v1_flows__flow_id__observability_funnel_get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"flow_id","in":"path","required":true,"schema":{"type":"string","title":"Flow Id"}},{"name":"since","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"ISO8601 UTC start","title":"Since"},"description":"ISO8601 UTC start"},{"name":"until","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"ISO8601 UTC end","title":"Until"},"description":"ISO8601 UTC end"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FunnelResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/flows/{flow_id}/observability/channels":{"get":{"tags":["Flows"],"summary":"Get Observability Channels","description":"Per-channel sent / delivered / opened / replied % for a flow.\n\nCounts every outbound message tagged with this flow's runs in the\nwindow, group by ``platform``. ``reply_pct`` is inbound messages\n(also tagged to a run of this flow) divided by ``sent`` per channel.\n\n**Required Scopes:** `flows:read`","operationId":"get_observability_channels_api_v1_flows__flow_id__observability_channels_get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"flow_id","in":"path","required":true,"schema":{"type":"string","title":"Flow Id"}},{"name":"since","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"ISO8601 UTC start","title":"Since"},"description":"ISO8601 UTC start"},{"name":"until","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"ISO8601 UTC end","title":"Until"},"description":"ISO8601 UTC end"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ChannelPerformanceResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/flows/{flow_id}/runs/{run_id}/replay":{"get":{"tags":["Flows"],"summary":"Get Run Replay","description":"Per-contact replay timeline for a single flow run.\n\nMerges ``flow_run_steps`` (engine audit trail) with ``messages``\n(outbound + inbound tagged with this ``flow_run_id``) into a single\ntime-ordered event stream. Returns the contact header + the run\nsummary + the full event list.\n\n**Required Scopes:** `flows:read`","operationId":"get_run_replay_api_v1_flows__flow_id__runs__run_id__replay_get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"flow_id","in":"path","required":true,"schema":{"type":"string","title":"Flow Id"}},{"name":"run_id","in":"path","required":true,"schema":{"type":"string","title":"Run Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReplayResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/flows/{flow_id}/runs/{run_id}/messages":{"get":{"tags":["Flows"],"summary":"List Flow Run Messages","description":"List every message attributed to a single flow run, oldest-first.\n\nReturns the chronological transcript of outbound (Send-node) and\ninbound (Wait-for-reply matched) messages stamped with this\n``flow_run_id``. Reuses the existing\n``idx_msg_flow_run_id`` / ``idx_messages_tenant_flow_run`` index.\n\n404 when the run does not exist on this tenant OR when the run\nexists but does not belong to ``flow_id`` — never leak cross-tenant\nruns.\n\n**Required Scopes:** `flows:read` AND `conversations:read`","operationId":"list_flow_run_messages_api_v1_flows__flow_id__runs__run_id__messages_get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"flow_id","in":"path","required":true,"schema":{"type":"string","title":"Flow Id"}},{"name":"run_id","in":"path","required":true,"schema":{"type":"string","title":"Run Id"}},{"name":"page","in":"query","required":false,"schema":{"type":"integer","minimum":1,"default":1,"title":"Page"}},{"name":"page_size","in":"query","required":false,"schema":{"type":"integer","maximum":100,"minimum":1,"default":50,"title":"Page Size"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FlowRunMessageListResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/contacts/{contact_id}/flow-runs":{"get":{"tags":["Flows"],"summary":"List Flow Runs For Contact","description":"List flow runs for a specific contact.\n\nUsed by the contact-detail \"Flows\" tab. When ``status`` is omitted the\nresponse defaults to active runs (``running`` + ``waiting``) — the rows\na user expects to see. Pass an explicit status to inspect history.\n\n**Required Scopes:** `flows:read` AND `contacts:read`","operationId":"list_flow_runs_for_contact_api_v1_contacts__contact_id__flow_runs_get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"contact_id","in":"path","required":true,"schema":{"type":"string","title":"Contact Id"}},{"name":"status","in":"query","required":false,"schema":{"anyOf":[{"$ref":"#/components/schemas/FlowRunStatus"},{"type":"null"}],"description":"Filter by run status. Defaults to active runs (running + waiting) when omitted.","title":"Status"},"description":"Filter by run status. Defaults to active runs (running + waiting) when omitted."},{"name":"flow_id","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Filter to a specific flow","title":"Flow Id"},"description":"Filter to a specific flow"},{"name":"page","in":"query","required":false,"schema":{"type":"integer","minimum":1,"default":1,"title":"Page"}},{"name":"page_size","in":"query","required":false,"schema":{"type":"integer","maximum":100,"minimum":1,"default":50,"title":"Page Size"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FlowRunListResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/social-comment-rules":{"get":{"tags":["Comment Rules"],"summary":"List Social Comment Rules","description":"List comment auto-reply rules, in evaluation order.\n\nRules are returned in the order they are EVALUATED (priority ascending —\nlower runs first), so the list reads top-to-bottom exactly as it behaves.\n\n**Required Scopes:** `flows:read`","operationId":"list_social_comment_rules_api_v1_social_comment_rules_get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"channel_id","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Filter by channel","title":"Channel Id"},"description":"Filter by channel"},{"name":"enabled","in":"query","required":false,"schema":{"anyOf":[{"type":"boolean"},{"type":"null"}],"description":"Filter by enabled state","title":"Enabled"},"description":"Filter by enabled state"},{"name":"page","in":"query","required":false,"schema":{"type":"integer","minimum":1,"default":1,"title":"Page"}},{"name":"page_size","in":"query","required":false,"schema":{"type":"integer","maximum":100,"minimum":1,"default":20,"title":"Page Size"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"post":{"tags":["Comment Rules"],"summary":"Create Social Comment Rule","description":"Create a comment auto-reply rule.\n\n**Required Scopes:** `flows:write`","operationId":"create_social_comment_rule_api_v1_social_comment_rules_post","security":[{"HTTPBearer":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SocialCommentRuleCreate"}}}},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/social-comment-rules/{rule_id}":{"get":{"tags":["Comment Rules"],"summary":"Get Social Comment Rule","description":"Get one comment auto-reply rule.\n\n**Required Scopes:** `flows:read`","operationId":"get_social_comment_rule_api_v1_social_comment_rules__rule_id__get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"rule_id","in":"path","required":true,"schema":{"type":"string","title":"Rule Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"patch":{"tags":["Comment Rules"],"summary":"Update Social Comment Rule","description":"Update a comment auto-reply rule.\n\nPartial: omitted fields are left untouched. ``channel_id`` cannot be\nchanged — moving a rule to another channel silently changes which posts it\ncan match, so that is a delete plus a create.\n\nThe MERGED rule is re-validated, not just the submitted fields: switching\n``action`` to ``start_flow`` without also sending a ``flow_id`` has to fail\neven though each field on its own looks fine.\n\n**Required Scopes:** `flows:write`","operationId":"update_social_comment_rule_api_v1_social_comment_rules__rule_id__patch","security":[{"HTTPBearer":[]}],"parameters":[{"name":"rule_id","in":"path","required":true,"schema":{"type":"string","title":"Rule Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SocialCommentRuleUpdate"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"tags":["Comment Rules"],"summary":"Delete Social Comment Rule","description":"Delete a comment auto-reply rule.\n\nA hard delete. Rules carry no history worth archiving — the comments they\nacted on keep their own ``auto_replied`` state and their reply messages\nindependently of the rule that produced them.\n\n**Required Scopes:** `flows:write`","operationId":"delete_social_comment_rule_api_v1_social_comment_rules__rule_id__delete","security":[{"HTTPBearer":[]}],"parameters":[{"name":"rule_id","in":"path","required":true,"schema":{"type":"string","title":"Rule Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeleteResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/analytics/dashboard":{"get":{"tags":["Analytics"],"summary":"Get Dashboard Metrics","description":"Get dashboard overview metrics\n\n**Required Scopes:** `analytics:read`\n\nReturns:\n- Total messages (inbound/outbound)\n- Total conversations (open/closed)\n- Total contacts\n- Active channels\n- RCS monthly active recipients (`rcs_monthly_active_recipients`,\n  `rcs_mau_period`): current Europe/Berlin month, for RCS agents billed\n  per monthly active recipient. Their sends are not in the message\n  counts. Null when the workspace has no such agent.","operationId":"get_dashboard_metrics_api_v1_analytics_dashboard_get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"start_date","in":"query","required":false,"schema":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"description":"Start date for metrics (ISO format)","title":"Start Date"},"description":"Start date for metrics (ISO format)"},{"name":"end_date","in":"query","required":false,"schema":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"description":"End date for metrics (ISO format)","title":"End Date"},"description":"End date for metrics (ISO format)"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DashboardMetricsResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/analytics/messages/by-channel":{"get":{"tags":["Analytics"],"summary":"Get Messages By Channel","description":"Get message counts grouped by channel type\n\n**Required Scopes:** `analytics:read`\n\nReturns:\n- List of {channel_type, message_count}","operationId":"get_messages_by_channel_api_v1_analytics_messages_by_channel_get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"start_date","in":"query","required":false,"schema":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"description":"Start date for metrics","title":"Start Date"},"description":"Start date for metrics"},{"name":"end_date","in":"query","required":false,"schema":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"description":"End date for metrics","title":"End Date"},"description":"End date for metrics"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ChannelMessageCount"},"title":"Response Get Messages By Channel Api V1 Analytics Messages By Channel Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/analytics/messages/over-time":{"get":{"tags":["Analytics"],"summary":"Get Messages Over Time","description":"Get message counts over time\n\n**Required Scopes:** `analytics:read`\n\nReturns:\n- List of {date, message_count}","operationId":"get_messages_over_time_api_v1_analytics_messages_over_time_get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"start_date","in":"query","required":false,"schema":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"description":"Start date for metrics","title":"Start Date"},"description":"Start date for metrics"},{"name":"end_date","in":"query","required":false,"schema":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"description":"End date for metrics","title":"End Date"},"description":"End date for metrics"},{"name":"interval","in":"query","required":false,"schema":{"type":"string","description":"Grouping interval (day, hour)","default":"day","title":"Interval"},"description":"Grouping interval (day, hour)"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/MessageTimePoint"},"title":"Response Get Messages Over Time Api V1 Analytics Messages Over Time Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/analytics/response-time":{"get":{"tags":["Analytics"],"summary":"Get Response Time Metrics","description":"Get response time metrics\n\n**Required Scopes:** `analytics:read`\n\nReturns:\n- Response rate percentage\n- Average response time\n- Conversations with/without response","operationId":"get_response_time_metrics_api_v1_analytics_response_time_get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"start_date","in":"query","required":false,"schema":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"description":"Start date for metrics","title":"Start Date"},"description":"Start date for metrics"},{"name":"end_date","in":"query","required":false,"schema":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"description":"End date for metrics","title":"End Date"},"description":"End date for metrics"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ResponseTimeMetricsResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/analytics/resolution-time":{"get":{"tags":["Analytics"],"summary":"Get Resolution Time Metrics","description":"Get resolution time metrics (time from conversation start to close)\n\n**Required Scopes:** `analytics:read`\n\nReturns:\n- Average resolution time in hours\n- Total closed conversations\n- Per-channel breakdown","operationId":"get_resolution_time_metrics_api_v1_analytics_resolution_time_get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"start_date","in":"query","required":false,"schema":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"description":"Start date for metrics","title":"Start Date"},"description":"Start date for metrics"},{"name":"end_date","in":"query","required":false,"schema":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"description":"End date for metrics","title":"End Date"},"description":"End date for metrics"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ResolutionTimeMetricsResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/analytics/time-metrics":{"get":{"tags":["Analytics"],"summary":"Get Time Metrics Over Time","description":"Get response time and resolution time metrics over time for charting\n\n**Required Scopes:** `analytics:read`\n\nReturns:\n- Timeline with daily averages (response_minutes, resolution_hours)\n- Per-channel breakdown for tooltip display","operationId":"get_time_metrics_over_time_api_v1_analytics_time_metrics_get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"start_date","in":"query","required":false,"schema":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"description":"Start date for metrics","title":"Start Date"},"description":"Start date for metrics"},{"name":"end_date","in":"query","required":false,"schema":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"description":"End date for metrics","title":"End Date"},"description":"End date for metrics"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TimeMetricsOverTimeResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/analytics/agents/performance":{"get":{"tags":["Analytics"],"summary":"Get Agent Performance","description":"Get performance metrics by agent\n\n**Required Scopes:** `analytics:read`\n\nReturns:\n- List of {user_id, conversation_count, message_count}","operationId":"get_agent_performance_api_v1_analytics_agents_performance_get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"start_date","in":"query","required":false,"schema":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"description":"Start date for metrics","title":"Start Date"},"description":"Start date for metrics"},{"name":"end_date","in":"query","required":false,"schema":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"description":"End date for metrics","title":"End Date"},"description":"End date for metrics"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/AgentPerformance"},"title":"Response Get Agent Performance Api V1 Analytics Agents Performance Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/analytics/conversations/stats":{"get":{"tags":["Analytics"],"summary":"Get Conversation Stats","description":"Get conversation statistics\n\n**Required Scopes:** `analytics:read`\n\nReturns:\n- Conversations by status (open/assigned/resolved/closed)\n- Conversations by assignment (assigned/unassigned)\n- new_conversations: total created in the date range\n- resolved_conversations: count with RESOLVED status\n- closed_conversations: count with CLOSED status\n- avg_duration_hours: average conversation duration (creation to close)\n- avg_messages_per_conversation: average messages per conversation","operationId":"get_conversation_stats_api_v1_analytics_conversations_stats_get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"start_date","in":"query","required":false,"schema":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"description":"Start date for metrics","title":"Start Date"},"description":"Start date for metrics"},{"name":"end_date","in":"query","required":false,"schema":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"description":"End date for metrics","title":"End Date"},"description":"End date for metrics"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConversationStatsResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/analytics/links/summary":{"get":{"tags":["Analytics"],"summary":"Get Link Performance Summary","description":"Get link tracking performance summary for the dashboard widget.\n\n**Required Scopes:** `analytics:read`\n\n**Database:** SHARD (tracked_links, link_clicks tables)\n\n**Returns:**\n- total_links: Total number of tracked links\n- total_clicks: Total clicks across all links\n- clicks_today: Clicks in the last 24 hours\n- clicks_this_week: Clicks in the last 7 days\n- top_performing_links: Top 5 most clicked links\n- clicks_by_day: Click counts for the last 7 days\n- device_breakdown: Clicks by device type (desktop, mobile, tablet)\n- link_tracking_enabled: Whether link tracking is enabled for this tenant","operationId":"get_link_performance_summary_api_v1_analytics_links_summary_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LinkPerformanceSummaryResponse"}}}}},"security":[{"HTTPBearer":[]}]}},"/api/v1/analytics/ai/performance":{"get":{"tags":["Analytics"],"summary":"Get Ai Performance","description":"Get AI/Bot performance metrics for the dashboard.\n\n**Required Scopes:** `analytics:read`\n\n**Database:** SHARD (bot_interaction_logs, bot_conversation_sessions, messages tables)\n\n**Query Parameters:**\n- start_date: Start date for metrics (defaults to 30 days ago)\n- end_date: End date for metrics (defaults to now)\n\n**Returns:**\n- total_bot_messages: Count of messages sent by bots\n- total_manual_messages: Count of outbound messages NOT from bots\n- bot_sessions_count: Total bot conversation sessions\n- avg_confidence_score: Average confidence score (0.0-1.0, multiply by 100 for %)\n- escalation_rate: Percentage of sessions that escalated to human agent\n- period_start: Start of the reporting period (ISO format)\n- period_end: End of the reporting period (ISO format)","operationId":"get_ai_performance_api_v1_analytics_ai_performance_get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"start_date","in":"query","required":false,"schema":{"anyOf":[{"type":"string","format":"date"},{"type":"null"}],"description":"Start date for metrics (YYYY-MM-DD)","title":"Start Date"},"description":"Start date for metrics (YYYY-MM-DD)"},{"name":"end_date","in":"query","required":false,"schema":{"anyOf":[{"type":"string","format":"date"},{"type":"null"}],"description":"End date for metrics (YYYY-MM-DD)","title":"End Date"},"description":"End date for metrics (YYYY-MM-DD)"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AIPerformanceResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/analytics/billing-summary":{"get":{"tags":["Analytics"],"summary":"Get Billing Summary","description":"Get billing-aligned analytics summary.\n\n**Required Scopes:** `analytics:read`\n\nUses the EXACT same query logic as the billing aggregation service to ensure\nanalytics numbers match billing 1:1. Breaks down outbound/inbound messages\nby platform, email campaigns by managed vs BYOK, push campaign messages,\nand knowledge base queries.\n\nIf no date range is provided, defaults to the current calendar month.","operationId":"get_billing_summary_api_v1_analytics_billing_summary_get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"start_date","in":"query","required":false,"schema":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"description":"Start of period (ISO format). Defaults to start of current month.","title":"Start Date"},"description":"Start of period (ISO format). Defaults to start of current month."},{"name":"end_date","in":"query","required":false,"schema":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"description":"End of period (ISO format). Defaults to now.","title":"End Date"},"description":"End of period (ISO format). Defaults to now."}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BillingSummaryResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/analytics/ctwa/ad-breakdown":{"get":{"tags":["Analytics"],"summary":"Get Ctwa Ad Breakdown","description":"Get Click-to-WhatsApp (CTWA) \"Conversations from Ads\" breakdown.\n\n**Required Scopes:** `analytics:read`\n\n**Database:** SHARD (conversation_ad_attribution table)\n\nPurely informational counts — NO cost or ROI data. Aggregates CTWA\nconversations by their originating ad/post (`source_id`) over the date\nrange, ranked by conversation count descending.\n\n**Conditional rendering:** When the tenant has no CTWA conversations in the\nrange, this returns `total_conversations: 0`, `has_data: false`, and an\nempty `ads` list. The frontend hides the \"Conversations from Ads\" section\nwhen `has_data` is false (equivalently `total_conversations == 0`). There is\nno separate presence-check endpoint.\n\nReturns:\n- total_conversations: Total CTWA conversations in range (sum of ad counts)\n- has_data: Convenience flag, true when total_conversations > 0\n- ads: List of {source_id, headline, source_type, source_url, count} ranked desc\n- period_start / period_end: ISO range bounds (defaults filled server-side)","operationId":"get_ctwa_ad_breakdown_api_v1_analytics_ctwa_ad_breakdown_get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"start_date","in":"query","required":false,"schema":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"description":"Start date for metrics (ISO format). Defaults to 30 days ago.","title":"Start Date"},"description":"Start date for metrics (ISO format). Defaults to 30 days ago."},{"name":"end_date","in":"query","required":false,"schema":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"description":"End date for metrics (ISO format). Defaults to now.","title":"End Date"},"description":"End date for metrics (ISO format). Defaults to now."}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CTWAAdBreakdownResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/analytics/subscribers/overview":{"get":{"tags":["Analytics"],"summary":"Get Subscriber Overview","description":"Get subscriber overview with counts by status, channel type, and list.\n\n**Required Scopes:** `analytics:read`\n\nReturns:\n- Total subscription count\n- Breakdown by status (subscribed, pending, unsubscribed, bounced, complained)\n- Breakdown by channel type with per-status counts\n- Breakdown by list with per-status counts","operationId":"get_subscriber_overview_api_v1_analytics_subscribers_overview_get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"days","in":"query","required":false,"schema":{"type":"integer","maximum":365,"minimum":1,"description":"Number of days to look back","default":30,"title":"Days"},"description":"Number of days to look back"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SubscriberOverviewResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/analytics/subscribers/trends":{"get":{"tags":["Analytics"],"summary":"Get Subscriber Trends","description":"Get subscriber growth trends over time.\n\n**Required Scopes:** `analytics:read`\n\nReturns:\n- Daily opt-in counts by channel type\n- Daily opt-out counts by channel type\n- Net growth, average daily opt-ins/opt-outs\n- Opt-in method breakdown","operationId":"get_subscriber_trends_api_v1_analytics_subscribers_trends_get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"days","in":"query","required":false,"schema":{"type":"integer","maximum":365,"minimum":1,"description":"Number of days to look back","default":30,"title":"Days"},"description":"Number of days to look back"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SubscriberTrendsResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/analytics/campaigns/performance":{"get":{"tags":["Analytics"],"summary":"Get Campaign Performance","description":"Get campaign performance analytics.\n\n**Required Scopes:** `analytics:read`\n\nReturns:\n- Aggregate campaign stats (total/completed, sent/delivered/read/failed).\n  Volume tiles are invoiced-aligned: non-email campaigns from\n  campaign_messages + email campaigns from email_campaigns.\n- Delivery and read rates\n- Email breakdown (`email`): single/transactional (1:1, from email_messages)\n  vs campaign/newsletter (from email_campaigns), each aligned to invoiced\n  usage. Legacy flat `email_*` fields mirror `email.campaign.*`.\n- Channel-type breakdown from campaign messages\n- Last 5 completed campaigns","operationId":"get_campaign_performance_api_v1_analytics_campaigns_performance_get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"days","in":"query","required":false,"schema":{"type":"integer","maximum":365,"minimum":1,"description":"Number of days to look back","default":30,"title":"Days"},"description":"Number of days to look back"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CampaignPerformanceResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/lists":{"post":{"tags":["Lists"],"summary":"Create List","description":"Create a new list (static, dynamic, or newsletter).\n\n- **Static lists**: Contacts are manually added/removed\n- **Dynamic lists**: Contacts are matched via segment conditions. The\n  conditions must start from list subscribers: a top-level `AND` with at\n  least one positive `has_optin` condition (`value: true`, `list_id` of a\n  static or newsletter list of this workspace) or a group of only such\n  conditions (`OR` = any of the lists, `AND` = all). Otherwise HTTP 422\n  `{code, message}`: `audience_list_membership_required`,\n  `audience_membership_list_invalid`, `audience_conditions_invalid`.\n- **Newsletter lists**: Contacts subscribe via widgets/deep links\n\n**Required Scopes:** `lists:create`","operationId":"create_list_api_v1_lists_post","security":[{"HTTPBearer":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ListCreate"}}}},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ListResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"get":{"tags":["Lists"],"summary":"List Lists","description":"List all lists for the tenant with pagination.\n\nReturns paginated list of all static, dynamic, and newsletter lists.\nEach list includes its computed contact count (for email subscriptions).\n\nAny authenticated team member can list contact lists (used for app initialization).\n\n**Response format:**\n```json\n{\n    \"items\": [...],\n    \"pagination\": {\n        \"total\": 25,\n        \"page\": 1,\n        \"page_size\": 20,\n        \"total_pages\": 2,\n        \"has_next\": true,\n        \"has_prev\": false\n    }\n}\n```","operationId":"list_lists_api_v1_lists_get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"page","in":"query","required":false,"schema":{"type":"integer","minimum":1,"description":"Page number (1-indexed)","default":1,"title":"Page"},"description":"Page number (1-indexed)"},{"name":"page_size","in":"query","required":false,"schema":{"type":"integer","maximum":100,"minimum":1,"description":"Items per page","default":20,"title":"Page Size"},"description":"Items per page"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ListsListResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/lists/subscription-stats":{"get":{"tags":["Lists"],"summary":"Get Lists Subscription Stats","description":"Batch opt-in / opt-out / net-change counts for every list in the window.\n\nReturns a row for EVERY list the tenant has (zeros for lists with no\nsubscription activity) so the frontend can map results by ``list_id``.\n\n- ``opt_ins`` — subscriptions whose confirmed-opt-in timestamp\n  (``opt_in_confirmed_at``) falls in the window.\n- ``opt_outs`` — subscriptions with status ``unsubscribed`` whose\n  ``unsubscribed_at`` falls in the window.\n- ``net_change`` — ``opt_ins - opt_outs``.\n\nCounts are a current-state approximation (a re-subscribe NULLs\n``unsubscribed_at``), so churny opt-outs are undercounted.\n\n**Required Scopes:** `lists:read`\n\nNOTE: this static path is declared BEFORE ``GET /{list_id}`` so it is not\ncaptured as a list id.","operationId":"get_lists_subscription_stats_api_v1_lists_subscription_stats_get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"window_days","in":"query","required":false,"schema":{"type":"integer","maximum":365,"minimum":1,"description":"Rolling window size in days","default":30,"title":"Window Days"},"description":"Rolling window size in days"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ListSubscriptionStatsResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/lists/{list_id}/subscription-activity":{"get":{"tags":["Lists"],"summary":"Get List Subscription Activity","description":"Per-list opt-in / opt-out event drill-down (paginated, newest first).\n\n- ``direction`` filters to opt-in events (``in``), opt-out events (``out``),\n  or both (``all``), all within the window.\n- ``last_message_in_list`` is populated only for ``unsubscribed`` events (the\n  last outbound campaign message this contact received on this channel that\n  belongs to the list), with ``attribution`` of ``exact`` / ``ambiguous`` /\n  ``none``. It is null for ``subscribed`` events.\n\nA single current-state subscription row can surface both a ``subscribed`` and\nan ``unsubscribed`` event when both timestamps fall in the window.\n\n**Required Scopes:** `lists:read` OR `contacts:read`","operationId":"get_list_subscription_activity_api_v1_lists__list_id__subscription_activity_get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"list_id","in":"path","required":true,"schema":{"type":"string","title":"List Id"}},{"name":"window_days","in":"query","required":false,"schema":{"type":"integer","maximum":365,"minimum":1,"description":"Rolling window size in days","default":30,"title":"Window Days"},"description":"Rolling window size in days"},{"name":"direction","in":"query","required":false,"schema":{"type":"string","pattern":"^(all|in|out)$","description":"all | in | out","default":"all","title":"Direction"},"description":"all | in | out"},{"name":"page","in":"query","required":false,"schema":{"type":"integer","minimum":1,"description":"Page number (1-indexed)","default":1,"title":"Page"},"description":"Page number (1-indexed)"},{"name":"page_size","in":"query","required":false,"schema":{"type":"integer","maximum":100,"minimum":1,"description":"Items per page","default":25,"title":"Page Size"},"description":"Items per page"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SubscriptionActivityResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/lists/{list_id}":{"get":{"tags":["Lists"],"summary":"Get List","description":"Get a specific list by ID.\n\nReturns full list details including segment conditions and contact count.\n\n**Required Scopes:** `lists:read`","operationId":"get_list_api_v1_lists__list_id__get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"list_id","in":"path","required":true,"schema":{"type":"string","title":"List Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ListResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"put":{"tags":["Lists"],"summary":"Update List","description":"Update a list.\n\nUpdates name, description, segment conditions, slug, or newsletter config.\nFor dynamic lists, contact count is automatically recalculated, and new\n`segment_conditions` must start from list subscribers (same rule and 422\ncodes as create). Name / description-only updates of existing lists are\nnot re-validated.\n\n**Required Scopes:** `lists:update`","operationId":"update_list_api_v1_lists__list_id__put","security":[{"HTTPBearer":[]}],"parameters":[{"name":"list_id","in":"path","required":true,"schema":{"type":"string","title":"List Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ListUpdate"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ListResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"tags":["Lists"],"summary":"Delete List","description":"Delete a list.\n\nPermanently removes the list and all associated subscriptions.\n\n**Required Scopes:** `lists:delete`","operationId":"delete_list_api_v1_lists__list_id__delete","security":[{"HTTPBearer":[]}],"parameters":[{"name":"list_id","in":"path","required":true,"schema":{"type":"string","title":"List Id"}}],"responses":{"204":{"description":"Successful Response"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/lists/{list_id}/contacts":{"get":{"tags":["Lists"],"summary":"Get List Contacts","description":"Get all contacts in a list.\n\nFor static lists, returns subscribed contacts.\nFor dynamic lists, evaluates segment conditions in real-time.\n\n**Required Scopes:** `lists:read` OR `contacts:read`","operationId":"get_list_contacts_api_v1_lists__list_id__contacts_get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"list_id","in":"path","required":true,"schema":{"type":"string","title":"List Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ListContactsResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/lists/{list_id}/members":{"post":{"tags":["Lists"],"summary":"Add Contact To List","description":"Add a contact to a list (static or newsletter).\n\n- **Static lists**: channel_type should be None (uses email by default)\n- **Newsletter lists**: `channel_type` is **required** (email, telegram,\n  messenger, instagram, etc.); `channel_id` is additionally required for the\n  scoped channels telegram / messenger / instagram.\n\nNote: for backwards compatibility a newsletter request that omits\n`channel_type` is still accepted and falls back to `email`, but this\nbehavior is DEPRECATED and will be removed in a future release — always send\n`channel_type` explicitly.\n\nSame contact can subscribe to same newsletter via multiple channels.\n\n**Required Scopes:** `lists:update`","operationId":"add_contact_to_list_api_v1_lists__list_id__members_post","security":[{"HTTPBearer":[]}],"parameters":[{"name":"list_id","in":"path","required":true,"schema":{"type":"string","title":"List Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AddContactToListRequest"}}}},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AddMemberResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/lists/{list_id}/members/{contact_id}":{"delete":{"tags":["Lists"],"summary":"Remove Contact From List","description":"Remove a contact from a list.\n\n- **Static lists**: Removes the contact completely\n- **Newsletter lists**: Removes the subscription for a specific channel;\n  `channel_type` is **required**. (For backwards compatibility a missing\n  `channel_type` still falls back to `email`, but this is DEPRECATED and\n  will be removed in a future release.)\n- **Dynamic lists**: Cannot remove (membership determined by conditions)\n\n**Required Scopes:** `lists:update`","operationId":"remove_contact_from_list_api_v1_lists__list_id__members__contact_id__delete","security":[{"HTTPBearer":[]}],"parameters":[{"name":"list_id","in":"path","required":true,"schema":{"type":"string","title":"List Id"}},{"name":"contact_id","in":"path","required":true,"schema":{"type":"string","title":"Contact Id"}},{"name":"channel_type","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Required for newsletter unsubscriptions (email, telegram, messenger, instagram, ...). DEPRECATED fallback: if omitted for a newsletter list it defaults to email, but this fallback will be removed in a future release — always pass channel_type.","title":"Channel Type"},"description":"Required for newsletter unsubscriptions (email, telegram, messenger, instagram, ...). DEPRECATED fallback: if omitted for a newsletter list it defaults to email, but this fallback will be removed in a future release — always pass channel_type."}],"responses":{"204":{"description":"Successful Response"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/lists/evaluate-count":{"post":{"tags":["Lists"],"summary":"Evaluate Conditions Count","description":"Count contacts matching inline segment conditions.\n\nAccepts segment conditions directly (no saved list required) and returns\nthe count of matching contacts without loading them into memory.\n\n**Required Scopes:** `lists:read`\n\n**Response:**\n```json\n{\"count\": 42}\n```","operationId":"evaluate_conditions_count_api_v1_lists_evaluate_count_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SegmentConditionsRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"HTTPBearer":[]}]}},"/api/v1/lists/{list_id}/count":{"post":{"tags":["Lists"],"summary":"Count Dynamic List","description":"Count contacts matching a saved dynamic list's conditions.\n\nEfficiently counts matching contacts using SELECT COUNT(*) instead of\nloading all contacts into memory.\n\n**Required Scopes:** `lists:read`\n\n**Response:**\n```json\n{\"count\": 42}\n```","operationId":"count_dynamic_list_api_v1_lists__list_id__count_post","security":[{"HTTPBearer":[]}],"parameters":[{"name":"list_id","in":"path","required":true,"schema":{"type":"string","title":"List Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/lists/{list_id}/evaluate":{"post":{"tags":["Lists"],"summary":"Evaluate Dynamic List","description":"Preview contacts that match segment conditions.\n\nUsed for testing/previewing dynamic list conditions before saving.\nTemporarily applies the provided conditions and returns matching contacts.\n\n**Required Scopes:** `lists:read`","operationId":"evaluate_dynamic_list_api_v1_lists__list_id__evaluate_post","security":[{"HTTPBearer":[]}],"parameters":[{"name":"list_id","in":"path","required":true,"schema":{"type":"string","title":"List Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SegmentConditionsRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ListContactsResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/lists/{list_id}/signup-options":{"get":{"tags":["Lists"],"summary":"Get List Signup Options","description":"Get signup options (deep links + QR codes) for a newsletter list.\n\nGenerates channel-specific signup links and QR codes for newsletter\nsubscriptions. Useful for widgets and marketing materials.\n\nOnly available for newsletter-type lists.\n\n**Required Scopes:** `lists:read`\n\n**Response:**\n```json\n{\n    \"list_id\": \"...\",\n    \"list_name\": \"Weekly Newsletter\",\n    \"list_slug\": \"weekly-newsletter\",\n    \"signup_options\": {\n        \"telegram\": {\"direct_link\": \"...\", \"qr_code_base64\": \"...\"},\n        \"messenger\": {\"direct_link\": \"...\", \"qr_code_base64\": \"...\"}\n    }\n}\n```","operationId":"get_list_signup_options_api_v1_lists__list_id__signup_options_get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"list_id","in":"path","required":true,"schema":{"type":"string","title":"List Id"}},{"name":"include_qr","in":"query","required":false,"schema":{"type":"boolean","description":"Whether to generate QR codes","default":true,"title":"Include Qr"},"description":"Whether to generate QR codes"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ListSignupOptionsResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/knowledge-base/documents":{"post":{"tags":["Knowledge Base"],"summary":"Create Document","description":"Create a new knowledge base document (FAQ or manual with text content).\n\n**Required Scopes:** `knowledge_base:create`\n\nFor file uploads, use POST /documents/upload instead.\nFor website crawling, use POST /documents/crawl instead.\n\nIf folder_id is provided, the document will be uploaded to that folder's\nVertex AI corpus (multi-corpus architecture). If not provided, defaults\nto the General folder.","operationId":"create_document_api_v1_knowledge_base_documents_post","security":[{"HTTPBearer":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/KnowledgeBaseDocumentCreate"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/KnowledgeBaseDocumentResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"get":{"tags":["Knowledge Base"],"summary":"List Documents","description":"List knowledge base documents for the tenant.\n\n**Required Scopes:** `knowledge_base:read`","operationId":"list_documents_api_v1_knowledge_base_documents_get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"document_type","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Filter by document type","title":"Document Type"},"description":"Filter by document type"},{"name":"search","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Search by title or source URL","title":"Search"},"description":"Search by title or source URL"},{"name":"folder_id","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Filter by folder ID","title":"Folder Id"},"description":"Filter by folder ID"},{"name":"status","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Filter by lifecycle status. Accepts a single status or a comma-separated list (OR filter). Values: pending_upload, pending_ingestion, ready, deleting, failed. Useful for polling: e.g. ?status=pending_ingestion,deleting","title":"Status"},"description":"Filter by lifecycle status. Accepts a single status or a comma-separated list (OR filter). Values: pending_upload, pending_ingestion, ready, deleting, failed. Useful for polling: e.g. ?status=pending_ingestion,deleting"},{"name":"page","in":"query","required":false,"schema":{"type":"integer","minimum":1,"description":"Page number (1-indexed)","default":1,"title":"Page"},"description":"Page number (1-indexed)"},{"name":"page_size","in":"query","required":false,"schema":{"type":"integer","maximum":100,"minimum":1,"description":"Items per page","default":20,"title":"Page Size"},"description":"Items per page"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/KnowledgeBaseDocumentListResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/knowledge-base/documents/upload":{"post":{"tags":["Knowledge Base"],"summary":"Upload Document File","description":"Upload a file (PDF, DOCX, etc.) as a knowledge base document.\n\n**Required Scopes:** `knowledge_base:create`\n\nSupported file types: PDF, DOCX, TXT, MD.\n\n**Async pipeline (B-4):**\n1. The file bytes are uploaded to GCS synchronously.\n2. A row is inserted with ``status='pending_ingestion'`` and a fresh ``job_id``.\n3. A ``kb_add_file`` Pub/Sub message is published to the KB administration\n   worker which performs the Vertex AI RAG import.\n4. This endpoint returns 202 immediately. Poll GET ``/jobs/{job_id}`` or\n   GET ``/documents/{id}`` to observe when ``status`` transitions to\n   ``ready`` (or ``failed`` with ``last_error`` populated).","operationId":"upload_document_file_api_v1_knowledge_base_documents_upload_post","requestBody":{"content":{"multipart/form-data":{"schema":{"$ref":"#/components/schemas/Body_upload_document_file_api_v1_knowledge_base_documents_upload_post"}}},"required":true},"responses":{"202":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/KnowledgeBaseDocumentResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"HTTPBearer":[]}]}},"/api/v1/knowledge-base/documents/upload-multiple":{"post":{"tags":["Knowledge Base"],"summary":"Upload Multiple Documents","description":"Upload multiple files at once to the knowledge base.\n\n**Required Scopes:** `knowledge_base:create`\n\nUp to 200 files per request. Each file is uploaded to GCS, gets its own\n``status='pending_ingestion'`` DB row + ``job_id``, and is dispatched to\nthe KB administration worker as an independent ``kb_add_file`` Pub/Sub\nmessage. Partial success is possible — a per-file GCS or publish failure\nrolls back only that file's row and surfaces as a ``success=false`` entry.\n\nSupported file types: PDF, DOCX, TXT, MD.","operationId":"upload_multiple_documents_api_v1_knowledge_base_documents_upload_multiple_post","requestBody":{"content":{"multipart/form-data":{"schema":{"$ref":"#/components/schemas/Body_upload_multiple_documents_api_v1_knowledge_base_documents_upload_multiple_post"}}},"required":true},"responses":{"202":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MultiFileUploadResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"HTTPBearer":[]}]}},"/api/v1/knowledge-base/documents/crawl/preview":{"post":{"tags":["Knowledge Base"],"summary":"Preview Crawl Url","description":"Pre-crawl sanity check for a website URL (always HTTP 200).\n\n**Required Scopes:** `knowledge_base:admin`\n\nValidates the URL (scheme, hostname with a real domain ending, DNS, private\nnetwork block, reachability) and, when reachable, detects a sitemap and\ncounts the pages it lists so the UI can show \"sitemap found, N pages\"\nbefore the user starts the crawl.\n\n`valid=false` ⇒ `error_code` is one of `invalid_url`, `invalid_hostname`,\n`dns_unresolvable`, `blocked`, `unreachable`, `timeout`, `http_error`.\nThe same checks run server-side on every crawl start; this endpoint only\nlets the UI show the outcome early.","operationId":"preview_crawl_url_api_v1_knowledge_base_documents_crawl_preview_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CrawlPreviewRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CrawlPreviewResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"HTTPBearer":[]}]}},"/api/v1/knowledge-base/crawl/limits":{"get":{"tags":["Knowledge Base"],"summary":"Get Crawl Limits Endpoint","description":"Get the website-crawl page limit for the current workspace.\n\n**Required Scopes:** `knowledge_base:create`\n\n`max_pages` is the most pages a single crawl or re-crawl may request\n(`page_limit`). Requests above it are rejected with 422\n`crawl_page_limit_exceeded`. The limit is 2000 pages by default\n(`default_max_pages`); `is_custom=true` means it was raised for this\nworkspace. Contact `support_email` to request a higher limit.","operationId":"get_crawl_limits_endpoint_api_v1_knowledge_base_crawl_limits_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CrawlLimitsResponse"}}}}},"security":[{"HTTPBearer":[]}]}},"/api/v1/knowledge-base/documents/crawl":{"post":{"tags":["Knowledge Base"],"summary":"Crawl Website","description":"Start asynchronous website crawl job.\n\n**Required Scopes:** `knowledge_base:admin`\n\nThis endpoint immediately returns a job_id that can be used to track progress.\nThe actual crawling happens in a Pub/Sub background worker.\n\nUse GET /documents/crawl/{job_id}/status to check progress.","operationId":"crawl_website_api_v1_knowledge_base_documents_crawl_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebsiteCrawlRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebsiteCrawlJobResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"HTTPBearer":[]}]}},"/api/v1/knowledge-base/documents/crawl/{job_id}/status":{"get":{"tags":["Knowledge Base"],"summary":"Get Crawl Job Status","description":"Get status of website crawl job.\n\n**Required Scopes:** `knowledge_base:read`\n\nPoll this endpoint to track progress of an async crawl operation.\nStatus values: discovering, uploading, completed, failed","operationId":"get_crawl_job_status_api_v1_knowledge_base_documents_crawl__job_id__status_get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"job_id","in":"path","required":true,"schema":{"type":"string","title":"Job Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CrawlJobStatusResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/knowledge-base/documents/crawl/{job_id}/cancel":{"post":{"tags":["Knowledge Base"],"summary":"Cancel Crawl Job","description":"Cancel a running website crawl job.\n\n**Required Scopes:** `knowledge_base:admin`\n\nOnly jobs in QUEUED, DISCOVERING, or UPLOADING status can be cancelled.\nThe worker will cooperatively check for CANCELLED status and stop processing.","operationId":"cancel_crawl_job_api_v1_knowledge_base_documents_crawl__job_id__cancel_post","security":[{"HTTPBearer":[]}],"parameters":[{"name":"job_id","in":"path","required":true,"schema":{"type":"string","title":"Job Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SuccessResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/knowledge-base/documents/crawl/active":{"get":{"tags":["Knowledge Base"],"summary":"List Active Crawl Jobs","description":"List all active (non-completed) crawl jobs for the tenant.\n\n**Required Scopes:** `knowledge_base:read`\n\nReturns jobs that are currently in queued, discovering, uploading, or processing status.\nUse this to show running jobs when the page reloads.","operationId":"list_active_crawl_jobs_api_v1_knowledge_base_documents_crawl_active_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ActiveCrawlJobsResponse"}}}}},"security":[{"HTTPBearer":[]}]}},"/api/v1/knowledge-base/documents/{document_id}":{"delete":{"tags":["Knowledge Base"],"summary":"Delete Document","description":"Queue an async delete for a knowledge base document.\n\n**Required Scopes:** `knowledge_base:delete`\n\n**Async pipeline (B-4):**\n1. The row is flipped to ``status='deleting'`` with a fresh ``job_id``.\n2. A ``kb_delete_file`` Pub/Sub message is published.\n3. The KB administration worker performs the RAG file delete and then\n   removes the row. Poll GET ``/jobs/{job_id}`` to observe completion.","operationId":"delete_document_api_v1_knowledge_base_documents__document_id__delete","security":[{"HTTPBearer":[]}],"parameters":[{"name":"document_id","in":"path","required":true,"schema":{"type":"string","title":"Document Id"}}],"responses":{"202":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/KnowledgeBaseDeleteResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"patch":{"tags":["KB Folders"],"summary":"Update Document Folder","description":"Move a document to a different folder.\n\n**Required Scopes:** `knowledge_base:update`\n\nSet folder_id to null to move to root (unorganized).\n\nReturns the updated document along with corpus move status.\nIf corpus_moved is false, AI search may not work correctly in the new folder.","operationId":"update_document_folder_api_v1_knowledge_base_documents__document_id__patch","security":[{"HTTPBearer":[]}],"parameters":[{"name":"document_id","in":"path","required":true,"schema":{"type":"string","title":"Document Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DocumentMoveRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DocumentMoveResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/knowledge-base/documents/{document_id}/retry":{"post":{"tags":["Knowledge Base"],"summary":"Retry Document","description":"Retry ingestion for a single knowledge base document.\n\n**Required Scopes:** `knowledge_base:create`\n\nRetryable documents are either ``status='failed'`` OR a *stuck*\n``status='pending_ingestion'`` row — one left un-imported under a crawl/\ningestion job that has since finished (see ``_pending_ingestion_is_stuck``:\nold enough and owned by no still-running job). Any other status — including\na ``pending_ingestion`` row whose ingestion job is still running — returns\n409. The GCS object already exists from the original upload, so this does\nNOT re-upload the file bytes — it re-derives the ``gcs_uri`` + folder corpus\nfrom the stored metadata, flips the row to ``status='pending_ingestion'``\nwith a fresh ``job_id`` and ``last_error=NULL``, and re-publishes the\n``kb_add_file`` Pub/Sub message. Returns the updated document (202).","operationId":"retry_document_api_v1_knowledge_base_documents__document_id__retry_post","security":[{"HTTPBearer":[]}],"parameters":[{"name":"document_id","in":"path","required":true,"schema":{"type":"string","title":"Document Id"}}],"responses":{"202":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/KnowledgeBaseDocumentResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/knowledge-base/folders/{folder_id}/retry-failed":{"post":{"tags":["Knowledge Base"],"summary":"Retry Failed Documents In Folder","description":"Retry every failed document in a folder.\n\n**Required Scopes:** `knowledge_base:create`\n\nRe-queues every retryable document in the folder using the same per-document\nlogic as the single retry endpoint: all ``status='failed'`` documents, plus\nany ``status='pending_ingestion'`` document that is *stuck* — left un-imported\nunder a crawl/ingestion job that has since finished (see\n``_pending_ingestion_is_stuck``). A ``pending_ingestion`` row still owned by a\nrunning job is skipped, never re-queued. A folder with nothing to retry is not\nan error — it returns ``retried_count: 0``.\n\nReturns ``{\"success\": true, \"retried_count\": N}``.","operationId":"retry_failed_documents_in_folder_api_v1_knowledge_base_folders__folder_id__retry_failed_post","security":[{"HTTPBearer":[]}],"parameters":[{"name":"folder_id","in":"path","required":true,"schema":{"type":"string","title":"Folder Id"}}],"responses":{"202":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RetryFailedResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/knowledge-base/documents/bulk-delete":{"post":{"tags":["Knowledge Base"],"summary":"Bulk Delete Documents","description":"Queue async deletion for many documents at once.\n\n**Required Scopes:** `knowledge_base:delete`\n\nTransactional safety: rows are flipped to ``status='deleting'`` in a\nsingle transaction, then one ``kb_delete_file`` Pub/Sub message is\npublished per document. If publishing fails mid-batch, we revert all\naffected rows back to ``status='ready'`` and return 500.","operationId":"bulk_delete_documents_api_v1_knowledge_base_documents_bulk_delete_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BulkDeleteRequest"}}},"required":true},"responses":{"202":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BulkDeleteResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"HTTPBearer":[]}]}},"/api/v1/knowledge-base/folders/{folder_id}":{"delete":{"tags":["Knowledge Base"],"summary":"Delete Folder","description":"Queue async deletion for a folder and all of its documents.\n\n**Required Scopes:** `knowledge_base:delete`\n\n1. The folder is flipped to ``status='deleting'``.\n2. All contained documents are flipped to ``status='deleting'`` and each\n   is dispatched as a ``kb_delete_file`` job.\n3. A single ``kb_delete_folder_finalize`` job is published; the worker\n   polls for document drain, then deletes the Vertex AI corpus and the\n   folder row.\n\nSystem folders cannot be deleted.","operationId":"delete_folder_api_v1_knowledge_base_folders__folder_id__delete","security":[{"HTTPBearer":[]}],"parameters":[{"name":"folder_id","in":"path","required":true,"schema":{"type":"string","title":"Folder Id"}}],"responses":{"202":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FolderDeleteResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"get":{"tags":["KB Folders"],"summary":"Get Folder","description":"Get a single folder by ID.\n\n**Required Scopes:** `knowledge_base:read`","operationId":"get_folder_api_v1_knowledge_base_folders__folder_id__get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"folder_id","in":"path","required":true,"schema":{"type":"string","title":"Folder Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/KBFolderResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"put":{"tags":["KB Folders"],"summary":"Update Folder","description":"Update a folder (rename, move, reorder, toggle AI searchability).\n\n**Required Scopes:** `knowledge_base:update`\n\nSystem folders cannot be modified (except is_ai_searchable).","operationId":"update_folder_api_v1_knowledge_base_folders__folder_id__put","security":[{"HTTPBearer":[]}],"parameters":[{"name":"folder_id","in":"path","required":true,"schema":{"type":"string","title":"Folder Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/KBFolderUpdate"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/KBFolderResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/knowledge-base/jobs/{job_id}":{"get":{"tags":["Knowledge Base"],"summary":"Get Kb Job Status","description":"Poll the status of a KB admin job (upload, delete, folder delete).\n\n**Required Scopes:** `knowledge_base:read`\n\nA ``job_id`` can belong to either a document operation (uploaded or\ndeletion) or a folder deletion. The returned ``resource_type`` tells\nthe client which. Returns 404 if no resource carries this job_id.","operationId":"get_kb_job_status_api_v1_knowledge_base_jobs__job_id__get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"job_id","in":"path","required":true,"schema":{"type":"string","title":"Job Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/JobStatusResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/knowledge-base/search":{"post":{"tags":["Knowledge Base"],"summary":"Search Knowledge Base","description":"Search the knowledge base using Vertex AI RAG.\n\n**Required Scopes:** `knowledge_base:read`\n\n**Requires AI Features:** This endpoint requires AI features to be enabled\nin the billing account settings.\n\nReturns AI-generated answer with citations and sources.","operationId":"search_knowledge_base_api_v1_knowledge_base_search_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SearchKnowledgeRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SearchKnowledgeResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"HTTPBearer":[]}]}},"/api/v1/knowledge-base/tickets":{"get":{"tags":["Knowledge Base"],"summary":"List Ticket Summaries","description":"List all conversation summaries stored in the knowledge base.\n\n**Required Scopes:** `knowledge_base:read`\n\nReturns conversations that have been AI-summarized.","operationId":"list_ticket_summaries_api_v1_knowledge_base_tickets_get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"page","in":"query","required":false,"schema":{"type":"integer","minimum":1,"description":"Page number (1-indexed)","default":1,"title":"Page"},"description":"Page number (1-indexed)"},{"name":"page_size","in":"query","required":false,"schema":{"type":"integer","maximum":100,"minimum":1,"description":"Items per page","default":20,"title":"Page Size"},"description":"Items per page"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TicketSummaryListResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/knowledge-base/tickets/{ticket_id}":{"delete":{"tags":["Knowledge Base"],"summary":"Delete Ticket From Kb","description":"Remove a conversation/ticket summary from the knowledge base.\n\n**Required Scopes:** `knowledge_base:delete`\n\nThe ticket_id parameter can be either a conversation ID (new) or legacy ticket ID.\nThis removes the document from Vertex AI RAG Corpus and clears\nthe gemini_document_name field.","operationId":"delete_ticket_from_kb_api_v1_knowledge_base_tickets__ticket_id__delete","security":[{"HTTPBearer":[]}],"parameters":[{"name":"ticket_id","in":"path","required":true,"schema":{"type":"string","title":"Ticket Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/KnowledgeBaseDeleteResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/knowledge-base/documents/crawled-domains":{"get":{"tags":["Knowledge Base"],"summary":"List Crawled Domains","description":"List previously crawled domains with stored URL history.\n\n**Required Scopes:** `knowledge_base:read`\n\nReturns domains that have been crawled and have stored URL lists for\ndeterministic re-crawling. Use the returned history_id with the\n/documents/recrawl endpoint.\n\nDomains are only eligible for bulk re-crawl if they were last crawled\n14+ days ago. Individual page refreshes are always allowed.","operationId":"list_crawled_domains_api_v1_knowledge_base_documents_crawled_domains_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CrawledDomainsResponse"}}}}},"security":[{"HTTPBearer":[]}]}},"/api/v1/knowledge-base/documents/recrawl":{"post":{"tags":["Knowledge Base"],"summary":"Recrawl Domain","description":"Re-crawl a domain with non-destructive crawl-then-reconcile.\n\n**Required Scopes:** `knowledge_base:create`\n\n**Modes:**\n- `existing_pages`: Re-crawl the exact URLs from previous crawl history (faster, predictable scope)\n- `full_rescan`: Re-discover pages via sitemap/BFS from scratch (finds new pages, more thorough)\n\nBoth modes update existing documents in-place by matching source_url, create new documents\nfor newly discovered pages, and remove orphaned documents only after successful completion.\nExisting content remains intact if the crawl fails mid-way.\n\n**Note:** Domains must be at least 14 days old to be eligible for re-crawl.\nUse individual page refresh for more recent updates.","operationId":"recrawl_domain_api_v1_knowledge_base_documents_recrawl_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RecrawlRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebsiteCrawlJobResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"HTTPBearer":[]}]}},"/api/v1/knowledge-base/documents/{document_id}/refresh":{"post":{"tags":["Knowledge Base"],"summary":"Refresh Document","description":"Refresh a single document by re-fetching its source URL.\n\n**Required Scopes:** `knowledge_base:update`\n\nOnly works for website documents that have a source_url.\nRe-crawls the single page and updates the document content in-place.\n\nUses the document's folder corpus for multi-corpus architecture.","operationId":"refresh_document_api_v1_knowledge_base_documents__document_id__refresh_post","security":[{"HTTPBearer":[]}],"parameters":[{"name":"document_id","in":"path","required":true,"schema":{"type":"string","title":"Document Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RefreshDocumentResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/knowledge-base/documents/{document_id}/raw":{"get":{"tags":["Knowledge Base"],"summary":"Get Document Raw","description":"Stream the raw UTF-8 text content of an editable KB document from GCS.\n\n**Required Scopes:** `knowledge_base:read`\n\nSupported document types: ``manual``, ``uploaded_doc``, ``faq`` — and only\nfor extensions ``.md`` / ``.txt`` / ``.html``. Returns 400 for PDFs,\nimages, crawled website pages, or any other non-editable type.\n\nFor inline-created FAQ/manual docs (no filename on record) the raw\ncontent is served from ``upload_metadata.content`` to avoid a spurious\nGCS miss for rows that predate the GCS-only flow.","operationId":"get_document_raw_api_v1_knowledge_base_documents__document_id__raw_get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"document_id","in":"path","required":true,"schema":{"type":"string","title":"Document Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/KBDocumentRawResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"put":{"tags":["Knowledge Base"],"summary":"Put Document Raw","description":"Replace the raw text content of an editable KB document.\n\n**Required Scopes:** `knowledge_base:update`\n\nBehaviour:\n  1. Validates size (≤ 1 MB) — returns 413 otherwise.\n  2. Validates that the document type + extension are editable\n     (otherwise 400, same rules as GET ``/raw``).\n  3. Uploads the new bytes to GCS (same path pattern as\n     ``upload_to_gcs()`` — ``rag_uploads/{tenant_id}/doc_{id}_{uuid}.{ext}``).\n  4. Updates the DB row: ``gemini_file_name`` / ``upload_metadata.gcs_uri``\n     point at the new blob, ``status`` → ``pending_ingestion``, a fresh\n     ``job_id`` is set, ``updated_at`` = ``utc_now()``.\n  5. Publishes a ``kb_add_file`` message to ``PUBSUB_TOPIC_KB_ADMINISTRATION``\n     so the KB administration worker performs the Vertex AI RAG import.\n     When ingestion completes, status transitions back to ``ready``.\n\nHTML is stored as-is — no sanitization (user controls the content).","operationId":"put_document_raw_api_v1_knowledge_base_documents__document_id__raw_put","security":[{"HTTPBearer":[]}],"parameters":[{"name":"document_id","in":"path","required":true,"schema":{"type":"string","title":"Document Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/KBDocumentRawUpdate"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/KnowledgeBaseDocumentResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/knowledge-base/folders":{"get":{"tags":["KB Folders"],"summary":"List Folders","description":"List all folders as a tree structure.\n\n**Required Scopes:** `knowledge_base:read`\n\nReturns hierarchical folder structure with document counts.\nSystem folders (General, Websites, Ticket Summaries) are auto-created.","operationId":"list_folders_api_v1_knowledge_base_folders_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/KBFolderTreeResponse"}}}}},"security":[{"HTTPBearer":[]}]},"post":{"tags":["KB Folders"],"summary":"Create Folder","description":"Create a new folder.\n\n**Required Scopes:** `knowledge_base:create`\n\nSystem folders cannot be created via API.\nMaximum folder depth is 3 levels.","operationId":"create_folder_api_v1_knowledge_base_folders_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/KBFolderCreate"}}},"required":true},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/KBFolderResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"HTTPBearer":[]}]}},"/api/v1/knowledge-base/folders/{folder_id}/documents":{"get":{"tags":["KB Folders"],"summary":"Get Folder Documents","description":"List documents in a folder.\n\n**Required Scopes:** `knowledge_base:read`\n\nSet include_subfolders=true to include documents from all descendant folders.","operationId":"get_folder_documents_api_v1_knowledge_base_folders__folder_id__documents_get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"folder_id","in":"path","required":true,"schema":{"type":"string","title":"Folder Id"}},{"name":"include_subfolders","in":"query","required":false,"schema":{"type":"boolean","description":"Include documents from child folders","default":false,"title":"Include Subfolders"},"description":"Include documents from child folders"},{"name":"page","in":"query","required":false,"schema":{"type":"integer","minimum":1,"description":"Page number (1-indexed)","default":1,"title":"Page"},"description":"Page number (1-indexed)"},{"name":"page_size","in":"query","required":false,"schema":{"type":"integer","maximum":100,"minimum":1,"description":"Items per page","default":20,"title":"Page Size"},"description":"Items per page"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/KnowledgeBaseDocumentListResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/knowledge-base/documents/bulk-move":{"post":{"tags":["KB Folders"],"summary":"Bulk Move Documents","description":"Move multiple documents to a folder.\n\n**Required Scopes:** `knowledge_base:update`\n\nMaximum 100 documents per request.","operationId":"bulk_move_documents_api_v1_knowledge_base_documents_bulk_move_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BulkMoveRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BulkMoveResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"HTTPBearer":[]}]}},"/api/v1/knowledge-base/folders/{folder_id}/recrawl":{"post":{"tags":["KB Folders"],"summary":"Recrawl Website Folder","description":"Re-crawl a website folder.\n\n**Required Scopes:** `knowledge_base:admin`\n\nOnly applicable to folders under \"Websites\" that have documents\nwith source URLs. Will re-fetch all pages and update content.","operationId":"recrawl_website_folder_api_v1_knowledge_base_folders__folder_id__recrawl_post","security":[{"HTTPBearer":[]}],"parameters":[{"name":"folder_id","in":"path","required":true,"schema":{"type":"string","title":"Folder Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/FolderRecrawlRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebsiteCrawlJobResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/knowledge-base/bots/{bot_id}/kb-folders":{"get":{"tags":["KB Folders"],"summary":"Get Bot Kb Folders","description":"Get KB folder configuration for a bot.\n\n**Required Scopes:** `automation:read`\n\nReturns list of folders the bot uses for knowledge base search.\nIf empty, bot searches all folders.","operationId":"get_bot_kb_folders_api_v1_knowledge_base_bots__bot_id__kb_folders_get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"bot_id","in":"path","required":true,"schema":{"type":"string","title":"Bot Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BotKBFoldersResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"put":{"tags":["KB Folders"],"summary":"Set Bot Kb Folders","description":"Set KB folder configuration for a bot.\n\n**Required Scopes:** `automation:update`\n\nReplaces any existing folder assignments.\nTo search all folders, pass an empty list.","operationId":"set_bot_kb_folders_api_v1_knowledge_base_bots__bot_id__kb_folders_put","security":[{"HTTPBearer":[]}],"parameters":[{"name":"bot_id","in":"path","required":true,"schema":{"type":"string","title":"Bot Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BotKBFoldersRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BotKBFoldersResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/knowledge-base/managed-faqs":{"get":{"tags":["FAQ Management"],"summary":"List Managed Faqs","description":"List all managed FAQ documents, optionally filtered by folder.\n\n**Required Scopes:** `knowledge_base:read`","operationId":"list_managed_faqs_api_v1_knowledge_base_managed_faqs_get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"folder_id","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Filter by folder ID","title":"Folder Id"},"description":"Filter by folder ID"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ManagedFAQListResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"post":{"tags":["FAQ Management"],"summary":"Create Managed Faq","description":"Create a new managed FAQ document.\n\nFolder validation:\n- Cannot create FAQs in system folders 'websites' or 'ticket-summaries'\n- If no folder_id is provided, auto-assigns to the 'General' system folder\n\n**Required Scopes:** `knowledge_base:create`","operationId":"create_managed_faq_api_v1_knowledge_base_managed_faqs_post","security":[{"HTTPBearer":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ManagedFAQCreate"}}}},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ManagedFAQResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/knowledge-base/managed-faqs/{document_id}":{"get":{"tags":["FAQ Management"],"summary":"Get Managed Faq","description":"Get a managed FAQ document with all its Q&A items.\n\n**Required Scopes:** `knowledge_base:read`","operationId":"get_managed_faq_api_v1_knowledge_base_managed_faqs__document_id__get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"document_id","in":"path","required":true,"schema":{"type":"string","title":"Document Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ManagedFAQDetailResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"put":{"tags":["FAQ Management"],"summary":"Update Managed Faq","description":"Update a managed FAQ document title.\n\n**Required Scopes:** `knowledge_base:update`","operationId":"update_managed_faq_api_v1_knowledge_base_managed_faqs__document_id__put","security":[{"HTTPBearer":[]}],"parameters":[{"name":"document_id","in":"path","required":true,"schema":{"type":"string","title":"Document Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ManagedFAQUpdate"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ManagedFAQResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"tags":["FAQ Management"],"summary":"Delete Managed Faq","description":"Delete a managed FAQ document and all its items.\n\n**Required Scopes:** `knowledge_base:delete`","operationId":"delete_managed_faq_api_v1_knowledge_base_managed_faqs__document_id__delete","security":[{"HTTPBearer":[]}],"parameters":[{"name":"document_id","in":"path","required":true,"schema":{"type":"string","title":"Document Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FAQOperationResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/knowledge-base/managed-faqs/{document_id}/sync":{"post":{"tags":["FAQ Management"],"summary":"Sync Managed Faq","description":"Sync a managed FAQ document to Gemini RAG.\n\nThis compiles all Q&A items and uploads them to Gemini.\n\n**Required Scopes:** `knowledge_base:update`","operationId":"sync_managed_faq_api_v1_knowledge_base_managed_faqs__document_id__sync_post","security":[{"HTTPBearer":[]}],"parameters":[{"name":"document_id","in":"path","required":true,"schema":{"type":"string","title":"Document Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FAQSyncResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/knowledge-base/faq-items":{"get":{"tags":["FAQ Management"],"summary":"List Faq Items","description":"List all FAQ items for a managed FAQ document.\n\n**Required Scopes:** `knowledge_base:read`","operationId":"list_faq_items_api_v1_knowledge_base_faq_items_get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"document_id","in":"query","required":true,"schema":{"type":"string","title":"Document Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/FAQItemResponse"},"title":"Response List Faq Items Api V1 Knowledge Base Faq Items Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"post":{"tags":["FAQ Management"],"summary":"Create Faq Item","description":"Create a FAQ entry.\n\nTwo shapes, decided by `document_id`:\n\n- **standalone (default)** — omit `document_id`. The entry lives directly in\n  a folder (`folder_id`, defaulting to the tenant's *Managed FAQ* system\n  folder) and owns ONE `faq_<id>.md` RAG file. This is the only shape new\n  entries should use; the compiled \"document container\" is retired.\n- **document-scoped (legacy)** — pass `document_id` and the entry is\n  appended to that managed FAQ document, which is recompiled and re-synced\n  as a whole. Unchanged, for back-compat.\n\nA standalone entry is indexed only when it is `published` — a `draft` is\ncreated but deliberately kept out of retrieval.\n\n**Required Scopes:** `knowledge_base:create`","operationId":"create_faq_item_api_v1_knowledge_base_faq_items_post","security":[{"HTTPBearer":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/FAQItemCreate"}}}},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FAQItemResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/knowledge-base/faq-entries":{"get":{"tags":["FAQ Management"],"summary":"List Faq Entries","description":"List FAQ entries across folders, with filtering and sorting.\n\nUnlike `/faq-items`, this reaches standalone Managed FAQ entries — the ones\nwith no compiled parent document, which the document-scoped listing cannot\nsee at all.\n\nSorting by `quality` puts unscored entries (quality_score NULL) LAST rather\nthan treating them as zero: an unrated entry is unknown, not bad.\n\nBulk-migration **ledger rows** are hidden unless `include_ledger=true`.\nThey are archived bookkeeping rows (they stop the migration reprocessing a\nrejected cluster), and their `question` is raw cluster text — in a list they\nread as broken entries. They are detected structurally, not by text\nguessing; see `faq_migration_service.is_ledger_row`.\n\n**Required Scopes:** `knowledge_base:read`","operationId":"list_faq_entries_api_v1_knowledge_base_faq_entries_get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"folder_id","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Restrict to one KB folder","title":"Folder Id"},"description":"Restrict to one KB folder"},{"name":"document_id","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Restrict to one compiled parent document","title":"Document Id"},"description":"Restrict to one compiled parent document"},{"name":"status","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"draft | published | needs_review | archived. Accepts a comma-separated list (e.g. 'draft,needs_review') for a UNION.","title":"Status"},"description":"draft | published | needs_review | archived. Accepts a comma-separated list (e.g. 'draft,needs_review') for a UNION."},{"name":"origin","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"manual | auto_conversation | migrated | correction","title":"Origin"},"description":"manual | auto_conversation | migrated | correction"},{"name":"language","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Filter by language tag","title":"Language"},"description":"Filter by language tag"},{"name":"search","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Substring match on question/answer","title":"Search"},"description":"Substring match on question/answer"},{"name":"flagged_only","in":"query","required":false,"schema":{"type":"boolean","description":"Only entries with flagged_count > 0","default":false,"title":"Flagged Only"},"description":"Only entries with flagged_count > 0"},{"name":"review_only","in":"query","required":false,"schema":{"type":"boolean","description":"The review queue: entries needing attention — status in (draft, needs_review) OR flagged_count > 0. Takes precedence over `status`/`flagged_only`.","default":false,"title":"Review Only"},"description":"The review queue: entries needing attention — status in (draft, needs_review) OR flagged_count > 0. Takes precedence over `status`/`flagged_only`."},{"name":"include_ledger","in":"query","required":false,"schema":{"type":"boolean","description":"Include bulk-migration ledger rows (archived bookkeeping entries whose question is raw cluster text). Excluded by default.","default":false,"title":"Include Ledger"},"description":"Include bulk-migration ledger rows (archived bookkeeping entries whose question is raw cluster text). Excluded by default."},{"name":"sort_by","in":"query","required":false,"schema":{"type":"string","description":"uses | quality | flags | retrieved | created_at | updated_at | sort_order","default":"updated_at","title":"Sort By"},"description":"uses | quality | flags | retrieved | created_at | updated_at | sort_order"},{"name":"sort_dir","in":"query","required":false,"schema":{"type":"string","description":"asc | desc","default":"desc","title":"Sort Dir"},"description":"asc | desc"},{"name":"page","in":"query","required":false,"schema":{"type":"integer","minimum":1,"default":1,"title":"Page"}},{"name":"page_size","in":"query","required":false,"schema":{"type":"integer","maximum":200,"minimum":1,"default":50,"title":"Page Size"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FAQItemListResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/knowledge-base/faq-entries/{entry_id}/resolve-flags":{"post":{"tags":["FAQ Management"],"summary":"Resolve Faq Entry Flags","description":"Clear an entry's flags after a reviewer has looked at it.\n\nSets ``flagged_count`` to 0 and writes an audit revision. Does NOT touch\ncontent, status or the corpus — resolving flags is review bookkeeping, not an\nedit, so retrieval is unaffected and no re-sync happens.\n\n**Required Scopes:** `knowledge_base:update`","operationId":"resolve_faq_entry_flags_api_v1_knowledge_base_faq_entries__entry_id__resolve_flags_post","security":[{"HTTPBearer":[]}],"parameters":[{"name":"entry_id","in":"path","required":true,"schema":{"type":"string","title":"Entry Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/knowledge-base/faq-entries/duplicate-clusters":{"get":{"tags":["FAQ Management"],"summary":"Get Duplicate Clusters","description":"Clusters of live entries that look like duplicates of each other.\n\nSame heuristic as the KB-health dashboard (exact match on the\nalphanumeric-normalised question) so the two surfaces agree on what a\nduplicate is. Each cluster is a candidate for `POST /faq-entries/merge`.\nLedger rows and archived entries are excluded.\n\n**Required Scopes:** `knowledge_base:read`","operationId":"get_duplicate_clusters_api_v1_knowledge_base_faq_entries_duplicate_clusters_get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"folder_id","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Restrict to one KB folder","title":"Folder Id"},"description":"Restrict to one KB folder"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/knowledge-base/faq-entries/merge":{"post":{"tags":["FAQ Management"],"summary":"Merge Faq Entries","description":"Consolidate duplicate FAQ entries into one.\n\nApproval-gated and NEVER automatic — nothing in generation/migration/\ncorrection calls this. ``dry_run`` defaults to TRUE and only PREVIEWS\n(merged_stats, files that would be deleted, question variants). An execute\n(``dry_run=false``) is additionally gated by the server-side\n``FAQ_MERGE_EXECUTION_ENABLED`` flag; while that flag is off the execute path\nreturns **403** so the destructive RAG/GCS delete cannot run by accident\nagainst the shared production database. See ``faq_merge_service`` for the full\nsafety model (three independent guards + the orphan basename guardrail).\n\nOn execute the winner inherits summed usage stats, unioned rating mass\n(quality recomputed via the Wilson lower bound), unioned source\nconversations, and the losers' question phrasings; losers are de-indexed and\narchived with a merge tombstone; the winner is re-synced to RAG.\n\n**Required Scopes:** `knowledge_base:delete`","operationId":"merge_faq_entries_api_v1_knowledge_base_faq_entries_merge_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/_MergeRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"HTTPBearer":[]}]}},"/api/v1/knowledge-base/faq-items/batch":{"post":{"tags":["FAQ Management"],"summary":"Batch Create Faq Items","description":"Create multiple FAQ items in a single request.\n\nAll items must belong to the same document. Triggers ONE background sync\nafter all items are saved.\n\n**Required Scopes:** `knowledge_base:create`","operationId":"batch_create_faq_items_api_v1_knowledge_base_faq_items_batch_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/FAQItemBatchCreate"}}},"required":true},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FAQBatchCreateResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"HTTPBearer":[]}]}},"/api/v1/knowledge-base/faq-items/{item_id}":{"get":{"tags":["FAQ Management"],"summary":"Get Faq Item","description":"Get a single FAQ item.\n\n**Required Scopes:** `knowledge_base:read`","operationId":"get_faq_item_api_v1_knowledge_base_faq_items__item_id__get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"item_id","in":"path","required":true,"schema":{"type":"string","title":"Item Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FAQItemResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"put":{"tags":["FAQ Management"],"summary":"Update Faq Item","description":"Update a FAQ item: content, lifecycle status, language, folder.\n\nThis is also the publish/unpublish/archive route — `FAQItemUpdate` carries\n`status` and there is no separate publish endpoint. Because the corpus\nfollows `status`, flipping a draft to `published` is what indexes it and\nputs it in front of customers.\n\nEvery field the schema accepts is forwarded to the service. It previously\nforwarded only `question`/`answer`, so a publish returned 200 while the\nentry stayed a draft, unindexed and invisible to the assistant.\n\nThe Vertex round-trip (upload + import, tens of seconds) is NOT held open\ninside the request: a standalone entry's re-index is handed to the\n`resync_faq_entry` job on the KB administration worker — the same path the\ncreate route and the review queue use. The response returns as soon as the\nrow is committed, so an entry can read `published` for a few seconds before\n`rag_file_name` is populated. The job re-reads the entry's current status,\nso rapid publish/unpublish pairs converge on the last write instead of\nracing.\n\nA legacy document-scoped entry additionally schedules a recompile of its\nparent document.\n\n**Required Scopes:** `knowledge_base:update`","operationId":"update_faq_item_api_v1_knowledge_base_faq_items__item_id__put","security":[{"HTTPBearer":[]}],"parameters":[{"name":"item_id","in":"path","required":true,"schema":{"type":"string","title":"Item Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/FAQItemUpdate"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FAQItemResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"tags":["FAQ Management"],"summary":"Delete Faq Item","description":"Delete a FAQ item.\n\nAutomatically triggers a background sync to Vertex AI RAG after deletion.\n\n**Required Scopes:** `knowledge_base:delete`","operationId":"delete_faq_item_api_v1_knowledge_base_faq_items__item_id__delete","security":[{"HTTPBearer":[]}],"parameters":[{"name":"item_id","in":"path","required":true,"schema":{"type":"string","title":"Item Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FAQOperationResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/knowledge-base/faq-items/reorder":{"post":{"tags":["FAQ Management"],"summary":"Reorder Faq Items","description":"Reorder FAQ items within a document.\n\nAutomatically triggers a background sync to Vertex AI RAG after reorder.\n\n**Required Scopes:** `knowledge_base:update`","operationId":"reorder_faq_items_api_v1_knowledge_base_faq_items_reorder_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/FAQItemReorderRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FAQOperationResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"HTTPBearer":[]}]}},"/api/v1/knowledge-base/faq-migration/estimate":{"get":{"tags":["FAQ Migration"],"summary":"Estimate Faq Migration","description":"Preview a bulk migration: how many summaries, how many clusters, what it costs.\n\nRead-only and side-effect free — it loads the tenant's ticket summaries and\nruns the same clustering the migration would, but makes no LLM call and\nwrites nothing. This is what the \"migrate history\" button shows before the\nuser commits.\n\nNot cheap (it clusters the whole pending corpus), so call it on demand, not\non a timer. `/faq-migration/status` is the poll-safe endpoint.\n\n**Required Scopes:** `knowledge_base:read`\n**Plan features:** `knowledge_base` + `ai_features` (fail-closed: 403 `feature_disabled`)","operationId":"estimate_faq_migration_api_v1_knowledge_base_faq_migration_estimate_get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"limit","in":"query","required":false,"schema":{"anyOf":[{"type":"integer","maximum":20000,"minimum":1},{"type":"null"}],"description":"Only consider this many summaries (default: all, max 20000)","title":"Limit"},"description":"Only consider this many summaries (default: all, max 20000)"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FAQMigrationEstimateResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/knowledge-base/faq-migration/start":{"post":{"tags":["FAQ Migration"],"summary":"Start Faq Migration","description":"Start (or preview) the bulk migration of ticket summaries into FAQ drafts.\n\n`dry_run=true` is handled **inline and never enqueues anything**: it loads\nand clusters the summaries and returns what it would have processed. No\nGemini call, no row written, no folder touched — so it cannot possibly\nchange the tenant's knowledge base. Everything else publishes one\n`migrate_ticket_summaries` job; the worker processes a bounded number of\nclusters and re-enqueues itself until nothing is left, so this returns\nimmediately and the UI polls `/faq-migration/status`.\n\nEverything the migration imports lands as a **draft**, whatever the\ntenant's `faq_generation_mode` says.\n\n**409** when a migration is already in flight — the job chain would\notherwise fork, double-billing the tenant for the same clusters. The\nresponse body carries the run that is holding it:\n\n    {\"detail\": {\"code\": \"migration_already_running\",\n                \"message\": \"...\",\n                \"run\": {...} | null,\n                \"last_activity_at\": \"...Z\" | null}}\n\nPass `force=true` to override, once you know the other run is dead.\n\n**Required Scopes:** `knowledge_base:manage`\n**Plan features:** `knowledge_base` + `ai_features` (fail-closed: 403 `feature_disabled`)","operationId":"start_faq_migration_api_v1_knowledge_base_faq_migration_start_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/FAQMigrationStartRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FAQMigrationStartResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"HTTPBearer":[]}]}},"/api/v1/knowledge-base/faq-migration/status":{"get":{"tags":["FAQ Migration"],"summary":"Get Faq Migration Status","description":"Progress of the bulk migration, from what it has actually persisted.\n\nPoll-safe: counts are read back from the rows the migration wrote, with no\nclustering and no LLM call.\n\n**What is and is not knowable.** The migration keeps no run record and no\ncursor — it is resumable purely from `faq_items.source_conversation_ids`.\nSo the backend can substantiate exactly one ratio,\n`percent_summaries_processed` (summaries consumed / summaries that exist),\nand it deliberately reports nothing about clusters remaining or time\nleft, because nothing persists that. `running` is likewise an inference\n(`running_evidence` says from what), not a recorded state.\n\n**Required Scopes:** `knowledge_base:read`\n**Plan features:** `knowledge_base` + `ai_features` (fail-closed: 403 `feature_disabled`)","operationId":"get_faq_migration_status_api_v1_knowledge_base_faq_migration_status_get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"include_estimate","in":"query","required":false,"schema":{"type":"boolean","description":"Also run a fresh clustering pass (same work as /faq-migration/estimate). Expensive — never poll with this on","default":false,"title":"Include Estimate"},"description":"Also run a fresh clustering pass (same work as /faq-migration/estimate). Expensive — never poll with this on"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FAQMigrationStatusResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/conversations/{conversation_id}/suggest":{"post":{"tags":["Agent Suggestions"],"summary":"Get Agent Suggestions","description":"Get AI-powered suggestions for agent based on conversation context.\n\nSearches the knowledge base using recent messages as context and returns\nrelevant suggestions from FAQs, manuals, and past tickets.\n\nArgs:\n    conversation_id: Conversation ID to get suggestions for\n\nReturns:\n    Dict with:\n    - suggestion_id: ID for tracking acceptance\n    - answer: AI-generated response\n    - sources: List of relevant documents/tickets\n    - query: Query used for search\n\n**🔐 Required Scopes:** `conversations:read`\n","operationId":"get_agent_suggestions_api_v1_conversations__conversation_id__suggest_post","security":[{"HTTPBearer":[]}],"parameters":[{"name":"conversation_id","in":"path","required":true,"schema":{"type":"string","title":"Conversation Id"}}],"requestBody":{"content":{"application/json":{"schema":{"anyOf":[{"$ref":"#/components/schemas/SuggestRequest"},{"type":"null"}],"title":"Request"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AgentSuggestionResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/suggestions/{suggestion_id}/accept":{"post":{"tags":["Agent Suggestions"],"summary":"Accept Suggestion","description":"Mark a suggestion as accepted by the agent.\n\nThis tracks which suggestions are helpful for improving the knowledge base.\n\nArgs:\n    suggestion_id: Suggestion ID to mark as accepted\n    accepted_source_id: Optional ID of the specific source that was used\n    ai_answer_event_id: The event id returned by /suggest. Pass it so the\n        acceptance lands on the right event and the FAQ entries behind the\n        answer get credited (times_in_sent_reply). Omitted, the event is\n        matched back from the suggestion's conversation + query.\n\nReturns:\n    Success message\n\n**🔐 Required Scopes:** `conversations:update`\n","operationId":"accept_suggestion_api_v1_suggestions__suggestion_id__accept_post","security":[{"HTTPBearer":[]}],"parameters":[{"name":"suggestion_id","in":"path","required":true,"schema":{"type":"string","title":"Suggestion Id"}},{"name":"accepted_source_id","in":"query","required":false,"schema":{"type":"string","title":"Accepted Source Id"}},{"name":"ai_answer_event_id","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Ai Answer Event Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SuggestionAcceptResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/conversations/{conversation_id}/suggestions":{"get":{"tags":["Agent Suggestions"],"summary":"Get Suggestion History","description":"Get suggestion history for a conversation.\n\nReturns list of previous suggestions made for this conversation.\n\nArgs:\n    conversation_id: Conversation ID\n\nReturns:\n    List of suggestion records\n\n**🔐 Required Scopes:** `conversations:read`\n","operationId":"get_suggestion_history_api_v1_conversations__conversation_id__suggestions_get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"conversation_id","in":"path","required":true,"schema":{"type":"string","title":"Conversation Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"type":"object","additionalProperties":true},"title":"Response Get Suggestion History Api V1 Conversations  Conversation Id  Suggestions Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/conversations/{conversation_id}/search-similar-tickets":{"post":{"tags":["Agent Suggestions"],"summary":"Search Similar Tickets For Conversation","description":"**Retired.** Always returns `410 Gone`.\n\nSimilar ticket search was retired in October 2026 (conversation summaries\nare no longer indexed for search). Summaries are still stored on each\nconversation; list them with `GET /api/v1/knowledge-base/tickets`.\n\n**Required Scopes:** `conversations:read`","operationId":"search_similar_tickets_for_conversation_api_v1_conversations__conversation_id__search_similar_tickets_post","deprecated":true,"security":[{"HTTPBearer":[]}],"parameters":[{"name":"conversation_id","in":"path","required":true,"schema":{"type":"string","title":"Conversation Id"}}],"responses":{"410":{"description":"Gone. Similar ticket search has been retired.","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/ai-answers/{event_id}":{"get":{"tags":["AI Answers"],"summary":"Get Ai Answer","description":"Get one AI answer event with its sources and rating state.\n\n**Required Scopes:** `conversations:read`","operationId":"get_ai_answer_api_v1_ai_answers__event_id__get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"event_id","in":"path","required":true,"schema":{"type":"string","title":"Event Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AIAnswerEventResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/conversations/{conversation_id}/ai-answers":{"get":{"tags":["AI Answers"],"summary":"List Conversation Ai Answers","description":"List AI answers generated in a conversation.\n\nThe rating UI renders a control next to a *message*, so results are also\nfilterable by ``message_id`` — that is the key the frontend holds.\n\n**Required Scopes:** `conversations:read`","operationId":"list_conversation_ai_answers_api_v1_conversations__conversation_id__ai_answers_get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"conversation_id","in":"path","required":true,"schema":{"type":"string","title":"Conversation Id"}},{"name":"message_id","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Only the event that produced this message","title":"Message Id"},"description":"Only the event that produced this message"},{"name":"page","in":"query","required":false,"schema":{"type":"integer","minimum":1,"default":1,"title":"Page"}},{"name":"page_size","in":"query","required":false,"schema":{"type":"integer","maximum":200,"minimum":1,"default":50,"title":"Page Size"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AIAnswerEventListResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/ai-answers/{event_id}/rate":{"post":{"tags":["AI Answers"],"summary":"Rate Ai Answer","description":"Rate an AI answer 👍 / 👎 / ✗ (upsert — re-rating replaces).\n\n``reason_category`` (``factually_wrong`` | ``outdated`` | ``incomplete`` |\n``off_topic`` | ``wrong_tone`` | ``policy_violation`` | ``hallucinated`` |\n``other``) is optional:\n\n* ``down`` without a reason is accepted — a bare 👎 that only feeds the\n  quality statistics.\n* ``wrong`` without a reason is stored as ``factually_wrong``.\n\nA KB correction is opened (and an analysis job enqueued) when:\n\n* the verdict is ``wrong`` — always;\n* the verdict is ``down`` **with** a ``reason_category`` — always\n  (``comment`` / ``better_answer`` optional);\n* the verdict is ``down`` without a reason but with substantive text\n  (>= 15 chars) in ``comment`` and/or ``better_answer`` — legacy fallback.\n\n``correction_opened`` / ``correction_id`` in the response tell the caller\nwhich happened, so the UI can confirm it (\"Sent for review\") and link to\nthe review queue.\n\nOpening a correction never publishes anything by itself: a ``down``-born\ncorrection is review-only even for tenants in ``kb_correction_mode=auto``.\n\nReturns the recomputed scores of every FAQ entry the verdict touched, so\nthe UI can reflect the consequence immediately.\n\n**Required Scopes:** `conversations:write`","operationId":"rate_ai_answer_api_v1_ai_answers__event_id__rate_post","security":[{"HTTPBearer":[]}],"parameters":[{"name":"event_id","in":"path","required":true,"schema":{"type":"string","title":"Event Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AIAnswerRateRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AIAnswerRateResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"tags":["AI Answers"],"summary":"Delete Ai Answer Rating","description":"Withdraw your own rating. Affected entry scores are recomputed.\n\n**Required Scopes:** `conversations:write`","operationId":"delete_ai_answer_rating_api_v1_ai_answers__event_id__rate_delete","security":[{"HTTPBearer":[]}],"parameters":[{"name":"event_id","in":"path","required":true,"schema":{"type":"string","title":"Event Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeleteResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/kb-corrections":{"get":{"tags":["KB Corrections"],"summary":"List Kb Corrections","description":"The correction review queue.\n\n**Required Scopes:** `knowledge_base:read`","operationId":"list_kb_corrections_api_v1_kb_corrections_get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"status","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"open|analyzing|proposed|applied|rejected|failed","title":"Status"},"description":"open|analyzing|proposed|applied|rejected|failed"},{"name":"page","in":"query","required":false,"schema":{"type":"integer","minimum":1,"default":1,"title":"Page"}},{"name":"page_size","in":"query","required":false,"schema":{"type":"integer","maximum":100,"minimum":1,"default":25,"title":"Page Size"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/KBCorrectionListResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"post":{"tags":["KB Corrections"],"summary":"Create Kb Correction","description":"Report something in the knowledge base as wrong.\n\nReporting is deliberately an everyday agent permission — the person who\nnotices the mistake is the one on the conversation. Deciding what the KB\nsays still needs `knowledge_base:manage`.\n\n**Required Scopes:** `conversations:write`","operationId":"create_kb_correction_api_v1_kb_corrections_post","security":[{"HTTPBearer":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/KBCorrectionCreate"}}}},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/KBCorrectionResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/kb-corrections/{correction_id}":{"get":{"tags":["KB Corrections"],"summary":"Get Kb Correction","description":"Correction detail, including the answer it came from and the entries it\nwould change.\n\n**Required Scopes:** `knowledge_base:read`","operationId":"get_kb_correction_api_v1_kb_corrections__correction_id__get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"correction_id","in":"path","required":true,"schema":{"type":"string","title":"Correction Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/KBCorrectionResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/kb-corrections/{correction_id}/approve":{"post":{"tags":["KB Corrections"],"summary":"Approve Kb Correction","description":"Apply a correction's proposed changes to the knowledge base.\n\nEvery applied change writes a ``faq_entry_revisions`` row, which is what\nmakes this reversible in one click.\n\n**Required Scopes:** `knowledge_base:manage` — plus `automation:update`\nwhen any change that would be applied is an ``append_bot_rule`` (it\nrewrites the bot's instructions); otherwise 403 ``missing_scope``.","operationId":"approve_kb_correction_api_v1_kb_corrections__correction_id__approve_post","security":[{"HTTPBearer":[]}],"parameters":[{"name":"correction_id","in":"path","required":true,"schema":{"type":"string","title":"Correction Id"}}],"requestBody":{"content":{"application/json":{"schema":{"anyOf":[{"$ref":"#/components/schemas/KBCorrectionApprove"},{"type":"null"}],"title":"Payload"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/KBCorrectionResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/kb-corrections/{correction_id}/reject":{"post":{"tags":["KB Corrections"],"summary":"Reject Kb Correction","description":"Decline a proposed correction. Nothing touches the knowledge base.\n\nA policy correction routed to an open agent skill suggestion has its rule\nremoved from that suggestion; with no rule left the suggestion is\ndismissed. The answer rating itself is kept.\n\n**Required Scopes:** `knowledge_base:manage`","operationId":"reject_kb_correction_api_v1_kb_corrections__correction_id__reject_post","security":[{"HTTPBearer":[]}],"parameters":[{"name":"correction_id","in":"path","required":true,"schema":{"type":"string","title":"Correction Id"}}],"requestBody":{"content":{"application/json":{"schema":{"anyOf":[{"$ref":"#/components/schemas/KBCorrectionReject"},{"type":"null"}],"title":"Payload"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/KBCorrectionResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/faq-entries/{faq_item_id}/revisions":{"get":{"tags":["KB Corrections"],"summary":"List Faq Entry Revisions","description":"Full change history of a FAQ entry, newest first.\n\n**Required Scopes:** `knowledge_base:read`","operationId":"list_faq_entry_revisions_api_v1_faq_entries__faq_item_id__revisions_get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"faq_item_id","in":"path","required":true,"schema":{"type":"string","title":"Faq Item Id"}},{"name":"page","in":"query","required":false,"schema":{"type":"integer","minimum":1,"default":1,"title":"Page"}},{"name":"page_size","in":"query","required":false,"schema":{"type":"integer","maximum":100,"minimum":1,"default":25,"title":"Page Size"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FAQEntryRevisionListResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/faq-revisions/{revision_id}/revert":{"post":{"tags":["KB Corrections"],"summary":"Revert Faq Revision","description":"Undo one revision, restoring the entry's previous question/answer/status.\n\nReturns the NEW revision row that records the revert — a revert is itself\na change, never a silent rewrite of history.\n\n**Required Scopes:** `knowledge_base:manage`","operationId":"revert_faq_revision_api_v1_faq_revisions__revision_id__revert_post","security":[{"HTTPBearer":[]}],"parameters":[{"name":"revision_id","in":"path","required":true,"schema":{"type":"string","title":"Revision Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FAQEntryRevisionResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/kb-health":{"get":{"tags":["KB Health"],"summary":"Get Kb Health","description":"Knowledge-base health dashboard.\n\nBuckets: most used, never used, flagged, low quality, possible duplicates,\nstale. Note ``never_used`` counts *sent replies*, not retrievals — an entry\nthat keeps getting retrieved but never makes it into an answer is exactly\nthe kind of dead weight this view exists to surface.\n\nBulk-migration ledger rows are excluded from every bucket and every total.\n\n**Required Scopes:** `knowledge_base:read`","operationId":"get_kb_health_api_v1_kb_health_get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"folder_id","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Restrict to one KB folder","title":"Folder Id"},"description":"Restrict to one KB folder"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/KBHealthResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/oauth-apps/apps":{"get":{"tags":["OAuth2 Apps"],"summary":"List Apps","description":"List registered OAuth apps for the current tenant.\n\n**Required Scopes:** `api:admin`","operationId":"list_apps_api_v1_oauth_apps_apps_get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"active_only","in":"query","required":false,"schema":{"type":"boolean","description":"Filter to active apps only","default":true,"title":"Active Only"},"description":"Filter to active apps only"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/OAuthAppResponse"},"title":"Response List Apps Api V1 Oauth Apps Apps Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"post":{"tags":["OAuth2 Apps"],"summary":"Create App","description":"Register a new OAuth app for the current tenant.\n\nReturns the client_secret **once** - it cannot be retrieved again.\nStore it securely!\n\nNote: Public unverified apps are limited to OIDC and read-only scopes.\nTo use write/create/delete scopes publicly, submit for verification.\n\n**Required Scopes:** `api:admin`","operationId":"create_app_api_v1_oauth_apps_apps_post","security":[{"HTTPBearer":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/OAuthAppCreate"}}}},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OAuthAppCreateResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/oauth-apps/apps/{app_id}":{"get":{"tags":["OAuth2 Apps"],"summary":"Get App","description":"Get OAuth app details.\n\n**Required Scopes:** `api:admin`","operationId":"get_app_api_v1_oauth_apps_apps__app_id__get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"app_id","in":"path","required":true,"schema":{"type":"string","title":"App Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OAuthAppResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"put":{"tags":["OAuth2 Apps"],"summary":"Update App","description":"Update an OAuth app's settings.\n\nNote: Public unverified apps are limited to OIDC and read-only scopes.\nTo use write/create/delete scopes publicly, submit for verification.\n\n**Required Scopes:** `api:admin`","operationId":"update_app_api_v1_oauth_apps_apps__app_id__put","security":[{"HTTPBearer":[]}],"parameters":[{"name":"app_id","in":"path","required":true,"schema":{"type":"string","title":"App Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/OAuthAppUpdate"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OAuthAppResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"tags":["OAuth2 Apps"],"summary":"Delete App","description":"Delete an OAuth app.\n\nThis revokes all authorizations and removes the app.\n\n**Required Scopes:** `api:admin`","operationId":"delete_app_api_v1_oauth_apps_apps__app_id__delete","security":[{"HTTPBearer":[]}],"parameters":[{"name":"app_id","in":"path","required":true,"schema":{"type":"string","title":"App Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OAuthDeleteResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/oauth-apps/apps/{app_id}/regenerate-secret":{"post":{"tags":["OAuth2 Apps"],"summary":"Regenerate App Secret","description":"Regenerate the client secret for an OAuth app.\n\nThis immediately invalidates the old secret. The new secret is returned\n**once** and cannot be retrieved again.\n\nExisting access tokens remain valid until they expire, but the old secret\ncan no longer be used for token exchanges or refreshes.\n\n**Required Scopes:** `api:admin`","operationId":"regenerate_app_secret_api_v1_oauth_apps_apps__app_id__regenerate_secret_post","security":[{"HTTPBearer":[]}],"parameters":[{"name":"app_id","in":"path","required":true,"schema":{"type":"string","title":"App Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OAuthAppCreateResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/oauth-apps/authorizations":{"get":{"tags":["OAuth2 Apps"],"summary":"List Authorizations","description":"List user's connected OAuth apps.\n\nShows apps the user has authorized to access their account.\n\nBy default (`scope=tenant`) only authorizations belonging to the\ncurrently-active tenant are returned, preventing cross-tenant leakage on\nthe Connected Apps settings page. Pass `scope=all` to list the user's\nconnected apps across every tenant they belong to.\n\n**Requires authentication.**","operationId":"list_authorizations_api_v1_oauth_apps_authorizations_get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"scope","in":"query","required":false,"schema":{"enum":["tenant","all"],"type":"string","description":"Which authorizations to return. 'tenant' (default) limits the list to the currently-active tenant; 'all' returns the user's connected apps across every tenant.","default":"tenant","title":"Scope"},"description":"Which authorizations to return. 'tenant' (default) limits the list to the currently-active tenant; 'all' returns the user's connected apps across every tenant."}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/OAuthAuthorizationResponse"},"title":"Response List Authorizations Api V1 Oauth Apps Authorizations Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/oauth-apps/authorizations/{authorization_id}":{"delete":{"tags":["OAuth2 Apps"],"summary":"Revoke Authorization","description":"Revoke access for a connected app.\n\nImmediately invalidates all tokens for this app-user combination.\n\n**Requires authentication.**","operationId":"revoke_authorization_api_v1_oauth_apps_authorizations__authorization_id__delete","security":[{"HTTPBearer":[]}],"parameters":[{"name":"authorization_id","in":"path","required":true,"schema":{"type":"string","title":"Authorization Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OAuthDeleteResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/widgets":{"post":{"tags":["Widgets"],"summary":"Create Widget","description":"Create a new widget\n\n**Required Scopes:** `widgets:create`","operationId":"create_widget_api_v1_widgets_post","security":[{"HTTPBearer":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WidgetCreate"}}}},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WidgetResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"get":{"tags":["Widgets"],"summary":"List Widgets","description":"List all widgets for the tenant","operationId":"list_widgets_api_v1_widgets_get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"skip","in":"query","required":false,"schema":{"type":"integer","default":0,"title":"Skip"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","default":100,"title":"Limit"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/WidgetResponse"},"title":"Response List Widgets Api V1 Widgets Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/widgets/{widget_id}":{"get":{"tags":["Widgets"],"summary":"Get Widget","description":"Get a specific widget","operationId":"get_widget_api_v1_widgets__widget_id__get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"widget_id","in":"path","required":true,"schema":{"type":"string","title":"Widget Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WidgetResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"put":{"tags":["Widgets"],"summary":"Update Widget","description":"Update a widget\n\n**Required Scopes:** `widgets:update`","operationId":"update_widget_api_v1_widgets__widget_id__put","security":[{"HTTPBearer":[]}],"parameters":[{"name":"widget_id","in":"path","required":true,"schema":{"type":"string","title":"Widget Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WidgetUpdate"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WidgetResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"tags":["Widgets"],"summary":"Delete Widget","description":"Delete a widget\n\n**Required Scopes:** `widgets:delete`","operationId":"delete_widget_api_v1_widgets__widget_id__delete","security":[{"HTTPBearer":[]}],"parameters":[{"name":"widget_id","in":"path","required":true,"schema":{"type":"string","title":"Widget Id"}}],"responses":{"204":{"description":"Successful Response"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/widgets/{widget_id}/embed-code":{"get":{"tags":["Widgets"],"summary":"Get Embed Code","description":"Get the embed code for a widget","operationId":"get_embed_code_api_v1_widgets__widget_id__embed_code_get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"widget_id","in":"path","required":true,"schema":{"type":"string","title":"Widget Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WidgetEmbedCodeResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/widgets/{widget_id}/analytics":{"get":{"tags":["Widgets"],"summary":"Get Widget Analytics","description":"Get analytics data for a widget\n\nReturns aggregated counts by event type and daily breakdown\n\n**Required Scopes:** `widgets:read`","operationId":"get_widget_analytics_api_v1_widgets__widget_id__analytics_get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"widget_id","in":"path","required":true,"schema":{"type":"string","title":"Widget Id"}},{"name":"days","in":"query","required":false,"schema":{"type":"integer","default":30,"title":"Days"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WidgetAnalyticsResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/widgets/{widget_id}/identity-verification":{"get":{"tags":["Widgets"],"summary":"Get Identity Verification","description":"Get the identity verification status of a widget\n\nReturns whether signed identities are verified, whether a secret exists,\nthe last 4 characters of the secret and when it was generated. Never\nreturns the secret itself.\n\n**Required Scopes:** `widgets:read`","operationId":"get_identity_verification_api_v1_widgets__widget_id__identity_verification_get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"widget_id","in":"path","required":true,"schema":{"type":"string","title":"Widget Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WidgetIdentityVerificationResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"put":{"tags":["Widgets"],"summary":"Update Identity Verification","description":"Enable or disable identity verification for a widget\n\nBody: `{\"enabled\": true}` or `{\"enabled\": false}`. Enabling requires a\nsecret (generate one with the rotate endpoint first), otherwise 409\n`secret_required`.\n\n**Required Scopes:** `widgets:update`","operationId":"update_identity_verification_api_v1_widgets__widget_id__identity_verification_put","security":[{"HTTPBearer":[]}],"parameters":[{"name":"widget_id","in":"path","required":true,"schema":{"type":"string","title":"Widget Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WidgetIdentityVerificationUpdate"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WidgetIdentityVerificationResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/widgets/{widget_id}/identity-verification/rotate":{"post":{"tags":["Widgets"],"summary":"Rotate Identity Verification Secret","description":"Generate a new identity verification secret for a widget\n\nReturns the new secret. The previous secret stops working immediately, so\nevery identity signed with it falls back to unverified until your server\nsigns with the new one. Store the secret on your server only; never put it\nin browser code.\n\nOnly a signed-in Owner or Admin in the SendSeven app can call this. API\nkeys, OAuth apps (including MCP clients) and embed tokens get 403\n`interactive_session_required`, whatever their scopes.\n\n**Required Scopes:** `settings:admin`","operationId":"rotate_identity_verification_secret_api_v1_widgets__widget_id__identity_verification_rotate_post","security":[{"HTTPBearer":[]}],"parameters":[{"name":"widget_id","in":"path","required":true,"schema":{"type":"string","title":"Widget Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WidgetIdentitySecretRotateResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/widgets/{widget_id}/identity-verification/reveal":{"post":{"tags":["Widgets"],"summary":"Reveal Identity Verification Secret","description":"Reveal the current identity verification secret of a widget\n\nReturns 404 `secret_not_found` when no secret has been generated yet.\n\nOnly a signed-in Owner or Admin in the SendSeven app can call this. API\nkeys, OAuth apps (including MCP clients) and embed tokens get 403\n`interactive_session_required`, whatever their scopes.\n\n**Required Scopes:** `settings:admin`","operationId":"reveal_identity_verification_secret_api_v1_widgets__widget_id__identity_verification_reveal_post","security":[{"HTTPBearer":[]}],"parameters":[{"name":"widget_id","in":"path","required":true,"schema":{"type":"string","title":"Widget Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WidgetIdentitySecretRevealResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/live-chat/sessions":{"get":{"tags":["Live Chat"],"summary":"List Live Chat Sessions","description":"List active live chat sessions for the tenant\nOptionally filter by agent_id\n\n**🔐 Required Scopes:** `conversations:read`\n","operationId":"list_live_chat_sessions_api_v1_live_chat_sessions_get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"agent_id","in":"query","required":false,"schema":{"type":"string","title":"Agent Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/LiveChatSessionResponse"},"title":"Response List Live Chat Sessions Api V1 Live Chat Sessions Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/live-chat/sessions/{session_id}":{"get":{"tags":["Live Chat"],"summary":"Get Live Chat Session","description":"Get a specific live chat session\n\n**🔐 Required Scopes:** `conversations:read`\n","operationId":"get_live_chat_session_api_v1_live_chat_sessions__session_id__get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"session_id","in":"path","required":true,"schema":{"type":"string","title":"Session Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LiveChatSessionResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/live-chat/sessions/{session_id}/assign":{"post":{"tags":["Live Chat"],"summary":"Assign Agent To Session","description":"Assign an agent to a live chat session\nCreates conversation if it doesn't exist\n\n**🔐 Required Scopes:** `conversations:update`\n","operationId":"assign_agent_to_session_api_v1_live_chat_sessions__session_id__assign_post","security":[{"HTTPBearer":[]}],"parameters":[{"name":"session_id","in":"path","required":true,"schema":{"type":"string","title":"Session Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/LiveChatAgentAssignRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LiveChatSessionResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/live-chat/sessions/{session_id}/messages":{"post":{"tags":["Live Chat"],"summary":"Agent Send Message","description":"Send a message as an agent in a live chat session\n\n**Required Scopes:** `conversations:update`\n\n409 ``{\"error\": \"CONTACT_BLOCKED\", ...}`` when the visitor's contact is blocked.","operationId":"agent_send_message_api_v1_live_chat_sessions__session_id__messages_post","security":[{"HTTPBearer":[]}],"parameters":[{"name":"session_id","in":"path","required":true,"schema":{"type":"string","title":"Session Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/LiveChatMessageSend"}}}},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LiveChatMessageSendResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/live-chat/sessions/{session_id}/end":{"post":{"tags":["Live Chat"],"summary":"Agent End Session","description":"End a live chat session (agent side)\n\n**🔐 Required Scopes:** `conversations:update`\n","operationId":"agent_end_session_api_v1_live_chat_sessions__session_id__end_post","security":[{"HTTPBearer":[]}],"parameters":[{"name":"session_id","in":"path","required":true,"schema":{"type":"string","title":"Session Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LiveChatEndResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/email-providers":{"get":{"tags":["Email Providers"],"summary":"List Providers","description":"List all active (non-archived) email provider configurations for the tenant","operationId":"list_providers_api_v1_email_providers_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"items":{"$ref":"#/components/schemas/EmailProviderConfigResponse"},"type":"array","title":"Response List Providers Api V1 Email Providers Get"}}}}},"security":[{"HTTPBearer":[]}]},"post":{"tags":["Email Providers"],"summary":"Create Provider","description":"Create a new email provider configuration\n\n- Encrypts API key before storage\n- If set as default, removes default flag from other providers\n- Validates provider configuration","operationId":"create_provider_api_v1_email_providers_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/EmailProviderConfigCreate"}}},"required":true},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/EmailProviderConfigResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"HTTPBearer":[]}]}},"/api/v1/email-providers/archived":{"get":{"tags":["Email Providers"],"summary":"List Archived Providers","description":"List all archived email provider configurations for the tenant (admin/audit)\n\nReturns archived providers with metadata but without sensitive credentials.\nUseful for audit trails and reviewing historical configurations.\n\n**Required Scopes:** `settings:read`","operationId":"list_archived_providers_api_v1_email_providers_archived_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"items":{"$ref":"#/components/schemas/ArchivedEmailProviderResponse"},"type":"array","title":"Response List Archived Providers Api V1 Email Providers Archived Get"}}}}},"security":[{"HTTPBearer":[]}]}},"/api/v1/email-providers/{provider_id}":{"get":{"tags":["Email Providers"],"summary":"Get Provider","description":"Get a specific email provider configuration (excludes archived)\n\n**🔐 Required Scopes:** `settings:read`\n","operationId":"get_provider_api_v1_email_providers__provider_id__get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"provider_id","in":"path","required":true,"schema":{"type":"string","title":"Provider Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/EmailProviderConfigResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"patch":{"tags":["Email Providers"],"summary":"Update Provider","description":"Update email provider configuration\n\n- Can update name, API key, config, active status, default status\n- If setting as default, removes default flag from other providers\n\n**🔐 Required Scopes:** `settings:update`\n","operationId":"update_provider_api_v1_email_providers__provider_id__patch","security":[{"HTTPBearer":[]}],"parameters":[{"name":"provider_id","in":"path","required":true,"schema":{"type":"string","title":"Provider Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/EmailProviderConfigUpdate"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/EmailProviderConfigResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"tags":["Email Providers"],"summary":"Delete Provider","description":"Archive (soft-delete) email provider configuration\n\n- Archives the provider instead of hard deleting for audit trail\n- Clears sensitive credentials (API keys) while preserving metadata\n- If this was the default provider, unsets the default flag\n\n**Required Scopes:** `settings:update`","operationId":"delete_provider_api_v1_email_providers__provider_id__delete","security":[{"HTTPBearer":[]}],"parameters":[{"name":"provider_id","in":"path","required":true,"schema":{"type":"string","title":"Provider Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ArchiveEmailProviderResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/email-providers/{provider_id}/test":{"post":{"tags":["Email Providers"],"summary":"Test Provider","description":"Send a test email to verify provider configuration\n\n- Validates API credentials by actually sending an email\n- Returns success/failure status and provider message ID","operationId":"test_provider_api_v1_email_providers__provider_id__test_post","security":[{"HTTPBearer":[]}],"parameters":[{"name":"provider_id","in":"path","required":true,"schema":{"type":"string","title":"Provider Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TestEmailRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TestEmailResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/email-providers/{provider_id}/set-default":{"patch":{"tags":["Email Providers"],"summary":"Set Default Provider","description":"Set a provider as the default for the tenant\n\n- Removes default flag from all other providers\n- Sets this provider as default\n\n**🔐 Required Scopes:** `settings:update`\n","operationId":"set_default_provider_api_v1_email_providers__provider_id__set_default_patch","security":[{"HTTPBearer":[]}],"parameters":[{"name":"provider_id","in":"path","required":true,"schema":{"type":"string","title":"Provider Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/EmailProviderConfigResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/email-providers/{provider_id}/configure-webhooks":{"post":{"tags":["Email Providers"],"summary":"Configure Webhooks","description":"Auto-configure SendGrid event webhooks via API\n\n- Sets up event webhook URL with tenant_id and config_id parameters\n- Enables all event types (delivered, open, click, bounce, spam_report, etc.)\n- Only works for SendGrid providers\n\nReturns:\n    - success: bool\n    - webhook_url: configured webhook URL\n    - message: status message\n    - current_settings: current webhook configuration (if successful)","operationId":"configure_webhooks_api_v1_email_providers__provider_id__configure_webhooks_post","security":[{"HTTPBearer":[]}],"parameters":[{"name":"provider_id","in":"path","required":true,"schema":{"type":"string","title":"Provider Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConfigureWebhooksResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/email-providers/{provider_id}/refresh-domains":{"post":{"tags":["Email Providers"],"summary":"Refresh Verified Domains","description":"Refresh the list of verified domains from the provider API.\n\nFetches current verified domains and updates provider config.\nSupports SendGrid and Mailgun. For Mailgun a domain-scoped SENDING key\n401s on /v3/domains and the live list cannot be read — in that case the\nstored verified_domains degrades to the manually-configured domain.","operationId":"refresh_verified_domains_api_v1_email_providers__provider_id__refresh_domains_post","security":[{"HTTPBearer":[]}],"parameters":[{"name":"provider_id","in":"path","required":true,"schema":{"type":"string","title":"Provider Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RefreshDomainsResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/email-providers/{provider_id}/webhook-settings":{"get":{"tags":["Email Providers"],"summary":"Get Webhook Settings","description":"Get current SendGrid webhook settings for a provider\n\nOnly works for SendGrid providers.\n\n**🔐 Required Scopes:** `settings:read`\n","operationId":"get_webhook_settings_api_v1_email_providers__provider_id__webhook_settings_get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"provider_id","in":"path","required":true,"schema":{"type":"string","title":"Provider Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookSettingsResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/email-templates":{"post":{"tags":["Email Templates"],"summary":"Create Layout Template","description":"Create a new email template with variable system\n\n**Required Scopes:** `email:create`\n\n- Define structural framework with MJML\n- Use {{variable_name}} placeholders for customization\n- Set color palette and fonts for brand consistency\n- MJML is validated and compiled to HTML on creation\n- Variables use lowercase_with_underscores naming convention\n\n**Variable System:**\n- Template variables: {{hero_title}}, {{cta_url}}, {{main_content}}\n- Contact variables: {{contact.name}}, {{contact.email}}\n- Company variables: {{company.name}}\n- System variables: {{system.year}}","operationId":"create_layout_template_api_v1_email_templates_post","security":[{"HTTPBearer":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/EmailLayoutTemplateCreate"}}}},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/EmailLayoutTemplateResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"get":{"tags":["Email Templates"],"summary":"List Layout Templates","description":"List email templates for the tenant\n\n**Required Scopes:** `email:read`\n\n- Can filter by category (business, creative, minimal)\n- Can filter by system template flag\n- Can filter by active status\n- By default excludes hidden templates (use include_hidden=true to include)\n- Returns list without full MJML source (for performance)\n- Supports pagination with limit and offset","operationId":"list_layout_templates_api_v1_email_templates_get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"category","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Filter by category","title":"Category"},"description":"Filter by category"},{"name":"is_system_template","in":"query","required":false,"schema":{"anyOf":[{"type":"boolean"},{"type":"null"}],"description":"Filter by system template flag","title":"Is System Template"},"description":"Filter by system template flag"},{"name":"is_active","in":"query","required":false,"schema":{"anyOf":[{"type":"boolean"},{"type":"null"}],"description":"Filter by active status","title":"Is Active"},"description":"Filter by active status"},{"name":"include_hidden","in":"query","required":false,"schema":{"type":"boolean","description":"Include hidden/archived templates","default":false,"title":"Include Hidden"},"description":"Include hidden/archived templates"},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":100,"minimum":1,"description":"Max results","default":50,"title":"Limit"},"description":"Max results"},{"name":"offset","in":"query","required":false,"schema":{"type":"integer","minimum":0,"description":"Results offset","default":0,"title":"Offset"},"description":"Results offset"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/EmailLayoutTemplateListResponse"},"title":"Response List Layout Templates Api V1 Email Templates Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/email-templates/{template_id}":{"get":{"tags":["Email Templates"],"summary":"Get Layout Template","description":"Get a specific email template with full MJML source\n\n**🔐 Required Scopes:** `email:read`\n","operationId":"get_layout_template_api_v1_email_templates__template_id__get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"template_id","in":"path","required":true,"schema":{"type":"string","title":"Template Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/EmailLayoutTemplateResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"patch":{"tags":["Email Templates"],"summary":"Update Layout Template","description":"Update email template with variable system\n\n- If MJML source is updated, recompiles to HTML\n- Creates new version (increments version number)\n- System templates cannot be modified\n- Template changes will affect future EmailContents using this template\n- Variables use {{variable_name}} syntax\n\n**🔐 Required Scopes:** `email:update`\n","operationId":"update_layout_template_api_v1_email_templates__template_id__patch","security":[{"HTTPBearer":[]}],"parameters":[{"name":"template_id","in":"path","required":true,"schema":{"type":"string","title":"Template Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/EmailLayoutTemplateUpdate"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/EmailLayoutTemplateResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"tags":["Email Templates"],"summary":"Delete Layout Template","description":"Delete email template\n\n**Required Scopes:** `email:delete`\n\n- System templates cannot be deleted\n- Templates with EmailContents referencing them cannot be deleted (409 Conflict)\n- Use this endpoint to clean up unused templates","operationId":"delete_layout_template_api_v1_email_templates__template_id__delete","security":[{"HTTPBearer":[]}],"parameters":[{"name":"template_id","in":"path","required":true,"schema":{"type":"string","title":"Template Id"}}],"responses":{"204":{"description":"Successful Response"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/email-templates/{template_id}/duplicate":{"post":{"tags":["Email Templates"],"summary":"Duplicate Layout Template","description":"Duplicate an email template\n\n**Required Scopes:** `email:create`\n\nCreates a copy of the template with a new name. All content fields are copied\n(MJML, HTML, category, colors, thumbnail, description). The duplicate is\nalways a non-system, non-hidden template owned by the requesting tenant.","operationId":"duplicate_layout_template_api_v1_email_templates__template_id__duplicate_post","security":[{"HTTPBearer":[]}],"parameters":[{"name":"template_id","in":"path","required":true,"schema":{"type":"string","title":"Template Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/EmailLayoutTemplateDuplicate"}}}},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/EmailLayoutTemplateResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/email-templates/preview":{"post":{"tags":["Email Templates"],"summary":"Preview Layout","description":"Preview MJML template without saving (with variable replacement)\n\n- Accepts MJML source with {{variable}} placeholders\n- Replaces variables with example values\n- **Brand variables are auto-filled from tenant settings**\n- Compiles MJML to HTML\n- Returns HTML for preview\n- Does not save to database\n\n**Brand Variables (auto-filled):**\n- `{{company.brand_color_primary}}` - Primary brand color\n- `{{company.brand_color_secondary}}` - Secondary brand color\n- `{{company.brand_color_accent}}` - Accent brand color\n- `{{company.logo_url}}` - Company logo URL\n- `{{company.name}}` - Company name\n\n**Example Request:**\n```json\n{\n  \"mjml_source\": \"<mjml><mj-body><mj-text>{{hero_title}}</mj-text></mj-body></mjml>\",\n  \"variable_values\": {\n    \"hero_title\": \"Welcome to Our Platform\"\n  }\n}\n```\n\n**🔐 Required Scopes:** `email:read`\n","operationId":"preview_layout_api_v1_email_templates_preview_post","requestBody":{"content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Preview Request"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/EmailLayoutPreviewResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"HTTPBearer":[]}]}},"/api/v1/email-contents/variables":{"get":{"tags":["Email Contents"],"summary":"Get Email Variables","description":"Get all available email template variables organized by category.\n\nReturns a comprehensive list of variables that can be used in email templates:\n\n- **contact**: Recipient contact information (name, email, phone, etc.)\n- **company**: Your company/organization details (including brand colors)\n- **campaign**: Auto-filled campaign variables (unsubscribe link, etc.)\n- **system**: System variables (current year, date, etc.)\n- **custom_fields**: Your tenant's custom contact fields\n\n**Required Scopes:** `email:read`\n\n**Usage in templates:**\n```\nHello {{contact.name}}!\n\nWelcome to {{company.name}}.\n\n<div style=\"background: {{company.brand_color_primary}}\">...</div>\n\n(c) {{system.year}} All rights reserved.\n\n<a href=\"{{campaign.unsubscribe_url}}\">Unsubscribe</a>\n```\n\n**Custom variables:** You can use any `{{variable_name}}` in your template.\nValues are provided when creating email content from a layout.","operationId":"get_email_variables_api_v1_email_contents_variables_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/EmailVariablesResponse"}}}}},"security":[{"HTTPBearer":[]}]}},"/api/v1/email-contents":{"post":{"tags":["Email Contents"],"summary":"Create Email Content","description":"Create a new email content with variable system.\n\n**Required Scopes:** `email:create`\n\nThe variable system supports 4 types of variables:\n- **Template variables**: Custom content (e.g., {{hero_title}}, {{cta_url}})\n- **Contact variables**: Auto-filled from contact data (e.g., {{contact.name}})\n- **Company variables**: Auto-filled from company data (e.g., {{company.name}})\n- **System variables**: Auto-filled by platform (e.g., {{system.year}})\n\n**Variable Naming Convention:**\n- Must use lowercase_with_underscores format\n- Can contain dots for nested properties (contact.field_name)\n- Examples: hero_title (valid), heroTitle (invalid), hero-title (invalid)\n\n**Features:**\n- Full MJML content with {{variable}} placeholders\n- Optional layout reference (analytics only, no FK constraint)\n- Variable values for form mode (optional)\n- Subject line supports {{variables}} for personalization\n- Variables are automatically extracted and validated","operationId":"create_email_content_api_v1_email_contents_post","security":[{"HTTPBearer":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/EmailContentCreate"}}}},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/EmailContentResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"get":{"tags":["Email Contents"],"summary":"List Email Contents","description":"List email contents for the tenant with pagination.\n\n**Required Scopes:** `email:read`\n\n- Can filter by layout template ID (optional reference)\n- By default excludes hidden contents (use include_hidden=true to include)\n- Returns list without full MJML source (for performance)\n- Includes basic layout template info","operationId":"list_email_contents_api_v1_email_contents_get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"page","in":"query","required":false,"schema":{"type":"integer","minimum":1,"description":"Page number (1-indexed)","default":1,"title":"Page"},"description":"Page number (1-indexed)"},{"name":"page_size","in":"query","required":false,"schema":{"type":"integer","maximum":100,"minimum":1,"description":"Items per page","default":20,"title":"Page Size"},"description":"Items per page"},{"name":"layout_id","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Filter by layout template ID","title":"Layout Id"},"description":"Filter by layout template ID"},{"name":"include_hidden","in":"query","required":false,"schema":{"type":"boolean","description":"Include hidden/archived contents","default":false,"title":"Include Hidden"},"description":"Include hidden/archived contents"},{"name":"exclude_system","in":"query","required":false,"schema":{"type":"boolean","description":"Exclude system templates","default":false,"title":"Exclude System"},"description":"Exclude system templates"},{"name":"exclude_doi","in":"query","required":false,"schema":{"type":"boolean","description":"Exclude double opt-in templates (they have their own page)","default":true,"title":"Exclude Doi"},"description":"Exclude double opt-in templates (they have their own page)"},{"name":"search","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Case-insensitive search across name and subject_line","title":"Search"},"description":"Case-insensitive search across name and subject_line"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PaginatedResponse_EmailContentListItem_"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/email-contents/{content_id}":{"get":{"tags":["Email Contents"],"summary":"Get Email Content","description":"Get a specific email content with full details.\n\n**Required Scopes:** `email:read`","operationId":"get_email_content_api_v1_email_contents__content_id__get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"content_id","in":"path","required":true,"schema":{"type":"string","title":"Content Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/EmailContentResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"patch":{"tags":["Email Contents"],"summary":"Update Email Content","description":"Update email content with variable system.\n\n**Required Scopes:** `email:create`\n\n**Variable Replacement Examples:**\n- Template: {{hero_title}} -> \"Welcome to Our Amazing Platform\"\n- Contact: {{contact.name}} -> Actual contact's name at send time\n- Company: {{company.name}} -> Actual company name at send time\n- System: {{system.year}} -> Current year (e.g., 2025)\n\n**Features:**\n- Update MJML source, variable values, subject, etc.\n- Creates new version (increments version number)\n- Variables are automatically re-extracted and validated\n- Content changes will affect future campaigns using this content","operationId":"update_email_content_api_v1_email_contents__content_id__patch","security":[{"HTTPBearer":[]}],"parameters":[{"name":"content_id","in":"path","required":true,"schema":{"type":"string","title":"Content Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/EmailContentUpdate"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/EmailContentResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"tags":["Email Contents"],"summary":"Delete Email Content","description":"Delete email content.\n\n**Required Scopes:** `email:admin`\n\n- Email contents with campaigns referencing them cannot be deleted (409 Conflict)\n- Use this endpoint to clean up unused content","operationId":"delete_email_content_api_v1_email_contents__content_id__delete","security":[{"HTTPBearer":[]}],"parameters":[{"name":"content_id","in":"path","required":true,"schema":{"type":"string","title":"Content Id"}}],"responses":{"204":{"description":"Successful Response"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/email-contents/{content_id}/duplicate":{"post":{"tags":["Email Contents"],"summary":"Duplicate Email Content","description":"Duplicate an email content.\n\n**Required Scopes:** `email:create`\n\nCreates a copy of the content with a new name. All content fields are copied\n(MJML, HTML, variables, subject line, preview text, etc.). The duplicate is\nalways a non-system, non-hidden content owned by the requesting tenant.","operationId":"duplicate_email_content_api_v1_email_contents__content_id__duplicate_post","security":[{"HTTPBearer":[]}],"parameters":[{"name":"content_id","in":"path","required":true,"schema":{"type":"string","title":"Content Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/EmailContentDuplicate"}}}},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/EmailContentResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/email-contents/{content_id}/preview":{"post":{"tags":["Email Contents"],"summary":"Preview Email Content","description":"Preview email content with test variables.\n\n**Required Scopes:** `email:read`\n\n**Use Case:**\n- Test how email will look with specific contact/company data\n- Preview personalization before launching campaigns\n- Debug variable replacement issues\n\n**Example Request:**\n```json\n{\n  \"variables\": {\n    \"contact.name\": \"Jane Smith\",\n    \"contact.email\": \"jane@example.com\",\n    \"company.name\": \"Tech Solutions Inc\",\n    \"hero_title\": \"Exclusive Offer for You\",\n    \"cta_url\": \"https://example.com/special-offer\"\n  }\n}\n```\n\n**Features:**\n- Compiles content with provided variable values\n- Returns rendered HTML and subject line\n- Returns list of variables used in content\n- Useful for testing personalization before sending campaign","operationId":"preview_email_content_api_v1_email_contents__content_id__preview_post","security":[{"HTTPBearer":[]}],"parameters":[{"name":"content_id","in":"path","required":true,"schema":{"type":"string","title":"Content Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/EmailContentPreviewRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/EmailContentPreviewResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/email-campaigns/recipient-count":{"post":{"tags":["Email Campaigns"],"summary":"Get Recipient Count","description":"Audience preview and price for an UNSAVED campaign draft.\n\nCounts come from the same recipient resolution the send uses (subscribed\nlist members only, spam complaints / hard bounces / suppressed addresses\nexcluded, duplicate addresses collapsed), so `subscribed` is the number of\nemails the campaign will actually send.\n\nAudience: `audience_conditions` (Audience Builder tree) wins; else\n`all_contacts`; else `list_ids`; else `subscription_categories`; nothing\nselected returns zeros. A tree must hold a top-level positive list\nmembership (static or newsletter list of this workspace), else HTTP 422\n`{code, message}` with `audience_list_membership_required`,\n`audience_membership_list_invalid` or `audience_conditions_invalid`;\nits membership lists replace `list_ids`, and only contacts with an\nactive email subscription on them are counted.\n\n- **total**: contacts matching the audience before exclusions\n- **subscribed** / **recipient_count**: valid recipients\n- **unsubscribed**, **bounced**, **duplicate_email_skipped_count**,\n  **no_email_count**: why the rest are excluded\n  (`total` = the sum of all five)\n- **cost**: present when `email_provider_config_id` or\n  `email_integration_id` is given -- the real price from the tenant's\n  pricing model (managed vs own-provider rate, same as the invoice),\n  before the plan's included messages. An id that is not one of this\n  workspace's providers / integrations returns HTTP 404\n  (\"Email provider configuration not found\" / \"Email integration not found\").\n\nA request with `audience_conditions` also needs `contacts:read` (HTTP 403):\nan inline tree can probe contact data through the counts.\n\n**Required Scopes:** `campaigns:read` (+ `contacts:read` with `audience_conditions`)","operationId":"get_recipient_count_api_v1_email_campaigns_recipient_count_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RecipientCountRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RecipientCountResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"HTTPBearer":[]}]}},"/api/v1/email-campaigns":{"post":{"tags":["Email Campaigns"],"summary":"Create Campaign","description":"Create a new email campaign.\n\n- Creates both base Campaign and EmailCampaign records\n- Validates provider/integration and content exist\n- Estimates recipient count\n- Audience: `list_ids` and/or `subscription_categories`, or\n  `audience_conditions` (Audience Builder tree). A tree must hold a\n  top-level positive list membership; the campaign's lists are derived\n  from it and `list_ids` is ignored. Rejected trees return HTTP 422\n  `{code, message}`: `audience_list_membership_required`,\n  `audience_membership_list_invalid`, `audience_conditions_invalid`.\n- Optional `send_rate_limit` (emails per minute, 1-60000) and\n  `send_batch_size` (recipients per batch, at least 1). Rejected values\n  return HTTP 422 `{code, message}`: `send_rate_limit_out_of_range`,\n  `send_batch_size_invalid`, `send_pacing_unavailable`.\n\n**Required Scopes:** `campaigns:create`","operationId":"create_campaign_api_v1_email_campaigns_post","security":[{"HTTPBearer":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/EmailCampaignCreate"}}}},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/EmailCampaignResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"get":{"tags":["Email Campaigns"],"summary":"List Campaigns","description":"List email campaigns with pagination.\n\nReturns a paginated list of email campaigns for the tenant.\n\n**Required Scopes:** `campaigns:read`\n\n**Response format:**\n```json\n{\n    \"items\": [...],\n    \"pagination\": {\n        \"total\": 50,\n        \"page\": 1,\n        \"page_size\": 20,\n        \"total_pages\": 3,\n        \"has_next\": true,\n        \"has_prev\": false\n    }\n}\n```","operationId":"list_campaigns_api_v1_email_campaigns_get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"page","in":"query","required":false,"schema":{"type":"integer","minimum":1,"description":"Page number (1-indexed)","default":1,"title":"Page"},"description":"Page number (1-indexed)"},{"name":"page_size","in":"query","required":false,"schema":{"type":"integer","maximum":100,"minimum":1,"description":"Items per page","default":20,"title":"Page Size"},"description":"Items per page"},{"name":"campaign_status","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Filter by campaign status","title":"Campaign Status"},"description":"Filter by campaign status"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/EmailCampaignListResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/email-campaigns/unsubscribe-counts":{"get":{"tags":["Email Campaigns"],"summary":"Get Email Campaign Unsubscribe Counts","description":"Batch opt-out counts for the email campaigns INDEX page.\n\nONE grouped query over ``unsubscribe_tokens`` (used tokens) for the given\nemail campaign ids (``EmailCampaign.id``, tenant-scoped). All email opt-outs\nare link/one-click, so per campaign ``exact == total`` and ``estimated`` is\nalways 0. Campaigns with no opt-outs are OMITTED — treat a missing id as 0.\n\n**Required Scopes:** `campaigns:read`","operationId":"get_email_campaign_unsubscribe_counts_api_v1_email_campaigns_unsubscribe_counts_get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"ids","in":"query","required":true,"schema":{"type":"string","description":"Comma-separated email campaign ids (max 200)","title":"Ids"},"description":"Comma-separated email campaign ids (max 200)"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CampaignUnsubscribeCountsResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/email-campaigns/{campaign_id}":{"get":{"tags":["Email Campaigns"],"summary":"Get Campaign","description":"Get email campaign details.\n\n**Required Scopes:** `campaigns:read`","operationId":"get_campaign_api_v1_email_campaigns__campaign_id__get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"campaign_id","in":"path","required":true,"schema":{"type":"string","title":"Campaign Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/EmailCampaignResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"patch":{"tags":["Email Campaigns"],"summary":"Update Campaign","description":"Update email campaign.\n\n- Can only update campaigns in DRAFT or SCHEDULED status\n- Once sending/sent, campaigns are immutable -- except the send speed\n  (`send_rate_limit`, emails per minute) and batch size\n  (`send_batch_size`): a sending, paused or failed campaign may change\n  ONLY these two (explicit `null` clears one). The next emails and the\n  next batch use the new values. Switching them on while the campaign is\n  sending moves the recipients not sent yet into batch 1 at the new speed.\n- Rejected values return HTTP 422 `{code, message}`:\n  `send_rate_limit_out_of_range`, `send_batch_size_invalid`,\n  `send_pacing_unavailable`, `send_pacing_not_editable` (any other status,\n  e.g. completed or cancelled).\n- `audience_conditions` (Audience Builder tree): omit to leave unchanged,\n  explicit `null` clears it (the campaign keeps its current lists). While\n  a tree is set the campaign's lists are derived from its membership lists\n  and any `list_ids` sent are ignored. Rejected trees return HTTP 422\n  `{code, message}`: `audience_list_membership_required`,\n  `audience_membership_list_invalid`, `audience_conditions_invalid`.\n\n**Required Scopes:** `campaigns:update`","operationId":"update_campaign_api_v1_email_campaigns__campaign_id__patch","security":[{"HTTPBearer":[]}],"parameters":[{"name":"campaign_id","in":"path","required":true,"schema":{"type":"string","title":"Campaign Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/EmailCampaignUpdate"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/EmailCampaignResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"tags":["Email Campaigns"],"summary":"Delete Campaign","description":"Delete email campaign.\n\n- Sending, throttled (paused by the send limit) and completed campaigns\n  cannot be deleted: 400 `Cannot delete campaign that has been sent`.\n  Pause the campaign first, then delete it.\n- A paused, cancelled or failed campaign can be deleted; the emails it\n  already sent stay on the invoice (they are kept for billing before the\n  delete, in the month they were sent).\n\n**Required Scopes:** `campaigns:delete`","operationId":"delete_campaign_api_v1_email_campaigns__campaign_id__delete","security":[{"HTTPBearer":[]}],"parameters":[{"name":"campaign_id","in":"path","required":true,"schema":{"type":"string","title":"Campaign Id"}}],"responses":{"204":{"description":"Successful Response"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/email-campaigns/{campaign_id}/send":{"post":{"tags":["Email Campaigns"],"summary":"Send Campaign","description":"Send or schedule email campaign.\n\n- If send_test=true, sends to test_emails only (TODO)\n- Otherwise, queues campaign to Pub/Sub for async sending\n- Updates campaign status to SENDING\n- Deducts balance upfront (prepaid model)\n- Send speed and batches: with a `send_rate_limit` and/or\n  `send_batch_size` saved on the campaign, the emails go out at that\n  speed and in batches of that size. After each batch the campaign pauses\n  (`status = \"paused\"`, `throttle_reason = \"batch_complete\"`) until\n  `POST /email-campaigns/{id}/send-next-batch`. A saved batch size that\n  cannot be used returns HTTP 422 `{code, message}` before anything is\n  sent (`send_pacing_unavailable`, `send_batch_size_invalid`).\n- HTTP 409 \"This campaign is already being sent\" when the campaign is\n  already `sending` / `throttled` / `paused`, or a concurrent request (a\n  second click, the scheduled send) started it first -- nothing is queued\n  twice. HTTP 409 \"This campaign was paused or cancelled while it was\n  being prepared for sending. Refresh and try again.\" when that happened\n  during the send.\n\n**Required Scopes:** `campaigns:send`","operationId":"send_campaign_api_v1_email_campaigns__campaign_id__send_post","security":[{"HTTPBearer":[]}],"parameters":[{"name":"campaign_id","in":"path","required":true,"schema":{"type":"string","title":"Campaign Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/app__schemas__email_campaign_schema__SendCampaignRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SendCampaignResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/email-campaigns/{campaign_id}/schedule":{"post":{"tags":["Email Campaigns"],"summary":"Schedule Campaign","description":"Schedule an email campaign for future sending.\n\nAccepts a local datetime and IANA timezone string. The backend converts to UTC\nfor storage. The original timezone is preserved for display purposes.\n\nThe scheduled time must be at least 5 minutes in the future.\n\n**Required Scopes:** `campaigns:update`","operationId":"schedule_campaign_api_v1_email_campaigns__campaign_id__schedule_post","security":[{"HTTPBearer":[]}],"parameters":[{"name":"campaign_id","in":"path","required":true,"schema":{"type":"string","title":"Campaign Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ScheduleEmailCampaignRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/EmailCampaignResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/email-campaigns/{campaign_id}/cancel-schedule":{"post":{"tags":["Email Campaigns"],"summary":"Cancel Schedule","description":"Cancel a scheduled email campaign and move it back to draft.\n\nThis clears the scheduled_at time and reverts the status to draft,\nallowing the user to edit and reschedule. The timezone is preserved\nfor convenience if the user reschedules later.\n\n**Required Scopes:** `campaigns:update`","operationId":"cancel_schedule_api_v1_email_campaigns__campaign_id__cancel_schedule_post","security":[{"HTTPBearer":[]}],"parameters":[{"name":"campaign_id","in":"path","required":true,"schema":{"type":"string","title":"Campaign Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/EmailCampaignResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/email-campaigns/{campaign_id}/cancel":{"post":{"tags":["Email Campaigns"],"summary":"Cancel Campaign","description":"Cancel scheduled, sending or paused campaign.\n\n- Updates status to CANCELLED (also from `paused`, including a campaign\n  waiting for its next batch)\n- Stops further Pub/Sub processing\n- Recipients still waiting for a later batch are not sent\n- Does NOT refund balance (already deducted)\n\nNeeds the same scope as send / pause / resume (`campaigns:send`):\nstopping a send is a send-control action, not a content edit.\n\n**Required Scopes:** `campaigns:send`","operationId":"cancel_campaign_api_v1_email_campaigns__campaign_id__cancel_post","security":[{"HTTPBearer":[]}],"parameters":[{"name":"campaign_id","in":"path","required":true,"schema":{"type":"string","title":"Campaign Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/EmailCampaignResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/email-campaigns/{campaign_id}/pause":{"post":{"tags":["Email Campaigns"],"summary":"Pause Email Campaign","description":"Pause a sending email campaign\n\nOnly a campaign in `sending` or `throttled` can be paused (anything else\nreturns HTTP 400). Emails already handed to the email provider are not\nrecalled; everyone else waits until `POST /email-campaigns/{id}/resume`.\n\n**Required Scopes:** `campaigns:send`","operationId":"pause_email_campaign_api_v1_email_campaigns__campaign_id__pause_post","security":[{"HTTPBearer":[]}],"parameters":[{"name":"campaign_id","in":"path","required":true,"schema":{"type":"string","title":"Campaign Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/EmailCampaignResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/email-campaigns/{campaign_id}/resume":{"post":{"tags":["Email Campaigns"],"summary":"Resume Email Campaign","description":"Resume a paused email campaign\n\nOnly a `paused` campaign can be resumed (anything else returns HTTP 400).\nSending continues with everyone who has not been sent yet. If a send\nspeed or batch size was saved while the campaign was paused, it applies\nfrom now on. A campaign that finished a batch\n(`throttle_reason = \"batch_complete\"`) sends its next batch, exactly\nlike `POST /email-campaigns/{id}/send-next-batch` with an empty body.\nA campaign sent in batches that ended as `completed` or `failed` while\nrecipients were still waiting is re-opened the same way. HTTP 422\n`send_pacing_unavailable` while sending at a set speed or in batches is\nbriefly unavailable.\n\n**Required Scopes:** `campaigns:send`","operationId":"resume_email_campaign_api_v1_email_campaigns__campaign_id__resume_post","security":[{"HTTPBearer":[]}],"parameters":[{"name":"campaign_id","in":"path","required":true,"schema":{"type":"string","title":"Campaign Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/EmailCampaignResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/email-campaigns/{campaign_id}/send-next-batch":{"post":{"tags":["Email Campaigns"],"summary":"Send Next Email Batch","description":"Send the next batch of an email campaign sent in batches\n\nOnly for a campaign that finished a batch: `status = \"paused\"` with\n`throttle_reason = \"batch_complete\"`, or one that ended as `completed` /\n`failed` while recipients were still waiting (it continues its unfinished\nbatch first, if any). HTTP 422 `send_pacing_unavailable` while batch\nsending is briefly unavailable. Anything else returns HTTP 409\n`{\"code\": \"campaign_not_awaiting_batch\", \"message\": ...}` (also when two\nrequests race: only the first one sends a batch).\n\nBody (optional): `{\"batch_size\": 500}` sends 500 recipients now and saves\n500 as the campaign's batch size (1-1,000,000; anything else is HTTP 422\n`{\"code\": \"send_batch_size_invalid\", \"message\": ...}`);\n`{\"all_remaining\": true}` sends everyone who is still waiting. An empty\nbody sends the next batch at the saved batch size. Recipients who\nunsubscribed, bounced or were suppressed\nsince the campaign was prepared are skipped (`skipped_ineligible`).\n\n**Response (200):** `{batch_number, armed, skipped_ineligible, remaining,\ncampaign}`.\n\n**Required Scopes:** `campaigns:send`","operationId":"send_next_email_batch_api_v1_email_campaigns__campaign_id__send_next_batch_post","security":[{"HTTPBearer":[]}],"parameters":[{"name":"campaign_id","in":"path","required":true,"schema":{"type":"string","title":"Campaign Id"}}],"requestBody":{"content":{"application/json":{"schema":{"anyOf":[{"$ref":"#/components/schemas/CampaignSendNextBatchRequest"},{"type":"null"}],"title":"Batch Request"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/EmailSendNextBatchResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/email-campaigns/{campaign_id}/batches":{"get":{"tags":["Email Campaigns"],"summary":"List Email Campaign Batches","description":"Per-batch delivery counts of an email campaign sent in batches\n\nOne item per batch with its recipient counts, oldest batch first.\n`remaining` is the number of recipients waiting for a later batch,\n`current_batch_number` the batch the campaign is on and\n`send_batch_size` the saved batch size. A campaign that is not sent in\nbatches returns an empty list.\n\n**Required Scopes:** `campaigns:read`","operationId":"list_email_campaign_batches_api_v1_email_campaigns__campaign_id__batches_get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"campaign_id","in":"path","required":true,"schema":{"type":"string","title":"Campaign Id"}},{"name":"page","in":"query","required":false,"schema":{"type":"integer","minimum":1,"description":"Page number (1-indexed)","default":1,"title":"Page"},"description":"Page number (1-indexed)"},{"name":"page_size","in":"query","required":false,"schema":{"type":"integer","maximum":200,"minimum":1,"description":"Batches per page","default":50,"title":"Page Size"},"description":"Batches per page"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CampaignBatchListResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/email-campaigns/{campaign_id}/send-plan":{"get":{"tags":["Email Campaigns"],"summary":"Get Email Campaign Send Plan","description":"Send plan of an email campaign: speed, batches and estimated duration\n\nRead-only estimate for the campaign's audience at its saved send speed\nand batch size, or at the values given as query parameters (to preview a\nchange before saving it). `eta_seconds` estimates the time to send\neveryone who is left (time paused between batches not included).\n`pacing_available = false` means send speed and batches are not\navailable for this account yet.\n\nOut-of-range query values return HTTP 422 `{code, message}`\n(`send_rate_limit_out_of_range`, `send_batch_size_invalid`).\n\n**Required Scopes:** `campaigns:read`","operationId":"get_email_campaign_send_plan_api_v1_email_campaigns__campaign_id__send_plan_get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"campaign_id","in":"path","required":true,"schema":{"type":"string","title":"Campaign Id"}},{"name":"send_rate_limit","in":"query","required":false,"schema":{"anyOf":[{"type":"integer"},{"type":"null"}],"description":"Preview this send speed (emails per minute, 1-60000) instead of the saved one","title":"Send Rate Limit"},"description":"Preview this send speed (emails per minute, 1-60000) instead of the saved one"},{"name":"send_batch_size","in":"query","required":false,"schema":{"anyOf":[{"type":"integer"},{"type":"null"}],"description":"Preview this batch size (>= 1) instead of the saved one","title":"Send Batch Size"},"description":"Preview this batch size (>= 1) instead of the saved one"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SendPlanResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/email-campaigns/{campaign_id}/analytics":{"get":{"tags":["Email Campaigns"],"summary":"Get Campaign Analytics","description":"Get detailed campaign analytics.\n\nReturns overall stats, rates, time-series data, and top performing links.\n`held_count` is the number of recipients waiting for a later batch\n(campaigns sent in batches); they are neither sent nor failed.\n\n**Required Scopes:** `analytics:read`","operationId":"get_campaign_analytics_api_v1_email_campaigns__campaign_id__analytics_get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"campaign_id","in":"path","required":true,"schema":{"type":"string","title":"Campaign Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/EmailCampaignAnalyticsResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/email-campaigns/{campaign_id}/unsubscribes":{"get":{"tags":["Email Campaigns"],"summary":"Get Campaign Unsubscribes","description":"List recipients who opted out via THIS email campaign's unsubscribe link.\n\nCampaign-attributed opt-outs, sourced from ``unsubscribe_tokens`` (SHARD)\nstamped with the ``email_campaign_id`` at send time. Ordered by\n``unsubscribed_at`` DESC, paginated (``page``/``page_size``, default 25).\n\nOnly LINK-method opt-outs exist today; ``method_counts.keyword`` is reserved\n(``null``) for a future keyword-based source that lands once an events table\nexists.\n\n**Required Scopes:** `analytics:read`","operationId":"get_campaign_unsubscribes_api_v1_email_campaigns__campaign_id__unsubscribes_get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"campaign_id","in":"path","required":true,"schema":{"type":"string","title":"Campaign Id"}},{"name":"page","in":"query","required":false,"schema":{"type":"integer","minimum":1,"default":1,"title":"Page"}},{"name":"page_size","in":"query","required":false,"schema":{"type":"integer","maximum":100,"minimum":1,"default":25,"title":"Page Size"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/app__schemas__email_campaign_schema__CampaignUnsubscribesResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/email-campaigns/{campaign_id}/analytics/detailed":{"get":{"tags":["Email Campaigns"],"summary":"Get Campaign Detailed Analytics","description":"Get detailed campaign analytics with device breakdown and link performance.\n\nReturns advanced metrics including:\n- Link performance with CTR per link\n- Click timestamps per link\n\n**Required Scopes:** `analytics:read`","operationId":"get_campaign_detailed_analytics_api_v1_email_campaigns__campaign_id__analytics_detailed_get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"campaign_id","in":"path","required":true,"schema":{"type":"string","title":"Campaign Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/EmailCampaignDetailedAnalyticsResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/email-campaigns/{campaign_id}/recipients":{"get":{"tags":["Email Campaigns"],"summary":"Get Campaign Recipients","description":"List an email campaign's recipients with delivery status.\n\nGroups: `successful` = sent/delivered/opened/clicked, `failed` =\nfailed/bounced/complained, `all` = the union of both, `held` =\nrecipients waiting for a later batch (campaigns sent in batches; status\n`held`, not part of `all`). Each item:\n`{id, contact_id, name, identifier, status, error_reason, sent_at,\ndelivered_at, failed_at}` where `identifier` is the email address and\n`error_reason` is the bounce reason (bounces) or the send error.\n\n**Required Scopes:** `campaigns:read`","operationId":"get_campaign_recipients_api_v1_email_campaigns__campaign_id__recipients_get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"campaign_id","in":"path","required":true,"schema":{"type":"string","title":"Campaign Id"}},{"name":"status","in":"query","required":false,"schema":{"type":"string","description":"Recipient group: failed | successful | held | all","default":"all","title":"Status"},"description":"Recipient group: failed | successful | held | all"},{"name":"page","in":"query","required":false,"schema":{"type":"integer","minimum":1,"description":"Page number (1-indexed)","default":1,"title":"Page"},"description":"Page number (1-indexed)"},{"name":"page_size","in":"query","required":false,"schema":{"type":"integer","maximum":100,"minimum":1,"description":"Items per page","default":50,"title":"Page Size"},"description":"Items per page"},{"name":"search","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Search email address or contact name","title":"Search"},"description":"Search email address or contact name"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CampaignRecipientListResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/email-campaigns/{campaign_id}/recipients/{recipient_id}/retry":{"post":{"tags":["Email Campaigns"],"summary":"Retry Email Campaign Recipient","description":"Retry a single failed recipient of an email campaign.\n\nCalls the retry execution in ``EmailCampaignService.retry_single_recipient``\nand returns the updated recipient item so the UI can update the row inline.\n\n- Recipient not found -> 404\n- Campaign not `completed`, `failed` or `paused` -> 409 \"Recipients can\n  only be retried on a completed, failed or paused campaign\"\n- Recipient reported the email as spam (`complained`) -> 409 \"This\n  recipient reported the email as spam and cannot be sent to again\"\n- Recipient not retryable in its current state (e.g. not failed) -> 409\n- Recipient permanently undeliverable (hard email bounce) -> 422\n\nA `completed` / `failed` campaign goes back to `sending` and the recipient\nis sent right away. A `paused` campaign stays paused: the recipient waits\nand is sent together with everyone else when the campaign is resumed.\nCampaigns sent in batches: the recipient rejoins the current batch\n(status `held`) and is sent at the campaign's send speed.\n\n**Required Scopes:** `campaigns:send`","operationId":"retry_email_campaign_recipient_api_v1_email_campaigns__campaign_id__recipients__recipient_id__retry_post","security":[{"HTTPBearer":[]}],"parameters":[{"name":"campaign_id","in":"path","required":true,"schema":{"type":"string","title":"Campaign Id"}},{"name":"recipient_id","in":"path","required":true,"schema":{"type":"string","title":"Recipient Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CampaignRecipientItem"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/email-campaigns/{campaign_id}/recipients/{recipient_id}":{"delete":{"tags":["Email Campaigns"],"summary":"Delete Email Campaign Recipient","description":"Remove a single recipient from an email campaign.\n\nHard-deletes the per-recipient row (tenant + campaign ownership validated),\nthen recomputes the email campaign's denormalized counters.\n\n- HTTP 409 \"This recipient is queued for sending and cannot be removed\n  while the campaign is sending or paused. Cancel the campaign first.\"\n  for a `queued` recipient of a `sending` / `throttled` / `paused`\n  campaign (its email may already be on its way).\n\n**Required Scopes:** `campaigns:update`","operationId":"delete_email_campaign_recipient_api_v1_email_campaigns__campaign_id__recipients__recipient_id__delete","security":[{"HTTPBearer":[]}],"parameters":[{"name":"campaign_id","in":"path","required":true,"schema":{"type":"string","title":"Campaign Id"}},{"name":"recipient_id","in":"path","required":true,"schema":{"type":"string","title":"Recipient Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeleteResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/email-campaigns/{campaign_id}/recipients/remove-failed":{"post":{"tags":["Email Campaigns"],"summary":"Remove Failed Email Campaign Recipients","description":"Bulk-remove all failed recipients (failed/bounced/complained) from an email\ncampaign.\n\nHard-deletes every failed-group row for this campaign and recomputes the\nemail campaign counters.\n\n**Required Scopes:** `campaigns:update`","operationId":"remove_failed_email_campaign_recipients_api_v1_email_campaigns__campaign_id__recipients_remove_failed_post","security":[{"HTTPBearer":[]}],"parameters":[{"name":"campaign_id","in":"path","required":true,"schema":{"type":"string","title":"Campaign Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RemoveFailedRecipientsResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/email-campaigns/{campaign_id}/recipients/{recipient_id}/unsubscribe":{"post":{"tags":["Email Campaigns"],"summary":"Unsubscribe Email Campaign Recipient","description":"Opt an email recipient's contact out of this campaign's lists.\n\nThis is DISTINCT from `DELETE /{campaign_id}/recipients/{recipient_id}`\n(\"Remove\"), which only deletes the per-recipient row and does NOT change\nsubscription state. This endpoint resolves the recipient's contact, computes\nthe campaign's originating list(s) (`campaign_lists` on the base campaign ∩\nthe contact's currently-subscribed lists) and opts the contact out of all of\nthem via the subscription chokepoint, writing the opt-out ledger with\ncampaign attribution (`method='manual'`). ``{campaign_id}`` is the\n``EmailCampaign.id`` (as elsewhere on this router).\n\n- Recipient (or its campaign) not found for this tenant -> 404\n- Recipient has no linked contact -> 422\n- Contact subscribed to none of the campaign's lists (or the campaign targets\n  no lists) -> 200 with an empty `unsubscribed_lists`\n- Contact already opted out of all of the campaign's lists -> 200 with\n  `already_unsubscribed: true`\n\n**Required Scopes:** `campaigns:update`","operationId":"unsubscribe_email_campaign_recipient_api_v1_email_campaigns__campaign_id__recipients__recipient_id__unsubscribe_post","security":[{"HTTPBearer":[]}],"parameters":[{"name":"campaign_id","in":"path","required":true,"schema":{"type":"string","title":"Campaign Id"}},{"name":"recipient_id","in":"path","required":true,"schema":{"type":"string","title":"Recipient Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RecipientUnsubscribeResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/email-campaigns/{campaign_id}/estimate-cost":{"get":{"tags":["Email Campaigns"],"summary":"Estimate Campaign Cost","description":"Estimate campaign sending cost and check balance.\n\nReturns detailed cost breakdown including:\n- **recipient_count**: Number of email recipients\n- **cost_per_email**: Cost per email in cents\n- **total_cost**: Total cost in cents\n- **total_cost_eur**: Total cost formatted as EUR string (e.g., \"0.80\")\n- **current_balance**: Current account balance in cents\n- **current_balance_eur**: Balance formatted as EUR string\n- **sufficient_balance**: Whether the account has enough balance to send\n\nUse this endpoint before sending to verify costs and balance availability.\n\n**Required Scopes:** `campaigns:read`","operationId":"estimate_campaign_cost_api_v1_email_campaigns__campaign_id__estimate_cost_get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"campaign_id","in":"path","required":true,"schema":{"type":"string","title":"Campaign Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CostEstimateResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/email-contacts/import":{"post":{"tags":["Email Contacts"],"summary":"Import Email Contacts","description":"Import contacts from CSV file\n\n**Required Scopes:** `contacts:create`\n\nImports contacts from a CSV file. At least one of `email`, `phone`,\n`whatsapp_id`, `display_name`, `first_name`, or `last_name` is\nrequired per row. Optionally creates email subscriptions for contacts\nthat have an email address.\n\n**Supported columns (all optional except for the row anchor above):**\n\n| Column | Maps to | Normalization |\n|--------|---------|---------------|\n| `first_name` | `Contact.first_name` | trim |\n| `last_name` | `Contact.last_name` | trim |\n| `display_name` (alias: `name`) | `Contact.name` | trim; falls back to `\"{first} {last}\"` |\n| `email` | `Contact.email` + `ContactMethod` (EMAIL) | trim + lowercase |\n| `phone` | `Contact.phone` + `ContactMethod` (PHONE) | parsed via libphonenumber to E.164 |\n| `whatsapp_id` | `ContactMethod` (WHATSAPP_ID) | digits-only (strips `+`, spaces, formatting) |\n| `languages` | `Contact.languages` | comma-separated ISO 639-1 codes; quote the cell if more than one |\n| `birthday` | `Contact.birthday` | `YYYY-MM-DD` |\n| `tags` | `contact_tags` (many-to-many) | comma-separated; quote the cell; missing tags auto-created |\n| `linkedin` | `ContactMethod` (LINKEDIN) | trim |\n| `instagram_handle` | `ContactMethod` (INSTAGRAM_HANDLE) | trim; strips leading `@` |\n| `facebook` | `ContactMethod` (FACEBOOK) | trim |\n| `homepage` | `ContactMethod` (HOMEPAGE) | trim |\n| `custom_fields` | `Contact.custom_data` | JSON object string |\n\nHeader names are matched case-insensitively. Common aliases are\naccepted out-of-the-box: `firstname`/`given` → `first_name`,\n`lastname`/`surname` → `last_name`, `full name` → `display_name`,\n`lang`/`language` → `languages`, `dob`/`born` → `birthday`,\n`wa`/`wa_id` → `whatsapp_id`, `ig` → `instagram_handle`,\n`fb` → `facebook`, `website`/`url` → `homepage`, `labels` → `tags`.\n\n**CSV quoting:** Cells containing commas (e.g. multiple tags or\nlanguages) **must be quoted** per RFC 4180.\n\n**Example CSV:**\n```\nfirst_name,last_name,display_name,email,phone,whatsapp_id,languages,birthday,tags,linkedin,instagram_handle,facebook,homepage,custom_fields\nJohn,Doe,John Doe,john.doe@acme.com,+1 234 567 89,+1 234 567 89,\"en,de\",1990-04-12,\"vip,lead\",john-doe,@johndoe,john.doe.42,https://acme.com,\"{\"company\":\"Acme Corp\"}\"\nJane,Smith,,jane@acme.com,+49 30 12345678,4930123456789,de,,\"customer\",,,,,\n,,Acme Bot,,,,en,,\"bot\",,,,,\n```\n\n**Subscription creation:**\n- Set `create_subscription=true` to create email subscriptions\n- Requires `optin_confirmed=true` when creating subscriptions (GDPR/CAN-SPAM compliance)\n- Subscriptions only created for contacts with a valid email address\n- Use `list_ids` to subscribe to multiple lists (comma-separated)\n\n**Process:**\n1. Validates the CSV has at least one anchor column (see above).\n2. Validates and normalizes each row (phone → E.164, whatsapp_id\n   → digits-only, email → lowercase, birthday → date).\n3. Checks for duplicates by email and phone within tenant (subject\n   to the tenant's `duplicate_contact_mode`).\n4. Creates contacts and one `ContactMethod` row per non-empty\n   method field. Tags are looked up or auto-created within the\n   tenant and attached to the contact.\n5. Optionally creates email subscriptions and adds to specified lists.\n\n**Returns:**\n- Summary with total, imported, duplicates, invalid_rows, and errors","operationId":"import_email_contacts_api_v1_email_contacts_import_post","security":[{"HTTPBearer":[]}],"parameters":[{"name":"create_subscription","in":"query","required":false,"schema":{"type":"boolean","description":"Create email subscriptions for imported contacts","default":false,"title":"Create Subscription"},"description":"Create email subscriptions for imported contacts"},{"name":"optin_confirmed","in":"query","required":false,"schema":{"type":"boolean","description":"User confirms all contacts have valid opt-in consent","default":false,"title":"Optin Confirmed"},"description":"User confirms all contacts have valid opt-in consent"},{"name":"list_id","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Optional list ID to add imported contacts to","title":"List Id"},"description":"Optional list ID to add imported contacts to"},{"name":"list_ids","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Comma-separated list IDs to subscribe contacts to","title":"List Ids"},"description":"Comma-separated list IDs to subscribe contacts to"},{"name":"identifiers_verified","in":"query","required":false,"schema":{"type":"boolean","description":"Same as the identifiers_verified form field; either one enables it.","default":false,"title":"Identifiers Verified"},"description":"Same as the identifiers_verified form field; either one enables it."}],"requestBody":{"required":true,"content":{"multipart/form-data":{"schema":{"$ref":"#/components/schemas/Body_import_email_contacts_api_v1_email_contacts_import_post"}}}},"responses":{"202":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ImportJobStartResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/email-contacts/import/{job_id}/status":{"get":{"tags":["Email Contacts"],"summary":"Get Import Job Status","description":"Get the status/progress of an async contact import job.\n\n**Required Scopes:** `contacts:create`\n\nPoll this endpoint after POST /email-contacts/import. When ``status`` is\n``completed`` the full import summary is returned under ``summary``; when\n``failed`` see ``error_message``.","operationId":"get_import_job_status_api_v1_email_contacts_import__job_id__status_get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"job_id","in":"path","required":true,"schema":{"type":"string","title":"Job Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ImportJobStatusResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/email-contacts/import/template":{"get":{"tags":["Email Contacts"],"summary":"Download Import Template","description":"Download a contact-import template (CSV or XLSX) with 1–2 example rows.\n\n**Required Scopes:** `contacts:create`\n\nThe headers are EXACTLY the contact-export format (auto-detected by the\nimporter with no manual mapping): a leading ``contact_id`` column (empty\nin the template — fill it to update an existing contact, leave it empty\nto create), the standard contact fields, and one ``custom.<key>`` column\nper active custom-field definition of the tenant.\n\nNote: ``telegram_id`` / ``messenger_id`` / ``instagram_id`` appear for\nexport parity but are ignored on import (channel-scoped platform IDs\ncannot be imported).","operationId":"download_import_template_api_v1_email_contacts_import_template_get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"format","in":"query","required":false,"schema":{"type":"string","pattern":"^(csv|xlsx)$","description":"Template file format: csv | xlsx","default":"csv","title":"Format"},"description":"Template file format: csv | xlsx"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/email-contacts/sniff":{"post":{"tags":["Email Contacts"],"summary":"Sniff Csv","description":"Sniff a CSV/XLSX file's dialect and headers.\n\n**Required Scopes:** `contacts:create`\n\nReturns the detected delimiter, quote character, header column names,\nand a best-effort suggested phone region (from the tenant's\ncompany_country) so the frontend preview parser matches the backend\nimporter exactly. Does NOT import anything. For ``.xlsx`` uploads the\nheaders come from the first sheet's first row and ``delimiter``/\n``quotechar`` are reported as ``\",\"`` / ``'\"'`` (the dialect the\nbackend's xlsx→csv conversion emits).\n\n**Returns:** `{ delimiter, quotechar, headers, suggested_phone_region }`","operationId":"sniff_csv_api_v1_email_contacts_sniff_post","requestBody":{"content":{"multipart/form-data":{"schema":{"$ref":"#/components/schemas/Body_sniff_csv_api_v1_email_contacts_sniff_post"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CsvSniffResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"HTTPBearer":[]}]}},"/api/v1/email-contacts":{"get":{"tags":["Email Contacts"],"summary":"List Email Contacts","description":"List email contacts with filters\n\n**Filters:**\n- subscription_status: pending, subscribed, unsubscribed\n- category: newsletter, promotions, updates, etc.\n- search: Search by email or name\n\n**Pagination:**\n- limit: Max results per page (1-100)\n- offset: Number of results to skip\n\n**🔐 Required Scopes:** `contacts:read`\n","operationId":"list_email_contacts_api_v1_email_contacts_get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"subscription_status","in":"query","required":false,"schema":{"anyOf":[{"$ref":"#/components/schemas/SubscriptionStatusEnum"},{"type":"null"}],"description":"Filter by subscription status","title":"Subscription Status"},"description":"Filter by subscription status"},{"name":"category","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Filter by category","title":"Category"},"description":"Filter by category"},{"name":"search","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Search by email or name","title":"Search"},"description":"Search by email or name"},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":100,"minimum":1,"description":"Maximum number of results","default":50,"title":"Limit"},"description":"Maximum number of results"},{"name":"offset","in":"query","required":false,"schema":{"type":"integer","minimum":0,"description":"Pagination offset","default":0,"title":"Offset"},"description":"Pagination offset"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/EmailContactListResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/email-contacts/{subscription_id}":{"get":{"tags":["Email Contacts"],"summary":"Get Email Contact","description":"Get email contact details by subscription ID\n\n**🔐 Required Scopes:** `contacts:read`\n","operationId":"get_email_contact_api_v1_email_contacts__subscription_id__get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"subscription_id","in":"path","required":true,"schema":{"type":"string","title":"Subscription Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/EmailContactResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"patch":{"tags":["Email Contacts"],"summary":"Update Email Contact","description":"Update email contact subscription\n\n**Updatable fields:**\n- name\n- phone\n- subscription_status (pending, subscribed, unsubscribed)\n- subscription_category\n- unsubscribe_reason (when unsubscribing)\n\n**🔐 Required Scopes:** `contacts:update`\n","operationId":"update_email_contact_api_v1_email_contacts__subscription_id__patch","security":[{"HTTPBearer":[]}],"parameters":[{"name":"subscription_id","in":"path","required":true,"schema":{"type":"string","title":"Subscription Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/EmailContactUpdate"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/EmailContactResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"tags":["Email Contacts"],"summary":"Delete Email Contact","description":"Delete email contact subscription\n\n**Note:** This permanently removes the email subscription.\nThe contact record itself is not deleted.\n\n**🔐 Required Scopes:** `contacts:delete`\n","operationId":"delete_email_contact_api_v1_email_contacts__subscription_id__delete","security":[{"HTTPBearer":[]}],"parameters":[{"name":"subscription_id","in":"path","required":true,"schema":{"type":"string","title":"Subscription Id"}}],"responses":{"204":{"description":"Successful Response"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/email-settings":{"get":{"tags":["Email Settings"],"summary":"Get Email Settings","description":"Get tenant email settings.\n\nReturns configured settings or defaults if not yet configured.\n\n**Database:** PRIMARY (tenants table), SHARD (email_provider_configs)\n\n**🔐 Required Scopes:** `settings:read`\n","operationId":"get_email_settings_api_v1_email_settings_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/EmailSettingsResponse"}}}}},"security":[{"HTTPBearer":[]}]},"delete":{"tags":["Email Settings"],"summary":"Reset Email Settings","description":"Reset email settings to defaults (clears all custom settings).\n\n**Database:** PRIMARY (tenants table)\n\n**🔐 Required Scopes:** `settings:admin`\n","operationId":"reset_email_settings_api_v1_email_settings_delete","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/EmailSettingsResetResponse"}}}}},"security":[{"HTTPBearer":[]}]},"patch":{"tags":["Email Settings"],"summary":"Update Email Settings","description":"Update tenant email settings (partial update).\n\nOnly updates provided fields. Unspecified fields remain unchanged.\n\n**Database:** PRIMARY (tenants table)\n\n**Example:**\n```json\n{\n    \"default_from_name\": \"ACME Corp\",\n    \"default_from_email\": \"hello@acme.com\",\n    \"double_opt_in_enabled\": true\n}\n```\n\n**🔐 Required Scopes:** `settings:update`\n","operationId":"update_email_settings_api_v1_email_settings_patch","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/EmailSettingsUpdate"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/EmailSettingsResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"HTTPBearer":[]}]}},"/api/v1/email-settings/validate-footer":{"post":{"tags":["Email Settings"],"summary":"Validate Compliance Footer","description":"Validate compliance footer HTML without saving\n\nUse this endpoint to preview validation results in the template builder.\n\n**Request Body:**\n```json\n{\n    \"html\": \"<p>Footer HTML with {{unsubscribe_link}} and {{company.address}}</p>\"\n}\n```\n\n**Response:**\n```json\n{\n    \"is_valid\": true,\n    \"errors\": [],\n    \"warnings\": [\"Removed <style> tag\"],\n    \"sanitized_html\": \"<p>Footer HTML...</p>\"\n}\n```\n\n**🔐 Required Scopes:** `settings:read`\n","operationId":"validate_compliance_footer_api_v1_email_settings_validate_footer_post","requestBody":{"content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Payload"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ComplianceFooterValidationResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"HTTPBearer":[]}]}},"/api/v1/double-opt-in/send":{"post":{"tags":["Double Opt-In"],"summary":"Send Confirmation Email","description":"Send double opt-in confirmation email to contact\n\nThis endpoint:\n1. Creates or finds contact by email\n2. Creates pending subscription record\n3. Generates secure confirmation token\n4. Sends confirmation email with link\n\n**GDPR Compliance:** IP address and user agent are stored for audit trail\n\n**Rate Limiting:** Maximum 3 confirmation emails per email per hour (TODO: implement)\n\nArgs:\n    request_data: Email confirmation request details\n    tenant_id: Tenant ID from auth token\n\nReturns:\n    Confirmation details including token ID and expiry time\n\nRaises:\n    HTTPException 400: If email sending fails\n    HTTPException 500: If internal error occurs\n\n**🔐 Required Scopes:** `email:update`\n","operationId":"send_confirmation_email_api_v1_double_opt_in_send_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DoubleOptInSendRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DoubleOptInSendResponse"}}}},"400":{"description":"No list given, a list is not in this workspace, or no email sender is configured"},"500":{"description":"The confirmation email could not be sent (generic message, no internal detail)"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"HTTPBearer":[]}]}},"/api/v1/double-opt-in/resend":{"post":{"tags":["Double Opt-In"],"summary":"Resend Confirmation Email","description":"Resend confirmation email for pending subscription\n\nIf the original token has expired, a new token will be generated.\nIf the original token is still valid, the same confirmation link will be resent.\n\nArgs:\n    request_data: Email address to resend confirmation to\n    tenant_id: Tenant ID from auth token\n\nReturns:\n    Success message\n\nRaises:\n    HTTPException 404: If no pending confirmation found for this email\n    HTTPException 400: If resend fails\n\n**🔐 Required Scopes:** `email:update`\n","operationId":"resend_confirmation_email_api_v1_double_opt_in_resend_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DoubleOptInResendRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"400":{"description":"A list is not in this workspace, or no email sender is configured"},"404":{"description":"No pending confirmation for this email address"},"500":{"description":"The confirmation email could not be resent (generic message, no internal detail)"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"HTTPBearer":[]}]}},"/api/v1/double-opt-in/pending":{"get":{"tags":["Double Opt-In"],"summary":"Get Pending Confirmations","description":"Get list of pending email confirmations\n\nShows all contacts awaiting email confirmation for this tenant.\nUseful for admin dashboard to track conversion rates.\n\nArgs:\n    limit: Maximum number of results (default 100, max 1000)\n    offset: Pagination offset (default 0)\n    tenant_id: Tenant ID from auth token\n\nReturns:\n    List of pending confirmations with pagination info\n\n**🔐 Required Scopes:** `email:read`\n","operationId":"get_pending_confirmations_api_v1_double_opt_in_pending_get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"limit","in":"query","required":false,"schema":{"type":"integer","default":100,"title":"Limit"}},{"name":"offset","in":"query","required":false,"schema":{"type":"integer","default":0,"title":"Offset"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PendingConfirmationListResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/double-opt-in/templates":{"get":{"tags":["Double Opt-In Templates"],"summary":"List Templates","description":"List all double opt-in templates available to the tenant.\n\nArgs:\n    include_system: Include system templates (default: True)\n    tenant_id: Current tenant ID\n    current_user: Current authenticated user\n\nReturns:\n    List of templates (tenant custom + system if include_system=True)","operationId":"list_templates_api_v1_double_opt_in_templates_get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"include_system","in":"query","required":false,"schema":{"type":"boolean","default":true,"title":"Include System"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/DoubleOptInTemplateResponse"},"title":"Response List Templates Api V1 Double Opt In Templates Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"post":{"tags":["Double Opt-In Templates"],"summary":"Create Template","description":"Create a new double opt-in template.\n\nIf copy_from_system=True, finds the system template and duplicates its EmailContent.\nIf copy_from_system=False, uses the provided email_content_id.\n\nArgs:\n    template_data: Template creation data\n    tenant_id: Current tenant ID\n    current_user: Current authenticated user\n\nReturns:\n    Created template\n\nRaises:\n    400: If validation fails\n    404: If email_content_id or system template not found\n\n**🔐 Required Scopes:** `email:read`\n","operationId":"create_template_api_v1_double_opt_in_templates_post","security":[{"HTTPBearer":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DoubleOptInTemplateCreate"}}}},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DoubleOptInTemplateResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/double-opt-in/templates/{template_id}":{"get":{"tags":["Double Opt-In Templates"],"summary":"Get Template","description":"Get a specific template by ID.\n\nArgs:\n    template_id: Template ID\n    tenant_id: Current tenant ID\n    current_user: Current authenticated user\n\nReturns:\n    Template details\n\nRaises:\n    404: If template not found or not accessible","operationId":"get_template_api_v1_double_opt_in_templates__template_id__get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"template_id","in":"path","required":true,"schema":{"type":"string","title":"Template Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DoubleOptInTemplateResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"patch":{"tags":["Double Opt-In Templates"],"summary":"Update Template","description":"Update a template.\n\nArgs:\n    template_id: Template ID\n    template_data: Fields to update\n    tenant_id: Current tenant ID\n    current_user: Current authenticated user\n\nReturns:\n    Updated template\n\nRaises:\n    404: If template not found\n    403: If trying to edit system template","operationId":"update_template_api_v1_double_opt_in_templates__template_id__patch","security":[{"HTTPBearer":[]}],"parameters":[{"name":"template_id","in":"path","required":true,"schema":{"type":"string","title":"Template Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DoubleOptInTemplateUpdate"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DoubleOptInTemplateResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"tags":["Double Opt-In Templates"],"summary":"Delete Template","description":"Delete a template.\n\nArgs:\n    template_id: Template ID\n    tenant_id: Current tenant ID\n    current_user: Current authenticated user\n\nRaises:\n    404: If template not found\n    403: If trying to delete system template","operationId":"delete_template_api_v1_double_opt_in_templates__template_id__delete","security":[{"HTTPBearer":[]}],"parameters":[{"name":"template_id","in":"path","required":true,"schema":{"type":"string","title":"Template Id"}}],"responses":{"204":{"description":"Successful Response"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/double-opt-in/templates/{template_id}/copy":{"post":{"tags":["Double Opt-In Templates"],"summary":"Copy System Template","description":"Copy a system template to tenant's custom templates.\n\nThis allows tenants to customize system templates. Creates a duplicate\nof the EmailContent and a new DoubleOptInEmailTemplate with tenant_id.\n\nArgs:\n    template_id: System template ID to copy\n    copy_data: Optional overrides for name, description, button_text, subject_line\n    tenant_id: Current tenant ID\n    current_user: Current authenticated user\n\nReturns:\n    New template owned by tenant\n\nRaises:\n    404: If system template not found\n    400: If trying to copy non-system template\n\n**🔐 Required Scopes:** `email:create`\n","operationId":"copy_system_template_api_v1_double_opt_in_templates__template_id__copy_post","security":[{"HTTPBearer":[]}],"parameters":[{"name":"template_id","in":"path","required":true,"schema":{"type":"string","title":"Template Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DoubleOptInTemplateCopyRequest"}}}},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DoubleOptInTemplateResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/double-opt-in/templates/{template_id}/preview":{"get":{"tags":["Double Opt-In Templates"],"summary":"Preview Template","description":"Generate HTML preview for a double opt-in template.\n\nUses example/placeholder values for confirmation_url, email, etc.\n\nArgs:\n    template_id: Template ID\n    tenant_id: Current tenant ID\n    current_user: Current authenticated user\n\nReturns:\n    {\"html\": \"...rendered HTML...\", \"subject\": \"...subject line...\"}\n\nRaises:\n    400: If the template has no renderable email content yet\n    404: If template or email content not found\n    500: If MJML compilation fails\n\n**🔐 Required Scopes:** `email:read`\n","operationId":"preview_template_api_v1_double_opt_in_templates__template_id__preview_get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"template_id","in":"path","required":true,"schema":{"type":"string","title":"Template Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/email-integrations":{"get":{"tags":["Email Integrations"],"summary":"List Email Integrations","description":"List all active (non-archived) email integrations for the tenant.\n\n**Required Scopes:** `email_integrations:read`\n\nNote: Archived integrations are excluded. Use GET /archived to view archived integrations.","operationId":"list_email_integrations_api_v1_email_integrations_get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"integration_type","in":"query","required":false,"schema":{"anyOf":[{"$ref":"#/components/schemas/EmailIntegrationType"},{"type":"null"}],"title":"Integration Type"}},{"name":"active_only","in":"query","required":false,"schema":{"type":"boolean","default":true,"title":"Active Only"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/EmailIntegrationResponse"},"title":"Response List Email Integrations Api V1 Email Integrations Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"post":{"tags":["Email Integrations"],"summary":"Create Email Integration","description":"Create a new email integration.\n\n**Required Scopes:** `email_integrations:create`\n\nIntegration Types:\n- `gmail` - OAuth2 connection to Gmail\n- `smtp_imap` - Generic SMTP/IMAP server\n- `sendgrid` - Customer's own SendGrid account (deprecated for new integrations)\n- `ses_managed` - SendSeven managed SES (auto-setup: creates tenant, config set, starts domain verification)\n- `proxy` - Email forwarding via proxy address\n\nFor `ses_managed` integrations with `ses_domain` provided:\n- Automatically creates SES Configuration Set\n- Automatically starts domain verification\n- Returns DNS records that must be added to complete verification","operationId":"create_email_integration_api_v1_email_integrations_post","security":[{"HTTPBearer":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/EmailIntegrationCreate"}}}},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/EmailIntegrationCreateResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/email-integrations/archived":{"get":{"tags":["Email Integrations"],"summary":"List Archived Email Integrations","description":"List all archived email integrations for the tenant.\n\n**Required Scopes:** `email_integrations:read`\n\nReturns integrations that have been deleted (soft-deleted/archived).\nUseful for audit purposes and billing history.\n\nNote: Sensitive credentials are cleared when an integration is archived.","operationId":"list_archived_email_integrations_api_v1_email_integrations_archived_get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"integration_type","in":"query","required":false,"schema":{"anyOf":[{"$ref":"#/components/schemas/EmailIntegrationType"},{"type":"null"}],"title":"Integration Type"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ArchivedEmailIntegrationResponse"},"title":"Response List Archived Email Integrations Api V1 Email Integrations Archived Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/email-integrations/{integration_id}":{"get":{"tags":["Email Integrations"],"summary":"Get Email Integration","description":"Get a specific email integration by ID.\n\n**Required Scopes:** `email_integrations:read`","operationId":"get_email_integration_api_v1_email_integrations__integration_id__get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"integration_id","in":"path","required":true,"schema":{"type":"string","title":"Integration Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/EmailIntegrationResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"patch":{"tags":["Email Integrations"],"summary":"Update Email Integration","description":"Update an existing email integration.\n\n**Required Scopes:** `email_integrations:update`","operationId":"update_email_integration_api_v1_email_integrations__integration_id__patch","security":[{"HTTPBearer":[]}],"parameters":[{"name":"integration_id","in":"path","required":true,"schema":{"type":"string","title":"Integration Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/EmailIntegrationUpdate"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/EmailIntegrationResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"tags":["Email Integrations"],"summary":"Delete Email Integration","description":"Archive (soft-delete) an email integration with full resource cleanup.\n\n**Required Scopes:** `email_integrations:delete`\n\nThis archives the integration and cleans up all associated resources:\n- For SES managed integrations: removes domain identity from AWS SES\n  (only if no other active integration uses the same domain), deletes\n  the SES configuration set, and removes the SES tenant\n- Deletes DNS verification records from the database\n- Deactivates proxy mappings for proxy integrations\n- Clears sensitive credentials (OAuth tokens, passwords, API keys)\n- Marks the integration as archived with timestamp\n- Preserves the integration record for billing history and audit purposes\n\nThe integration will no longer appear in the list or be usable.\nAfter archiving, the same domain can be re-added to a new integration.","operationId":"delete_email_integration_api_v1_email_integrations__integration_id__delete","security":[{"HTTPBearer":[]}],"parameters":[{"name":"integration_id","in":"path","required":true,"schema":{"type":"string","title":"Integration Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ArchiveIntegrationResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/email-integrations/{integration_id}/test":{"post":{"tags":["Email Integrations"],"summary":"Test Email Integration","description":"Test the connection for an email integration.\n\n**Required Scopes:** `email_integrations:update`\n\nReturns connection status and any errors.","operationId":"test_email_integration_api_v1_email_integrations__integration_id__test_post","security":[{"HTTPBearer":[]}],"parameters":[{"name":"integration_id","in":"path","required":true,"schema":{"type":"string","title":"Integration Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConnectionTestResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/email-integrations/{integration_id}/sync":{"post":{"tags":["Email Integrations"],"summary":"Sync Email Integration","description":"Trigger a manual sync for an email integration.\n\n**Required Scopes:** `email_integrations:update`\n\nThis is typically used for SMTP/IMAP or Gmail integrations to fetch new emails.","operationId":"sync_email_integration_api_v1_email_integrations__integration_id__sync_post","security":[{"HTTPBearer":[]}],"parameters":[{"name":"integration_id","in":"path","required":true,"schema":{"type":"string","title":"Integration Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SyncResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/email-integrations/{integration_id}/dns-records":{"get":{"tags":["Email Integrations"],"summary":"Get Dns Records","description":"Get DNS records required for domain verification.\n\n**Required Scopes:** `email_integrations:read`\n\nReturns DKIM, SPF, and DMARC records that need to be set in DNS.","operationId":"get_dns_records_api_v1_email_integrations__integration_id__dns_records_get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"integration_id","in":"path","required":true,"schema":{"type":"string","title":"Integration Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/DnsRecordResponse"},"title":"Response Get Dns Records Api V1 Email Integrations  Integration Id  Dns Records Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/email-integrations/{integration_id}/verify-dns":{"post":{"tags":["Email Integrations"],"summary":"Verify Dns","description":"Trigger DNS verification check.\n\n**Required Scopes:** `email_integrations:update`\n\nChecks if the required DNS records have been properly configured.","operationId":"verify_dns_api_v1_email_integrations__integration_id__verify_dns_post","security":[{"HTTPBearer":[]}],"parameters":[{"name":"integration_id","in":"path","required":true,"schema":{"type":"string","title":"Integration Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DnsVerificationCheckResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/email-integrations/{integration_id}/add-domain":{"post":{"tags":["Email Integrations"],"summary":"Add Domain","description":"Start domain verification for an SES managed integration.\n\n**Required Scopes:** `email_integrations:update`\n\nThis:\n1. Creates an email identity for the domain in SES\n2. Returns DNS records (DKIM, SPF, DMARC) that need to be added\n3. Associates the domain with the SES tenant once verified\n\nThe customer must add these DNS records to their domain before emails can be sent.","operationId":"add_domain_api_v1_email_integrations__integration_id__add_domain_post","security":[{"HTTPBearer":[]}],"parameters":[{"name":"integration_id","in":"path","required":true,"schema":{"type":"string","title":"Integration Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DomainVerificationRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DomainVerificationResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/email-integrations/{integration_id}/domain-status":{"get":{"tags":["Email Integrations"],"summary":"Get Domain Status","description":"Check domain verification status for an SES managed integration.\n\n**Required Scopes:** `email_integrations:read`\n\nReturns the current verification status and DNS records.","operationId":"get_domain_status_api_v1_email_integrations__integration_id__domain_status_get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"integration_id","in":"path","required":true,"schema":{"type":"string","title":"Integration Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DomainVerificationResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/email-integrations/{integration_id}/pause-sending":{"post":{"tags":["Email Integrations"],"summary":"Pause Sending","description":"Pause email sending for an SES managed integration.\n\n**Required Scopes:** `email_integrations:update`\n\nThis manually pauses the SES tenant, stopping all email sending.\nUse this if you need to temporarily disable sending (e.g., suspected abuse).","operationId":"pause_sending_api_v1_email_integrations__integration_id__pause_sending_post","security":[{"HTTPBearer":[]}],"parameters":[{"name":"integration_id","in":"path","required":true,"schema":{"type":"string","title":"Integration Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SendingStatusResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/email-integrations/{integration_id}/resume-sending":{"post":{"tags":["Email Integrations"],"summary":"Resume Sending","description":"Resume email sending for an SES managed integration.\n\n**Required Scopes:** `email_integrations:update`\n\nThis resumes sending for a paused SES tenant.\n\nNote: If the tenant was auto-paused due to reputation issues,\nyou should address the underlying problem before resuming.","operationId":"resume_sending_api_v1_email_integrations__integration_id__resume_sending_post","security":[{"HTTPBearer":[]}],"parameters":[{"name":"integration_id","in":"path","required":true,"schema":{"type":"string","title":"Integration Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SendingStatusResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/email-integrations/{integration_id}/test-alias":{"post":{"tags":["Email Integrations"],"summary":"Add Gmail Alias","description":"Add a Gmail send-as alias.\n\nAdds the alias email and creates a corresponding mailbox.\nThe alias MUST be configured in Gmail under Settings > Accounts and Import > Send mail as,\notherwise Gmail will silently send from the primary email instead.\n\n**Required Scopes:** `email_integrations:update`","operationId":"add_gmail_alias_api_v1_email_integrations__integration_id__test_alias_post","security":[{"HTTPBearer":[]}],"parameters":[{"name":"integration_id","in":"path","required":true,"schema":{"type":"string","title":"Integration Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TestAliasRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TestAliasResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/email-integrations/{integration_id}/aliases/{alias_email}":{"delete":{"tags":["Email Integrations"],"summary":"Remove Gmail Alias","description":"Remove a verified Gmail send-as alias.\n\nDeactivates the corresponding mailbox (preserves history).\n\n**Required Scopes:** `email_integrations:update`","operationId":"remove_gmail_alias_api_v1_email_integrations__integration_id__aliases__alias_email__delete","security":[{"HTTPBearer":[]}],"parameters":[{"name":"integration_id","in":"path","required":true,"schema":{"type":"string","title":"Integration Id"}},{"name":"alias_email","in":"path","required":true,"schema":{"type":"string","title":"Alias Email"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/email-integrations/{integration_id}/gmail/sync":{"post":{"tags":["Email Integrations"],"summary":"Sync Gmail Integration","description":"Trigger a manual sync for a Gmail integration.\n\n**Required Scopes:** `email_integrations:update`\n\nThis will:\n1. Fetch new messages from Gmail since last sync\n2. Process and store them in the system\n3. Update the integration's history ID","operationId":"sync_gmail_integration_api_v1_email_integrations__integration_id__gmail_sync_post","security":[{"HTTPBearer":[]}],"parameters":[{"name":"integration_id","in":"path","required":true,"schema":{"type":"string","title":"Integration Id"}},{"name":"full_sync","in":"query","required":false,"schema":{"type":"boolean","default":false,"title":"Full Sync"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GmailSyncResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/email-integrations/conversations/{conversation_id}/email-thread":{"get":{"tags":["Email Integrations"],"summary":"Get email thread for conversation","description":"Get the full email thread for a conversation.\n\n**Required Scopes:** `email_integrations:read` OR `conversations:read`\n\nReturns all email messages associated with this conversation, including:\n- RFC 5322 headers (Message-ID, In-Reply-To, References)\n- From, To, CC, BCC addresses\n- Subject and body (text + signed URL for HTML)\n- Attachments with download URLs\n- Thread relationships for proper email client display\n\nThis is the primary endpoint for the email thread view in Conversations UI.","operationId":"get_conversation_email_thread_api_v1_email_integrations_conversations__conversation_id__email_thread_get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"conversation_id","in":"path","required":true,"schema":{"type":"string","title":"Conversation Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/EmailThreadResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/email-integrations/conversations/{conversation_id}/compose-email":{"post":{"tags":["Email Integrations"],"summary":"Compose and send new outbound email","description":"Compose and send a new outbound email (not a reply).\n\n**Required Scopes:** `email_integrations:update` OR `messages:create`\n\nUse this endpoint for initiating new email conversations where there's\nno existing message to reply to.\n\nFeatures:\n- Plain text and/or HTML body support (rich text)\n- Subject line required\n- CC/BCC support\n- Attachment support via attachment IDs\n\nThe email is sent via the conversation's associated email integration.","operationId":"compose_new_email_api_v1_email_integrations_conversations__conversation_id__compose_email_post","security":[{"HTTPBearer":[]}],"parameters":[{"name":"conversation_id","in":"path","required":true,"schema":{"type":"string","title":"Conversation Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/EmailComposeRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/EmailReplyResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/email-integrations/conversations/{conversation_id}/compose-from-template":{"post":{"tags":["Email Integrations"],"summary":"Compose and send email from an EmailContent template","description":"Compose and send an outbound email using a pre-designed EmailContent template.\n\n**Required Scopes:** `email_integrations:update` OR `messages:create`\n\nThe template is compiled with the conversation contact's data (name, email, phone)\nsubstituted into template variables. Optional variable_overrides allow the caller\nto override or supplement template variable values before compilation.\n\nUses the same 3-tier sending identity resolution as compose-email:\nmailbox -> integration -> channel.","operationId":"compose_from_template_api_v1_email_integrations_conversations__conversation_id__compose_from_template_post","security":[{"HTTPBearer":[]}],"parameters":[{"name":"conversation_id","in":"path","required":true,"schema":{"type":"string","title":"Conversation Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/EmailComposeFromTemplateRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/EmailReplyResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/email-integrations/conversations/{conversation_id}/preview-template":{"post":{"tags":["Email Integrations"],"summary":"Preview an EmailContent template with conversation contact data","description":"Preview an EmailContent template compiled with the conversation contact's data.\n\n**Required Scopes:** `email_integrations:read`\n\nReturns the fully compiled HTML and subject line with all template variables\nreplaced using the actual contact data from the conversation. Also resolves\nthe sending identity so the frontend can display the from address.\n\nUse this endpoint to show a live preview before sending via compose-from-template.","operationId":"preview_template_api_v1_email_integrations_conversations__conversation_id__preview_template_post","security":[{"HTTPBearer":[]}],"parameters":[{"name":"conversation_id","in":"path","required":true,"schema":{"type":"string","title":"Conversation Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/EmailTemplatePreviewRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/EmailTemplatePreviewResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/email-integrations/conversations/{conversation_id}/email-summary":{"get":{"tags":["Email Integrations"],"summary":"Get email summary for conversation sidebar","description":"Get email summary for the conversation sidebar.\n\n**Required Scopes:** `email_integrations:read` OR `conversations:read`\n\nReturns:\n- Whether this conversation has email messages\n- Total message and attachment counts\n- Latest email information\n- All unique participants in the thread\n- Email integration type and ID\n\nThis is a lightweight endpoint for displaying email info in the sidebar.","operationId":"get_conversation_email_summary_api_v1_email_integrations_conversations__conversation_id__email_summary_get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"conversation_id","in":"path","required":true,"schema":{"type":"string","title":"Conversation Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConversationEmailSummary"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/email-integrations/email-messages/{email_message_id}":{"get":{"tags":["Email Integrations"],"summary":"Get single email message detail","description":"Get detailed information for a single email message.\n\n**Required Scopes:** `email_integrations:read`\n\nReturns full email details including:\n- All headers\n- Full text body\n- Signed URL for HTML body\n- Attachment list with download URLs\n\nUse this endpoint when expanding a collapsed email in the thread view.","operationId":"get_email_message_detail_api_v1_email_integrations_email_messages__email_message_id__get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"email_message_id","in":"path","required":true,"schema":{"type":"string","title":"Email Message Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/EmailMessageDetailResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"tags":["Email Integrations"],"summary":"Delete Email Message","description":"Permanently delete an email message.\n\nThis action cannot be undone. Consider using hide instead for soft-delete.\n\n**Required Scopes:** `email_integrations:delete`","operationId":"delete_email_message_api_v1_email_integrations_email_messages__email_message_id__delete","security":[{"HTTPBearer":[]}],"parameters":[{"name":"email_message_id","in":"path","required":true,"schema":{"type":"string","title":"Email Message Id"}}],"responses":{"204":{"description":"Successful Response"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/email-integrations/email-messages/{email_message_id}/thread":{"get":{"tags":["Email Integrations"],"summary":"Get all messages in email thread","description":"Get all messages in the same thread as the specified email message.\n\n**Required Scopes:** `email_integrations:read`\n\nThis loads the full email chain using the thread_id from the specified message.\nUseful for viewing the complete thread context.","operationId":"get_email_thread_by_message_api_v1_email_integrations_email_messages__email_message_id__thread_get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"email_message_id","in":"path","required":true,"schema":{"type":"string","title":"Email Message Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/EmailThreadResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/email-integrations/email-messages/{email_message_id}/reply":{"post":{"tags":["Email Integrations"],"summary":"Send email reply","description":"Send a reply to an email message.\n\n**Required Scopes:** `email_integrations:update` OR `messages:create`\n\nFeatures:\n- Plain text and/or HTML body support (rich text)\n- CC/BCC handling with option to preserve original CCs\n- Reply-all option\n- Attachment support via attachment IDs\n- Automatic subject line (Re: original) with override option\n\nThe reply is queued for sending via the configured email integration.\nThread headers (In-Reply-To, References) are automatically set.","operationId":"send_email_reply_api_v1_email_integrations_email_messages__email_message_id__reply_post","security":[{"HTTPBearer":[]}],"parameters":[{"name":"email_message_id","in":"path","required":true,"schema":{"type":"string","title":"Email Message Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/EmailReplyRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/EmailReplyResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/email-integrations/inline-images":{"post":{"tags":["Email Integrations"],"summary":"Upload inline image for rich text email","description":"Upload an inline image for use in rich text email replies.\n\n**Required Scopes:** `email_integrations:update`\n\nReturns:\n- attachment_id: ID to reference in reply\n- content_id: CID for use in HTML (e.g., `<img src=\"cid:xxx\">`)\n- url: Signed URL for preview in composer\n\nUsage in HTML body:\n```html\n<img src=\"cid:{{content_id}}\" alt=\"Inline image\">\n```\n\nThen include the attachment_id in the reply request's attachment_ids array.","operationId":"upload_inline_image_api_v1_email_integrations_inline_images_post","security":[{"HTTPBearer":[]}],"parameters":[{"name":"filename","in":"query","required":false,"schema":{"type":"string","default":"","title":"Filename"}},{"name":"content_type","in":"query","required":false,"schema":{"type":"string","default":"","title":"Content Type"}},{"name":"request","in":"query","required":true,"schema":{"title":"Request"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InlineImageUploadResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/email-integrations/inline-images/upload":{"post":{"tags":["Email Integrations"],"summary":"Upload inline image via multipart form","description":"Upload an inline image via multipart form data.\n\n**Required Scopes:** `email_integrations:update`\n\nThis is an alternative to the raw body upload endpoint,\ndesigned for standard HTML file upload forms.\n\nForm fields:\n- file: The image file\n\nReturns same response as /inline-images endpoint.","operationId":"upload_inline_image_multipart_api_v1_email_integrations_inline_images_upload_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InlineImageUploadResponse"}}}}},"security":[{"HTTPBearer":[]}]}},"/api/v1/email-integrations/email-messages/{email_message_id}/hide":{"post":{"tags":["Email Integrations"],"summary":"Hide Email Message","description":"Hide (soft-delete/archive) an email message.\n\nThe message will be hidden from the conversation view but kept in the database.\nUse unhide to restore it.\n\n**Required Scopes:** `email_integrations:update`","operationId":"hide_email_message_api_v1_email_integrations_email_messages__email_message_id__hide_post","security":[{"HTTPBearer":[]}],"parameters":[{"name":"email_message_id","in":"path","required":true,"schema":{"type":"string","title":"Email Message Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/EmailMessageResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/email-integrations/email-messages/{email_message_id}/unhide":{"post":{"tags":["Email Integrations"],"summary":"Unhide Email Message","description":"Unhide (restore) a hidden email message.\n\n**Required Scopes:** `email_integrations:update`","operationId":"unhide_email_message_api_v1_email_integrations_email_messages__email_message_id__unhide_post","security":[{"HTTPBearer":[]}],"parameters":[{"name":"email_message_id","in":"path","required":true,"schema":{"type":"string","title":"Email Message Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/EmailMessageResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/email-integrations/email-messages/{email_message_id}/block-sender":{"post":{"tags":["Email Integrations"],"summary":"Block Sender From Message","description":"Block the sender of an email message.\n\nAdds the sender's email address to the blacklist. Future emails from this\nsender will be skipped during polling.\n\nOptionally hide all existing messages from this sender.\n\nThe whole contact (all channels) is blocked only when the address clearly\nbelongs to exactly one contact AND the caller also has `contacts:update`;\notherwise only the address itself is blocked.\n\n**Required Scopes:** `email_integrations:update` (+ `contacts:update` for\na contact-level block)","operationId":"block_sender_from_message_api_v1_email_integrations_email_messages__email_message_id__block_sender_post","security":[{"HTTPBearer":[]}],"parameters":[{"name":"email_message_id","in":"path","required":true,"schema":{"type":"string","title":"Email Message Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BlockSenderRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BlockedSenderResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/email-integrations/blocked-senders":{"get":{"tags":["Email Integrations"],"summary":"List Blocked Senders","description":"List all blocked email senders.\n\n**Required Scopes:** `email_integrations:read`","operationId":"list_blocked_senders_api_v1_email_integrations_blocked_senders_get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"page","in":"query","required":false,"schema":{"type":"integer","minimum":1,"description":"Page number (1-indexed)","default":1,"title":"Page"},"description":"Page number (1-indexed)"},{"name":"page_size","in":"query","required":false,"schema":{"type":"integer","maximum":100,"minimum":1,"description":"Items per page","default":20,"title":"Page Size"},"description":"Items per page"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BlockedSenderListResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"post":{"tags":["Email Integrations"],"summary":"Block Sender Directly","description":"Directly block an email address.\n\nAdds the email address to the blacklist. Future emails from this\nsender will be skipped during polling.\n\nThe whole contact (all channels) is blocked only when the address clearly\nbelongs to exactly one contact AND the caller also has `contacts:update`;\notherwise only the address itself is blocked.\n\n**Required Scopes:** `email_integrations:update` (+ `contacts:update` for\na contact-level block)","operationId":"block_sender_directly_api_v1_email_integrations_blocked_senders_post","security":[{"HTTPBearer":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BlockSenderDirectRequest"}}}},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BlockedSenderResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/email-integrations/blocked-senders/{email_address}":{"delete":{"tags":["Email Integrations"],"summary":"Unblock Sender","description":"Unblock an email sender (remove from blacklist).\n\nUnblocks the owning contact as a whole only when the caller also has\n`contacts:update`; otherwise only the address itself is unblocked.\n\n**Required Scopes:** `email_integrations:update` (+ `contacts:update` for\na contact-level unblock)","operationId":"unblock_sender_api_v1_email_integrations_blocked_senders__email_address__delete","security":[{"HTTPBearer":[]}],"parameters":[{"name":"email_address","in":"path","required":true,"schema":{"type":"string","title":"Email Address"}}],"responses":{"204":{"description":"Successful Response"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"get":{"tags":["Email Integrations"],"summary":"Get Blocked Sender","description":"Get details of a specific blocked sender.\n\n**Required Scopes:** `email_integrations:read`","operationId":"get_blocked_sender_api_v1_email_integrations_blocked_senders__email_address__get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"email_address","in":"path","required":true,"schema":{"type":"string","title":"Email Address"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BlockedSenderResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/email-connections":{"get":{"tags":["Email Connections"],"summary":"List Email Connections","description":"List all email connections for the tenant.\n\nReturns a unified view of both email_provider_configs (SendGrid, Mailgun, SES)\nand email_integrations (Gmail, SMTP/IMAP, Proxy).\n\nEach connection includes computed capabilities:\n- can_send_campaigns: Can send bulk email campaigns\n- can_receive_support: Can receive inbound support emails\n- can_reply_support: Can send replies to support emails\n\n**Required Scopes:** `email_integrations:read`","operationId":"list_email_connections_api_v1_email_connections_get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"include_inactive","in":"query","required":false,"schema":{"type":"boolean","default":false,"title":"Include Inactive"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/EmailConnectionListResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/email-connections/campaign-provider":{"get":{"tags":["Email Connections"],"summary":"Get Campaign Provider","description":"Get the current campaign provider for the tenant.\n\nReturns the email connection that is configured as the active campaign provider.\nOnly one connection can be the campaign provider at a time.\n\n**Required Scopes:** `email_integrations:read`","operationId":"get_campaign_provider_api_v1_email_connections_campaign_provider_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/EmailConnectionResponse"}}}}},"security":[{"HTTPBearer":[]}]}},"/api/v1/email-connections/{connection_id}":{"get":{"tags":["Email Connections"],"summary":"Get Email Connection","description":"Get a single email connection by ID.\n\nSearches both email_provider_configs and email_integrations tables.\n\n**Required Scopes:** `email_integrations:read`","operationId":"get_email_connection_api_v1_email_connections__connection_id__get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"connection_id","in":"path","required":true,"schema":{"type":"string","title":"Connection Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/EmailConnectionResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"tags":["Email Connections"],"summary":"Delete Email Connection","description":"Delete an email connection.\n\nSearches both email_provider_configs and email_integrations tables\nand deletes the matching connection.\n\n**WARNING:** This is a destructive operation. If you delete a campaign provider,\nyou will need to set a new one before sending campaigns.\n\n**Required Scopes:** `email_integrations:delete`","operationId":"delete_email_connection_api_v1_email_connections__connection_id__delete","security":[{"HTTPBearer":[]}],"parameters":[{"name":"connection_id","in":"path","required":true,"schema":{"type":"string","title":"Connection Id"}}],"responses":{"204":{"description":"Successful Response"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/email-connections/{connection_id}/set-campaign-provider":{"patch":{"tags":["Email Connections"],"summary":"Set Campaign Provider","description":"Set this connection as the active campaign provider.\n\nOnly connections with can_send_campaigns=True can be set as campaign providers\n(SendGrid, Mailgun, SES).\n\nThis will unset any other campaign provider for the tenant.\nOnly one connection can be the campaign provider at a time.\n\n**Required Scopes:** `email_integrations:update`","operationId":"set_campaign_provider_api_v1_email_connections__connection_id__set_campaign_provider_patch","security":[{"HTTPBearer":[]}],"parameters":[{"name":"connection_id","in":"path","required":true,"schema":{"type":"string","title":"Connection Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SetCampaignProviderResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/email-connections/{connection_id}/link-outbound":{"patch":{"tags":["Email Connections"],"summary":"Link Outbound Provider","description":"Link an outbound provider to a proxy integration.\n\nThis allows proxy integrations (which can receive inbound emails)\nto send replies using a SendGrid, Mailgun, or SES provider.\n\nOnly proxy integrations can have linked outbound providers.\n\n**Required Scopes:** `email_integrations:update`","operationId":"link_outbound_provider_api_v1_email_connections__connection_id__link_outbound_patch","security":[{"HTTPBearer":[]}],"parameters":[{"name":"connection_id","in":"path","required":true,"schema":{"type":"string","title":"Connection Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/LinkOutboundProviderRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LinkOutboundProviderResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"tags":["Email Connections"],"summary":"Unlink Outbound Provider","description":"Unlink the outbound provider from a proxy integration.\n\nAfter unlinking, the proxy integration will no longer be able to send replies.\n\n**Required Scopes:** `email_integrations:update`","operationId":"unlink_outbound_provider_api_v1_email_connections__connection_id__link_outbound_delete","security":[{"HTTPBearer":[]}],"parameters":[{"name":"connection_id","in":"path","required":true,"schema":{"type":"string","title":"Connection Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UnlinkOutboundProviderResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/unsubscribes":{"get":{"tags":["Unsubscribe"],"summary":"List Unsubscribes","description":"List all unsubscribes for tenant (authenticated)\n\nReturns paginated list of unsubscribe events with filtering options.","operationId":"list_unsubscribes_api_v1_unsubscribes_get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"campaign_id","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Campaign Id"}},{"name":"email_campaign_id","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Email Campaign Id"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":100,"default":50,"title":"Limit"}},{"name":"offset","in":"query","required":false,"schema":{"type":"integer","minimum":0,"default":0,"title":"Offset"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UnsubscribeListResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/unsubscribes/stats":{"get":{"tags":["Unsubscribe"],"summary":"Get Unsubscribe Stats","description":"Get unsubscribe statistics (authenticated)\n\nReturns aggregate statistics about unsubscribes for reporting/analytics.","operationId":"get_unsubscribe_stats_api_v1_unsubscribes_stats_get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"campaign_id","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Campaign Id"}},{"name":"email_campaign_id","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Email Campaign Id"}},{"name":"days","in":"query","required":false,"schema":{"type":"integer","maximum":365,"minimum":1,"default":30,"title":"Days"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UnsubscribeStats"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/tracked-links":{"post":{"tags":["Link Tracking"],"summary":"Create Tracked Link","description":"Create a tracked link with short URL.\n\nReturns a short URL that redirects to the original URL while tracking clicks.\n\nArgs:\n    request: Link creation request\n    tenant_id: Tenant ID from auth token\n\nReturns:\n    TrackedLinkResponse: Created tracked link with short URL\n\n**🔐 Required Scopes:** `campaigns:update`\n","operationId":"create_tracked_link_api_v1_tracked_links_post","security":[{"HTTPBearer":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateTrackedLinkRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TrackedLinkResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"get":{"tags":["Link Tracking"],"summary":"List Tracked Links","description":"List all tracked links for tenant with pagination and filtering.\n\n**Required Scopes:** `link_tracking:read`\n\nQuery Parameters:\n- page: Page number (1-indexed, default: 1)\n- page_size: Items per page (1-100, default: 20)\n- search: Search term for original_url, label, short_code, or custom_alias\n- feature_type: Filter by feature type (email_campaign, conversation, etc.)\n- tags: Filter by tags (returns links matching ANY of the provided tags)\n\nReturns:\n    Paginated list of tracked links with total count","operationId":"list_tracked_links_api_v1_tracked_links_get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"page","in":"query","required":false,"schema":{"type":"integer","minimum":1,"description":"Page number (1-indexed)","default":1,"title":"Page"},"description":"Page number (1-indexed)"},{"name":"page_size","in":"query","required":false,"schema":{"type":"integer","maximum":100,"minimum":1,"description":"Items per page (max 100)","default":20,"title":"Page Size"},"description":"Items per page (max 100)"},{"name":"search","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Search in original_url, label, short_code, or custom_alias","title":"Search"},"description":"Search in original_url, label, short_code, or custom_alias"},{"name":"feature_type","in":"query","required":false,"schema":{"anyOf":[{"$ref":"#/components/schemas/FeatureType"},{"type":"null"}],"description":"Filter by feature type","title":"Feature Type"},"description":"Filter by feature type"},{"name":"tags","in":"query","required":false,"schema":{"anyOf":[{"type":"array","items":{"type":"string"}},{"type":"null"}],"description":"Filter by tags (any match)","title":"Tags"},"description":"Filter by tags (any match)"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TrackedLinkListResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/tracked-links/bulk":{"post":{"tags":["Link Tracking"],"summary":"Bulk Create Tracked Links","description":"Create multiple tracked links at once.\n\nUseful for email campaigns with multiple links.\nMaximum 100 URLs per request.\n\nArgs:\n    request: Bulk creation request\n    tenant_id: Tenant ID from auth token\n\nReturns:\n    BulkCreateLinksResponse: Mapping of original URLs to short URLs\n\n**🔐 Required Scopes:** `campaigns:update`\n","operationId":"bulk_create_tracked_links_api_v1_tracked_links_bulk_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BulkCreateLinksRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BulkCreateLinksResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"HTTPBearer":[]}]}},"/api/v1/tracked-links/{tracked_link_id}/analytics":{"get":{"tags":["Link Tracking"],"summary":"Get Link Analytics","description":"Get analytics for a specific tracked link.\n\nIncludes:\n- Total clicks\n- Unique clicks (by IP)\n- Device breakdown\n- Click timeline\n\nArgs:\n    tracked_link_id: Tracked link ID\n    tenant_id: Tenant ID from auth token\n\nReturns:\n    LinkAnalyticsResponse: Analytics data\n\n**🔐 Required Scopes:** `analytics:read`\n","operationId":"get_link_analytics_api_v1_tracked_links__tracked_link_id__analytics_get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"tracked_link_id","in":"path","required":true,"schema":{"type":"string","title":"Tracked Link Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LinkAnalyticsResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/tracked-links/feature/{feature_type}/{feature_id}/analytics":{"get":{"tags":["Link Tracking"],"summary":"Get Feature Analytics","description":"Get aggregate analytics for all links in a feature.\n\nUse this to get analytics for:\n- Email campaigns\n- Conversations\n- Automations\n- Reach campaigns\n\nArgs:\n    feature_type: Feature type (email_campaign, conversation, etc.)\n    feature_id: Feature ID (campaign ID, conversation ID, etc.)\n    tenant_id: Tenant ID from auth token\n\nReturns:\n    FeatureAnalyticsResponse: Aggregate analytics\n\n**🔐 Required Scopes:** `analytics:read`\n","operationId":"get_feature_analytics_api_v1_tracked_links_feature__feature_type___feature_id__analytics_get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"feature_type","in":"path","required":true,"schema":{"$ref":"#/components/schemas/FeatureType"}},{"name":"feature_id","in":"path","required":true,"schema":{"type":"string","title":"Feature Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FeatureAnalyticsResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/tracked-links/extract-urls":{"post":{"tags":["Link Tracking"],"summary":"Extract Urls","description":"Extract URLs from HTML or text content.\n\nUtility endpoint for finding links before creating tracked versions.\n\nArgs:\n    content: Dict with 'html' or 'text' key\n    tenant_id: Tenant ID from auth token\n\nReturns:\n    Dict with extracted URLs\n\n**🔐 Required Scopes:** `campaigns:update`\n","operationId":"extract_urls_api_v1_tracked_links_extract_urls_post","requestBody":{"content":{"application/json":{"schema":{"additionalProperties":{"type":"string"},"type":"object","title":"Content"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExtractUrlsResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"HTTPBearer":[]}]}},"/api/v1/tracked-links/check-alias/{alias}":{"get":{"tags":["Link Tracking"],"summary":"Check Alias Availability","description":"Check if a custom alias is available.\n\n**Required Scopes:** `link_tracking:read`\n\nChecks both short_code and custom_alias columns to ensure uniqueness.\n\nArgs:\n    alias: The alias to check\n\nReturns:\n    Dict with availability status","operationId":"check_alias_availability_api_v1_tracked_links_check_alias__alias__get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"alias","in":"path","required":true,"schema":{"type":"string","title":"Alias"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CheckAliasResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/tracked-links/{link_id}":{"get":{"tags":["Link Tracking"],"summary":"Get Tracked Link","description":"Get a single tracked link with detailed click statistics.\n\n**Required Scopes:** `link_tracking:read`\n\nReturns full link details including:\n- Basic link info (short_code, original_url, etc.)\n- Custom alias, label, and tags\n- Click statistics (total clicks, unique clicks)\n- First and last click timestamps\n\nArgs:\n    link_id: Tracked link UUID\n\nReturns:\n    TrackedLinkDetail with full link info and stats","operationId":"get_tracked_link_api_v1_tracked_links__link_id__get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"link_id","in":"path","required":true,"schema":{"type":"string","title":"Link Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TrackedLinkDetail"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"put":{"tags":["Link Tracking"],"summary":"Update Tracked Link","description":"Update a tracked link.\n\n**Required Scopes:** `link_tracking:create`\n\nUpdates the following fields (only provided fields are updated):\n- label: User-friendly label\n- tags: List of tags for categorization\n- custom_alias: Custom short code (must be unique)\n- is_active: Whether the link is active\n\nNote: Changing custom_alias will also update the short_code used in the URL.\n\nArgs:\n    link_id: Tracked link UUID\n    request: Update request with optional fields\n\nReturns:\n    TrackedLinkDetail with updated link info","operationId":"update_tracked_link_api_v1_tracked_links__link_id__put","security":[{"HTTPBearer":[]}],"parameters":[{"name":"link_id","in":"path","required":true,"schema":{"type":"string","title":"Link Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TrackedLinkUpdate"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TrackedLinkDetail"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"tags":["Link Tracking"],"summary":"Delete Tracked Link","description":"Delete a tracked link.\n\n**Required Scopes:** `link_tracking:delete`\n\nBy default, performs a soft delete (sets is_active=False).\nUse hard_delete=true to permanently remove the link and all click data.\n\nArgs:\n    link_id: Tracked link UUID\n    hard_delete: If true, permanently delete. If false, soft delete (default).\n\nReturns:\n    Success message","operationId":"delete_tracked_link_api_v1_tracked_links__link_id__delete","security":[{"HTTPBearer":[]}],"parameters":[{"name":"link_id","in":"path","required":true,"schema":{"type":"string","title":"Link Id"}},{"name":"hard_delete","in":"query","required":false,"schema":{"type":"boolean","description":"If true, permanently delete. If false, soft delete (deactivate).","default":false,"title":"Hard Delete"},"description":"If true, permanently delete. If false, soft delete (deactivate)."}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeleteTrackedLinkResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/tracked-links/manual":{"post":{"tags":["Link Tracking"],"summary":"Create Manual Tracked Link","description":"Create a manual tracked link for the standalone URL shortener.\n\n**Required Scopes:** `link_tracking:create`\n\nCreates a new shortened URL with an auto-generated 8-character code.\nUses feature_type=OTHER for manual links.\nUses tenant's preferred tracking domain from settings.\n\nArgs:\n    request: Link creation request with:\n        - original_url: Full destination URL (required)\n        - contact_id: Contact UUID for personalized tracking (optional)\n        - label: User-friendly label (optional)\n        - tags: List of tags for categorization (optional)\n        - expires_in_days: Days until expiration (optional, default: 365)\n\nReturns:\n    TrackedLinkDetail with the created link info","operationId":"create_manual_tracked_link_api_v1_tracked_links_manual_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TrackedLinkCreateManual"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TrackedLinkDetail"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"HTTPBearer":[]}]}},"/api/v1/settings/link-tracking":{"get":{"tags":["Link Tracking Settings"],"summary":"Get Link Tracking Settings","description":"Get tenant link tracking settings.\n\nReturns configured settings or defaults if not yet configured.\n\n**Required Scopes:** `settings:read`\n\n**Database:** PRIMARY (tenants table)","operationId":"get_link_tracking_settings_api_v1_settings_link_tracking_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LinkTrackingSettingsResponse"}}}}},"security":[{"HTTPBearer":[]}]},"put":{"tags":["Link Tracking Settings"],"summary":"Update Link Tracking Settings","description":"Update tenant link tracking settings.\n\nOnly updates provided fields. Unspecified fields remain unchanged.\n\n**Required Scopes:** `settings:update`\n\n**Database:** PRIMARY (tenants table)\n\n**Example:**\n```json\n{\n    \"tracking_domain\": \"c.sendseven.com\",\n    \"link_expiration_days\": 365\n}\n```","operationId":"update_link_tracking_settings_api_v1_settings_link_tracking_put","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/LinkTrackingSettingsUpdate"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LinkTrackingSettingsResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"HTTPBearer":[]}]},"delete":{"tags":["Link Tracking Settings"],"summary":"Reset Link Tracking Settings","description":"Reset link tracking settings to defaults (clears all custom settings).\n\n**Required Scopes:** `settings:admin`\n\n**Database:** PRIMARY (tenants table)","operationId":"reset_link_tracking_settings_api_v1_settings_link_tracking_delete","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}},"security":[{"HTTPBearer":[]}]}},"/api/v1/settings/link-tracking/domains":{"get":{"tags":["Link Tracking Settings"],"summary":"Get Available Tracking Domains","description":"Get list of available tracking domains.\n\nReturns all configured tracking domains that can be selected for link tracking.\n\n**Required Scopes:** Requires authentication (any authenticated user)\n\n**Database:** None (reads from config)\n\n**Response:**\n```json\n{\n    \"domains\": [\n        {\n            \"id\": \"sendseven\",\n            \"domain\": \"c.sendseven.com\",\n            \"format\": \"/{code}\",\n            \"label\": \"SendSeven (c.sendseven.com)\",\n            \"default\": true\n        },\n        ...\n    ],\n    \"default_domain\": \"c.sendseven.com\"\n}\n```","operationId":"get_available_tracking_domains_api_v1_settings_link_tracking_domains_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TrackingDomainsResponse"}}}}},"security":[{"HTTPBearer":[]}]}},"/api/v1/bounce-suppressions":{"get":{"tags":["Bounce Handling"],"summary":"Get Suppression List","description":"Get bounce suppression list\n\nRetrieve all suppressed email addresses with optional filtering.\n\n- **bounce_type**: Filter by 'hard' or 'soft' (optional)\n- **limit**: Max results per page (1-1000)\n- **offset**: Pagination offset\n\n**🔐 Required Scopes:** `analytics:read`\n","operationId":"get_suppression_list_api_v1_bounce_suppressions_get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"bounce_type","in":"query","required":false,"schema":{"anyOf":[{"type":"string","pattern":"^(hard|soft)$"},{"type":"null"}],"title":"Bounce Type"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":1000,"minimum":1,"default":100,"title":"Limit"}},{"name":"offset","in":"query","required":false,"schema":{"type":"integer","minimum":0,"default":0,"title":"Offset"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BounceSuppressionListResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/bounce-suppressions/bulk-add":{"post":{"tags":["Bounce Handling"],"summary":"Bulk Suppress Emails","description":"Manually add multiple emails to suppression list\n\nUse this endpoint to:\n- Import known bad emails from external sources\n- Bulk suppress problematic email addresses\n- Proactively block emails before sending\n\nMaximum 1000 emails per request.\n\n**🔐 Required Scopes:** `settings:update`\n","operationId":"bulk_suppress_emails_api_v1_bounce_suppressions_bulk_add_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BulkSuppressRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BulkSuppressResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"HTTPBearer":[]}]}},"/api/v1/bounce-suppressions/{email}":{"delete":{"tags":["Bounce Handling"],"summary":"Remove From Suppression","description":"Remove email from suppression list\n\nUse when:\n- Email is confirmed valid again\n- Manual approval to re-enable email\n- Contact provided updated email address\n\nThis will reset the contact's email_status to 'valid' and\nallow future emails to be sent.\n\n**🔐 Required Scopes:** `settings:update`\n","operationId":"remove_from_suppression_api_v1_bounce_suppressions__email__delete","security":[{"HTTPBearer":[]}],"parameters":[{"name":"email","in":"path","required":true,"schema":{"type":"string","title":"Email"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RemoveSuppressionResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/bounce-analytics":{"get":{"tags":["Bounce Handling"],"summary":"Get Bounce Analytics","description":"Get bounce analytics\n\nReturns comprehensive bounce statistics:\n- Total bounces by type (hard/soft)\n- Bounce rates\n- Top bounce categories\n- Most problematic domains\n\nOptionally filter by specific campaign.\n\n- **campaign_id**: Filter by campaign (optional)\n- **days**: Time period (1-365 days)\n\n**🔐 Required Scopes:** `analytics:read`\n","operationId":"get_bounce_analytics_api_v1_bounce_analytics_get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"campaign_id","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Campaign Id"}},{"name":"days","in":"query","required":false,"schema":{"type":"integer","maximum":365,"minimum":1,"default":30,"title":"Days"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BounceAnalyticsResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/contacts/{contact_id}/subscriptions":{"get":{"tags":["Subscriptions"],"summary":"Get contact subscriptions","description":"Get all subscriptions for a specific contact\n\n**🔐 Required Scopes:** `contacts:read`\n","operationId":"get_contact_subscriptions_api_v1_contacts__contact_id__subscriptions_get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"contact_id","in":"path","required":true,"schema":{"type":"string","title":"Contact Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ContactSubscriptionsResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"post":{"tags":["Subscriptions"],"summary":"Subscribe contact","description":"Subscribe a contact to a list via a specific channel\n\n**🔐 Required Scopes:** `contacts:update`\n","operationId":"subscribe_contact_api_v1_contacts__contact_id__subscriptions_post","security":[{"HTTPBearer":[]}],"parameters":[{"name":"contact_id","in":"path","required":true,"schema":{"type":"string","title":"Contact Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SubscriptionCreate"}}}},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SubscriptionResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/contacts/{contact_id}/subscriptions/{subscription_id}":{"delete":{"tags":["Subscriptions"],"summary":"Unsubscribe contact","description":"Unsubscribe a contact from a list\n\n**🔐 Required Scopes:** `contacts:update`\n","operationId":"unsubscribe_contact_api_v1_contacts__contact_id__subscriptions__subscription_id__delete","security":[{"HTTPBearer":[]}],"parameters":[{"name":"contact_id","in":"path","required":true,"schema":{"type":"string","title":"Contact Id"}},{"name":"subscription_id","in":"path","required":true,"schema":{"type":"string","title":"Subscription Id"}}],"requestBody":{"content":{"application/json":{"schema":{"anyOf":[{"$ref":"#/components/schemas/UnsubscribeRequest"},{"type":"null"}],"title":"Unsubscribe Data"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UnsubscribeResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/subscriptions/{subscription_id}":{"patch":{"tags":["Subscriptions"],"summary":"Update subscription","description":"Update a subscription status or metadata\n\n**🔐 Required Scopes:** `contacts:update`\n","operationId":"update_subscription_api_v1_subscriptions__subscription_id__patch","security":[{"HTTPBearer":[]}],"parameters":[{"name":"subscription_id","in":"path","required":true,"schema":{"type":"string","title":"Subscription Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SubscriptionUpdate"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SubscriptionResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/lists/{list_id}/subscribers":{"get":{"tags":["Subscriptions"],"summary":"Get list subscribers","description":"Get subscribers for a list with optional filtering by channel and status\n\n**🔐 Required Scopes:** `contacts:read`\n","operationId":"get_list_subscribers_api_v1_lists__list_id__subscribers_get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"list_id","in":"path","required":true,"schema":{"type":"string","title":"List Id"}},{"name":"channel_type","in":"query","required":false,"schema":{"anyOf":[{"$ref":"#/components/schemas/SubscriptionChannelType"},{"type":"null"}],"description":"Filter by channel type","title":"Channel Type"},"description":"Filter by channel type"},{"name":"status","in":"query","required":false,"schema":{"anyOf":[{"$ref":"#/components/schemas/SubscriptionStatus"},{"type":"null"}],"description":"Filter by status","default":"subscribed","title":"Status"},"description":"Filter by status"},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":1000,"minimum":1,"description":"Maximum number of results","default":100,"title":"Limit"},"description":"Maximum number of results"},{"name":"offset","in":"query","required":false,"schema":{"type":"integer","minimum":0,"description":"Pagination offset","default":0,"title":"Offset"},"description":"Pagination offset"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ListSubscribersResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/lists/{list_id}/subscription-stats":{"get":{"tags":["Subscriptions"],"summary":"Get subscription statistics","description":"Get subscription statistics for a list broken down by channel\n\n**🔐 Required Scopes:** `contacts:read`\n","operationId":"get_subscription_stats_api_v1_lists__list_id__subscription_stats_get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"list_id","in":"path","required":true,"schema":{"type":"string","title":"List Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SubscriptionStatsResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/subscriptions/confirm/{token}":{"post":{"tags":["Subscriptions"],"summary":"Confirm double opt-in","description":"Confirm a subscription using the double opt-in token (public endpoint). Returns only the confirmed subscription's status, channel type and confirmation time.","operationId":"confirm_subscription_api_v1_subscriptions_confirm__token__post","parameters":[{"name":"token","in":"path","required":true,"schema":{"type":"string","title":"Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConfirmOptInResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/roles":{"get":{"tags":["Roles & Permissions"],"summary":"List Roles","description":"List all roles available in this tenant.\n\n**Required Scopes:** `team:read`\n\nReturns:\n- System roles (OWNER, ADMIN, etc.) - read-only, shared across all tenants\n- Custom roles (tenant-specific) - can be created, updated, deleted","operationId":"list_roles_api_v1_roles_get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"include_system","in":"query","required":false,"schema":{"type":"boolean","description":"Include system roles (OWNER, ADMIN, etc.)","default":true,"title":"Include System"},"description":"Include system roles (OWNER, ADMIN, etc.)"},{"name":"page","in":"query","required":false,"schema":{"type":"integer","minimum":1,"description":"Page number (1-indexed)","default":1,"title":"Page"},"description":"Page number (1-indexed)"},{"name":"page_size","in":"query","required":false,"schema":{"type":"integer","maximum":100,"minimum":1,"description":"Items per page","default":20,"title":"Page Size"},"description":"Items per page"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RoleListResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"post":{"tags":["Roles & Permissions"],"summary":"Create Custom Role","description":"Create a custom role with specified scopes.\n\n**Required Scopes:** `team:admin`\n\nSystem roles cannot be created via API. You may only bake scopes you\nyourself hold into a custom role (privilege-escalation ceiling), and a role\ncarrying `account:billing_read` may only be created by the billing-account\nowner.","operationId":"create_custom_role_api_v1_roles_post","security":[{"HTTPBearer":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RoleCreate"}}}},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RoleResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/roles/scopes/available":{"get":{"tags":["Roles & Permissions"],"summary":"Get Available Scopes","description":"Get all available scopes in the system (for UI dropdown).\n\n**Required Scopes:** `team:read`\n\nReturns scopes grouped by resource (campaigns, contacts, etc.)","operationId":"get_available_scopes_api_v1_roles_scopes_available_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AvailableScopesResponse"}}}}},"security":[{"HTTPBearer":[]}]}},"/api/v1/roles/{role_id}":{"get":{"tags":["Roles & Permissions"],"summary":"Get Role","description":"Get role details with all scopes.\n\n**Required Scopes:** `team:read`","operationId":"get_role_api_v1_roles__role_id__get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"role_id","in":"path","required":true,"schema":{"type":"string","title":"Role Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RoleResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"put":{"tags":["Roles & Permissions"],"summary":"Update Role","description":"Update custom role metadata (name, description).\n\n**Required Scopes:** `team:admin`\n\nSystem roles cannot be updated.\nUse /roles/{id}/scopes endpoints to modify permissions.","operationId":"update_role_api_v1_roles__role_id__put","security":[{"HTTPBearer":[]}],"parameters":[{"name":"role_id","in":"path","required":true,"schema":{"type":"string","title":"Role Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RoleUpdate"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RoleResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"tags":["Roles & Permissions"],"summary":"Delete Role","description":"Delete custom role.\n\n**Required Scopes:** `team:admin`\n\nSystem roles cannot be deleted.\nUsers with this role will lose these permissions.","operationId":"delete_role_api_v1_roles__role_id__delete","security":[{"HTTPBearer":[]}],"parameters":[{"name":"role_id","in":"path","required":true,"schema":{"type":"string","title":"Role Id"}}],"responses":{"204":{"description":"Successful Response"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/roles/{role_id}/scopes":{"post":{"tags":["Roles & Permissions"],"summary":"Add Scopes To Role","description":"Add scopes to a custom role.\n\n**Required Scopes:** `team:admin`\n\nImportant: You can only grant scopes that you have permission for.\nSystem roles cannot be modified.","operationId":"add_scopes_to_role_api_v1_roles__role_id__scopes_post","security":[{"HTTPBearer":[]}],"parameters":[{"name":"role_id","in":"path","required":true,"schema":{"type":"string","title":"Role Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ScopeAdd"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RoleResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/roles/{role_id}/scopes/{scope}":{"delete":{"tags":["Roles & Permissions"],"summary":"Remove Scope From Role","description":"Remove scope from a custom role.\n\n**Required Scopes:** `team:admin`\n\nSystem roles cannot be modified. Detaching `account:billing_read` requires\nthe billing-account owner.","operationId":"remove_scope_from_role_api_v1_roles__role_id__scopes__scope__delete","security":[{"HTTPBearer":[]}],"parameters":[{"name":"role_id","in":"path","required":true,"schema":{"type":"string","title":"Role Id"}},{"name":"scope","in":"path","required":true,"schema":{"type":"string","title":"Scope"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ScopeRemoveResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/permissions/users/{user_id}":{"get":{"tags":["Roles & Permissions"],"summary":"Get User Permissions","description":"Get complete permission overview for a user\n\nReturns:\n- All assigned roles\n- Direct scope grants\n- Effective scopes (merged from roles + direct grants)\n\n**🔐 Required Scopes:** `team:read`\n","operationId":"get_user_permissions_api_v1_permissions_users__user_id__get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"user_id","in":"path","required":true,"schema":{"type":"string","title":"User Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserPermissionsResponseSchema"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/permissions/users/{user_id}/roles":{"get":{"tags":["Roles & Permissions"],"summary":"Get User Roles","description":"Get all roles assigned to a user in this tenant\n\n**🔐 Required Scopes:** `team:read`\n","operationId":"get_user_roles_api_v1_permissions_users__user_id__roles_get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"user_id","in":"path","required":true,"schema":{"type":"string","title":"User Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/UserRoleResponseSchema"},"title":"Response Get User Roles Api V1 Permissions Users  User Id  Roles Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"post":{"tags":["Roles & Permissions"],"summary":"Assign Role To User","description":"Assign a role to a user (multi-role supported)\n\nUser can have multiple roles - scopes are merged\n\nSecurity: Users can only assign roles with scopes they themselves possess.\nThis prevents privilege escalation attacks.\n\n**🔐 Required Scopes:** `team:update`\n","operationId":"assign_role_to_user_api_v1_permissions_users__user_id__roles_post","security":[{"HTTPBearer":[]}],"parameters":[{"name":"user_id","in":"path","required":true,"schema":{"type":"string","title":"User Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserRoleAssignmentSchema"}}}},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RoleAssignmentResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/permissions/users/{user_id}/roles/{role_id}":{"delete":{"tags":["Roles & Permissions"],"summary":"Remove Role From User","description":"Remove a role from a user\n\nSecurity: Users cannot remove their own system or owner roles.\nAlso prevents removing the last owner from a tenant.\n\n**🔐 Required Scopes:** `team:update`\n","operationId":"remove_role_from_user_api_v1_permissions_users__user_id__roles__role_id__delete","security":[{"HTTPBearer":[]}],"parameters":[{"name":"user_id","in":"path","required":true,"schema":{"type":"string","title":"User Id"}},{"name":"role_id","in":"path","required":true,"schema":{"type":"string","title":"Role Id"}}],"responses":{"204":{"description":"Successful Response"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/permissions/users/{user_id}/scopes":{"get":{"tags":["Roles & Permissions"],"summary":"Get User Effective Scopes","description":"Get all effective scopes for a user (merged from roles + direct grants)\n\n**🔐 Required Scopes:** `team:read`\n","operationId":"get_user_effective_scopes_api_v1_permissions_users__user_id__scopes_get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"user_id","in":"path","required":true,"schema":{"type":"string","title":"User Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserEffectiveScopesResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"post":{"tags":["Roles & Permissions"],"summary":"Grant Direct Scope","description":"Grant scope directly to user (bypasses roles)\n\nUse for temporary permissions or edge cases.\nRequires OWNER/ADMIN permission.\n\n**🔐 Required Scopes:** `team:admin`\n","operationId":"grant_direct_scope_api_v1_permissions_users__user_id__scopes_post","security":[{"HTTPBearer":[]}],"parameters":[{"name":"user_id","in":"path","required":true,"schema":{"type":"string","title":"User Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserScopeGrantSchema"}}}},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ScopeGrantResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/permissions/users/{user_id}/scopes/{scope_id}":{"delete":{"tags":["Roles & Permissions"],"summary":"Revoke Direct Scope","description":"Revoke a direct scope grant from a user\n\n**🔐 Required Scopes:** `team:admin`\n","operationId":"revoke_direct_scope_api_v1_permissions_users__user_id__scopes__scope_id__delete","security":[{"HTTPBearer":[]}],"parameters":[{"name":"user_id","in":"path","required":true,"schema":{"type":"string","title":"User Id"}},{"name":"scope_id","in":"path","required":true,"schema":{"type":"string","title":"Scope Id"}}],"responses":{"204":{"description":"Successful Response"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/permissions/me/scopes":{"get":{"tags":["Roles & Permissions"],"summary":"Get My Scopes","description":"Get current user's effective scopes (for frontend permission checking)\n\nThis endpoint is useful for:\n- Frontend to show/hide UI elements based on permissions\n- Debugging permission issues\n- Client-side validation before API calls","operationId":"get_my_scopes_api_v1_permissions_me_scopes_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MyScopesResponse"}}}}},"security":[{"HTTPBearer":[]}]}},"/api/v1/permissions/me/check":{"post":{"tags":["Roles & Permissions"],"summary":"Check My Permissions","description":"Check if current user has required scopes\n\nUseful for frontend to check multiple permissions at once\n\nExample:\nPOST /api/v1/permissions/me/check\n{\n    \"required_scopes\": [\"campaigns:update\", \"email:read\", \"lists:*\"]\n}\n\nReturns:\n{\n    \"has_all\": true,\n    \"results\": {\n        \"campaigns:update\": true,\n        \"email:read\": true,\n        \"lists:*\": true\n    }\n}","operationId":"check_my_permissions_api_v1_permissions_me_check_post","requestBody":{"content":{"application/json":{"schema":{"items":{"type":"string"},"type":"array","title":"Required Scopes"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PermissionCheckResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"HTTPBearer":[]}]}},"/api/v1/api-tokens":{"get":{"tags":["API Tokens"],"summary":"List Api Tokens","description":"List all API tokens for current user in this tenant.\n\n**Required Scopes:** `api:read`\n\nBy default, only shows active (non-expired, non-revoked) tokens.\nSet include_inactive=true to see all tokens.","operationId":"list_api_tokens_api_v1_api_tokens_get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"include_inactive","in":"query","required":false,"schema":{"type":"boolean","description":"Include inactive/expired tokens","default":false,"title":"Include Inactive"},"description":"Include inactive/expired tokens"},{"name":"page","in":"query","required":false,"schema":{"type":"integer","minimum":1,"description":"Page number (1-indexed)","default":1,"title":"Page"},"description":"Page number (1-indexed)"},{"name":"page_size","in":"query","required":false,"schema":{"type":"integer","maximum":100,"minimum":1,"description":"Items per page","default":20,"title":"Page Size"},"description":"Items per page"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiTokenListResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"post":{"tags":["API Tokens"],"summary":"Create Api Token","description":"Create a new API token.\n\n**Required Scopes:** `api:create`\n\nIMPORTANT: The plaintext token is returned ONLY ONCE.\nSave it securely - you won't be able to retrieve it again.\n\nToken format: s7_api_<32_random_hex_chars>\n\nDefault expiration: 90 days\nMax expiration: 365 days","operationId":"create_api_token_api_v1_api_tokens_post","security":[{"HTTPBearer":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiTokenCreate"}}}},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiTokenCreated"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/api-tokens/available-scopes":{"get":{"tags":["API Tokens"],"summary":"Get Available Scopes For Token","description":"Get all scopes that the current user can grant to API tokens.\n\n**Required Scopes:** `api:read`\n\nThis endpoint expands wildcard scopes (*:*, resource:*, etc.) to show\nall granular scopes the user can select when creating tokens.\n\nExample:\n- User has ['*:*'] -> Returns all 78 scopes\n- User has ['campaigns:*'] -> Returns all campaigns:* scopes\n- User has ['contacts:read', 'messages:create'] -> Returns exactly those scopes","operationId":"get_available_scopes_for_token_api_v1_api_tokens_available_scopes_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AvailableTokenScopesResponse"}}}}},"security":[{"HTTPBearer":[]}]}},"/api/v1/api-tokens/{token_id}":{"get":{"tags":["API Tokens"],"summary":"Get Api Token","description":"Get details of a specific API token.\n\n**Required Scopes:** `api:read`\n\nNote: The plaintext token is NOT included - it was only shown at creation.","operationId":"get_api_token_api_v1_api_tokens__token_id__get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"token_id","in":"path","required":true,"schema":{"type":"string","title":"Token Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiTokenResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"patch":{"tags":["API Tokens"],"summary":"Update Api Token","description":"Update API token metadata.\n\n**Required Scopes:** `api:create`\n\nCan update:\n- name: Change the human-readable name\n- is_active: Revoke (false) or reactivate (true) the token\n\nCannot update (create new token instead):\n- scopes\n- expires_at","operationId":"update_api_token_api_v1_api_tokens__token_id__patch","security":[{"HTTPBearer":[]}],"parameters":[{"name":"token_id","in":"path","required":true,"schema":{"type":"string","title":"Token Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiTokenUpdate"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiTokenResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"tags":["API Tokens"],"summary":"Revoke Api Token","description":"Revoke (delete) an API token permanently.\n\n**Required Scopes:** `api:delete`\n\nThis permanently deletes the token.\nAlternatively, use PATCH to set is_active=false to soft-revoke.","operationId":"revoke_api_token_api_v1_api_tokens__token_id__delete","security":[{"HTTPBearer":[]}],"parameters":[{"name":"token_id","in":"path","required":true,"schema":{"type":"string","title":"Token Id"}}],"responses":{"204":{"description":"Successful Response"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/api-tokens/{token_id}/regenerate":{"post":{"tags":["API Tokens"],"summary":"Regenerate Api Token","description":"Regenerate an API token (new token string, same scopes).\n\n**Required Scopes:** `api:create`\n\nThis creates a new token string while keeping the same:\n- Name\n- Scopes\n- Expiration date\n\nThe old token is immediately revoked.\nIMPORTANT: Save the new token - it will only be shown once.","operationId":"regenerate_api_token_api_v1_api_tokens__token_id__regenerate_post","security":[{"HTTPBearer":[]}],"parameters":[{"name":"token_id","in":"path","required":true,"schema":{"type":"string","title":"Token Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiTokenCreated"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/integrations":{"get":{"tags":["Integrations"],"summary":"List Integrations","description":"List available integrations for the Integrations Hub.\n\n**Required Scopes:** `settings:read`\n\nReturns a static-ish catalog of integrations with a per-tenant\n``connected`` status. For API-token integrations (Zapier, n8n) the tenant is\nconsidered connected when it has at least one active (non-revoked,\nnon-expired) API token. External-link cards (docs, partners) are never\n\"connected\".","operationId":"list_integrations_api_v1_integrations_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/IntegrationCatalogResponse"}}}}},"security":[{"HTTPBearer":[]}]}},"/api/v1/integrations/hubspot/status":{"get":{"tags":["HubSpot Integration"],"summary":"Hubspot Status","description":"Connection state + sync configuration for the current tenant.\n\n**Required Scopes:** `settings:read` (+ Full Suite plan)","operationId":"hubspot_status_api_v1_integrations_hubspot_status_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HubSpotStatusResponse"}}}}},"security":[{"HTTPBearer":[]}]}},"/api/v1/integrations/hubspot/settings":{"patch":{"tags":["HubSpot Integration"],"summary":"Hubspot Update Settings","description":"Update sync direction toggles and/or field-mapping overrides.\n\n**Required Scopes:** `settings:update` (+ Full Suite plan)","operationId":"hubspot_update_settings_api_v1_integrations_hubspot_settings_patch","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/HubSpotSettingsUpdate"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HubSpotStatusResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"HTTPBearer":[]}]}},"/api/v1/integrations/hubspot/connection":{"delete":{"tags":["HubSpot Integration"],"summary":"Hubspot Disconnect","description":"Disconnect HubSpot: best-effort token revoke, then status=disconnected.\nThe row is kept for audit / clean re-install.\n\n**Required Scopes:** `settings:update` (+ Full Suite plan)","operationId":"hubspot_disconnect_api_v1_integrations_hubspot_connection_delete","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HubSpotDisconnectResponse"}}}}},"security":[{"HTTPBearer":[]}]}},"/api/v1/integrations/hubspot/contacts/count":{"get":{"tags":["HubSpot Integration"],"summary":"Hubspot Contact Count","description":"Count the connected HubSpot portal's contacts, optionally scoped by a draft\n``filter`` (unsaved ``filter_groups``) and/or a ``list_id`` — this powers the\nlive \"X contacts match\" preview while a selective-sync filter is being\nedited. Backed by a HubSpot Search (``POST /crm/v3/objects/contacts/search``\nwith ``limit:0``) that returns the server-side ``total`` in ~0.3s; results\nare cached ≤5 min unless ``refresh=true``.\n\n**Required Scopes:** `settings:read` (+ Full Suite plan)","operationId":"hubspot_contact_count_api_v1_integrations_hubspot_contacts_count_get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"filter","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"URL-encoded filter_groups JSON (OR across groups, AND within). Omit `filter` AND `list_id` to count the whole portal.","title":"Filter"},"description":"URL-encoded filter_groups JSON (OR across groups, AND within). Omit `filter` AND `list_id` to count the whole portal."},{"name":"list_id","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Count only this HubSpot list's members","title":"List Id"},"description":"Count only this HubSpot list's members"},{"name":"refresh","in":"query","required":false,"schema":{"type":"boolean","description":"Bypass the ≤5-min server cache (live-preview edits)","default":false,"title":"Refresh"},"description":"Bypass the ≤5-min server cache (live-preview edits)"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HubSpotContactCountResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/integrations/hubspot/contact-sync-filter":{"get":{"tags":["HubSpot Integration"],"summary":"Hubspot Get Contact Sync Filter","description":"Read the persisted selective contact-sync filter. Returns the pre-feature\ndefault (``mode = none`` — import everything) when nothing is stored yet.\n\n**Required Scopes:** `settings:read` (+ Full Suite plan)","operationId":"hubspot_get_contact_sync_filter_api_v1_integrations_hubspot_contact_sync_filter_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HubSpotContactSyncFilterModel"}}}}},"security":[{"HTTPBearer":[]}]},"put":{"tags":["HubSpot Integration"],"summary":"Hubspot Put Contact Sync Filter","description":"Persist the selective contact-sync filter. The filter is VALIDATED against\nHubSpot on save (a HubSpot 400 surfaces its own limit message); persisting\nALSO increments ``sync_generation`` so a sync run using the previous filter\nself-interrupts. Narrowing stops adding non-matching contacts but does not\ndelete already-synced ones; widening does not auto-backfill (import is a\nseparate explicit action).\n\n**Required Scopes:** `settings:update` (+ Full Suite plan)","operationId":"hubspot_put_contact_sync_filter_api_v1_integrations_hubspot_contact_sync_filter_put","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/HubSpotContactSyncFilterModel"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HubSpotContactSyncFilterModel"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"HTTPBearer":[]}]}},"/api/v1/integrations/hubspot/sync/import":{"post":{"tags":["HubSpot Integration"],"summary":"Hubspot Import","description":"Import contacts from HubSpot (all contacts, or one list's members).\n\n**Required Scopes:** `contacts:create` or `contacts:update` (+ Full Suite plan)","operationId":"hubspot_import_api_v1_integrations_hubspot_sync_import_post","requestBody":{"content":{"application/json":{"schema":{"anyOf":[{"$ref":"#/components/schemas/HubSpotImportRequest"},{"type":"null"}],"title":"Data"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"HTTPBearer":[]}]}},"/api/v1/integrations/hubspot/sync/export":{"post":{"tags":["HubSpot Integration"],"summary":"Hubspot Export","description":"Export SendSeven contacts to HubSpot (batch upsert by email).\n\n**Required Scopes:** `contacts:read` (+ Full Suite plan)","operationId":"hubspot_export_api_v1_integrations_hubspot_sync_export_post","requestBody":{"content":{"application/json":{"schema":{"anyOf":[{"$ref":"#/components/schemas/HubSpotExportRequest"},{"type":"null"}],"title":"Data"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"HTTPBearer":[]}]}},"/api/v1/integrations/hubspot/lists/migrate":{"post":{"tags":["HubSpot Integration"],"summary":"Hubspot Migrate List","description":"Migrate a HubSpot list into a NEW SendSeven newsletter list (imports the\nmembers, creates the list, subscribes them via the unified subscription\nsystem) — the \"migrate leads -> send campaign\" flow.\n\n**Required Scopes:** `lists:create` or `contacts:create` (+ Full Suite plan)","operationId":"hubspot_migrate_list_api_v1_integrations_hubspot_lists_migrate_post","requestBody":{"content":{"application/json":{"schema":{"anyOf":[{"$ref":"#/components/schemas/HubSpotListMigrateRequest"},{"type":"null"}],"title":"Data"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"HTTPBearer":[]}]}},"/api/v1/integrations/hubspot/properties":{"get":{"tags":["HubSpot Integration"],"summary":"Hubspot List Properties","description":"List the connected portal's HubSpot contact properties — feeds the settings\nfield-mapping dropdown (replacing free-text property inputs). Each item:\n``name``, ``label``, ``type``, ``field_type``, ``group_name``, ``custom``.\n\nAlso returns ``targets``: the mappable-target catalogue for the\n\"Contact methods\" and \"Social media profiles\" settings sections —\n``{\"contact_methods\": [{\"target\": \"email\", \"label\": \"Email (primary)\"}, ...],\n\"social_profiles\": [{\"target\": \"linkedin\", \"label\": \"LinkedIn\"}, ...]}``.\nPair a HubSpot property (from ``items``) with one or more of these\ntargets to build the ``contact_method_mapping`` sent to ``PATCH\n/settings``.\n\n**Required Scopes:** `settings:read` (+ Full Suite plan)","operationId":"hubspot_list_properties_api_v1_integrations_hubspot_properties_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}},"security":[{"HTTPBearer":[]}]},"post":{"tags":["HubSpot Integration"],"summary":"Hubspot Create Property","description":"Create a new custom HubSpot contact property so it can be selected inline in\nthe field-mapping UI. Returns the created property in the same shape as the\nlist endpoint.\n\nNOTE: HubSpot requires the ``crm.schemas.contacts.write`` scope to create a\nproperty. If the connected portal did not grant it, this returns 400.\n\n**Required Scopes:** `settings:update` (+ Full Suite plan)","operationId":"hubspot_create_property_api_v1_integrations_hubspot_properties_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/HubSpotPropertyCreate"}}},"required":true},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"HTTPBearer":[]}]}},"/api/v1/integrations/hubspot/lists":{"get":{"tags":["HubSpot Integration"],"summary":"Hubspot List Hubspot Lists","description":"List the connected portal's HubSpot lists, each annotated with whether a\nSendSeven ``HUBSPOT_SEGMENT`` list already backs it — the picker for\n\"target a HubSpot list\".\n\n**Required Scopes:** `lists:read` (+ Full Suite plan)","operationId":"hubspot_list_hubspot_lists_api_v1_integrations_hubspot_lists_get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"after","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Opaque pagination cursor","title":"After"},"description":"Opaque pagination cursor"},{"name":"search","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Name search filter","title":"Search"},"description":"Name search filter"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HubSpotListsResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/integrations/hubspot/segments":{"post":{"tags":["HubSpot Integration"],"summary":"Hubspot Create Segment","description":"Create a SendSeven list whose membership is backed by a live HubSpot list.\nThe list can then be used as a campaign audience; members are resolved\nthrough the normal newsletter recipient funnel (reachability + opt-out +\nper-channel fan-out) at send time (or from cache in ``cached`` mode).\n\n**Required Scopes:** `lists:create` (+ Full Suite plan)","operationId":"hubspot_create_segment_api_v1_integrations_hubspot_segments_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/HubSpotSegmentCreate"}}},"required":true},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HubSpotSegmentResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"HTTPBearer":[]}]}},"/api/v1/integrations/hubspot/segments/{list_id}":{"patch":{"tags":["HubSpot Integration"],"summary":"Hubspot Update Segment","description":"Update a HubSpot segment's name and/or resolution config (refresh mode,\ninterval, unresolved policy, method scope).\n\n**Required Scopes:** `lists:update` (+ Full Suite plan)","operationId":"hubspot_update_segment_api_v1_integrations_hubspot_segments__list_id__patch","security":[{"HTTPBearer":[]}],"parameters":[{"name":"list_id","in":"path","required":true,"schema":{"type":"string","title":"List Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/HubSpotSegmentUpdate"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HubSpotSegmentResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/integrations/hubspot/segments/{list_id}/preview":{"post":{"tags":["HubSpot Integration"],"summary":"Hubspot Preview Segment","description":"Preview a segment's audience: resolve a sample of the HubSpot list live and\nreport how many members map to existing contacts, how many would be\nauto-created, and how many are unresolved under the segment's policy.\n\nDry-run: this is non-mutating — it resolves and counts WITHOUT creating any\nlink or contact (``would_create`` is a projection, not an action). The real\nsend/sync/cached-refresh paths still eagerly link and create.\n\n**Required Scopes:** `lists:read` (+ Full Suite plan)","operationId":"hubspot_preview_segment_api_v1_integrations_hubspot_segments__list_id__preview_post","security":[{"HTTPBearer":[]}],"parameters":[{"name":"list_id","in":"path","required":true,"schema":{"type":"string","title":"List Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HubSpotSegmentPreviewResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/webhook-endpoints/available-events":{"get":{"tags":["Webhooks"],"summary":"Get Available Events","description":"Get list of available webhook event types.\n\n**Required Scopes:** `webhooks:read`\n\nReturns all event types that can be subscribed to along with descriptions\nand categories.","operationId":"get_available_events_api_v1_webhook_endpoints_available_events_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AvailableEventsResponse"}}}}},"security":[{"HTTPBearer":[]}]}},"/api/v1/webhook-endpoints":{"get":{"tags":["Webhooks"],"summary":"List Webhook Endpoints","description":"List all webhook endpoints for the tenant.\n\n**Required Scopes:** `webhooks:read`","operationId":"list_webhook_endpoints_api_v1_webhook_endpoints_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"items":{"$ref":"#/components/schemas/WebhookEndpointResponse"},"type":"array","title":"Response List Webhook Endpoints Api V1 Webhook Endpoints Get"}}}}},"security":[{"HTTPBearer":[]}]},"post":{"tags":["Webhooks"],"summary":"Create Webhook Endpoint","description":"Create a new webhook endpoint.\n\n**Required Scopes:** `webhooks:create`\n\nCreates a webhook endpoint and returns the secret key for HMAC verification.\n**Important:** The secret key is shown only once - store it securely!\n\nAfter creation, a verification challenge is automatically sent.\nThe webhook must respond correctly to become active.","operationId":"create_webhook_endpoint_api_v1_webhook_endpoints_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookEndpointCreate"},"examples":{"message_events":{"summary":"Subscribe to message events","description":"Create a webhook that receives notifications for all inbound and outbound message events.","value":{"name":"CRM Message Sync","url":"https://api.mycrm.com/webhooks/sendseven","authorization_header":"Bearer sk_live_abc123xyz","subscribed_events":["message.received","message.sent","message.delivered","message.read","message.failed"],"retry_strategy":"exponential","max_retries":8,"timeout_seconds":30}},"conversation_events":{"summary":"Subscribe to conversation lifecycle","description":"Track conversation creation, assignment, and closure for CRM or helpdesk integration.","value":{"name":"Helpdesk Integration","url":"https://helpdesk.example.com/api/webhooks","subscribed_events":["conversation.created","conversation.assigned","conversation.closed","conversation.reopened"],"retry_strategy":"exponential","max_retries":5,"timeout_seconds":15}},"all_events":{"summary":"Subscribe to all events","description":"Receive all available event types for a comprehensive data sync.","value":{"name":"Data Warehouse Sync","url":"https://warehouse.example.com/ingest/sendseven","authorization_header":"Basic dXNlcjpwYXNz","subscribed_events":["message.received","message.sent","message.delivered","message.failed","message.read","email.received","email.sent","email.delivered","email.bounced","email.opened","email.complained","conversation.created","conversation.closed","conversation.assigned","conversation.reopened","contact.created","contact.updated","contact.deleted","contact.subscribed","contact.unsubscribed","link.clicked","attachment.summarized"],"retry_strategy":"exponential","max_retries":10,"timeout_seconds":30}}}}},"required":true},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookSecretResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"HTTPBearer":[]}]}},"/api/v1/webhook-endpoints/{webhook_id}":{"get":{"tags":["Webhooks"],"summary":"Get Webhook Endpoint","description":"Get a specific webhook endpoint.\n\n**Required Scopes:** `webhooks:read`","operationId":"get_webhook_endpoint_api_v1_webhook_endpoints__webhook_id__get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"webhook_id","in":"path","required":true,"schema":{"type":"string","title":"Webhook Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookEndpointResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"patch":{"tags":["Webhooks"],"summary":"Update Webhook Endpoint","description":"Update a webhook endpoint.\n\n**Required Scopes:** `webhooks:update`\n\nNote: If the URL is changed, the webhook must be re-verified.\n\nWebhooks managed by an OAuth integration (`verification_method='oauth_app'`,\ne.g. Zapier) have an immutable `url` and `subscribed_events`: attempting to\nchange either returns 403. Those endpoints were auto-verified on the OAuth\ngrant with no echo-challenge, so re-pointing the url here is disallowed —\nthe integration must delete + recreate instead. Other fields (name, active\ntoggle, retry/timeout, source filters, auth header) remain editable.","operationId":"update_webhook_endpoint_api_v1_webhook_endpoints__webhook_id__patch","security":[{"HTTPBearer":[]}],"parameters":[{"name":"webhook_id","in":"path","required":true,"schema":{"type":"string","title":"Webhook Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookEndpointUpdate"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookEndpointResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"tags":["Webhooks"],"summary":"Delete Webhook Endpoint","description":"Delete a webhook endpoint.\n\n**Required Scopes:** `webhooks:delete`\n\nThis also deletes all associated delivery history.","operationId":"delete_webhook_endpoint_api_v1_webhook_endpoints__webhook_id__delete","security":[{"HTTPBearer":[]}],"parameters":[{"name":"webhook_id","in":"path","required":true,"schema":{"type":"string","title":"Webhook Id"}}],"responses":{"204":{"description":"Successful Response"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/webhook-endpoints/{webhook_id}/verify":{"post":{"tags":["Webhooks"],"summary":"Verify Webhook Endpoint","description":"Re-trigger webhook verification.\n\n**Required Scopes:** `webhooks:update`\n\nSends a verification challenge to the webhook URL.\nThe endpoint must respond with the challenge to become verified and active.\n\nManual reactivation: if the endpoint was SUSPENDED by the circuit breaker\nand verification succeeds, the suspension is lifted into PROBATION — the\nqueued event backlog is replayed in order immediately, but the endpoint is\nonly counted as recovered (and the \"reactivated\" email only sent) once a\nreal delivery actually succeeds. If the next real delivery fails, the\nendpoint goes straight back to suspended. Identical to a successful\nautomatic reactivation attempt.","operationId":"verify_webhook_endpoint_api_v1_webhook_endpoints__webhook_id__verify_post","security":[{"HTTPBearer":[]}],"parameters":[{"name":"webhook_id","in":"path","required":true,"schema":{"type":"string","title":"Webhook Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookVerificationResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/webhook-endpoints/{webhook_id}/test":{"post":{"tags":["Webhooks"],"summary":"Send Test Webhook","description":"Send a test webhook event.\n\n**Required Scopes:** `webhooks:update`\n\nSends a test event to the webhook endpoint with sample data.\nThe webhook must be verified to receive test events.","operationId":"send_test_webhook_api_v1_webhook_endpoints__webhook_id__test_post","security":[{"HTTPBearer":[]}],"parameters":[{"name":"webhook_id","in":"path","required":true,"schema":{"type":"string","title":"Webhook Id"}}],"requestBody":{"content":{"application/json":{"schema":{"anyOf":[{"$ref":"#/components/schemas/WebhookTestRequest"},{"type":"null"}],"title":"Test Request"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookTestResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/webhook-endpoints/{webhook_id}/deliveries":{"get":{"tags":["Webhooks"],"summary":"Get Webhook Deliveries","description":"Get delivery history for a webhook endpoint.\n\n**Required Scopes:** `webhooks:read`\n\nReturns paginated list of webhook deliveries with request/response details.\n\nUse the `status` filter to find deliveries that can be retried:\n- `failed` - Deliveries that failed and exhausted retries\n- `dead_letter` - Deliveries moved to dead letter queue\n\nBoth `failed` and `dead_letter` deliveries can be manually retried.\n\n`queued` deliveries are events stored while the endpoint was suspended.\nThey are replayed automatically on reactivation, so they are never\nmanually retriable (`can_retry` is false).","operationId":"get_webhook_deliveries_api_v1_webhook_endpoints__webhook_id__deliveries_get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"webhook_id","in":"path","required":true,"schema":{"type":"string","title":"Webhook Id"}},{"name":"page","in":"query","required":false,"schema":{"type":"integer","minimum":1,"description":"Page number","default":1,"title":"Page"},"description":"Page number"},{"name":"page_size","in":"query","required":false,"schema":{"type":"integer","maximum":100,"minimum":1,"description":"Items per page","default":50,"title":"Page Size"},"description":"Items per page"},{"name":"status","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Filter by delivery status (pending, success, failed, retrying, dead_letter, queued)","title":"Status"},"description":"Filter by delivery status (pending, success, failed, retrying, dead_letter, queued)"},{"name":"event_type","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Filter by event type (e.g. message.sent, message.received, conversation.created)","title":"Event Type"},"description":"Filter by event type (e.g. message.sent, message.received, conversation.created)"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookDeliveryListResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/webhook-endpoints/{webhook_id}/regenerate-secret":{"post":{"tags":["Webhooks"],"summary":"Regenerate Webhook Secret","description":"Regenerate the webhook secret key.\n\n**Required Scopes:** `webhooks:update`\n\nGenerates a new secret key for HMAC signature verification.\n**Important:** The new secret key is shown only once - store it securely!\n\nYou will need to update your webhook consumer with the new secret.","operationId":"regenerate_webhook_secret_api_v1_webhook_endpoints__webhook_id__regenerate_secret_post","security":[{"HTTPBearer":[]}],"parameters":[{"name":"webhook_id","in":"path","required":true,"schema":{"type":"string","title":"Webhook Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookSecretResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/webhook-endpoints/{webhook_id}/deliveries/{delivery_id}/retry":{"post":{"tags":["Webhooks"],"summary":"Retry Webhook Delivery","description":"Manually retry a failed or dead_letter webhook delivery.\n\n**Required Scopes:** `webhooks:update`\n\nRe-queues a failed delivery for immediate sending. This creates a new\ndelivery record and publishes it to the webhooks queue for processing.\n\nCan only retry deliveries with status 'failed' or 'dead_letter'.","operationId":"retry_webhook_delivery_api_v1_webhook_endpoints__webhook_id__deliveries__delivery_id__retry_post","security":[{"HTTPBearer":[]}],"parameters":[{"name":"webhook_id","in":"path","required":true,"schema":{"type":"string","title":"Webhook Id"}},{"name":"delivery_id","in":"path","required":true,"schema":{"type":"string","title":"Delivery Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookRetryResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/alert-settings":{"get":{"tags":["Alerts"],"summary":"Get Alert Settings","description":"All three config layers + every destination visible to this tenant.\n\n**🔐 Required Scopes:** `alerts:read`\n","operationId":"get_alert_settings_api_v1_alert_settings_get","security":[{"HTTPBearer":[]}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AlertSettingsLayeredResponse"}}}}}},"put":{"tags":["Alerts"],"summary":"Put Alert Settings","description":"Upsert the settings row at ``scope``.\n\n``account`` writes the BA-default (applies to every tenant of the billing\naccount); ``tenant`` creates/updates this tenant's all-or-nothing\noverride. Single-row-per-scope is enforced HERE (MySQL treats NULLs as\ndistinct inside the unique constraint).\n\n**🔐 Required Scopes:** `alerts:update`\n","operationId":"put_alert_settings_api_v1_alert_settings_put","security":[{"HTTPBearer":[]}],"parameters":[{"name":"scope","in":"query","required":false,"schema":{"type":"string","pattern":"^(account|tenant)$","default":"account","title":"Scope"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AlertSettingsBody"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AlertSettingsResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/alert-settings/override":{"delete":{"tags":["Alerts"],"summary":"Delete Alert Settings Override","description":"Remove this tenant's override — it reverts to the BA default.\n\nTenant-scoped destination rows are kept (inert while no override exists)\nso re-enabling the override restores them.\n\n**🔐 Required Scopes:** `alerts:update`\n","operationId":"delete_alert_settings_override_api_v1_alert_settings_override_delete","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}},"security":[{"HTTPBearer":[]}]}},"/api/v1/alert-settings/destinations":{"post":{"tags":["Alerts"],"summary":"Create Alert Destination","operationId":"create_alert_destination_api_v1_alert_settings_destinations_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AlertDestinationCreate"}}},"required":true},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AlertDestinationResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"HTTPBearer":[]}],"description":"\n\n**🔐 Required Scopes:** `alerts:update`\n"}},"/api/v1/alert-settings/destinations/{destination_id}":{"put":{"tags":["Alerts"],"summary":"Update Alert Destination","operationId":"update_alert_destination_api_v1_alert_settings_destinations__destination_id__put","security":[{"HTTPBearer":[]}],"parameters":[{"name":"destination_id","in":"path","required":true,"schema":{"type":"string","title":"Destination Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AlertDestinationUpdate"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AlertDestinationResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"description":"\n\n**🔐 Required Scopes:** `alerts:update`\n"},"delete":{"tags":["Alerts"],"summary":"Delete Alert Destination","operationId":"delete_alert_destination_api_v1_alert_settings_destinations__destination_id__delete","security":[{"HTTPBearer":[]}],"parameters":[{"name":"destination_id","in":"path","required":true,"schema":{"type":"string","title":"Destination Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"description":"\n\n**🔐 Required Scopes:** `alerts:update`\n"}},"/api/v1/alert-settings/destinations/{destination_id}/send-verification":{"post":{"tags":["Alerts"],"summary":"Send Destination Verification","description":"Send a one-time code to the destination via ITS OWN channel\n(SMS / WhatsApp / email). Verifying through the same channel doubles as\nthe consent record for utility messages (plan §6.3) — ``verified_at`` is\nonly stamped after the code round-trips against the SAME target value\n(security review #1: the code is bound to the phone/email, not the row).\n\nAbuse limits (#2): per-destination hourly + per-tenant daily send quotas.\n\n**🔐 Required Scopes:** `alerts:update`\n","operationId":"send_destination_verification_api_v1_alert_settings_destinations__destination_id__send_verification_post","security":[{"HTTPBearer":[]}],"parameters":[{"name":"destination_id","in":"path","required":true,"schema":{"type":"string","title":"Destination Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/alert-settings/destinations/{destination_id}/verify":{"post":{"tags":["Alerts"],"summary":"Verify Destination","operationId":"verify_destination_api_v1_alert_settings_destinations__destination_id__verify_post","security":[{"HTTPBearer":[]}],"parameters":[{"name":"destination_id","in":"path","required":true,"schema":{"type":"string","title":"Destination Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DestinationVerifyRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"description":"\n\n**🔐 Required Scopes:** `alerts:update`\n"}},"/api/v1/alerts":{"get":{"tags":["Alerts"],"summary":"List Alerts","description":"Alert history for this tenant (SHARD), newest first.\n\n**🔐 Required Scopes:** `alerts:read`\n","operationId":"list_alerts_api_v1_alerts_get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"page","in":"query","required":false,"schema":{"type":"integer","minimum":1,"default":1,"title":"Page"}},{"name":"page_size","in":"query","required":false,"schema":{"type":"integer","maximum":100,"minimum":1,"default":20,"title":"Page Size"}},{"name":"severity","in":"query","required":false,"schema":{"anyOf":[{"type":"string","pattern":"^(warning|high|severe)$"},{"type":"null"}],"title":"Severity"}},{"name":"alert_type","in":"query","required":false,"schema":{"anyOf":[{"type":"string","maxLength":64},{"type":"null"}],"title":"Alert Type"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AlertHistoryResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/alert-settings/test":{"post":{"tags":["Alerts"],"summary":"Send Test Alert","description":"Send a test alert through the normal dispatch path.\n\nFounder decision #10: test alerts on paid channels are billed like real\nsends. Throttled (security review #3/#8) and the free-text message is\ndropped in favour of a fixed body (security review #12 — customer text\nmust not ride our WABA template / Slack markdown).\n\n**🔐 Required Scopes:** `alerts:update`\n","operationId":"send_test_alert_api_v1_alert_settings_test_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TestAlertRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TestAlertResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"HTTPBearer":[]}]}},"/api/v1/automation/templates":{"get":{"tags":["Automation"],"summary":"Get Bot Templates","description":"Get available bot templates.\n\nTwo response shapes:\n\n* **No ``language`` param (legacy)** — returns one template per bot_type\n  (Support, Sales, FAQ, …). Used by older UI flows.\n* **With ``language`` param (Phase 2)** — returns the unified assistant\n  preset gallery: 5 presets (support / sales / faq / email / custom),\n  each with a localised personality_prompt + greeting and\n  ``bot_type='assistant'``. Used by the new bot creator (Step 2).\n\n**Required Scopes:** `automation:read`","operationId":"get_bot_templates_api_v1_automation_templates_get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"language","in":"query","required":false,"schema":{"anyOf":[{"type":"string","maxLength":10},{"type":"null"}],"description":"Optional ISO-639-1 language code (e.g. 'en', 'de'). When provided, returns the Phase-2 assistant preset gallery filtered by language. When omitted, returns the legacy per-bot-type template list.","title":"Language"},"description":"Optional ISO-639-1 language code (e.g. 'en', 'de'). When provided, returns the Phase-2 assistant preset gallery filtered by language. When omitted, returns the legacy per-bot-type template list."}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"type":"object","additionalProperties":true},"title":"Response Get Bot Templates Api V1 Automation Templates Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/automation/templates/{bot_type}":{"get":{"tags":["Automation"],"summary":"Get Bot Template","description":"Get a specific bot template by type.\n\n**Required Scopes:** `automation:read`\n\nArgs:\n    bot_type: One of 'support', 'sales', 'custom'","operationId":"get_bot_template_api_v1_automation_templates__bot_type__get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"bot_type","in":"path","required":true,"schema":{"type":"string","title":"Bot Type"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Get Bot Template Api V1 Automation Templates  Bot Type  Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/automation/bots":{"post":{"tags":["Automation"],"summary":"Create Bot","description":"Create a new automation bot.\n\n**Required Scopes:** `automation:create`\n\nCreate an AI-powered bot that can handle customer conversations\nusing the knowledge base. Configure personality, escalation rules,\nand field collection requirements.\n\nKeyword bots are retired: ``bot_type='keyword'`` returns 422 with\n``detail.code = \"keyword_bots_retired\"``. Use a Flow with a keyword\ntrigger instead. Existing keyword bots keep working.","operationId":"create_bot_api_v1_automation_bots_post","security":[{"HTTPBearer":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AutomationBotCreate"}}}},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AutomationBotResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"get":{"tags":["Automation"],"summary":"List Bots","description":"List all automation bots.\n\n**Required Scopes:** `automation:read`\n\nReturns all bots for the tenant with basic information\nand activation rule counts.","operationId":"list_bots_api_v1_automation_bots_get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"active_only","in":"query","required":false,"schema":{"type":"boolean","description":"Only return active bots","default":false,"title":"Active Only"},"description":"Only return active bots"},{"name":"skip","in":"query","required":false,"schema":{"type":"integer","minimum":0,"description":"Pagination offset","default":0,"title":"Skip"},"description":"Pagination offset"},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":500,"minimum":1,"description":"Pagination limit","default":100,"title":"Limit"},"description":"Pagination limit"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/AutomationBotListResponse"},"title":"Response List Bots Api V1 Automation Bots Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/automation/bots/reorder":{"put":{"tags":["Automation"],"summary":"Reorder Bots","description":"Reorder automation bots by priority.\n\n**Required Scopes:** `automation:update`\n\nUpdates the priority of bots based on the order in the request.\nThe first bot in the list gets the highest priority, the last gets the lowest.\nHigher priority bots are evaluated first when matching context.\n\n**Request Body:**\n- `bot_ids`: List of bot IDs in desired priority order (first = highest priority)\n\n**Example:**\n```json\n{\"bot_ids\": [\"bot-uuid-1\", \"bot-uuid-2\", \"bot-uuid-3\"]}\n```\nAfter reordering:\n- bot-uuid-1: priority 2 (highest)\n- bot-uuid-2: priority 1\n- bot-uuid-3: priority 0 (lowest)","operationId":"reorder_bots_api_v1_automation_bots_reorder_put","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BotReorderRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BotReorderResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"HTTPBearer":[]}]}},"/api/v1/automation/bots/{bot_id}":{"get":{"tags":["Automation"],"summary":"Get Bot","description":"Get a specific automation bot.\n\n**Required Scopes:** `automation:read`\n\nReturns full bot configuration including personality settings,\nknowledge base configuration, and escalation rules.","operationId":"get_bot_api_v1_automation_bots__bot_id__get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"bot_id","in":"path","required":true,"schema":{"type":"string","title":"Bot Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AutomationBotResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"patch":{"tags":["Automation"],"summary":"Update Bot","description":"Update an automation bot.\n\n**Required Scopes:** `automation:update`\n\nUpdate bot configuration. Only provided fields will be updated.\nChanging a non-keyword bot's ``bot_type`` to ``keyword`` returns 422\n``keyword_bots_retired``; existing keyword bots stay fully editable.","operationId":"update_bot_api_v1_automation_bots__bot_id__patch","security":[{"HTTPBearer":[]}],"parameters":[{"name":"bot_id","in":"path","required":true,"schema":{"type":"string","title":"Bot Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AutomationBotUpdate"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AutomationBotResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"tags":["Automation"],"summary":"Delete Bot","description":"Delete an automation bot.\n\n**Required Scopes:** `automation:delete`\n\nPermanently deletes the bot and all associated activation rules,\nsessions, and interaction logs.","operationId":"delete_bot_api_v1_automation_bots__bot_id__delete","security":[{"HTTPBearer":[]}],"parameters":[{"name":"bot_id","in":"path","required":true,"schema":{"type":"string","title":"Bot Id"}}],"responses":{"204":{"description":"Successful Response"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/automation/bots/{bot_id}/convert-to-flow":{"post":{"tags":["Automation"],"summary":"Convert Bot To Flow Endpoint","description":"Convert a deprecated bot into an equivalent Flow.\n\n**Required Scopes:** ``automation:update``\n\nOnly ``router``, ``lead_qualification`` and ``appointment`` bots are\nconvertible — calling this on any other bot type returns 400.\n\nOn success (commit) the source bot is left in the DB but set to\n``is_active=false`` so historical conversations and interaction logs\nremain intact. The new flow is created in ``draft`` status — the\noperator should review + publish it manually.\n\nIdempotent: if a flow named ``\"<bot.name> (migrated)\"`` already\nexists for the tenant the endpoint returns 409 with the existing\nflow id so the caller can navigate to it.","operationId":"convert_bot_to_flow_endpoint_api_v1_automation_bots__bot_id__convert_to_flow_post","security":[{"HTTPBearer":[]}],"parameters":[{"name":"bot_id","in":"path","required":true,"schema":{"type":"string","title":"Bot Id"}},{"name":"dry_run","in":"query","required":false,"schema":{"type":"boolean","description":"When true (default false) returns the generated flow JSON without writing anything to the database. Use this from the UI to preview the conversion before committing.","default":false,"title":"Dry Run"},"description":"When true (default false) returns the generated flow JSON without writing anything to the database. Use this from the UI to preview the conversion before committing."}],"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Convert Bot To Flow Endpoint Api V1 Automation Bots  Bot Id  Convert To Flow Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/automation/bots/{bot_id}/avatar":{"post":{"tags":["Automation"],"summary":"Upload Bot Avatar","description":"Upload an avatar image for an automation bot.\n\nReplaces any existing bot avatar. The uploaded image is stored in GCP\nCloud Storage and a signed URL is saved to the bot's avatar_url field.\n\n**Required Scopes:** `automation:update`\n\n**Constraints:**\n- File type: PNG, JPG, JPEG\n- Max size: 2MB","operationId":"upload_bot_avatar_api_v1_automation_bots__bot_id__avatar_post","security":[{"HTTPBearer":[]}],"parameters":[{"name":"bot_id","in":"path","required":true,"schema":{"type":"string","title":"Bot Id"}}],"requestBody":{"required":true,"content":{"multipart/form-data":{"schema":{"$ref":"#/components/schemas/Body_upload_bot_avatar_api_v1_automation_bots__bot_id__avatar_post"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AutomationBotResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"tags":["Automation"],"summary":"Delete Bot Avatar","description":"Remove an automation bot's avatar.\n\nSets the bot's avatar_url to None.\n\n**Required Scopes:** `automation:update`","operationId":"delete_bot_avatar_api_v1_automation_bots__bot_id__avatar_delete","security":[{"HTTPBearer":[]}],"parameters":[{"name":"bot_id","in":"path","required":true,"schema":{"type":"string","title":"Bot Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/automation/bots/{bot_id}/rules":{"post":{"tags":["Automation"],"summary":"Create Activation Rule","description":"Create an activation rule for a bot.\n\n**Required Scopes:** `automation:update`\n\nActivation rules determine when and where a bot should be activated.\nRules are evaluated in priority order (highest first).\n\nRule types:\n- Channel filter: Only activate for specific channel types\n- Widget filter: Only activate for specific widgets\n- Schedule: Only activate during specific time ranges\n- Agent status: Only activate when no agents are online","operationId":"create_activation_rule_api_v1_automation_bots__bot_id__rules_post","security":[{"HTTPBearer":[]}],"parameters":[{"name":"bot_id","in":"path","required":true,"schema":{"type":"string","title":"Bot Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BotActivationRuleCreate"}}}},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BotActivationRuleResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"get":{"tags":["Automation"],"summary":"List Activation Rules","description":"List activation rules for a bot.\n\n**Required Scopes:** `automation:read`\n\nReturns all activation rules sorted by priority (highest first).","operationId":"list_activation_rules_api_v1_automation_bots__bot_id__rules_get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"bot_id","in":"path","required":true,"schema":{"type":"string","title":"Bot Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/BotActivationRuleResponse"},"title":"Response List Activation Rules Api V1 Automation Bots  Bot Id  Rules Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/automation/bots/{bot_id}/rules/{rule_id}":{"put":{"tags":["Automation"],"summary":"Update Activation Rule","description":"Update an activation rule.\n\n**Required Scopes:** `automation:update`\n\nUpdate rule configuration. Only provided fields will be updated.","operationId":"update_activation_rule_api_v1_automation_bots__bot_id__rules__rule_id__put","security":[{"HTTPBearer":[]}],"parameters":[{"name":"bot_id","in":"path","required":true,"schema":{"type":"string","title":"Bot Id"}},{"name":"rule_id","in":"path","required":true,"schema":{"type":"string","title":"Rule Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BotActivationRuleUpdate"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BotActivationRuleResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"tags":["Automation"],"summary":"Delete Activation Rule","description":"Delete an activation rule.\n\n**Required Scopes:** `automation:delete`\n\nPermanently deletes the activation rule.","operationId":"delete_activation_rule_api_v1_automation_bots__bot_id__rules__rule_id__delete","security":[{"HTTPBearer":[]}],"parameters":[{"name":"bot_id","in":"path","required":true,"schema":{"type":"string","title":"Bot Id"}},{"name":"rule_id","in":"path","required":true,"schema":{"type":"string","title":"Rule Id"}}],"responses":{"204":{"description":"Successful Response"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/automation/sessions":{"get":{"tags":["Automation"],"summary":"List Sessions","description":"List bot conversation sessions.\n\n**Required Scopes:** `automation:read`\n\nReturns all bot sessions with optional filtering by bot or status.","operationId":"list_sessions_api_v1_automation_sessions_get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"bot_id","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Filter by bot ID","title":"Bot Id"},"description":"Filter by bot ID"},{"name":"session_status","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Filter by status: active, escalated, completed, abandoned","title":"Session Status"},"description":"Filter by status: active, escalated, completed, abandoned"},{"name":"skip","in":"query","required":false,"schema":{"type":"integer","minimum":0,"description":"Pagination offset","default":0,"title":"Skip"},"description":"Pagination offset"},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":500,"minimum":1,"description":"Pagination limit","default":100,"title":"Limit"},"description":"Pagination limit"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/BotConversationSessionListResponse"},"title":"Response List Sessions Api V1 Automation Sessions Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/automation/sessions/{session_id}":{"get":{"tags":["Automation"],"summary":"Get Session","description":"Get a specific bot session.\n\n**Required Scopes:** `automation:read`\n\nReturns full session details including collected fields and escalation info.","operationId":"get_session_api_v1_automation_sessions__session_id__get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"session_id","in":"path","required":true,"schema":{"type":"string","title":"Session Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BotConversationSessionResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/automation/sessions/{session_id}/logs":{"get":{"tags":["Automation"],"summary":"List Session Logs","description":"List interaction logs for a session.\n\n**Required Scopes:** `automation:read`\n\nReturns all bot-user interactions for debugging and analysis.","operationId":"list_session_logs_api_v1_automation_sessions__session_id__logs_get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"session_id","in":"path","required":true,"schema":{"type":"string","title":"Session Id"}},{"name":"skip","in":"query","required":false,"schema":{"type":"integer","minimum":0,"description":"Pagination offset","default":0,"title":"Skip"},"description":"Pagination offset"},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":500,"minimum":1,"description":"Pagination limit","default":100,"title":"Limit"},"description":"Pagination limit"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/BotInteractionLogResponse"},"title":"Response List Session Logs Api V1 Automation Sessions  Session Id  Logs Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/automation/analytics":{"get":{"tags":["Automation"],"summary":"Get Analytics Summary","description":"Get analytics summary across all bots.\n\n**Required Scopes:** `automation:read` OR `analytics:read`\n\nReturns aggregate metrics and per-bot breakdown.","operationId":"get_analytics_summary_api_v1_automation_analytics_get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"period_days","in":"query","required":false,"schema":{"type":"integer","maximum":365,"minimum":1,"description":"Analysis period in days","default":30,"title":"Period Days"},"description":"Analysis period in days"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BotAnalyticsSummary"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/automation/bots/{bot_id}/analytics":{"get":{"tags":["Automation"],"summary":"Get Bot Analytics","description":"Get analytics for a specific bot.\n\n**Required Scopes:** `automation:read` OR `analytics:read`\n\nReturns detailed metrics including session stats, resolution rate,\nescalation reasons, and token usage.","operationId":"get_bot_analytics_api_v1_automation_bots__bot_id__analytics_get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"bot_id","in":"path","required":true,"schema":{"type":"string","title":"Bot Id"}},{"name":"period_days","in":"query","required":false,"schema":{"type":"integer","maximum":365,"minimum":1,"description":"Analysis period in days","default":30,"title":"Period Days"},"description":"Analysis period in days"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BotAnalyticsResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/automation/bots/{bot_id}/keyword-triggers":{"get":{"tags":["Automation"],"summary":"List Keyword Triggers","description":"List keyword triggers for a bot, sorted by priority DESC.\n\n**Required Scopes:** `automation:read`\n\nReturns all keyword triggers with their response sequences.\nOnly applicable to bots of type KEYWORD.","operationId":"list_keyword_triggers_api_v1_automation_bots__bot_id__keyword_triggers_get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"bot_id","in":"path","required":true,"schema":{"type":"string","title":"Bot Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/KeywordTriggerSchema"},"title":"Response List Keyword Triggers Api V1 Automation Bots  Bot Id  Keyword Triggers Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"post":{"tags":["Automation"],"summary":"Create Keyword Trigger","description":"Create a keyword trigger for a bot.\n\n**Required Scopes:** `automation:update`\n\nCreates a new keyword trigger with its response sequence.\nOnly applicable to bots of type KEYWORD.","operationId":"create_keyword_trigger_api_v1_automation_bots__bot_id__keyword_triggers_post","security":[{"HTTPBearer":[]}],"parameters":[{"name":"bot_id","in":"path","required":true,"schema":{"type":"string","title":"Bot Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/KeywordTriggerCreate"}}}},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/KeywordTriggerSchema"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/automation/bots/{bot_id}/keyword-triggers/reorder":{"put":{"tags":["Automation"],"summary":"Reorder Keyword Triggers","description":"Reorder keyword triggers by priority.\n\n**Required Scopes:** `automation:update`\n\nUpdates the priority of triggers based on the order in the request.\nThe first trigger in the list gets the highest priority.\nOnly applicable to bots of type KEYWORD.","operationId":"reorder_keyword_triggers_api_v1_automation_bots__bot_id__keyword_triggers_reorder_put","security":[{"HTTPBearer":[]}],"parameters":[{"name":"bot_id","in":"path","required":true,"schema":{"type":"string","title":"Bot Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/KeywordTriggersReorder"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/KeywordTriggerSchema"},"title":"Response Reorder Keyword Triggers Api V1 Automation Bots  Bot Id  Keyword Triggers Reorder Put"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/automation/bots/{bot_id}/keyword-triggers/{trigger_id}":{"get":{"tags":["Automation"],"summary":"Get Keyword Trigger","description":"Get a specific keyword trigger with its responses.\n\n**Required Scopes:** `automation:read`\n\nReturns the keyword trigger with its full response sequence.\nOnly applicable to bots of type KEYWORD.","operationId":"get_keyword_trigger_api_v1_automation_bots__bot_id__keyword_triggers__trigger_id__get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"bot_id","in":"path","required":true,"schema":{"type":"string","title":"Bot Id"}},{"name":"trigger_id","in":"path","required":true,"schema":{"type":"string","title":"Trigger Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/KeywordTriggerSchema"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"put":{"tags":["Automation"],"summary":"Update Keyword Trigger","description":"Update a keyword trigger.\n\n**Required Scopes:** `automation:update`\n\nUpdates trigger configuration. If responses are provided,\nexisting responses are replaced with the new set.\nOnly applicable to bots of type KEYWORD.","operationId":"update_keyword_trigger_api_v1_automation_bots__bot_id__keyword_triggers__trigger_id__put","security":[{"HTTPBearer":[]}],"parameters":[{"name":"bot_id","in":"path","required":true,"schema":{"type":"string","title":"Bot Id"}},{"name":"trigger_id","in":"path","required":true,"schema":{"type":"string","title":"Trigger Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/KeywordTriggerUpdate"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/KeywordTriggerSchema"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"tags":["Automation"],"summary":"Delete Keyword Trigger","description":"Delete a keyword trigger.\n\n**Required Scopes:** `automation:delete`\n\nPermanently deletes the trigger and all its responses.\nOnly applicable to bots of type KEYWORD.","operationId":"delete_keyword_trigger_api_v1_automation_bots__bot_id__keyword_triggers__trigger_id__delete","security":[{"HTTPBearer":[]}],"parameters":[{"name":"bot_id","in":"path","required":true,"schema":{"type":"string","title":"Bot Id"}},{"name":"trigger_id","in":"path","required":true,"schema":{"type":"string","title":"Trigger Id"}}],"responses":{"204":{"description":"Successful Response"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/automation/bots/{bot_id}/keyword-triggers/upload":{"post":{"tags":["Automation"],"summary":"Upload Keyword Trigger Media","description":"Upload a media file for use in keyword trigger responses.\n\n**Required Scopes:** `automation:update`\n\nUploads a file to cloud storage and returns an attachment record\nthat can be referenced by `attachment_id` in keyword trigger responses.\n\nAccepts images, videos, audio, and documents. Max file size: 50MB.\n\nReturns:\n    attachment_id, url, filename, and content_type for the uploaded file.","operationId":"upload_keyword_trigger_media_api_v1_automation_bots__bot_id__keyword_triggers_upload_post","security":[{"HTTPBearer":[]}],"parameters":[{"name":"bot_id","in":"path","required":true,"schema":{"type":"string","title":"Bot Id"}}],"requestBody":{"required":true,"content":{"multipart/form-data":{"schema":{"$ref":"#/components/schemas/Body_upload_keyword_trigger_media_api_v1_automation_bots__bot_id__keyword_triggers_upload_post"}}}},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/automation/bots/{bot_id}/translations/{lang_code}":{"delete":{"tags":["Automation"],"summary":"Delete Bot Translation","description":"Delete the cached translations for a single language from a bot.\n\nIdempotent: removing a language that is not present still returns 200 with the\n(unchanged) bot. Returns 404 only when the bot itself does not exist or is not\nowned by the tenant.\n\n**Required Scopes:** `automation:update`","operationId":"delete_bot_translation_api_v1_automation_bots__bot_id__translations__lang_code__delete","security":[{"HTTPBearer":[]}],"parameters":[{"name":"bot_id","in":"path","required":true,"schema":{"type":"string","title":"Bot Id"}},{"name":"lang_code","in":"path","required":true,"schema":{"type":"string","title":"Lang Code"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AutomationBotResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"put":{"tags":["Automation"],"summary":"Upsert Bot Translation","description":"Insert or replace the cached translations for a single language on a bot.\n\nThe request body is the per-language translations object (the value stored at\ntranslation_cache[lang_code]); it is free-form and may contain nested\nstructures such as routing_messages. Returns 404 only when the bot itself does\nnot exist or is not owned by the tenant.\n\n**Required Scopes:** `automation:update`","operationId":"upsert_bot_translation_api_v1_automation_bots__bot_id__translations__lang_code__put","security":[{"HTTPBearer":[]}],"parameters":[{"name":"bot_id","in":"path","required":true,"schema":{"type":"string","title":"Bot Id"}},{"name":"lang_code","in":"path","required":true,"schema":{"type":"string","title":"Lang Code"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Translations"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AutomationBotResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/automation/agents":{"get":{"tags":["AI Agents"],"summary":"List Agents","description":"List Conversational Agents.\n\n**Required Scopes:** `automation:read`","operationId":"list_agents_api_v1_automation_agents_get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"page","in":"query","required":false,"schema":{"type":"integer","minimum":1,"default":1,"title":"Page"}},{"name":"page_size","in":"query","required":false,"schema":{"type":"integer","maximum":100,"minimum":1,"default":50,"title":"Page Size"}},{"name":"search","in":"query","required":false,"schema":{"anyOf":[{"type":"string","maxLength":100},{"type":"null"}],"title":"Search"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PaginatedResponse_AgentResponse_"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"post":{"tags":["AI Agents"],"summary":"Create Agent","description":"Create a Conversational Agent (backing bot row + agent config, optional\ninline skills and knowledge sources) in one transaction.\n\n**Required Scopes:** `automation:create`","operationId":"create_agent_api_v1_automation_agents_post","security":[{"HTTPBearer":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AgentCreate"}}}},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AgentResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/automation/agents/{agent_id}":{"get":{"tags":["AI Agents"],"summary":"Get Agent","description":"Get one Conversational Agent.\n\n**Required Scopes:** `automation:read`","operationId":"get_agent_api_v1_automation_agents__agent_id__get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"agent_id","in":"path","required":true,"schema":{"type":"string","title":"Agent Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AgentResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"patch":{"tags":["AI Agents"],"summary":"Update Agent","description":"Update a Conversational Agent (persona, instructions, tools, handoff,\nlanguage and escalation settings).\n\n``tools_config`` is merged: keys omitted in the request keep their stored\nvalue. The response always carries concrete values for ``read_images``,\n``read_documents``, ``max_images_per_message`` (1-3) and\n``max_attachments_per_conversation`` (1-50). Writing ``read_images`` also\nsets the backing bot's ``vision_enabled``.\n\n**Required Scopes:** `automation:update`","operationId":"update_agent_api_v1_automation_agents__agent_id__patch","security":[{"HTTPBearer":[]}],"parameters":[{"name":"agent_id","in":"path","required":true,"schema":{"type":"string","title":"Agent Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AgentUpdate"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AgentResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"tags":["AI Agents"],"summary":"Delete Agent","description":"Delete a Conversational Agent and its backing bot (rules, sessions, skill\nlinks and knowledge sources go with it). Returns 409 ``agent_in_use`` with\nthe flows while a published or paused flow's run_assistant node targets it.\n\n**Required Scopes:** `automation:delete`","operationId":"delete_agent_api_v1_automation_agents__agent_id__delete","security":[{"HTTPBearer":[]}],"parameters":[{"name":"agent_id","in":"path","required":true,"schema":{"type":"string","title":"Agent Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeleteResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/automation/agents/{agent_id}/draft":{"get":{"tags":["AI Agents"],"summary":"Get Agent Draft","description":"The editor document: the saved draft (rebased onto the current live\nconfig) or, without a draft, the live config. ``has_unpublished_changes``,\na per-section ``changes`` summary and the publish ``validation`` are\nincluded. Reading never modifies anything.\n\n**Required Scopes:** `automation:read`","operationId":"get_agent_draft_api_v1_automation_agents__agent_id__draft_get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"agent_id","in":"path","required":true,"schema":{"type":"string","title":"Agent Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AgentDraftResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"put":{"tags":["AI Agents"],"summary":"Save Agent Draft","description":"Save (autosave) the agent draft. Fields left out keep their previous\ndraft value. Validated like the live endpoints (same 422 codes); new\nknowledge sources / channel rules get server ids. The live agent is not\nchanged until ``POST /agents/{id}/publish``. A draft identical to the\nlive config is cleared.\n\n**Required Scopes:** `automation:update`","operationId":"save_agent_draft_api_v1_automation_agents__agent_id__draft_put","security":[{"HTTPBearer":[]}],"parameters":[{"name":"agent_id","in":"path","required":true,"schema":{"type":"string","title":"Agent Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AgentDraftPut"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AgentDraftResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"tags":["AI Agents"],"summary":"Discard Agent Draft","description":"Discard the draft; returns the live config as the editor document.\n\n**Required Scopes:** `automation:update`","operationId":"discard_agent_draft_api_v1_automation_agents__agent_id__draft_delete","security":[{"HTTPBearer":[]}],"parameters":[{"name":"agent_id","in":"path","required":true,"schema":{"type":"string","title":"Agent Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AgentDraftResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/automation/agents/{agent_id}/publish":{"post":{"tags":["AI Agents"],"summary":"Publish Agent","description":"Publish the draft in one transaction. Changes made to the live agent since\nthe draft was started are kept (three-way merge). Without a draft, a\nnever-published agent is simply made live. 422 ``publish_blocked`` lists\nthe blocking issues (``name_missing``, ``ai_disclosure_missing``,\n``goal_invalid``, ``live_source_invalid``, ``skill_missing``).\n\n**Required Scopes:** `automation:update`","operationId":"publish_agent_api_v1_automation_agents__agent_id__publish_post","security":[{"HTTPBearer":[]}],"parameters":[{"name":"agent_id","in":"path","required":true,"schema":{"type":"string","title":"Agent Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AgentDraftResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/automation/agents/{agent_id}/avatar":{"post":{"tags":["AI Agents"],"summary":"Upload Agent Avatar","description":"Upload an avatar into the agent DRAFT (live after publish). PNG, JPEG or\nWebP, max 2 MB; the content is checked (SVG is rejected). 422\n``invalid_avatar`` otherwise.\n\n**Required Scopes:** `automation:update`","operationId":"upload_agent_avatar_api_v1_automation_agents__agent_id__avatar_post","security":[{"HTTPBearer":[]}],"parameters":[{"name":"agent_id","in":"path","required":true,"schema":{"type":"string","title":"Agent Id"}}],"requestBody":{"required":true,"content":{"multipart/form-data":{"schema":{"$ref":"#/components/schemas/Body_upload_agent_avatar_api_v1_automation_agents__agent_id__avatar_post"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AgentDraftResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/automation/agents/{agent_id}/skills":{"put":{"tags":["AI Agents"],"summary":"Replace Agent Skills","description":"Replace the agent's skill links (mode / pinned version / order).\n\nEach link may carry ``sources`` = ``{web_search_mode, live_source_ids,\nlive_mode}`` (per-skill source override, stored in\n``settings.skill_sources``): omitted => keep the stored override, ``null``\n=> remove it. ``live_source_ids`` must be ``live_source`` sources of this\nagent (max 5). Overrides of skills no longer attached are dropped.\n422 ``invalid_skill_sources`` on a bad override.\n\n**Required Scopes:** `automation:update`","operationId":"replace_agent_skills_api_v1_automation_agents__agent_id__skills_put","security":[{"HTTPBearer":[]}],"parameters":[{"name":"agent_id","in":"path","required":true,"schema":{"type":"string","title":"Agent Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AgentSkillsReplace"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AgentResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/automation/agents/{agent_id}/knowledge-sources":{"get":{"tags":["AI Agents"],"summary":"List Knowledge Sources","description":"List the agent's knowledge sources (bounded, single page).\n\n**Required Scopes:** `automation:read`","operationId":"list_knowledge_sources_api_v1_automation_agents__agent_id__knowledge_sources_get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"agent_id","in":"path","required":true,"schema":{"type":"string","title":"Agent Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PaginatedResponse_KnowledgeSourceResponse_"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"post":{"tags":["AI Agents"],"summary":"Create Knowledge Source","description":"Add a knowledge source (KB folder, whole KB, FAQ, web search with a\ndomain allowlist, or ``live_source``: one URL fetched live at answer time,\n``config = {url, match, label, description, ttl_minutes, fetcher, scope}``).\n\nLive sources: at most ``AGENT_LIVE_SOURCES_MAX_PER_AGENT`` (default 10) per\nagent (422 ``live_source_limit``), label unique per agent case-insensitively\n(422 ``live_source_label_taken``), URL must be public http(s) (422\n``invalid_live_source_url`` with ``error_code``).\n\n**Required Scopes:** `automation:update`","operationId":"create_knowledge_source_api_v1_automation_agents__agent_id__knowledge_sources_post","security":[{"HTTPBearer":[]}],"parameters":[{"name":"agent_id","in":"path","required":true,"schema":{"type":"string","title":"Agent Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/KnowledgeSourceCreate"}}}},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/KnowledgeSourceResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/automation/agents/{agent_id}/knowledge-sources/{source_id}":{"patch":{"tags":["AI Agents"],"summary":"Update Knowledge Source","description":"Update a knowledge source. For a ``live_source`` the ``config`` patch is\nmerged over the stored config and re-validated (422 ``invalid_live_source``).\n\n**Required Scopes:** `automation:update`","operationId":"update_knowledge_source_api_v1_automation_agents__agent_id__knowledge_sources__source_id__patch","security":[{"HTTPBearer":[]}],"parameters":[{"name":"agent_id","in":"path","required":true,"schema":{"type":"string","title":"Agent Id"}},{"name":"source_id","in":"path","required":true,"schema":{"type":"string","title":"Source Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/KnowledgeSourceUpdate"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/KnowledgeSourceResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"tags":["AI Agents"],"summary":"Delete Knowledge Source","description":"Remove a knowledge source from the agent. Removing a ``live_source``\nalso removes its id from every per-skill override.\n\n**Required Scopes:** `automation:update`","operationId":"delete_knowledge_source_api_v1_automation_agents__agent_id__knowledge_sources__source_id__delete","security":[{"HTTPBearer":[]}],"parameters":[{"name":"agent_id","in":"path","required":true,"schema":{"type":"string","title":"Agent Id"}},{"name":"source_id","in":"path","required":true,"schema":{"type":"string","title":"Source Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeleteResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/automation/agents/{agent_id}/test":{"post":{"tags":["AI Agents"],"summary":"Test Agent","description":"Run one test-console turn. Nothing is sent to a customer and nothing is\nbilled (``credits`` reports what a live turn would bill). The turn is\nrecorded in the audit log with surface ``test``. Rate limited to 20\nturns per minute and 200 per hour per workspace (429 + Retry-After).\n\n``attachment_ids`` (max 5): files uploaded with ``POST /attachments/upload``\nthat the test customer \"sends\" with the message (``message`` may then be\nempty). Unknown or foreign ids return 422 ``attachment_not_found``. The\n``attachments`` trace lists each file as read or skipped (with the reason)\nand the AI credits it would cost in a live conversation.\n\n**Required Scopes:** `automation:update`","operationId":"test_agent_api_v1_automation_agents__agent_id__test_post","security":[{"HTTPBearer":[]}],"parameters":[{"name":"agent_id","in":"path","required":true,"schema":{"type":"string","title":"Agent Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AgentTestRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AgentTestResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/automation/agents/{agent_id}/turns":{"get":{"tags":["AI Agents"],"summary":"List Agent Turns","description":"Audit log of the agent's turns (newest first). Raw model contents are not\nreturned. ``contact_name`` and the conversation / message ids are only\nreturned for conversations the caller can open in the inbox; otherwise\nthey are null and ``contact_hidden`` is true (``conversation_key`` still\ngroups the turns of one conversation).\n\n**Required Scopes:** `automation:read`","operationId":"list_agent_turns_api_v1_automation_agents__agent_id__turns_get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"agent_id","in":"path","required":true,"schema":{"type":"string","title":"Agent Id"}},{"name":"page","in":"query","required":false,"schema":{"type":"integer","minimum":1,"default":1,"title":"Page"}},{"name":"page_size","in":"query","required":false,"schema":{"type":"integer","maximum":100,"minimum":1,"default":50,"title":"Page Size"}},{"name":"conversation_id","in":"query","required":false,"schema":{"anyOf":[{"type":"string","maxLength":36},{"type":"null"}],"title":"Conversation Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PaginatedResponse_AgentTurnResponse_"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/automation/agents/{agent_id}/feedback":{"get":{"tags":["AI Agents"],"summary":"Get Agent Feedback","description":"Answer feedback for an agent: 👍 / 👎 / wrong counts (total, per skill and\nper day) and the newest 👎 / wrong ratings with the customer question, the\nagent's answer, the correction, the skill(s) used and the KB correction it\nopened. Without inbox access to a rating's conversation, its\n``conversation_id``, ``message_id``, ``question`` and ``answer`` are null\nand ``contact_hidden`` is true. Ratings themselves are submitted with\n``POST /ai-answers/{event_id}/rate``, the same endpoint as FAQ bot replies.\n\n**Required Scopes:** `automation:read`","operationId":"get_agent_feedback_api_v1_automation_agents__agent_id__feedback_get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"agent_id","in":"path","required":true,"schema":{"type":"string","title":"Agent Id"}},{"name":"days","in":"query","required":false,"schema":{"type":"integer","maximum":90,"minimum":1,"description":"Window in days (answer events are kept 90 days)","default":30,"title":"Days"},"description":"Window in days (answer events are kept 90 days)"},{"name":"skill","in":"query","required":false,"schema":{"anyOf":[{"type":"string","maxLength":64},{"type":"null"}],"description":"Limit series + recent to one skill slug (\"__none__\" = no skill)","title":"Skill"},"description":"Limit series + recent to one skill slug (\"__none__\" = no skill)"},{"name":"recent_limit","in":"query","required":false,"schema":{"type":"integer","maximum":100,"minimum":1,"default":20,"title":"Recent Limit"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AgentFeedbackResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/automation/skills":{"get":{"tags":["AI Skills"],"summary":"List Skills","description":"List the tenant's skills library.\n\n**Required Scopes:** `automation:read`","operationId":"list_skills_api_v1_automation_skills_get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"search","in":"query","required":false,"schema":{"anyOf":[{"type":"string","maxLength":100},{"type":"null"}],"title":"Search"}},{"name":"include_archived","in":"query","required":false,"schema":{"type":"boolean","default":false,"title":"Include Archived"}},{"name":"page","in":"query","required":false,"schema":{"type":"integer","minimum":1,"default":1,"title":"Page"}},{"name":"page_size","in":"query","required":false,"schema":{"type":"integer","maximum":100,"minimum":1,"default":50,"title":"Page Size"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PaginatedResponse_SkillListItem_"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"post":{"tags":["AI Skills"],"summary":"Create Skill","description":"Create a skill (and its version 1). ``slug`` is derived from the title\nwhen omitted; an explicit slug that already exists returns 409.\n\n``goal`` (Beta, optional): structured goal stored on the version. An invalid\ngoal returns 422 ``{\"detail\": {\"code\": \"invalid_goal\", \"errors\": [{path, code,\nmessage}]}}``; 409 ``goal_unavailable`` while goals are not yet available.\n\n``attachment_instruction`` (Beta, optional): what the agent does with a\ncustomer's image or PDF when this skill is loaded --\n``{\"preset\": \"read_text\" | \"describe\" | \"context\" | \"custom\", \"custom_text\"}``\n(``custom_text`` 1-1000 chars, only for ``custom``). Omitted / null = ``context``.\n\n**Required Scopes:** `automation:create`","operationId":"create_skill_api_v1_automation_skills_post","security":[{"HTTPBearer":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SkillCreate"}}}},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SkillResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/automation/skills/import":{"post":{"tags":["AI Skills"],"summary":"Import Skills","description":"Import skills in the seed format ``{slug?, title, description, mode?, skill,\ntool_allowlist?, goal?, attachment_instruction?}`` (an omitted ``goal`` /\n``attachment_instruction`` keeps the existing value on update).\nAn existing slug gets a new version when its content changed (``updated``),\notherwise it is reported as ``unchanged``. Invalid items are reported in\n``errors`` without stopping the others.\n\n**Required Scopes:** `automation:create`, `automation:update`","operationId":"import_skills_api_v1_automation_skills_import_post","requestBody":{"content":{"application/json":{"schema":{"title":"Payload","description":"{\"skills\":[{slug?, title, description, mode?, skill}]}, a single skill object, or a list"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SkillImportResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"HTTPBearer":[]}]}},"/api/v1/automation/skills/{skill_id}":{"get":{"tags":["AI Skills"],"summary":"Get Skill","description":"Get a skill including the body of its current version.\n\n**Required Scopes:** `automation:read`","operationId":"get_skill_api_v1_automation_skills__skill_id__get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"skill_id","in":"path","required":true,"schema":{"type":"string","title":"Skill Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SkillResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"patch":{"tags":["AI Skills"],"summary":"Update Skill","description":"Update a skill. A title, description, body, tool_allowlist, ``goal`` or\n``attachment_instruction`` change (``null`` clears either) creates a new\nimmutable version; an omitted ``goal`` / ``attachment_instruction`` is\ncarried forward. default_mode / is_archived only change the skill.\nInvalid goal → 422 ``invalid_goal`` (see create).\n\n**Required Scopes:** `automation:update`","operationId":"update_skill_api_v1_automation_skills__skill_id__patch","security":[{"HTTPBearer":[]}],"parameters":[{"name":"skill_id","in":"path","required":true,"schema":{"type":"string","title":"Skill Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SkillUpdate"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SkillResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"tags":["AI Skills"],"summary":"Delete Skill","description":"Delete a skill. A skill still linked to an agent or referenced by a flow\nis archived instead (``action: \"archived\"``); otherwise it is deleted with\nall its versions (``action: \"deleted\"``).\n\n**Required Scopes:** `automation:delete`","operationId":"delete_skill_api_v1_automation_skills__skill_id__delete","security":[{"HTTPBearer":[]}],"parameters":[{"name":"skill_id","in":"path","required":true,"schema":{"type":"string","title":"Skill Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SkillDeleteResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/automation/skills/{skill_id}/versions":{"get":{"tags":["AI Skills"],"summary":"List Skill Versions","description":"List a skill's versions (newest first; bodies omitted, see ``body_chars``).\n\n**Required Scopes:** `automation:read`","operationId":"list_skill_versions_api_v1_automation_skills__skill_id__versions_get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"skill_id","in":"path","required":true,"schema":{"type":"string","title":"Skill Id"}},{"name":"page","in":"query","required":false,"schema":{"type":"integer","minimum":1,"default":1,"title":"Page"}},{"name":"page_size","in":"query","required":false,"schema":{"type":"integer","maximum":100,"minimum":1,"default":50,"title":"Page Size"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PaginatedResponse_SkillVersionListItem_"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/automation/skills/{skill_id}/versions/{version_id}":{"get":{"tags":["AI Skills"],"summary":"Get Skill Version","description":"Get one skill version including its body.\n\n**Required Scopes:** `automation:read`","operationId":"get_skill_version_api_v1_automation_skills__skill_id__versions__version_id__get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"skill_id","in":"path","required":true,"schema":{"type":"string","title":"Skill Id"}},{"name":"version_id","in":"path","required":true,"schema":{"type":"string","title":"Version Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SkillVersionResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/automation/skills/{skill_id}/versions/{version_id}/restore":{"post":{"tags":["AI Skills"],"summary":"Restore Skill Version","description":"Restore an old version by copying it into a NEW current version.\n\n**Required Scopes:** `automation:update`","operationId":"restore_skill_version_api_v1_automation_skills__skill_id__versions__version_id__restore_post","security":[{"HTTPBearer":[]}],"parameters":[{"name":"skill_id","in":"path","required":true,"schema":{"type":"string","title":"Skill Id"}},{"name":"version_id","in":"path","required":true,"schema":{"type":"string","title":"Version Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SkillResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/automation/skills/{skill_id}/usage":{"get":{"tags":["AI Skills"],"summary":"Get Skill Usage","description":"Where a skill is used: linked agents and non-archived flows whose\nrun_assistant node sets this ``skill_slug``.\n\n**Required Scopes:** `automation:read`","operationId":"get_skill_usage_api_v1_automation_skills__skill_id__usage_get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"skill_id","in":"path","required":true,"schema":{"type":"string","title":"Skill Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SkillUsageResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/automation/skills/{skill_id}/goal-stats":{"get":{"tags":["AI Skills"],"summary":"Get Skill Goal Stats","description":"Goal funnel (Beta) for a skill over the last ``days`` (max 90): distinct goal\nactivations started / completed / handed off, overall and per skill version.\nTest-console turns are excluded.\n\n**Required Scopes:** `automation:read`","operationId":"get_skill_goal_stats_api_v1_automation_skills__skill_id__goal_stats_get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"skill_id","in":"path","required":true,"schema":{"type":"string","title":"Skill Id"}},{"name":"days","in":"query","required":false,"schema":{"type":"integer","maximum":90,"minimum":1,"default":30,"title":"Days"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SkillGoalStatsResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/automation/agents/{agent_id}/skill-suggestions":{"get":{"tags":["AI Agents"],"summary":"List Skill Suggestions","operationId":"list_skill_suggestions_api_v1_automation_agents__agent_id__skill_suggestions_get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"agent_id","in":"path","required":true,"schema":{"type":"string","title":"Agent Id"}},{"name":"status","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Status"}},{"name":"skill_id","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Skill Id"}},{"name":"page","in":"query","required":false,"schema":{"type":"integer","minimum":1,"default":1,"title":"Page"}},{"name":"page_size","in":"query","required":false,"schema":{"type":"integer","maximum":100,"minimum":1,"default":25,"title":"Page Size"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response List Skill Suggestions Api V1 Automation Agents  Agent Id  Skill Suggestions Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"description":"\n\n**🔐 Required Scopes:** `automation:read`\n"}},"/api/v1/automation/agents/{agent_id}/skill-suggestions/generate":{"post":{"tags":["AI Agents"],"summary":"Generate Skill Suggestion","operationId":"generate_skill_suggestion_api_v1_automation_agents__agent_id__skill_suggestions_generate_post","security":[{"HTTPBearer":[]}],"parameters":[{"name":"agent_id","in":"path","required":true,"schema":{"type":"string","title":"Agent Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SkillSuggestionGenerateRequest"}}}},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Generate Skill Suggestion Api V1 Automation Agents  Agent Id  Skill Suggestions Generate Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"description":"\n\n**🔐 Required Scopes:** `automation:update`\n"}},"/api/v1/automation/agents/{agent_id}/skill-suggestions/{suggestion_id}/approve":{"post":{"tags":["AI Agents"],"summary":"Approve Skill Suggestion","operationId":"approve_skill_suggestion_api_v1_automation_agents__agent_id__skill_suggestions__suggestion_id__approve_post","security":[{"HTTPBearer":[]}],"parameters":[{"name":"agent_id","in":"path","required":true,"schema":{"type":"string","title":"Agent Id"}},{"name":"suggestion_id","in":"path","required":true,"schema":{"type":"string","title":"Suggestion Id"}}],"requestBody":{"content":{"application/json":{"schema":{"anyOf":[{"$ref":"#/components/schemas/SkillSuggestionApproveRequest"},{"type":"null"}],"title":"Body"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Approve Skill Suggestion Api V1 Automation Agents  Agent Id  Skill Suggestions  Suggestion Id  Approve Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"description":"\n\n**🔐 Required Scopes:** `automation:update`\n"}},"/api/v1/automation/agents/{agent_id}/skill-suggestions/{suggestion_id}/dismiss":{"post":{"tags":["AI Agents"],"summary":"Dismiss Skill Suggestion","operationId":"dismiss_skill_suggestion_api_v1_automation_agents__agent_id__skill_suggestions__suggestion_id__dismiss_post","security":[{"HTTPBearer":[]}],"parameters":[{"name":"agent_id","in":"path","required":true,"schema":{"type":"string","title":"Agent Id"}},{"name":"suggestion_id","in":"path","required":true,"schema":{"type":"string","title":"Suggestion Id"}}],"requestBody":{"content":{"application/json":{"schema":{"anyOf":[{"$ref":"#/components/schemas/SkillSuggestionDismissRequest"},{"type":"null"}],"title":"Body"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Dismiss Skill Suggestion Api V1 Automation Agents  Agent Id  Skill Suggestions  Suggestion Id  Dismiss Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"description":"\n\n**🔐 Required Scopes:** `automation:update`\n"}},"/api/v1/automation/agents/insights/overview":{"get":{"tags":["AI Agents"],"summary":"Get Agents Insights Overview","description":"Workspace-wide agent insights for the last ``days`` days: goal funnel per\nskill (activations started / completed), skills by usage (turns that\nloaded the skill, number of agents) and the number of knowledge-gap FAQ\ndrafts waiting for review. Test-console turns are excluded.\n\n**Required Scopes:** `automation:read`","operationId":"get_agents_insights_overview_api_v1_automation_agents_insights_overview_get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"days","in":"query","required":false,"schema":{"type":"integer","description":"Window in days: 7, 30 or 90","default":30,"title":"Days"},"description":"Window in days: 7, 30 or 90"},{"name":"agent_id","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Only this agent's turns and knowledge-gap drafts","title":"Agent Id"},"description":"Only this agent's turns and knowledge-gap drafts"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AgentInsightsOverviewResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/automation/agents/{agent_id}/analytics":{"get":{"tags":["AI Agents"],"summary":"Get Agent Analytics","description":"Agent analytics over the last ``days`` calendar days (workspace timezone,\ntoday included): turn outcomes, KB / web / live-source usage, enforced\nlookups, answer-check (grounding) results, credits, and a zero-filled\ndaily series. Test-console turns and plan-gated turns are excluded. At\nmost 20,000 turns are aggregated (``truncated: true`` beyond).\n\n**Required Scopes:** `automation:read`","operationId":"get_agent_analytics_api_v1_automation_agents__agent_id__analytics_get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"agent_id","in":"path","required":true,"schema":{"type":"string","title":"Agent Id"}},{"name":"days","in":"query","required":false,"schema":{"type":"integer","description":"Window in days: 7, 30 or 90","default":30,"title":"Days"},"description":"Window in days: 7, 30 or 90"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AgentAnalyticsResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/automation/agents/{agent_id}/kb-gap-suggestions":{"post":{"tags":["AI Agents"],"summary":"Run Kb Gap Suggestions","description":"Draft FAQ entries for questions this agent recently answered from web\nsearch while the knowledge base had nothing. Drafts land in the FAQ review\nlist (status ``draft``) and are never published automatically. One run per\nagent at a time (409 ``gap_run_in_progress``).\n\n**Required Scopes:** `automation:update` AND `knowledge_base:create` (it writes FAQ drafts)","operationId":"run_kb_gap_suggestions_api_v1_automation_agents__agent_id__kb_gap_suggestions_post","security":[{"HTTPBearer":[]}],"parameters":[{"name":"agent_id","in":"path","required":true,"schema":{"type":"string","title":"Agent Id"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/KbGapRunRequest","default":{"days":30}}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/KbGapRunResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"get":{"tags":["AI Agents"],"summary":"List Kb Gap Suggestions","description":"FAQ drafts created from this agent's knowledge gaps (not archived), newest\nfirst.\n\n**Required Scopes:** `automation:read`","operationId":"list_kb_gap_suggestions_api_v1_automation_agents__agent_id__kb_gap_suggestions_get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"agent_id","in":"path","required":true,"schema":{"type":"string","title":"Agent Id"}},{"name":"page","in":"query","required":false,"schema":{"type":"integer","minimum":1,"default":1,"title":"Page"}},{"name":"page_size","in":"query","required":false,"schema":{"type":"integer","maximum":100,"minimum":1,"default":20,"title":"Page Size"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PaginatedResponse_KbGapDraftItem_"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/automation/agent-builder/estimate":{"get":{"tags":["AI Agents"],"summary":"Estimate Agent Build","description":"AI credits one successful AI agent build costs — shown before the user starts.\n\nPrice = credits x the plan's AI-credit rate. Failed builds and creating the\nagent are never charged.\n\n**Required Scopes:** `automation:create`","operationId":"estimate_agent_build_api_v1_automation_agent_builder_estimate_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AgentBuilderEstimateResponse"}}}}},"security":[{"HTTPBearer":[]}]}},"/api/v1/automation/agent-builder/generate":{"post":{"tags":["AI Agents"],"summary":"Generate Agent Draft","description":"Queue an AI agent build: research the company website (domain-restricted web\nsearch; up to 3 of its pages are read to propose live sources) and draft a\nfoundation skill plus 3-6 tailored skills. Poll ``/jobs/{job_id}``. Nothing is saved.\n\nLimits per workspace: 5 builds/hour, 20/day, one active build at a time.\nRepeating a request with the same ``request_id`` returns the same job and is\nnever charged twice.\n\n**Required Scopes:** `automation:create`","operationId":"generate_agent_draft_api_v1_automation_agent_builder_generate_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AgentBuilderGenerateRequest"}}},"required":true},"responses":{"202":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AgentBuilderJobResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"HTTPBearer":[]}]}},"/api/v1/automation/agent-builder/jobs/{job_id}":{"get":{"tags":["AI Agents"],"summary":"Get Agent Build Job","description":"Poll an AI agent build (kept 24h). Another workspace's job is a 404.\n\n**Required Scopes:** `automation:create`","operationId":"get_agent_build_job_api_v1_automation_agent_builder_jobs__job_id__get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"job_id","in":"path","required":true,"schema":{"type":"string","maxLength":36,"title":"Job Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AgentBuilderJobResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/automation/agent-builder/create":{"post":{"tags":["AI Agents"],"summary":"Create Agent From Build","description":"Create the reviewed agent from a successful build: its skills (version 1),\nthe agent (INACTIVE — test it, then activate), knowledge sources and the\nchannel activation rule, in one transaction. Idempotent per ``build_id``:\ncalling it again returns the same agent. Not charged.\n\n**Required Scopes:** `automation:create`","operationId":"create_agent_from_build_api_v1_automation_agent_builder_create_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AgentBuilderCreateRequest"}}},"required":true},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AgentResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"HTTPBearer":[]}]}},"/api/v1/automation/hub/stats":{"get":{"tags":["Automation"],"summary":"Get Hub Stats","description":"Agents overview: KPIs + one row per bot (agents and classic bots, incl.\npaused) in priority order.\n\n**Required Scopes:** `automation:read`\n\nA conversation counts once per bot; it is *handed off* when a human was\nbrought in, otherwise *resolved*. Percentages are null below\n``min_sample`` (20) conversations per bot.","operationId":"get_hub_stats_api_v1_automation_hub_stats_get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"days","in":"query","required":false,"schema":{"type":"integer","description":"Range in days: 7, 30 or 90","default":30,"title":"Days"},"description":"Range in days: 7, 30 or 90"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HubStatsResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/automation/bots/{bot_id}/duplicate":{"post":{"tags":["Automation"],"summary":"Duplicate Bot","description":"Duplicate a Conversational Agent inside the same workspace.\n\n**Required Scopes:** `automation:create`\n\nThe copy is an unpublished draft, paused, and placed last. It keeps settings, skills (incl.\npinned versions) and knowledge sources, but no activation rules,\nconversations, ratings or history. Classic bots return 422\n``duplicate_classic_not_supported``.","operationId":"duplicate_bot_api_v1_automation_bots__bot_id__duplicate_post","security":[{"HTTPBearer":[]}],"parameters":[{"name":"bot_id","in":"path","required":true,"schema":{"type":"string","title":"Bot Id"}}],"requestBody":{"content":{"application/json":{"schema":{"anyOf":[{"$ref":"#/components/schemas/DuplicateBotRequest"},{"type":"null"}],"title":"Payload"}}}},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AutomationBotResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/automation/hub/templates":{"get":{"tags":["Automation"],"summary":"List Hub Templates","description":"Agent templates for \"Aus Vorlage\" (keys only; display copy is client-side).\n\n**Required Scopes:** `automation:read`","operationId":"list_hub_templates_api_v1_automation_hub_templates_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HubTemplateList"}}}}},"security":[{"HTTPBearer":[]}]}},"/api/v1/automation/hub/templates/{key}/create":{"post":{"tags":["Automation"],"summary":"Create From Template","description":"Create a paused, unpublished (draft) Conversational Agent from a template, in the workspace\nlanguage (German or English). No AI credits are used.\n\n**Required Scopes:** `automation:create`","operationId":"create_from_template_api_v1_automation_hub_templates__key__create_post","security":[{"HTTPBearer":[]}],"parameters":[{"name":"key","in":"path","required":true,"schema":{"type":"string","title":"Key"}}],"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TemplateCreateResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/settings/business-hours":{"get":{"tags":["Tenants"],"summary":"Get Business Hours","description":"Get the workspace business hours (weekly schedule + timezone).\n\n`configured: false` means none are set; agents then keep each rule's own times.\n\n**Required Scopes:** `settings:read` or `automation:read`","operationId":"get_business_hours_api_v1_settings_business_hours_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BusinessHoursResponse"}}}}},"security":[{"HTTPBearer":[]}]},"put":{"tags":["Tenants"],"summary":"Put Business Hours","description":"Set the workspace business hours. Agents whose activation rules use the\n\"business hours\" / \"outside business hours\" preset follow them (unless the\nagent has its own schedule).\n\n**Required Scopes:** `settings:update`","operationId":"put_business_hours_api_v1_settings_business_hours_put","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BusinessHoursUpdate"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BusinessHoursResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"HTTPBearer":[]}]},"delete":{"tags":["Tenants"],"summary":"Delete Business Hours","description":"Remove the workspace business hours (back to \"not configured\").\n\n**Required Scopes:** `settings:update`","operationId":"delete_business_hours_api_v1_settings_business_hours_delete","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}},"security":[{"HTTPBearer":[]}]}},"/api/v1/settings/business-hours/usage":{"get":{"tags":["Tenants"],"summary":"Get Business Hours Usage","description":"List the agents that follow the workspace business hours, so a change can be\nannounced before saving: `routing` = an activation rule uses the \"business\nhours\" / \"outside business hours\" preset without the agent's own schedule;\n`handoff` = the team-availability (handoff) hours follow the workspace\nhours. Covers the live config and any unpublished draft.\n\n**Required Scopes:** `settings:read` or `automation:read`","operationId":"get_business_hours_usage_api_v1_settings_business_hours_usage_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BusinessHoursUsageResponse"}}}}},"security":[{"HTTPBearer":[]}]}},"/api/v1/whatsapp-templates":{"get":{"tags":["WhatsApp Templates"],"summary":"List Templates","description":"List WhatsApp message templates for the tenant\n\n**Required Scopes:** `channels:read`\n\nReturns all synced WhatsApp templates, optionally filtered by channel or status.\nTemplates must be synced first using POST /sync endpoint.","operationId":"list_templates_api_v1_whatsapp_templates_get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"channel_id","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Filter by channel ID","title":"Channel Id"},"description":"Filter by channel ID"},{"name":"status","in":"query","required":false,"schema":{"anyOf":[{"$ref":"#/components/schemas/TemplateStatus"},{"type":"null"}],"description":"Filter by template status","title":"Status"},"description":"Filter by template status"},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":500,"minimum":1,"description":"Max results to return","default":100,"title":"Limit"},"description":"Max results to return"},{"name":"offset","in":"query","required":false,"schema":{"type":"integer","minimum":0,"description":"Pagination offset","default":0,"title":"Offset"},"description":"Pagination offset"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WhatsAppTemplateListResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"post":{"tags":["WhatsApp Templates"],"summary":"Create Template","description":"Create a new WhatsApp message template\n\n**Required Scopes:** `channels:admin`\n\nSubmits a new template to Meta for approval. Templates typically take\n24-48 hours to be reviewed and approved.\n\nTemplate name must be lowercase, using only letters, numbers, and underscores.\n\nComponents structure:\n- HEADER (optional): TEXT, IMAGE, VIDEO, or DOCUMENT\n- BODY (required): Main message text with {{1}}, {{2}} placeholders\n- FOOTER (optional): Small text at bottom\n- BUTTONS (optional): Up to 3 buttons\n\nExample:\n```json\n{\n    \"channel_id\": \"uuid\",\n    \"name\": \"order_confirmation\",\n    \"category\": \"UTILITY\",\n    \"language\": \"en\",\n    \"components\": [\n        {\"type\": \"HEADER\", \"format\": \"TEXT\", \"text\": \"Order Update\"},\n        {\"type\": \"BODY\", \"text\": \"Hi {{1}}, your order #{{2}} has been shipped!\",\n         \"example\": {\"body_text\": [[\"John\", \"12345\"]]}},\n        {\"type\": \"FOOTER\", \"text\": \"Reply STOP to unsubscribe\"},\n        {\"type\": \"BUTTONS\", \"buttons\": [\n            {\"type\": \"URL\", \"text\": \"Track Order\", \"url\": \"https://example.com/track/{{1}}\"}\n        ]}\n    ]\n}\n```\n\nCAROUSEL templates carry their cards in a `CAROUSEL` component's `cards`\narray (up to 10). Each card mirrors a mini-template — exactly one\nIMAGE/VIDEO header, an OPTIONAL BODY, and a REQUIRED BUTTONS component with\n1-2 buttons — and Meta requires every card to share the same header format,\nthe same body-presence, and an identical button layout. Those structural\nrules are validated up front (400 on failure) before the template is\nsubmitted to Meta; in particular a button-less card is caught here instead\nof coming back from Meta as an unexplained \"Invalid parameter\".\n\nAUTHENTICATION templates are not free-form: WhatsApp generates the message\ntext, so no HEADER and no BODY/FOOTER text are accepted. The only options\nare `{\"type\": \"BODY\", \"add_security_recommendation\": true}`,\n`{\"type\": \"FOOTER\", \"code_expiration_minutes\": 1..90}` and exactly one\n`COPY_CODE` button (label max 25 chars; added as \"Copy Code\" when omitted),\nplus the optional top-level `message_send_ttl_seconds` (30-900). Any other\ncomponent shape is rejected with an actionable 400 before Meta is called.","operationId":"create_template_api_v1_whatsapp_templates_post","security":[{"HTTPBearer":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateTemplateRequest"}}}},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateTemplateResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/whatsapp-templates/fields":{"get":{"tags":["WhatsApp Templates"],"summary":"Get Available Fields","description":"Get available contact fields for variable mapping\n\n**Required Scopes:** `channels:read`\n\nReturns list of contact fields that can be mapped to template placeholders.","operationId":"get_available_fields_api_v1_whatsapp_templates_fields_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AvailableFieldsResponse"}}}}},"security":[{"HTTPBearer":[]}]}},"/api/v1/whatsapp-templates/suggest-mapping":{"get":{"tags":["WhatsApp Templates"],"summary":"Suggest Mapping","description":"Suggest canonical ``variable_mapping`` keys for a list of placeholder names.\n\n**Required Scopes:** `channels:read`\n\nReturns a dict keyed by the input name. Unknown names map to ``None`` so\nthe caller can render an empty dropdown for the user to fill in.\n\nExample:\n```\nGET /api/v1/whatsapp-templates/suggest-mapping?names=first_name,workspace,xyz\n-> {\"first_name\": \"contact.first_name\", \"workspace\": \"tenant.name\", \"xyz\": null}\n```","operationId":"suggest_mapping_api_v1_whatsapp_templates_suggest_mapping_get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"names","in":"query","required":true,"schema":{"type":"string","description":"Comma-separated list of NAMED template placeholder names (e.g. ``first_name,workspace,order_id``).","title":"Names"},"description":"Comma-separated list of NAMED template placeholder names (e.g. ``first_name,workspace,order_id``)."}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":{"anyOf":[{"type":"string"},{"type":"null"}]},"title":"Response Suggest Mapping Api V1 Whatsapp Templates Suggest Mapping Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/whatsapp-templates/library":{"get":{"tags":["WhatsApp Templates"],"summary":"List Template Library","description":"Browse Meta's pre-approved WhatsApp **Template Library**.\n\n**Required Scopes:** `channels:read`\n\nMeta maintains a catalog of ready-made UTILITY and AUTHENTICATION templates.\nAdopting one unchanged (see `POST /library/adopt`) creates it in the\ncustomer's WABA already **approved** — no 24h review.\n\nResults are cached server-side (12h) because the catalog is global and\nnear-static while Meta rate-limits template endpoints aggressively. When\nMeta is unreachable but a previous catalog is cached, that copy is returned\nwith `stale: true` and a `warning` instead of an error.\n\n`already_added` marks entries the tenant already has on this channel, so the\nUI can avoid a guaranteed Meta duplicate rejection.","operationId":"list_template_library_api_v1_whatsapp_templates_library_get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"channel_id","in":"query","required":true,"schema":{"type":"string","description":"WhatsApp channel whose WABA credentials are used to read the catalog","title":"Channel Id"},"description":"WhatsApp channel whose WABA credentials are used to read the catalog"},{"name":"language","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Meta language code, e.g. ``en_US``. Omit for all languages.","title":"Language"},"description":"Meta language code, e.g. ``en_US``. Omit for all languages."},{"name":"search","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Free-text search across the catalog","title":"Search"},"description":"Free-text search across the catalog"},{"name":"topic","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"e.g. ``PAYMENTS``","title":"Topic"},"description":"e.g. ``PAYMENTS``"},{"name":"usecase","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"e.g. ``LOW_BALANCE_WARNING``","title":"Usecase"},"description":"e.g. ``LOW_BALANCE_WARNING``"},{"name":"industry","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"e.g. ``FINANCIAL_SERVICES``","title":"Industry"},"description":"e.g. ``FINANCIAL_SERVICES``"},{"name":"name","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Exact library template name","title":"Name"},"description":"Exact library template name"},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":500,"minimum":1,"description":"Max entries to return","default":100,"title":"Limit"},"description":"Max entries to return"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LibraryTemplateListResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/whatsapp-templates/library/adopt":{"post":{"tags":["WhatsApp Templates"],"summary":"Adopt Library Template","description":"Add a template from Meta's Template Library to the customer's WABA.\n\n**Required Scopes:** `channels:admin` **or** `messages:create`\n\n`messages:create` is accepted on purpose: the primary entry point is an\nagent in /conversations who has no approved template and needs one to open\nthe 24h window. Adopting an unchanged library template is a pre-approved,\nMeta-curated action — gating it behind channel administration would break\nexactly the flow it exists for.\n\nBehaviour:\n- Unchanged library templates come back from Meta already `approved` and are\n  persisted as such. We deliberately do **not** re-sync to confirm: sync is\n  the rate-limited call, and the customer's next action is to send.\n- Idempotent — re-adopting a template the tenant already has returns the\n  existing row with `already_existed: true` and HTTP 200.\n- Omit `button_inputs` to accept the library entry's own example button\n  values (the one-click path). Supply them to point links/phone numbers at\n  the customer's own destinations.","operationId":"adopt_library_template_api_v1_whatsapp_templates_library_adopt_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdoptLibraryTemplateRequest"}}},"required":true},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdoptLibraryTemplateResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"HTTPBearer":[]}]}},"/api/v1/whatsapp-templates/{template_id}":{"get":{"tags":["WhatsApp Templates"],"summary":"Get Template","description":"Get a specific WhatsApp template by ID\n\n**Required Scopes:** `channels:read`","operationId":"get_template_api_v1_whatsapp_templates__template_id__get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"template_id","in":"path","required":true,"schema":{"type":"string","title":"Template Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WhatsAppTemplateResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"patch":{"tags":["WhatsApp Templates"],"summary":"Update Template","description":"Update local-only fields on a WhatsApp template.\n\n**Required Scopes:** `channels:admin`\n\nCurrently only ``default_header_media_url`` and\n``default_header_document_filename`` are mutable here. Template content\n(body text, header, buttons) is approved by Meta and cannot be edited in\nplace — to change those, delete and re-create the template.\n\nPass an empty string to clear either field; omit the field to leave it\nunchanged.","operationId":"update_template_api_v1_whatsapp_templates__template_id__patch","security":[{"HTTPBearer":[]}],"parameters":[{"name":"template_id","in":"path","required":true,"schema":{"type":"string","title":"Template Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateTemplateRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WhatsAppTemplateResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"tags":["WhatsApp Templates"],"summary":"Delete Template","description":"Delete a WhatsApp message template\n\n**Required Scopes:** `channels:admin`\n\nDeletes the template from both Meta and local database.\nNote: Deleting an approved template will make it unavailable immediately.","operationId":"delete_template_api_v1_whatsapp_templates__template_id__delete","security":[{"HTTPBearer":[]}],"parameters":[{"name":"template_id","in":"path","required":true,"schema":{"type":"string","title":"Template Id"}}],"responses":{"204":{"description":"Successful Response"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/whatsapp-templates/sync":{"post":{"tags":["WhatsApp Templates"],"summary":"Sync Templates","description":"Sync templates from WhatsApp Business API\n\n**Required Scopes:** `channels:admin`\n\nFetches all message templates from WhatsApp Business API and syncs them\nto the local database. This operation:\n- Adds new templates\n- Updates existing templates\n- Removes templates that no longer exist in WhatsApp\n\nThe channel must be a WhatsApp channel with valid credentials configured.","operationId":"sync_templates_api_v1_whatsapp_templates_sync_post","security":[{"HTTPBearer":[]}],"parameters":[{"name":"channel_id","in":"query","required":true,"schema":{"type":"string","description":"WhatsApp channel ID to sync templates for","title":"Channel Id"},"description":"WhatsApp channel ID to sync templates for"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WhatsAppTemplateSyncResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/whatsapp-templates/{template_id}/variable-mapping":{"put":{"tags":["WhatsApp Templates"],"summary":"Update Variable Mapping","description":"Update variable mapping for a template\n\n**Required Scopes:** `channels:update`\n\nMaps WhatsApp template placeholders ({{1}}, {{2}}, etc.) to contact fields\nfor automatic personalization when sending messages.\n\nExample mapping:\n```json\n{\n    \"variable_mapping\": {\n        \"1\": \"contact.first_name\",\n        \"2\": \"contact.company\"\n    }\n}\n```\n\nAvailable fields: contact.first_name, contact.last_name, contact.name,\ncontact.email, contact.phone, contact.company, contact.whatsapp_id (resolves to phone)","operationId":"update_variable_mapping_api_v1_whatsapp_templates__template_id__variable_mapping_put","security":[{"HTTPBearer":[]}],"parameters":[{"name":"template_id","in":"path","required":true,"schema":{"type":"string","title":"Template Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateVariableMappingRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateVariableMappingResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/whatsapp-templates/{template_id}/preview":{"post":{"tags":["WhatsApp Templates"],"summary":"Preview Template","description":"Preview a template with variable substitution\n\n**Required Scopes:** `channels:read`\n\nReturns the template with placeholders replaced using either:\n- Contact data (if contact_id provided)\n- Override values (if variable_values provided)\n- Sample data (if neither provided)\n\nUseful for testing variable mappings before sending.","operationId":"preview_template_api_v1_whatsapp_templates__template_id__preview_post","security":[{"HTTPBearer":[]}],"parameters":[{"name":"template_id","in":"path","required":true,"schema":{"type":"string","title":"Template Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PreviewTemplateRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PreviewTemplateResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/whatsapp-templates/{template_id_or_name}/send":{"post":{"tags":["WhatsApp Templates"],"summary":"Send Template","description":"Send a template message to a contact\n\n**Required Scopes:** `messages:create`\n\nSends the specified WhatsApp template to the contact. The template must be\napproved by WhatsApp before it can be sent.\n\nThe path segment accepts EITHER:\n\n* the template's DB ID (canonical UUID, e.g.\n  `f47ac10b-58cc-4372-a567-0e02b2c3d479`), OR\n* the template's WhatsApp `name` (e.g. `order_confirmation`).\n\nResolution rule: a 36-char canonical-form UUID is treated as the DB ID;\nanything else is treated as a name. Names are looked up by\n`(tenant_id, name)`. If the same name exists across multiple channels or\nlanguages, pass `conversation_id` in the body so the owning channel/WABA\ncan be inferred — otherwise the request returns 400 asking for the\nexplicit DB ID.\n\nVariables are automatically populated from:\n1. Override values in the request (highest priority)\n2. Mapped contact fields (from variable_mapping)\n3. Empty string (fallback)\n\nCreates or uses an existing conversation for the message.\n\n`variable_values` accepts a list (positional only) OR a dict. The accepted\nshape is determined by the template's `parameter_format`:\n\n- `positional` template: `[\"John\", \"ORD-1\"]` or `{\"1\": \"John\", \"2\": \"ORD-1\"}`\n- `named` template: `{\"first_name\": \"John\", \"order_id\": \"ORD-1\"}`\n\nSending the wrong shape returns 400 with an actionable error.","operationId":"send_template_api_v1_whatsapp_templates__template_id_or_name__send_post","security":[{"HTTPBearer":[]}],"parameters":[{"name":"template_id_or_name","in":"path","required":true,"schema":{"type":"string","title":"Template Id Or Name"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SendTemplateRequest"},"examples":{"with_variable_overrides":{"summary":"Send with custom variable values","description":"Override auto-mapped variables with custom values for this send.","value":{"contact_id":"c1d2e3f4-5678-9abc-def0-1234567890ab","variable_values":{"1":"John","2":"ORD-98765"}}},"with_auto_mapping":{"summary":"Send using auto-mapped variables","description":"Variables are automatically populated from the contact's profile via the template's variable mapping.","value":{"contact_id":"c1d2e3f4-5678-9abc-def0-1234567890ab"}},"with_media_header":{"summary":"Send with media header","description":"Send a template that has an image/video/document header.","value":{"contact_id":"c1d2e3f4-5678-9abc-def0-1234567890ab","variable_values":{"1":"Jane"},"header_media_url":"https://example.com/images/promo-banner.jpg"}},"in_existing_conversation":{"summary":"Send in an existing conversation","description":"Link the template message to a specific existing conversation.","value":{"contact_id":"c1d2e3f4-5678-9abc-def0-1234567890ab","conversation_id":"b5d8e2a1-4c3f-4a8b-9d2e-1f6a7b8c9d0e","variable_values":{"1":"Sarah","2":"INV-2026-001"}}},"with_url_button":{"summary":"Send with dynamic URL button suffix","description":"Template has a URL button whose URL contains a `{{1}}` placeholder (e.g. `https://example.com/orders/{{1}}`). The reserved key `url_suffix` in `variable_values` supplies ONLY the suffix portion that replaces `{{1}}` — do NOT pass the full URL. `url_suffix` coexists with the regular body variable keys (`\"1\"`, `\"2\"`, ... or named).","value":{"contact_id":"c1d2e3f4-5678-9abc-def0-1234567890ab","variable_values":{"1":"Sarah","2":"INV-2026-001","url_suffix":"track/INV-2026-001"}}},"with_otp_button":{"summary":"Send with OTP / COPY_CODE button","description":"Template has an authentication / OTP COPY_CODE button. The reserved key `otp_code` in `variable_values` supplies the code that the user copies when tapping the button. `otp_code` coexists with the regular body variable keys (`\"1\"`, `\"2\"`, ... or named).","value":{"contact_id":"c1d2e3f4-5678-9abc-def0-1234567890ab","variable_values":{"1":"123456","otp_code":"123456"}}}}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SendTemplateResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/whatsapp-templates/upload-media":{"post":{"tags":["WhatsApp Templates"],"summary":"Upload Template Media","description":"Upload media for WhatsApp template header\n\n**Required Scopes:** `channels:admin`\n\nUploads an image, video, or document to Meta's servers and returns a handle\nthat can be used when creating a template with a media header.\n\nSupported media types and formats:\n- **image**: JPEG, PNG (max 5MB)\n- **video**: MP4, 3GPP (max 16MB)\n- **document**: PDF (max 100MB)\n\nReturns:\n    - handle: The media handle to use in template creation\n    - file_name: Original file name\n    - media_type: Type of media uploaded","operationId":"upload_template_media_api_v1_whatsapp_templates_upload_media_post","requestBody":{"content":{"multipart/form-data":{"schema":{"$ref":"#/components/schemas/Body_upload_template_media_api_v1_whatsapp_templates_upload_media_post"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MediaUploadResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"HTTPBearer":[]}]}},"/api/v1/whatsapp/conversations/{conversation_id}/whatsapp-window":{"get":{"tags":["WhatsApp"],"summary":"Get Whatsapp Window Status","description":"Get WhatsApp messaging window status for a conversation\n\nWhatsApp Business API restricts sending free-form messages outside a window\nafter the last customer message. This endpoint returns the current window\nstatus so the UI can gate free-form vs template composition.\n\n**Two window kinds:**\n- `customer_service` (default): standard **24-hour** window measured from the\n  last inbound customer message. Once expired only template messages may be\n  sent (`requires_template` true).\n- `free_entry_point`: a **72-hour** Free Entry Point (FEP) window for\n  Click-to-WhatsApp / Page-CTA conversations (flagged at ingestion via\n  `custom_data.ctwa.is_free_entry_point`). Per Meta, the FEP conversation\n  \"is opened as soon as your reply message is delivered and lasts 72 hours\",\n  so we anchor it at the **business's first outbound message after the\n  referral** (the closest signal we have to delivery time). While open, any\n  message type (free-form OR template) is allowed and is free.\n\n**Important:** For fresh conversations where `last_customer_message_at` is null,\nwe check for recent inbound messages from the same contact on the same channel\n(across all conversations) to determine the window status.\n\n**Required Scopes:** `conversations:read`\n\nArgs:\n    conversation_id: The conversation ID to check\n    tenant_id: Tenant ID from authentication (automatic)\n\nReturns:\n    WhatsAppWindowStatusResponse (see schema): in_window, window_type,\n    remaining_seconds, requires_template, is_free_entry_point,\n    last_customer_message_at, window_end_at, free_entry_point_anchor_at.\n\nRaises:\n    404: Conversation not found or access denied","operationId":"get_whatsapp_window_status_api_v1_whatsapp_conversations__conversation_id__whatsapp_window_get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"conversation_id","in":"path","required":true,"schema":{"type":"string","title":"Conversation Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WhatsAppWindowStatusResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/whatsapp/conversations/{conversation_id}/call-window":{"get":{"tags":["WhatsApp"],"summary":"Get Whatsapp Call Window","description":"Whether an OUTBOUND WhatsApp call can be placed on this conversation now,\nplus the call-permission window expiry (Feature #34 backend half).\n\nMirrors ``/conversations/{id}/whatsapp-window`` (the 24h MESSAGING window) but\nfor the CALL-permission window. WhatsApp's business-initiated calling requires\nan open call-permission window — granted by the customer (a permission grant\nor a prior connected call) and lasting **7 days (168h)**. The \"Call\" button in\nthe conversation view gates on this.\n\n**What we can derive today (and the gaps):**\n- ``calling_disabled``  — the master switch (``WHATSAPP_CALLING_ENABLED``) is\n  off OR the channel's ``calling_enabled`` config flag is off. Hard block.\n- ``no_permission``     — we have NO signal of a granted call-permission\n  window: no prior CONNECTED (answered) call on this conversation. We anchor\n  the window at the most recent answered call's ``answered_at`` + 7 days\n  (the closest signal we persist). GAP: we do NOT yet persist Meta's explicit\n  ``callback_permission_status`` grant/expiry per contact, so a permission\n  granted WITHOUT a prior connected call is not reflected here — flagged for a\n  follow-up (persist the grant from the calls webhook).\n- ``calling`` allowed   — calling enabled AND a connected call within the last\n  7 days → ``can_call: true`` with ``call_window_expires_at``.\n\nResponse (frontend contract):\n  ``{ can_call: bool, call_window_expires_at: ISO8601|null, reason: str|null,\n      billable: bool, last_connected_call_at: ISO8601|null }``\n``reason`` ∈ {no_permission, opted_out, rate_limited, calling_disabled, null}.\nOutbound is billable-on-answer, so ``billable`` is always true when\n``can_call`` (the UI can color the button accordingly).\n\n**Required Scopes:** `calls:view`","operationId":"get_whatsapp_call_window_api_v1_whatsapp_conversations__conversation_id__call_window_get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"conversation_id","in":"path","required":true,"schema":{"type":"string","title":"Conversation Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Get Whatsapp Call Window Api V1 Whatsapp Conversations  Conversation Id  Call Window Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/whatsapp/channels/{channel_id}/flows":{"get":{"tags":["WhatsApp"],"summary":"List Whatsapp Flows","description":"List available WhatsApp Flows for a channel\n\nRetrieves the flows of the channel's WhatsApp Business Account from Meta.\nFlows built in SendSeven carry ``definition_id`` / ``definition_status``.\nFlows of ARCHIVED SendSeven WhatsApp Flows are hidden unless\n``include_archived=true``; Meta flows superseded by \"publish changes\" are\nalways hidden (they no longer serve new sends).\n\n**Required Scopes:** `channels:read`\n\nRaises:\n    404: Channel not found or access denied\n    400: Channel is not a WhatsApp channel","operationId":"list_whatsapp_flows_api_v1_whatsapp_channels__channel_id__flows_get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"channel_id","in":"path","required":true,"schema":{"type":"string","title":"Channel Id"}},{"name":"include_archived","in":"query","required":false,"schema":{"type":"boolean","description":"Also list Meta flows whose SendSeven WhatsApp Flow is archived","default":false,"title":"Include Archived"},"description":"Also list Meta flows whose SendSeven WhatsApp Flow is archived"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FetchFlowsResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/whatsapp/channels/{channel_id}/flows/{flow_id}":{"get":{"tags":["WhatsApp"],"summary":"Get Whatsapp Flow Details","description":"Get detailed information about a specific WhatsApp Flow\n\nReturns the flow's screens (with input fields, the raw `data` declaration\nas `data_schema`, and whether the screen is terminal), the entry screen,\nwhether the flow is static (no data endpoint — first-screen data must be\nsent with the message), and the saved default data per screen.\n\nThe flow JSON is cached for a few minutes; pass `refresh=true` to re-read it.\n\n**Required Scopes:** `channels:read`\n\nRaises:\n    404: Channel not found or access denied\n    400: Channel is not a WhatsApp channel","operationId":"get_whatsapp_flow_details_api_v1_whatsapp_channels__channel_id__flows__flow_id__get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"channel_id","in":"path","required":true,"schema":{"type":"string","title":"Channel Id"}},{"name":"flow_id","in":"path","required":true,"schema":{"type":"string","title":"Flow Id"}},{"name":"refresh","in":"query","required":false,"schema":{"type":"boolean","description":"Bypass the cached flow JSON and re-download it from Meta","default":false,"title":"Refresh"},"description":"Bypass the cached flow JSON and re-download it from Meta"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FlowDetailsResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/whatsapp/channels/{channel_id}/flows/{flow_id}/defaults":{"put":{"tags":["WhatsApp"],"summary":"Update Whatsapp Flow Defaults","description":"Save the default data for one screen of a WhatsApp Flow.\n\nThe saved data is used whenever this flow is sent without explicit `data`\n(from the conversation composer or as a template FLOW button). Send\n`data: null` to remove the saved default for that screen.\n\n**Required Scopes:** `channels:update`\n\nRaises:\n    404: Channel not found or access denied\n    400: Channel is not a WhatsApp channel\n    422: `{\"code\": \"invalid_defaults\", \"message\": ...}` (too large / not an object / too many flows)","operationId":"update_whatsapp_flow_defaults_api_v1_whatsapp_channels__channel_id__flows__flow_id__defaults_put","security":[{"HTTPBearer":[]}],"parameters":[{"name":"channel_id","in":"path","required":true,"schema":{"type":"string","title":"Channel Id"}},{"name":"flow_id","in":"path","required":true,"schema":{"type":"string","title":"Flow Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateFlowDefaultsRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateFlowDefaultsResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/whatsapp/flows/send":{"post":{"tags":["WhatsApp"],"summary":"Send Whatsapp Flow","description":"Send a WhatsApp Flow message (free-form, inside the 24-hour window)\n\nThe recipient is resolved from `conversation_id` (preferred), `contact_id`\nor `recipient` (phone number). Contacts without a phone number that are\nreachable via a WhatsApp business-scoped user ID are supported.\n\nFirst-screen data is merged per key: values in `data` win, then the saved\ndefault data for (flow_id, screen), then (for flows built in SendSeven) the\nflow's send-time data sources such as the contact profile, then the flow's\nexample data. When `screen` is omitted the flow's entry screen is used.\n\nString values in `data` (and in saved default data) may contain contact\nvariables, rendered per recipient before sending: `{{contact.first_name}}`,\n`{{contact.last_name}}`, `{{contact.name}}`, `{{contact.email}}`,\n`{{contact.phone}}`, `{{contact.birthday}}`, `{{contact.language}}` and\ncustom fields as `{{contact.custom.<key>}}`. Unknown or empty variables\nrender as an empty string. Example:\n`\"data\": {\"first_name\": \"{{contact.first_name}}\"}`.\n\nNote: the flow itself must bind the value as a dynamic property (e.g. a\nTextBody whose `text` is exactly `${data.first_name}`, declared in the\nscreen's `data`); a `${data.x}` written inside plain literal text is shown\nas-is by WhatsApp.\n\nA Flow message is a free-form interactive message, so it can only be sent\nwhile the customer service window is open. Outside the window use a\ntemplate with a Flow button.\n\n**Required Scopes:** `messages:create`\n\nRaises:\n    404: Channel, conversation or contact not found\n    400: Not a WhatsApp channel; `conversation_channel_mismatch`; `no_whatsapp_recipient`\n    409: CHANNEL_DISCONNECTED; `conversation_owned_elsewhere` (Custom\n        Inboxes: the contact is in an ongoing conversation a colleague\n        handles that you cannot open)\n    422: `outside_service_window`","operationId":"send_whatsapp_flow_api_v1_whatsapp_flows_send_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SendFlowRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SendFlowResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"HTTPBearer":[]}]}},"/api/v1/whatsapp/channels/{channel_id}/account-status":{"get":{"tags":["WhatsApp"],"summary":"Get Whatsapp Account Status","description":"Get WhatsApp Business Account status including billing/payment info.\n\nThis endpoint helps diagnose issues like \"Business eligibility payment issue\"\nby checking the WABA configuration and phone number status.\n\nPayment methods can be configured in two ways:\n1. Direct payment method on WABA (primary_funding_id)\n2. Shared credit line from Tech Provider/BSP (credit_lines edge)\n\nFor Tech Provider accounts, billing is typically handled through shared\ncredit lines, NOT primary_funding_id. This endpoint checks both methods.\n\n**Required Scopes:** `channels:read`\n\nArgs:\n    channel_id: The WhatsApp channel ID\n    tenant_id: Tenant ID from authentication (automatic)\n\nReturns:\n    Account status including:\n    - has_payment_issue: Boolean if NO payment method is configured\n    - has_billing_configured: Boolean if ANY billing method is set up\n    - billing_type: \"direct\" | \"credit_line\" | \"unknown\" | \"none\"\n    - has_credit_line: Boolean if credit line shared from Tech Provider\n    - account_status: WABA review status\n    - quality_rating: Phone number quality (GREEN, YELLOW, RED)\n    - messaging_limit: Current messaging tier\n    - payment_warning: Warning message if payment issue detected\n\nRaises:\n    404: Channel not found or access denied\n    400: Channel is not a WhatsApp channel","operationId":"get_whatsapp_account_status_api_v1_whatsapp_channels__channel_id__account_status_get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"channel_id","in":"path","required":true,"schema":{"type":"string","title":"Channel Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/whatsapp/channels/{channel_id}/sending-limit":{"get":{"tags":["WhatsApp"],"summary":"Get Whatsapp Sending Limit","description":"Get a WhatsApp channel's persisted *sending limit* (messaging tier).\n\nLean replacement for the live-fetching ``/account-status`` endpoint on the\nSettings → Channels page. Instead of hitting Meta on every page open, this\nendpoint serves a value persisted onto the channel's encrypted ``config`` and\nre-queries the provider at most **once per 24h per channel** (server-side,\nauthoritative gate). Safe to call on every page load: it is idempotent and\ncheap, and it NEVER 500s the page — a provider/token failure degrades to the\nlast-known value with ``stale=True`` and a soft ``error``.\n\n**Required Scopes:** `channels:read`\n\nArgs:\n    channel_id: The channel ID\n    tenant_id: Tenant ID from authentication (automatic)\n\nReturns:\n    The sending-limit contract dict:\n    ``{channel_id, channel_type, messaging_limit_tier, quality_rating,\n    throughput_level, phone_status, refreshed_at, last_checked_at, stale,\n    supported, error}``. For a non-WhatsApp channel ``supported`` is False\n    (still HTTP 200).\n\nRaises:\n    404: Channel not found or access denied","operationId":"get_whatsapp_sending_limit_api_v1_whatsapp_channels__channel_id__sending_limit_get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"channel_id","in":"path","required":true,"schema":{"type":"string","title":"Channel Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/whatsapp/channels/{channel_id}/coexistence-status":{"get":{"tags":["WhatsApp"],"summary":"Get Coexistence Status","description":"Get WhatsApp Coexistence sync status for a channel.\n\nPolled by the frontend after a user finishes the Coexistence onboarding\nbranch. Returns the current history-backfill and address-book sync\nstate plus any disconnection signals received via ``account_update``\nwebhooks.\n\n**Required Scopes:** ``channels:read``\n\nReturns 404 if the channel doesn't exist or doesn't belong to the\nrequesting tenant — existence is not leaked across tenants.\n\nArgs:\n    channel_id: Channel UUID to fetch status for.\n\nReturns:\n    See ``CoexistenceStatusResponse``.","operationId":"get_coexistence_status_api_v1_whatsapp_channels__channel_id__coexistence_status_get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"channel_id","in":"path","required":true,"schema":{"type":"string","title":"Channel Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CoexistenceStatusResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/canned-responses":{"get":{"tags":["Canned Responses"],"summary":"List Canned Responses","description":"List canned responses visible to the current user.\n\nReturns the user's own responses and all shared responses. Users with\n`canned_responses:settings` scope see all responses in the tenant.\n\n**Required Scopes:** `canned_responses:read`","operationId":"list_canned_responses_api_v1_canned_responses_get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"page","in":"query","required":false,"schema":{"type":"integer","minimum":1,"description":"Page number (1-indexed)","default":1,"title":"Page"},"description":"Page number (1-indexed)"},{"name":"page_size","in":"query","required":false,"schema":{"type":"integer","maximum":100,"minimum":1,"description":"Items per page","default":20,"title":"Page Size"},"description":"Items per page"},{"name":"category","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Filter by category","title":"Category"},"description":"Filter by category"},{"name":"search","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Search in title, content, and shortcut","title":"Search"},"description":"Search in title, content, and shortcut"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CannedResponseListResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"post":{"tags":["Canned Responses"],"summary":"Create Canned Response","description":"Create a new canned response.\n\nBy default, responses are personal (visible only to you). Set `is_shared=true`\nto make it visible to all team members (requires `canned_responses:settings` scope).\n\n**Required Scopes:** `canned_responses:create`","operationId":"create_canned_response_api_v1_canned_responses_post","security":[{"HTTPBearer":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CannedResponseCreate"}}}},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CannedResponseResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/canned-responses/categories":{"get":{"tags":["Canned Responses"],"summary":"List Categories","description":"Get unique categories for canned responses.\n\nReturns all categories from responses visible to the current user.\n\n**Required Scopes:** `canned_responses:read`","operationId":"list_categories_api_v1_canned_responses_categories_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/app__schemas__canned_response_schema__CategoryListResponse"}}}}},"security":[{"HTTPBearer":[]}]}},"/api/v1/canned-responses/by-shortcut/{shortcut}":{"get":{"tags":["Canned Responses"],"summary":"Get By Shortcut","description":"Get a canned response by its shortcut.\n\nUsed for slash command lookups (e.g., /greeting). The shortcut is\ncase-insensitive and the leading slash is optional.\n\n**Required Scopes:** `canned_responses:read`","operationId":"get_by_shortcut_api_v1_canned_responses_by_shortcut__shortcut__get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"shortcut","in":"path","required":true,"schema":{"type":"string","title":"Shortcut"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CannedResponseResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/canned-responses/{canned_response_id}":{"get":{"tags":["Canned Responses"],"summary":"Get Canned Response","description":"Get a specific canned response by ID.\n\n**Required Scopes:** `canned_responses:read`","operationId":"get_canned_response_api_v1_canned_responses__canned_response_id__get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"canned_response_id","in":"path","required":true,"schema":{"type":"string","title":"Canned Response Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CannedResponseResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"put":{"tags":["Canned Responses"],"summary":"Update Canned Response","description":"Update a canned response.\n\nUsers can only update their own responses. Users with `canned_responses:settings`\nscope can update any response.\n\n**Required Scopes:** `canned_responses:update`","operationId":"update_canned_response_api_v1_canned_responses__canned_response_id__put","security":[{"HTTPBearer":[]}],"parameters":[{"name":"canned_response_id","in":"path","required":true,"schema":{"type":"string","title":"Canned Response Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CannedResponseUpdate"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CannedResponseResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"tags":["Canned Responses"],"summary":"Delete Canned Response","description":"Delete a canned response.\n\nUsers can only delete their own responses. Users with `canned_responses:settings`\nscope can delete any response.\n\n**Required Scopes:** `canned_responses:delete`","operationId":"delete_canned_response_api_v1_canned_responses__canned_response_id__delete","security":[{"HTTPBearer":[]}],"parameters":[{"name":"canned_response_id","in":"path","required":true,"schema":{"type":"string","title":"Canned Response Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SuccessResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/saved-interactive-messages":{"get":{"tags":["Saved Interactive Messages"],"summary":"List Saved Interactive Messages","description":"List saved interactive messages visible to the current user.\n\nReturns the user's own messages and all shared messages. Users with\n`canned_responses:settings` scope see all messages in the tenant.\n\n**Required Scopes:** `messages:create`","operationId":"list_saved_interactive_messages_api_v1_saved_interactive_messages_get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"page","in":"query","required":false,"schema":{"type":"integer","minimum":1,"description":"Page number (1-indexed)","default":1,"title":"Page"},"description":"Page number (1-indexed)"},{"name":"page_size","in":"query","required":false,"schema":{"type":"integer","maximum":100,"minimum":1,"description":"Items per page","default":20,"title":"Page Size"},"description":"Items per page"},{"name":"type","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Filter by type: 'buttons' or 'list'","title":"Type"},"description":"Filter by type: 'buttons' or 'list'"},{"name":"category","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Filter by category","title":"Category"},"description":"Filter by category"},{"name":"search","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Search in title, body, and shortcut","title":"Search"},"description":"Search in title, body, and shortcut"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SavedInteractiveMessageListResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"post":{"tags":["Saved Interactive Messages"],"summary":"Create Saved Interactive Message","description":"Create a new saved interactive message template.\n\nBy default, messages are shared (visible to all team members). Set `is_shared=false`\nto make it personal. Only users with `canned_responses:settings` scope can create shared messages.\n\n## Buttons Type\n- Maximum 3 reply buttons\n- Each button has an `id` (returned when clicked) and `title` (max 20 chars)\n\n## List Type\n- Up to 10 sections with rows\n- Each row has `id`, `title` (max 24 chars), and optional `description`\n- Maximum 10 total rows across all sections\n\n**Required Scopes:** `messages:create`","operationId":"create_saved_interactive_message_api_v1_saved_interactive_messages_post","security":[{"HTTPBearer":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SavedInteractiveMessageCreate"}}}},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SavedInteractiveMessageResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/saved-interactive-messages/categories":{"get":{"tags":["Saved Interactive Messages"],"summary":"List Categories","description":"Get unique categories for saved interactive messages.\n\nReturns all categories from messages visible to the current user.\n\n**Required Scopes:** `messages:create`","operationId":"list_categories_api_v1_saved_interactive_messages_categories_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/app__schemas__saved_interactive_message__CategoryListResponse"}}}}},"security":[{"HTTPBearer":[]}]}},"/api/v1/saved-interactive-messages/by-shortcut/{shortcut}":{"get":{"tags":["Saved Interactive Messages"],"summary":"Get By Shortcut","description":"Get a saved interactive message by its shortcut.\n\nUsed for slash command lookups (e.g., /pricing). The shortcut is\ncase-insensitive and the leading slash is optional.\n\n**Required Scopes:** `messages:create`","operationId":"get_by_shortcut_api_v1_saved_interactive_messages_by_shortcut__shortcut__get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"shortcut","in":"path","required":true,"schema":{"type":"string","title":"Shortcut"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SavedInteractiveMessageResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/saved-interactive-messages/{message_id}":{"get":{"tags":["Saved Interactive Messages"],"summary":"Get Saved Interactive Message","description":"Get a specific saved interactive message by ID.\n\n**Required Scopes:** `messages:create`","operationId":"get_saved_interactive_message_api_v1_saved_interactive_messages__message_id__get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"message_id","in":"path","required":true,"schema":{"type":"string","title":"Message Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SavedInteractiveMessageResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"put":{"tags":["Saved Interactive Messages"],"summary":"Update Saved Interactive Message","description":"Update a saved interactive message.\n\nUsers can only update their own messages. Users with `canned_responses:settings`\nscope can update any message.\n\n**Required Scopes:** `messages:create`","operationId":"update_saved_interactive_message_api_v1_saved_interactive_messages__message_id__put","security":[{"HTTPBearer":[]}],"parameters":[{"name":"message_id","in":"path","required":true,"schema":{"type":"string","title":"Message Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SavedInteractiveMessageUpdate"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SavedInteractiveMessageResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"tags":["Saved Interactive Messages"],"summary":"Delete Saved Interactive Message","description":"Delete a saved interactive message.\n\nUsers can only delete their own messages. Users with `canned_responses:settings`\nscope can delete any message.\n\n**Required Scopes:** `messages:create`","operationId":"delete_saved_interactive_message_api_v1_saved_interactive_messages__message_id__delete","security":[{"HTTPBearer":[]}],"parameters":[{"name":"message_id","in":"path","required":true,"schema":{"type":"string","title":"Message Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SuccessResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/saved-interactive-messages/{message_id}/send":{"post":{"tags":["Saved Interactive Messages"],"summary":"Send Saved Interactive Message","description":"Send a saved interactive message to a contact.\n\nThis endpoint retrieves the saved interactive message template and sends it\nto the specified contact via the specified WhatsApp channel.\n\nThe saved message must be visible to the current user (either owned by them\nor shared).\n\n## Example Request\n```json\n{\n    \"channel_id\": \"uuid-of-whatsapp-channel\",\n    \"contact_id\": \"uuid-of-recipient-contact\",\n    \"meta\": {\n        \"campaign\": \"q4-sales\"\n    }\n}\n```\n\n**Required Scopes:** `messages:create`","operationId":"send_saved_interactive_message_api_v1_saved_interactive_messages__message_id__send_post","security":[{"HTTPBearer":[]}],"parameters":[{"name":"message_id","in":"path","required":true,"schema":{"type":"string","title":"Message Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SendSavedInteractiveMessageRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SendSavedInteractiveMessageResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/email-signatures/template":{"get":{"tags":["Email Signatures"],"summary":"Get Tenant Template","description":"Get the tenant's email signature template.\n\nReturns the tenant-wide signature template with placeholders.\nReturns 404 if no template has been created yet.\n\n**Required Scopes:** `email_signatures:read`","operationId":"get_tenant_template_api_v1_email_signatures_template_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SignatureTemplateResponse"}}}}},"security":[{"HTTPBearer":[]}]},"put":{"tags":["Email Signatures"],"summary":"Update Tenant Template","description":"Create or update the tenant's signature template.\n\nThis template serves as the master/default signature for the tenant.\nAgents can copy this template to customize their own signatures.\n\nSupported placeholders:\n- `{agent_name}` - Agent's display name\n- `{agent_email}` - Agent's email address\n- `{agent_phone}` - Agent's phone number (from TenantUser)\n- `{agent_title}` - Agent's job title (from TenantUser)\n- `{company_name}` - Company name (from Tenant)\n- `{company_logo}` - Company logo URL\n- `{company_website}` - Company website URL\n\n**Required Scopes:** `email_signatures:settings`","operationId":"update_tenant_template_api_v1_email_signatures_template_put","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SignatureTemplateUpdate"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SignatureTemplateResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"HTTPBearer":[]}]}},"/api/v1/email-signatures/template/preview":{"post":{"tags":["Email Signatures"],"summary":"Preview Template","description":"Preview a signature template with resolved placeholders.\n\nIf `content_html` is not provided in the request, uses the current\ntenant template. Optionally override placeholder values for preview.\n\n**Required Scopes:** `email_signatures:read`","operationId":"preview_template_api_v1_email_signatures_template_preview_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SignaturePreviewRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SignaturePreviewResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"HTTPBearer":[]}]}},"/api/v1/email-signatures/me":{"get":{"tags":["Email Signatures"],"summary":"Get My Signature","description":"Get the current user's email signature.\n\nReturns 404 if the user hasn't created a signature yet.\nUse POST /email-signatures/me/copy-from-template to initialize\nfrom the tenant template.\n\n**Required Scopes:** `email_signatures:read`","operationId":"get_my_signature_api_v1_email_signatures_me_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserSignatureResponse"}}}}},"security":[{"HTTPBearer":[]}]},"put":{"tags":["Email Signatures"],"summary":"Update My Signature","description":"Create or update the current user's email signature.\n\nThe signature HTML can include placeholders which will be resolved\nwhen emails are sent. However, users typically personalize the\nsignature with their actual values.\n\n**Required Scopes:** `email_signatures:update`","operationId":"update_my_signature_api_v1_email_signatures_me_put","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserSignatureUpdate"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserSignatureResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"HTTPBearer":[]}]}},"/api/v1/email-signatures/me/copy-from-template":{"post":{"tags":["Email Signatures"],"summary":"Copy Template To My Signature","description":"Copy the tenant's signature template to the current user's signature.\n\nThis creates a personalized copy of the tenant template that the user\ncan then customize. The template's placeholders are copied as-is and\nwill be resolved when emails are sent.\n\nReturns 404 if no tenant template exists.\n\n**Required Scopes:** `email_signatures:update`","operationId":"copy_template_to_my_signature_api_v1_email_signatures_me_copy_from_template_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserSignatureResponse"}}}}},"security":[{"HTTPBearer":[]}]}},"/api/v1/email-signatures/me/effective":{"get":{"tags":["Email Signatures"],"summary":"Get Effective Signature","description":"Get the fully resolved signature for the current user.\n\nReturns the user's signature (if exists) or falls back to tenant template,\nwith all placeholders resolved to actual values.\n\nThis is what would be appended to outgoing emails.\n\n**Required Scopes:** `email_signatures:read`","operationId":"get_effective_signature_api_v1_email_signatures_me_effective_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/EffectiveSignatureResponse"}}}}},"security":[{"HTTPBearer":[]}]}},"/api/v1/email-signatures/me/preview":{"get":{"tags":["Email Signatures"],"summary":"Preview My Signature","description":"Preview the current user's signature with placeholders resolved.\n\nReturns the user's signature (if exists) or falls back to tenant template,\nwith all placeholders resolved to the user's actual values.\n\n**Required Scopes:** `email_signatures:read`","operationId":"preview_my_signature_api_v1_email_signatures_me_preview_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SignaturePreviewResponse"}}}}},"security":[{"HTTPBearer":[]}]}},"/api/v1/invoices/preview":{"get":{"tags":["Invoices"],"summary":"Preview Upcoming Invoice","description":"Preview upcoming charges for the current billing period.\n\nShows detailed line items with connection-level breakdown:\n- \"WhatsApp Messages +49171231231\" - 100 x EUR 0.005\n- \"Telegram Messages @MyBot\" - 50 x EUR 0.005\n\n**Required Scopes:** `billing:read`","operationId":"preview_upcoming_invoice_api_v1_invoices_preview_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InvoicePreviewResponse"}}}}},"security":[{"HTTPBearer":[]}]}},"/api/v1/invoices":{"get":{"tags":["Invoices"],"summary":"List Invoices","description":"List past invoices from Stripe.\n\nReturns paginated list of invoices with status, amounts, and download links.\n\n**Required Scopes:** `billing:read`","operationId":"list_invoices_api_v1_invoices_get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"page","in":"query","required":false,"schema":{"type":"integer","minimum":1,"description":"Page number","default":1,"title":"Page"},"description":"Page number"},{"name":"page_size","in":"query","required":false,"schema":{"type":"integer","maximum":100,"minimum":1,"description":"Items per page","default":20,"title":"Page Size"},"description":"Items per page"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PaginatedInvoiceResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/invoices/{invoice_id}":{"get":{"tags":["Invoices"],"summary":"Get Invoice","description":"Get invoice details including line items.\n\nReturns full invoice with itemized charges showing connection-level detail.\n\n**Required Scopes:** `billing:read`","operationId":"get_invoice_api_v1_invoices__invoice_id__get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"invoice_id","in":"path","required":true,"schema":{"type":"string","title":"Invoice Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InvoiceDetailResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/channel-connect-tokens":{"post":{"tags":["Channel Connect"],"summary":"Create Token","description":"Create a new channel connect token.\n\n**Required Scopes:** `channels:create`\n\nGenerates a secure, time-limited token that can be shared with partners\nor clients to allow them to connect messaging channels without full\nplatform access.\n\nIMPORTANT: The raw token is returned ONLY ONCE. Save it securely.\n\n**Example:**\n```json\n{\n    \"name\": \"Acme Corp Setup\",\n    \"allowed_channel_types\": [\"telegram\", \"whatsapp\"],\n    \"expires_in_hours\": 48,\n    \"max_uses\": 5,\n    \"partner_name\": \"Acme Corporation\"\n}\n```","operationId":"create_token_api_v1_channel_connect_tokens_post","security":[{"HTTPBearer":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ChannelConnectTokenCreate"}}}},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ChannelConnectTokenCreatedResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"get":{"tags":["Channel Connect"],"summary":"List Tokens","description":"List channel connect tokens for this tenant.\n\n**Required Scopes:** `channels:read`\n\nBy default, only shows active (non-expired, non-revoked) tokens.\nUse query parameters to include inactive tokens.","operationId":"list_tokens_api_v1_channel_connect_tokens_get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"include_expired","in":"query","required":false,"schema":{"type":"boolean","description":"Include expired tokens","default":false,"title":"Include Expired"},"description":"Include expired tokens"},{"name":"include_revoked","in":"query","required":false,"schema":{"type":"boolean","description":"Include revoked tokens","default":false,"title":"Include Revoked"},"description":"Include revoked tokens"},{"name":"page","in":"query","required":false,"schema":{"type":"integer","minimum":1,"description":"Page number (1-indexed)","default":1,"title":"Page"},"description":"Page number (1-indexed)"},{"name":"page_size","in":"query","required":false,"schema":{"type":"integer","maximum":100,"minimum":1,"description":"Items per page","default":20,"title":"Page Size"},"description":"Items per page"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ChannelConnectTokenListResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/channel-connect-tokens/delegate":{"post":{"tags":["Channel Connect"],"summary":"Delegate Connect","description":"Hand a channel connect over to a colleague by email.\n\n**Required Scopes:** `channels:create`\n\nCreates a short-lived connect token scoped to the given channel types and\nmails the link to `email`. Intended for the channel setup wizard, where the\nperson doing the setup often lacks the third-party platform access the\nconnect needs (typically: they are not an admin of the Meta Business\nportfolio that owns the WhatsApp number or the Facebook Page).\n\nThe recipient needs no SendSeven account — the link is the credential — so it\nis deliberately restrictive compared to a partner token: two uses (one real\nattempt plus one retry), a 60-minute window after first use, and a single\nplatform.\n\nThe token is created BEFORE the email is attempted, and `connect_url` is\nalways returned. A failed send therefore degrades to \"copy the link and send\nit yourself\" rather than losing the token.","operationId":"delegate_connect_api_v1_channel_connect_tokens_delegate_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ChannelConnectDelegateRequest"}}},"required":true},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ChannelConnectDelegateResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"HTTPBearer":[]}]}},"/api/v1/channel-connect-tokens/{token_id}":{"get":{"tags":["Channel Connect"],"summary":"Get Token","description":"Get details of a specific channel connect token.\n\n**Required Scopes:** `channels:read`\n\nNote: The raw token value is NOT included - it was only shown at creation.","operationId":"get_token_api_v1_channel_connect_tokens__token_id__get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"token_id","in":"path","required":true,"schema":{"type":"string","title":"Token Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ChannelConnectTokenResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"tags":["Channel Connect"],"summary":"Revoke Token","description":"Revoke a channel connect token.\n\n**Required Scopes:** `channels:delete`\n\nRevoking a token immediately prevents any further use.\nThis action cannot be undone - create a new token if needed.\n\nOptionally provide a reason for audit purposes.","operationId":"revoke_token_api_v1_channel_connect_tokens__token_id__delete","security":[{"HTTPBearer":[]}],"parameters":[{"name":"token_id","in":"path","required":true,"schema":{"type":"string","title":"Token Id"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ChannelConnectTokenRevoke"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeleteResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/channel-connect-tokens/{token_id}/audit-logs":{"get":{"tags":["Channel Connect"],"summary":"Get Token Audit Logs","description":"Get audit logs for a specific channel connect token.\n\n**Required Scopes:** `channels:read`\n\nReturns all events related to this token including:\n- Token creation\n- Public page views\n- Connection attempts (success/failure)\n- Revocation","operationId":"get_token_audit_logs_api_v1_channel_connect_tokens__token_id__audit_logs_get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"token_id","in":"path","required":true,"schema":{"type":"string","title":"Token Id"}},{"name":"page","in":"query","required":false,"schema":{"type":"integer","minimum":1,"description":"Page number (1-indexed)","default":1,"title":"Page"},"description":"Page number (1-indexed)"},{"name":"page_size","in":"query","required":false,"schema":{"type":"integer","maximum":100,"minimum":1,"description":"Items per page","default":50,"title":"Page Size"},"description":"Items per page"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ChannelConnectAuditLogListResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/tenant-email-settings":{"get":{"tags":["Tenant Email Settings"],"summary":"Get Email Settings","description":"Get tenant email settings.\n\nReturns the tenant's email configuration including campaign defaults,\nsupport defaults, and settings. Creates default settings if none exist.\n\n**Required Scopes:** `email_integrations:read`\n\n**Database:** SHARD (email_settings table)","operationId":"get_email_settings_api_v1_tenant_email_settings_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TenantEmailSettingsResponse"}}}}},"security":[{"HTTPBearer":[]}]},"put":{"tags":["Tenant Email Settings"],"summary":"Update Email Settings","description":"Update tenant email settings.\n\nPartial update - only provided fields are changed. Validates from_email\nfields against sendable domains if provided.\n\n**Required Scopes:** `email_integrations:update`\n\n**Database:** SHARD (email_settings table)","operationId":"update_email_settings_api_v1_tenant_email_settings_put","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TenantEmailSettingsUpdate"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TenantEmailSettingsResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"HTTPBearer":[]}]}},"/api/v1/tenant-email-settings/sendable-domains":{"get":{"tags":["Tenant Email Settings"],"summary":"Get Sendable Domains","description":"List verified domains and addresses the tenant can send email from.\n\nAggregates sendable domains from all email integrations and provider configs:\n- SendGrid/SendGrid Managed/Mailgun BYOK providers with verified domains\n- SES managed integrations with verified domains\n- Gmail OAuth integrations (fixed address, support-only)\n- SMTP/IMAP integrations (fixed address, support-only)\n\nUse the `purpose` query parameter to filter:\n- `?purpose=campaign` - only campaign-capable sources\n- `?purpose=support` - all sources (support can use any sender)\n- omit - all sources (backwards compatible)\n\nEach entry includes a `campaign_capable` boolean indicating whether it can\nbe used for email campaigns.\n\n**Required Scopes:** `email_integrations:read`\n\n**Database:** SHARD (email_provider_configs, email_integrations tables)","operationId":"get_sendable_domains_api_v1_tenant_email_settings_sendable_domains_get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"purpose","in":"query","required":false,"schema":{"anyOf":[{"enum":["campaign","support"],"type":"string"},{"type":"null"}],"description":"Filter by intended use: 'campaign' returns only campaign-capable sources (SendGrid, Mailgun, SES); 'support' returns all sources; omit for all sources (default).","title":"Purpose"},"description":"Filter by intended use: 'campaign' returns only campaign-capable sources (SendGrid, Mailgun, SES); 'support' returns all sources; omit for all sources (default)."}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SendableDomainsResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/email-mailboxes":{"get":{"tags":["Email Mailboxes"],"summary":"List Email Mailboxes","description":"List email mailboxes for the tenant.\n\nReturns active mailboxes by default.  Set `active_only=false` to include\ndeactivated mailboxes.\n\nOptional filters:\n- `purpose=campaign` - only mailboxes with can_send_campaigns=True\n- `purpose=support` - only mailboxes with can_send_support=True\n- `for_user_id` - only mailboxes the given user can access (all + specific with user in list)\n\n**Required Scopes:** `channels:read`","operationId":"list_email_mailboxes_api_v1_email_mailboxes_get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"page","in":"query","required":false,"schema":{"type":"integer","minimum":1,"description":"Page number (1-indexed)","default":1,"title":"Page"},"description":"Page number (1-indexed)"},{"name":"page_size","in":"query","required":false,"schema":{"type":"integer","maximum":100,"minimum":1,"description":"Items per page","default":50,"title":"Page Size"},"description":"Items per page"},{"name":"active_only","in":"query","required":false,"schema":{"type":"boolean","description":"Only return active mailboxes","default":true,"title":"Active Only"},"description":"Only return active mailboxes"},{"name":"purpose","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Filter by capability: 'campaign' (can_send_campaigns=True) or 'support' (can_send_support=True)","title":"Purpose"},"description":"Filter by capability: 'campaign' (can_send_campaigns=True) or 'support' (can_send_support=True)"},{"name":"for_user_id","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Filter to mailboxes accessible by this user (respects access_mode)","title":"For User Id"},"description":"Filter to mailboxes accessible by this user (respects access_mode)"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/EmailMailboxListResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"post":{"tags":["Email Mailboxes"],"summary":"Create Email Mailbox","description":"Create a new email mailbox.\n\nEither `email_integration_id` or `email_provider_config_id` must be set\n(but not both).  If `is_default` is True the previous default mailbox\nfor the tenant is unset.\n\n**Required Scopes:** `channels:create`","operationId":"create_email_mailbox_api_v1_email_mailboxes_post","security":[{"HTTPBearer":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/EmailMailboxCreate"}}}},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/EmailMailboxResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/email-mailboxes/{mailbox_id}":{"get":{"tags":["Email Mailboxes"],"summary":"Get Email Mailbox","description":"Get a single email mailbox by ID.\n\n**Required Scopes:** `channels:read`","operationId":"get_email_mailbox_api_v1_email_mailboxes__mailbox_id__get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"mailbox_id","in":"path","required":true,"schema":{"type":"string","title":"Mailbox Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/EmailMailboxResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"put":{"tags":["Email Mailboxes"],"summary":"Update Email Mailbox","description":"Update an email mailbox.\n\nWhen setting `is_default=true`, the previous default mailbox for the\ntenant is automatically unset.\n\n**Required Scopes:** `channels:update`","operationId":"update_email_mailbox_api_v1_email_mailboxes__mailbox_id__put","security":[{"HTTPBearer":[]}],"parameters":[{"name":"mailbox_id","in":"path","required":true,"schema":{"type":"string","title":"Mailbox Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/EmailMailboxUpdate"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/EmailMailboxResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"tags":["Email Mailboxes"],"summary":"Delete Email Mailbox","description":"Delete an email mailbox (soft-delete: sets is_active=False).\n\n**Required Scopes:** `channels:delete`","operationId":"delete_email_mailbox_api_v1_email_mailboxes__mailbox_id__delete","security":[{"HTTPBearer":[]}],"parameters":[{"name":"mailbox_id","in":"path","required":true,"schema":{"type":"string","title":"Mailbox Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SuccessResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/inboxes":{"get":{"tags":["Custom Inboxes"],"summary":"List Inboxes","description":"List custom inboxes the caller can access.\n\nAdmins / owners see every inbox in the tenant. Other users see inboxes\nwhose ``access_mode='all'`` plus any inbox where they're listed in the\nACL.\n\nInboxes are saved conversation views, so reads are gated by\n``conversations:read`` (a dedicated ``inboxes:read`` scope is redundant\nand blocked Support Agent / Viewer roles unnecessarily).\n\n**Required Scopes:** ``conversations:read``","operationId":"list_inboxes_api_v1_inboxes_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InboxListResponse"}}}}},"security":[{"HTTPBearer":[]}]},"post":{"tags":["Custom Inboxes"],"summary":"Create Inbox","description":"Create a new inbox.\n\n**Required Scopes:** ``inboxes:create`` (+ plan feature ``custom_inboxes``)","operationId":"create_inbox_api_v1_inboxes_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/InboxCreate"}}},"required":true},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InboxOut"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"HTTPBearer":[]}]}},"/api/v1/inboxes/{inbox_id}":{"get":{"tags":["Custom Inboxes"],"summary":"Get Inbox","description":"Get a single inbox. 404 if it doesn't exist or the caller can't see it.\n\nInboxes are saved conversation views, so reads are gated by\n``conversations:read``.\n\n**Required Scopes:** ``conversations:read``","operationId":"get_inbox_api_v1_inboxes__inbox_id__get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"inbox_id","in":"path","required":true,"schema":{"type":"string","title":"Inbox Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InboxOut"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"patch":{"tags":["Custom Inboxes"],"summary":"Update Inbox","description":"Partial update.\n\n**Required Scopes:** ``inboxes:update`` (+ plan feature ``custom_inboxes``)","operationId":"update_inbox_api_v1_inboxes__inbox_id__patch","security":[{"HTTPBearer":[]}],"parameters":[{"name":"inbox_id","in":"path","required":true,"schema":{"type":"string","title":"Inbox Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/InboxUpdate"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InboxOut"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"tags":["Custom Inboxes"],"summary":"Delete Inbox","description":"Hard delete an inbox + child rows.\n\n**Required Scopes:** ``inboxes:delete`` (+ plan feature ``custom_inboxes``)","operationId":"delete_inbox_api_v1_inboxes__inbox_id__delete","security":[{"HTTPBearer":[]}],"parameters":[{"name":"inbox_id","in":"path","required":true,"schema":{"type":"string","title":"Inbox Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SuccessResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/inboxes/{inbox_id}/sender-options":{"get":{"tags":["Custom Inboxes"],"summary":"Get Inbox Sender Options","description":"List email senders the caller may use for composing a NEW email from\ninside this inbox.\n\nIf the inbox has exactly one receiving email connection and that\nconnection has a configured ``outbound_provider_id``, that's the\npre-selected default. Otherwise the caller picks from their accessible\nsupport-capable mailboxes.\n\nInboxes are saved conversation views, so reads are gated by\n``conversations:read``.\n\n**Required Scopes:** ``conversations:read``","operationId":"get_inbox_sender_options_api_v1_inboxes__inbox_id__sender_options_get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"inbox_id","in":"path","required":true,"schema":{"type":"string","title":"Inbox Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/inboxes/{inbox_id}/users":{"put":{"tags":["Custom Inboxes"],"summary":"Set Inbox Users","description":"Replace the full per-inbox ACL list.\n\n**Required Scopes:** ``inboxes:update`` (+ plan feature ``custom_inboxes``)","operationId":"set_inbox_users_api_v1_inboxes__inbox_id__users_put","security":[{"HTTPBearer":[]}],"parameters":[{"name":"inbox_id","in":"path","required":true,"schema":{"type":"string","title":"Inbox Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/InboxUsersUpdate"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InboxOut"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/users/{user_id}/inbox-access":{"get":{"tags":["User Inbox Access"],"summary":"Get per-user inbox access settings","description":"Get the target user's inbox-access mode + the inboxes they're listed in.\n\nReturns 200 with ``{mode: 'all', inbox_ids: []}`` for a freshly-added team\nmember who has no overrides — ``mode`` defaults to ``'all'`` (PRIMARY\nserver_default) and the SHARD grant list starts empty.\n\nReturns 404 if the target user is not a member of this tenant. We use 404\nrather than 403 to avoid leaking the existence of cross-tenant user IDs.\n\n**Required Scopes:** ``team:read``","operationId":"get_user_inbox_access_api_v1_users__user_id__inbox_access_get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"user_id","in":"path","required":true,"schema":{"type":"string","title":"User Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserInboxAccessResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"put":{"tags":["User Inbox Access"],"summary":"Update per-user inbox access settings","description":"Update the user's mode + replace their per-inbox grants.\n\nBehaviour:\n- Validates target user is a member of this tenant (404 otherwise — using\n  404 over 403 to avoid an existence oracle).\n- Validates every ``inbox_ids`` element belongs to this tenant (422\n  otherwise).\n- Updates ``tenant_users.inbox_access_mode`` on PRIMARY.\n- Reconciles the user's rows in ``custom_inbox_users`` on SHARD to the\n  target set (delete grants no longer wanted, insert only missing ones).\n\nCross-DB write contract:\n    The mode lives on PRIMARY; the grants live on SHARD. We cannot wrap\n    both writes in a single transaction. Order is PRIMARY first, then\n    SHARD. If the SHARD write fails AFTER the PRIMARY commit, the user is\n    left with the new mode but stale grants — we log loudly and re-raise\n    so the client sees the failure (and the frontend can avoid\n    invalidating its cache, prompting the operator to retry). A retry of\n    the same PUT is idempotent because PRIMARY will already be at the\n    target value and the SHARD reconcile only touches the delta (a\n    re-grant of an already-present inbox is a no-op, not a 1062 collision).\n\n**Required Scopes:** ``team:update``","operationId":"update_user_inbox_access_api_v1_users__user_id__inbox_access_put","security":[{"HTTPBearer":[]}],"parameters":[{"name":"user_id","in":"path","required":true,"schema":{"type":"string","title":"User Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserInboxAccessUpdate"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserInboxAccessResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/users/{user_id}/tags":{"get":{"tags":["User Inbox Access"],"summary":"List a team member's tags","description":"List the tags assigned to a team member.\n\nMember tags are used by custom inboxes with \"specific members\" access:\nmembers holding any of the inbox's access tags can see the inbox.\n\nReturns 404 if the user is not a member of this workspace.\n\n**Required Scopes:** `team:read`","operationId":"get_user_tags_api_v1_users__user_id__tags_get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"user_id","in":"path","required":true,"schema":{"type":"string","title":"User Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserTagsResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"put":{"tags":["User Inbox Access"],"summary":"Replace a team member's tags","description":"Replace the full set of tags assigned to a team member.\n\nSend every tag the member should hold; tags not in ``tag_ids`` are\nremoved, an empty list removes all. Re-sending the same set is a no-op.\n\n- 404 if the user is not a member of this workspace.\n- 422 if any ``tag_ids`` element is not a tag of this workspace.\n\n**Required Scopes:** `team:update`","operationId":"update_user_tags_api_v1_users__user_id__tags_put","security":[{"HTTPBearer":[]}],"parameters":[{"name":"user_id","in":"path","required":true,"schema":{"type":"string","title":"User Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserTagsUpdate"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserTagsResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/messages/{message_id}/translate":{"post":{"tags":["Chat Translation"],"summary":"Translate Message","description":"Translate an inbound message and cache the result in message meta.\n\nCached translations are stored in `message.meta.translations` so repeated\nrequests for the same language return instantly.\n\n**Required Scopes:** `messages:read`","operationId":"translate_message_api_v1_messages__message_id__translate_post","security":[{"HTTPBearer":[]}],"parameters":[{"name":"message_id","in":"path","required":true,"schema":{"type":"string","title":"Message Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TranslateMessageRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TranslateMessageResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/email-messages/{email_message_id}/translate":{"post":{"tags":["Chat Translation"],"summary":"Translate Email Message","description":"Translate an email message body and cache the result in email metadata.\n\nCached translations are stored in `email_metadata.translations` so repeated\nrequests for the same language return instantly.\n\n**Required Scopes:** `messages:read`","operationId":"translate_email_message_api_v1_email_messages__email_message_id__translate_post","security":[{"HTTPBearer":[]}],"parameters":[{"name":"email_message_id","in":"path","required":true,"schema":{"type":"string","title":"Email Message Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TranslateMessageRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TranslateMessageResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/translate":{"post":{"tags":["Chat Translation"],"summary":"Translate Text","description":"Translate arbitrary text for outbound message composition.\n\nWhen `target_language` is \"auto\", the language is detected from\n`context_messages` (recent inbound messages from the contact).\n\n**Required Scopes:** `messages:create`","operationId":"translate_text_api_v1_translate_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TranslateTextRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TranslateTextResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"HTTPBearer":[]}]}},"/api/v1/audit-log":{"get":{"tags":["Audit Log"],"summary":"List Audit Log","description":"List API audit log entries for the current tenant.\n\nReturns paginated, sanitized records of API calls made against this tenant.\nSorted by most recent first.\n\n**Required Scopes:** `settings:admin`","operationId":"list_audit_log_api_v1_audit_log_get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"method","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Filter by HTTP method (GET, POST, etc.)","title":"Method"},"description":"Filter by HTTP method (GET, POST, etc.)"},{"name":"status_code","in":"query","required":false,"schema":{"anyOf":[{"type":"integer"},{"type":"null"}],"description":"Filter by status code","title":"Status Code"},"description":"Filter by status code"},{"name":"path","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Filter by path (partial match)","title":"Path"},"description":"Filter by path (partial match)"},{"name":"start_date","in":"query","required":false,"schema":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"description":"Filter from date (ISO 8601)","title":"Start Date"},"description":"Filter from date (ISO 8601)"},{"name":"end_date","in":"query","required":false,"schema":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"description":"Filter to date (ISO 8601)","title":"End Date"},"description":"Filter to date (ISO 8601)"},{"name":"page","in":"query","required":false,"schema":{"type":"integer","minimum":1,"description":"Page number","default":1,"title":"Page"},"description":"Page number"},{"name":"page_size","in":"query","required":false,"schema":{"type":"integer","maximum":100,"minimum":1,"description":"Items per page","default":50,"title":"Page Size"},"description":"Items per page"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/sms/available-numbers":{"get":{"tags":["SMS"],"summary":"List MSISDNs available to purchase in a country","description":"List numbers offered by seven.io for the given country.\n\nResults are cached per-tenant for 5 minutes to keep the pick-a-number\npicker snappy and to avoid hammering seven.io when a customer pages\nthrough options.\n\n**Required Scopes:** ``channels:read``","operationId":"list_available_numbers_api_v1_sms_available_numbers_get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"country","in":"query","required":true,"schema":{"type":"string","minLength":2,"maxLength":2,"description":"ISO-3166 alpha-2","title":"Country"},"description":"ISO-3166 alpha-2"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AvailableNumbersListResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/sms/balance":{"get":{"tags":["SMS"],"summary":"Current prepaid SMS balance","description":"Return the tenant's current seven.io subaccount balance.\n\n**Required Scopes:** ``channels:read``","operationId":"get_balance_api_v1_sms_balance_get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"force_refresh","in":"query","required":false,"schema":{"type":"boolean","default":false,"title":"Force Refresh"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BalanceResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/sms/low-balance-threshold":{"put":{"tags":["SMS"],"summary":"Set the low-balance warning threshold","description":"Set (or clear) the workspace's SMS low-balance **warning** threshold.\n\nWhen the prepaid balance drops below this amount the workspace owners and\nadmins receive the low-balance warning (email + in-app, at most once per\n24h). This is warn-only — it never triggers a charge (see auto-topup).\n\nBody: ``{\"threshold_eur\": <0 – 1000, max 2 decimals> | null}`` —\n``null`` removes the per-workspace override so the platform default\napplies again. Returns the updated settings resource (same shape as\n``GET /sms/balance``); the balance value is the cached one (no forced\nprovider refresh).\n\n**Required Scopes:** ``channels:update``","operationId":"update_low_balance_threshold_api_v1_sms_low_balance_threshold_put","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/LowBalanceThresholdUpdateRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BalanceResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"HTTPBearer":[]}]}},"/api/v1/sms/senders/verify":{"post":{"tags":["SMS"],"summary":"Start DTMF voice verification for an own MSISDN","description":"Kick off seven.io's DTMF voice verification.\n\nseven.io places an outbound call that reads a PIN aloud; the customer\nrepeats it back to our UI which POSTs it to\n:func:`confirm_sender` below.\n\n**Required Scopes:** ``channels:update``","operationId":"verify_sender_api_v1_sms_senders_verify_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SenderVerifyRequest"}}},"required":true},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SenderVerifyResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"HTTPBearer":[]}]}},"/api/v1/sms/senders/confirm":{"post":{"tags":["SMS"],"summary":"Confirm DTMF voice PIN and bind Channel","description":"Compare the PIN supplied by the customer to the one seven.io read aloud.\n\nOn success we flip the verification row to ``VERIFIED`` and create the\nSMS :class:`Channel`. On mismatch we flip to ``FAILED`` and 400.\n\n**Required Scopes:** ``channels:update``","operationId":"confirm_sender_api_v1_sms_senders_confirm_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SenderConfirmRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SenderConfirmResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"HTTPBearer":[]}]}},"/api/v1/sms/senders/alphanumeric":{"post":{"tags":["SMS"],"summary":"Register an alphanumeric SMS sender id (no verification)","description":"Register an alphanumeric sender id for outbound campaigns.\n\nEnforces seven.io's 11-character upper bound. Replies are not possible\nfor alphanumeric senders — the frontend copy communicates this.\n\n**Required Scopes:** ``channels:update``","operationId":"create_alphanumeric_sender_api_v1_sms_senders_alphanumeric_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AlphanumericSenderRequest"}}},"required":true},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AlphanumericSenderResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"HTTPBearer":[]}]}},"/api/v1/sms/senders":{"get":{"tags":["SMS"],"summary":"List the tenant's configured SMS senders","description":"List every SMS sender configured for the tenant.\n\nThe frontend cannot get this from ``/channels``: ``ChannelResponse``\ndeliberately omits ``Channel.config`` (it holds API tokens and secrets),\nso ``sender_type`` / ``mode`` / ``msisdn_or_alpha`` were invisible. This\nendpoint returns a hand-whitelisted projection of those non-secret facts.\n``seven_io_subaccount_id`` and every other credential stay server-side.\n\nInactive / archived senders ARE included so the UI can show them as\narchived, but they are never ``is_default_for_send``.\n\nArchived channels have ``config = None`` (``Channel.archive()`` calls\n``_clear_credentials()``), so ``sender_type`` is recovered from the\n``sms_phone_numbers.number_type`` row (which survives archiving) or from\nthe identifier shape. One case remains genuinely undecidable: an archived\nrow with no ``sms_phone_numbers`` row and a phone-shaped identifier, where\n``dedicated`` and ``verified_own`` are indistinguishable. Those rows are\nomitted (and logged) rather than guessed — ``pagination.total`` counts only\nrows that are actually returned. Active channels always carry\n``config[\"sender_type\"]``, so a live sender can never be omitted.\n\n``is_default_for_send`` marks the ONE sender an auto-selected send will\nactually use. It comes from the same\n:func:`app.services.sms.sender_identity.pick_default_sms_channel` helper\nthe send path in ``app/api/messages.py`` calls, so the badge is truthful\nby construction rather than by convention.\n\n**Required Scopes:** ``channels:read``","operationId":"list_senders_api_v1_sms_senders_get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"page","in":"query","required":false,"schema":{"type":"integer","minimum":1,"default":1,"title":"Page"}},{"name":"page_size","in":"query","required":false,"schema":{"type":"integer","maximum":200,"minimum":1,"default":50,"title":"Page Size"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PaginatedResponse_SmsSenderResponse_"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/sms/senders/{channel_id}":{"delete":{"tags":["SMS"],"summary":"Release a dedicated number or archive an alphanumeric/verified sender","description":"Delete / release an SMS sender.\n\nFor ``dedicated`` senders this calls\n:meth:`SmsProvisioningService.release_number` (which talks to\nseven.io). For ``alphanumeric`` / ``verified_own`` senders we only\narchive the :class:`Channel` row — no seven.io resource to release.\n\n**Required Scopes:** ``channels:delete``","operationId":"delete_sender_api_v1_sms_senders__channel_id__delete","security":[{"HTTPBearer":[]}],"parameters":[{"name":"channel_id","in":"path","required":true,"schema":{"type":"string","title":"Channel Id"}},{"name":"immediate","in":"query","required":false,"schema":{"type":"boolean","description":"If true, release seven.io number immediately","default":false,"title":"Immediate"},"description":"If true, release seven.io number immediately"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeleteSenderResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/sms/message-pricing":{"get":{"tags":["SMS"],"summary":"Estimated retail price to send one SMS to a country / number","description":"Estimate the tenant's retail per-segment SMS price.\n\nAccepts either a ``phone`` (preferred — country resolved by E.164\nprefix), a ``country`` ISO-2 hint, or both. Pricing is sourced live\nfrom seven.io's ``GET /api/pricing`` (cached 24h in Redis), with a\nbundled fallback file used if both Redis and the live API are\nunreachable.\n\nThe returned ``platform_fee_eur`` is the MIN price across all networks\nfor the destination country (a conservative estimate — actual route\nmay cost more for a specific recipient). ``sendseven_fee_eur`` is our\nflat margin. ``total_per_segment_eur = platform_fee + sendseven_fee``.\n\n**Required Scopes:** ``channels:read``","operationId":"message_pricing_api_v1_sms_message_pricing_get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"phone","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Recipient MSISDN (E.164)","title":"Phone"},"description":"Recipient MSISDN (E.164)"},{"name":"country","in":"query","required":false,"schema":{"anyOf":[{"type":"string","minLength":2,"maxLength":3},{"type":"null"}],"description":"ISO-3166 alpha-2 (e.g. 'DE') — alternative to phone","title":"Country"},"description":"ISO-3166 alpha-2 (e.g. 'DE') — alternative to phone"},{"name":"text","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Optional message body. When supplied, response includes segment count + total cost.","title":"Text"},"description":"Optional message body. When supplied, response includes segment count + total cost."},{"name":"append_opt_out_link","in":"query","required":false,"schema":{"type":"boolean","description":"Price the text as a Reach campaign with the SMS opt-out link enabled: a representative ' Unsubscribe: https://c.sendseven.com/<code>' suffix (same length and encoding as the real per-recipient link) is appended before segments, char_count and total are computed.","default":false,"title":"Append Opt Out Link"},"description":"Price the text as a Reach campaign with the SMS opt-out link enabled: a representative ' Unsubscribe: https://c.sendseven.com/<code>' suffix (same length and encoding as the real per-recipient link) is appended before segments, char_count and total are computed."}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MessagePricingResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/sms/pricelist":{"get":{"tags":["SMS"],"summary":"Full per-country SMS pricelist (platform fee + SendSeven margin)","description":"Return the full SMS pricelist as a per-country table.\n\nUsed by the frontend pricing page. Pricing is sourced live from\nseven.io (cached 24h), with a bundled fallback file as last resort.\nThe ``source`` field reports which one served this request.\n\n**Required Scopes:** ``channels:read``","operationId":"get_sms_pricelist_api_v1_sms_pricelist_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SmsPricelistResponse"}}}}},"security":[{"HTTPBearer":[]}]}}},"components":{"schemas":{"AIAnswerEventListResponse":{"properties":{"items":{"items":{"$ref":"#/components/schemas/AIAnswerEventResponse"},"type":"array","title":"Items"},"pagination":{"$ref":"#/components/schemas/PaginationMeta"}},"type":"object","required":["items","pagination"],"title":"AIAnswerEventListResponse"},"AIAnswerEventResponse":{"properties":{"id":{"type":"string","title":"Id"},"tenant_id":{"type":"string","title":"Tenant Id"},"conversation_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Conversation Id"},"message_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Message Id"},"bot_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Bot Id"},"surface":{"type":"string","title":"Surface","description":"suggest | bot | flow | team_chat | auto_reply"},"user_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"User Id"},"query_text":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Query Text"},"retrieval_query":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Retrieval Query"},"answer_text":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Answer Text"},"model":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Model"},"confidence":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Confidence"},"outcome":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Outcome","description":"shown | accepted | sent | discarded"},"flagged":{"type":"boolean","title":"Flagged","default":false},"created_at":{"type":"string","format":"date-time","title":"Created At"},"agent_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Agent Id","description":"ai_agents id when a Conversational Agent wrote the answer"},"agent_turn_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Agent Turn Id","description":"Agent turn trace id (GET /automation/agents/{id}/turns)"},"agent_context":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Agent Context","description":"model, model_tier, outcome, skills[{slug, title, version, version_id, skill_id, via}], tools"},"sources":{"items":{"$ref":"#/components/schemas/AIAnswerSource"},"type":"array","title":"Sources"},"my_rating":{"anyOf":[{"$ref":"#/components/schemas/AIAnswerRatingSummary"},{"type":"null"}],"description":"The requesting user's own verdict, if they rated it"},"rating_counts":{"additionalProperties":{"type":"integer"},"type":"object","title":"Rating Counts","description":"e.g. {\"up\": 3, \"down\": 1, \"wrong\": 0}"},"correction_ids":{"items":{"type":"string"},"type":"array","title":"Correction Ids","description":"Corrections raised from this answer"}},"type":"object","required":["id","tenant_id","surface","created_at"],"title":"AIAnswerEventResponse","description":"One AI answer + everything the sources panel and rating control need."},"AIAnswerRateRequest":{"properties":{"verdict":{"type":"string","title":"Verdict","description":"up | down | wrong"},"reason_category":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Reason Category","description":"Why the answer was bad: factually_wrong | outdated | incomplete | off_topic | wrong_tone | policy_violation | hallucinated | other. Optional for 'down' (without it the 👎 is a statistic only; with it a review-only correction is opened). For 'wrong' it defaults to 'factually_wrong'."},"comment":{"anyOf":[{"type":"string","maxLength":5000},{"type":"null"}],"title":"Comment"},"better_answer":{"anyOf":[{"type":"string","maxLength":20000},{"type":"null"}],"title":"Better Answer","description":"What the answer should have said"},"source_attribution":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Source Attribution","description":"faq_items.id values the rater explicitly blames. Takes full weight with no sharing (ignored for 'up' verdicts)."}},"type":"object","required":["verdict"],"title":"AIAnswerRateRequest"},"AIAnswerRateResponse":{"properties":{"success":{"type":"boolean","title":"Success","default":true},"rating":{"$ref":"#/components/schemas/AIAnswerRatingSummary"},"ai_answer_event_id":{"type":"string","title":"Ai Answer Event Id"},"correction_opened":{"type":"boolean","title":"Correction Opened","description":"True when this rating opened a KB correction and queued it for AI analysis. Always true for 'wrong'; true for 'down' only when the rater wrote substantive text in comment and/or better_answer. False means the verdict was recorded as a score signal only — the UI should NOT promise the feedback becomes a FAQ entry.","default":false},"correction_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Correction Id","description":"The kb_corrections id when correction_opened is true — link the user to /knowledge-base/corrections. Null otherwise."},"updated_entries":{"items":{"$ref":"#/components/schemas/EntryScoreUpdate"},"type":"array","title":"Updated Entries"}},"type":"object","required":["rating","ai_answer_event_id"],"title":"AIAnswerRateResponse"},"AIAnswerRatingSummary":{"properties":{"id":{"type":"string","title":"Id"},"verdict":{"type":"string","title":"Verdict"},"reason_category":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Reason Category"},"comment":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Comment"},"better_answer":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Better Answer"},"rated_by_user_id":{"type":"string","title":"Rated By User Id"},"source_attribution":{"anyOf":[{"items":{"additionalProperties":true,"type":"object"},"type":"array"},{"type":"null"}],"title":"Source Attribution"},"agent_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Agent Id","description":"Conversational Agent that produced the rated answer (null for FAQ bots)"},"agent_context":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Agent Context","description":"Snapshot at rating time: model, model_tier, skills[{slug, version, ...}], tools, turn_id"},"created_at":{"type":"string","format":"date-time","title":"Created At"},"updated_at":{"type":"string","format":"date-time","title":"Updated At"}},"type":"object","required":["id","verdict","rated_by_user_id","created_at","updated_at"],"title":"AIAnswerRatingSummary","description":"The current user's verdict on an answer, if any."},"AIAnswerSource":{"properties":{"kind":{"type":"string","title":"Kind","description":"faq_entry | document | website | history"},"entry_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Entry Id","description":"faq_items.id when kind=faq_entry"},"document_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Document Id","description":"knowledge_base_documents.id"},"conversation_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Conversation Id","description":"Source conversation when kind=history"},"title":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Title"},"uri":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Uri","description":"gs:// or https:// location"},"excerpt":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Excerpt","description":"Matched chunk, <= 500 chars"},"score":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Score","description":"Retrieval similarity, if known"},"rank":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Rank","description":"1-based position in the result set"},"cited":{"type":"boolean","title":"Cited","description":"The generator grounded on this source","default":false},"entry_status":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Entry Status"},"entry_quality_score":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Entry Quality Score","description":"NULL = not enough rating mass to judge"},"entry_verified_by_usage":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Entry Verified By Usage"},"entry_times_in_sent_reply":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Entry Times In Sent Reply"},"entry_flagged_count":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Entry Flagged Count"}},"type":"object","required":["kind"],"title":"AIAnswerSource","description":"One retrieved chunk that fed an answer."},"AIPerformanceResponse":{"properties":{"total_bot_messages":{"type":"integer","title":"Total Bot Messages","description":"Count of messages sent by bots"},"total_manual_messages":{"type":"integer","title":"Total Manual Messages","description":"Count of outbound messages NOT from bots"},"bot_sessions_count":{"type":"integer","title":"Bot Sessions Count","description":"Total bot conversation sessions"},"avg_confidence_score":{"type":"number","title":"Avg Confidence Score","description":"Average confidence score (0.0-1.0, multiply by 100 for %)"},"escalation_rate":{"type":"number","title":"Escalation Rate","description":"Percentage of sessions that escalated to human agent"},"period_start":{"type":"string","title":"Period Start","description":"Start of the reporting period (ISO format)"},"period_end":{"type":"string","title":"Period End","description":"End of the reporting period (ISO format)"}},"type":"object","required":["total_bot_messages","total_manual_messages","bot_sessions_count","avg_confidence_score","escalation_rate","period_start","period_end"],"title":"AIPerformanceResponse","description":"AI/Bot performance metrics for dashboard.\n\nReturned by GET /api/v1/analytics/ai/performance","example":{"avg_confidence_score":0.85,"bot_sessions_count":150,"escalation_rate":15.5,"period_end":"2026-01-18T00:00:00Z","period_start":"2026-01-01T00:00:00Z","total_bot_messages":500,"total_manual_messages":200}},"AccessMode":{"type":"string","enum":["all","specific"],"title":"AccessMode","description":"Who can use this mailbox"},"AccountBillingAccessRequest":{"properties":{"enabled":{"type":"boolean","title":"Enabled","description":"True grants the member a direct `account:billing_read` scope; False revokes it"}},"type":"object","required":["enabled"],"title":"AccountBillingAccessRequest","description":"Request body for toggling a member's account-wide usage access."},"AccountBillingAccessResponse":{"properties":{"user_id":{"type":"string","title":"User Id","description":"Target user ID"},"account_billing_read_enabled":{"type":"boolean","title":"Account Billing Read Enabled","description":"Whether the member now holds a direct `account:billing_read` grant"}},"type":"object","required":["user_id","account_billing_read_enabled"],"title":"AccountBillingAccessResponse","description":"Result of a member account-wide-usage access toggle."},"AccountInsightsResponse":{"properties":{"channel_id":{"type":"string","title":"Channel Id"},"platform":{"type":"string","title":"Platform","description":"`instagram` or `messenger`."},"object_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Object Id","description":"The IG account id / FB page id the insights are for."},"authorized":{"type":"boolean","title":"Authorized","description":"False when the connection lacks the insights permission.","default":true},"error_code":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Error Code","description":"`insights_not_authorized`, `insights_unavailable`, or `insights_error`. Null on success."},"message":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Message","description":"Human-readable detail."},"metrics":{"$ref":"#/components/schemas/SocialInsightsMetrics"},"retrieved_at":{"type":"string","title":"Retrieved At","description":"When these numbers were fetched (nothing is stored)."}},"type":"object","required":["channel_id","platform","retrieved_at"],"title":"AccountInsightsResponse","description":"Live account-/page-level insights for one connected channel."},"ActiveCrawlJobSummary":{"properties":{"job_id":{"type":"string","title":"Job Id","description":"Unique job ID"},"status":{"type":"string","title":"Status","description":"Current status"},"message":{"type":"string","title":"Message","description":"Human-readable status message"},"base_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Base Url","description":"Base URL being crawled"},"discovered_count":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Discovered Count","description":"Unique pages discovered so far"},"pages_crawled":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Pages Crawled","description":"Pages successfully crawled so far"},"total_pages":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Total Pages","description":"Total unique pages to crawl"},"documents_created":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Documents Created","description":"Documents created so far"},"progress_percent":{"type":"integer","title":"Progress Percent","description":"Progress percentage (0-100)","default":0},"created_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Created At","description":"ISO datetime when job was created"},"started_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Started At","description":"ISO datetime when job started"}},"type":"object","required":["job_id","status","message"],"title":"ActiveCrawlJobSummary","description":"Summary of an active crawl job for listing.","example":{"base_url":"https://docs.example.com","created_at":"2025-01-15T10:30:00Z","discovered_count":43,"documents_created":0,"job_id":"job-uuid-123","message":"Processing pages... 15/43","pages_crawled":15,"progress_percent":35,"started_at":"2025-01-15T10:30:05Z","status":"uploading","total_pages":43}},"ActiveCrawlJobsResponse":{"properties":{"jobs":{"items":{"$ref":"#/components/schemas/ActiveCrawlJobSummary"},"type":"array","title":"Jobs","description":"List of active crawl jobs"},"total":{"type":"integer","title":"Total","description":"Total number of active jobs","default":0}},"type":"object","title":"ActiveCrawlJobsResponse","description":"Response for listing active crawl jobs.","example":{"jobs":[{"base_url":"https://docs.example.com","discovered_count":25,"job_id":"job-uuid-123","message":"Discovering pages...","status":"discovering"}],"total":1}},"AddContactToListRequest":{"properties":{"contact_id":{"type":"string","title":"Contact Id","description":"ID of the contact to add"},"channel_type":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Channel Type","description":"Required for newsletter subscriptions (email, telegram, messenger, instagram, ...). DEPRECATED fallback: if omitted for a newsletter list it defaults to email, but this fallback will be removed in a future release — always pass channel_type."},"channel_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Channel Id","description":"Channel ID (which bot/page). Required only for scoped newsletter channels: telegram, messenger, instagram."}},"type":"object","required":["contact_id"],"title":"AddContactToListRequest","description":"Schema for adding a contact to a list","examples":[{"contact_id":"cnt-a1b2c3d4"},{"channel_type":"telegram","contact_id":"cnt-a1b2c3d4"}]},"AddMemberResponse":{"properties":{"success":{"type":"boolean","title":"Success","description":"Whether the operation succeeded","default":true},"message":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Message","description":"Optional success message"},"member_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Member Id","description":"ID of the membership/subscription record"}},"type":"object","title":"AddMemberResponse","description":"Response for adding a contact to a list","example":{"member_id":"sub-a1b2c3d4","message":"Contact added to list","success":true}},"AddTagsAction":{"properties":{"type":{"type":"string","const":"add_tags","title":"Type"},"tags":{"items":{"type":"string"},"type":"array","minItems":1,"title":"Tags","description":"Tag IDs to add (tenant-owned)"}},"type":"object","required":["type","tags"],"title":"AddTagsAction"},"AddToListAction":{"properties":{"type":{"type":"string","const":"add_to_list","title":"Type"},"list_id":{"type":"string","title":"List Id","description":"List ID to subscribe the contacts to (tenant-owned)"},"channel_type":{"type":"string","enum":["email","sms","whatsapp"],"title":"Channel Type","description":"Subscription channel type for the list membership (default email). Contacts with an existing subscription row for this list+channel (any status, including unsubscribed) are skipped — a bulk action never resurrects an opt-out.","default":"email"}},"type":"object","required":["type","list_id"],"title":"AddToListAction"},"AdoptLibraryTemplateRequest":{"properties":{"channel_id":{"type":"string","title":"Channel Id","description":"WhatsApp channel whose WABA receives the template"},"library_template_name":{"type":"string","title":"Library Template Name","description":"``name`` of the library entry (e.g. ``delivery_update_1``)"},"language":{"type":"string","title":"Language","description":"Meta language code to create it in","default":"en_US"},"category":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Category","description":"Defaults to the library entry's category (UTILITY / AUTHENTICATION)"},"name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Name","description":"Name for the created template. Defaults to the library template name. Normalised to lowercase/underscores."},"button_inputs":{"anyOf":[{"items":{"$ref":"#/components/schemas/LibraryButtonInput"},"type":"array"},{"type":"null"}],"title":"Button Inputs","description":"Customer-specific button values. Omit to accept the library entry's own example values — this is what the one-click flow in /conversations does."},"snapshot":{"anyOf":[{"$ref":"#/components/schemas/LibraryTemplate"},{"type":"null"}],"description":"The library entry exactly as the client displayed it. Used as the content source when Meta rate-limits the confirmation lookup, so a 429 never blocks the customer's first send."}},"type":"object","required":["channel_id","library_template_name"],"title":"AdoptLibraryTemplateRequest"},"AdoptLibraryTemplateResponse":{"properties":{"success":{"type":"boolean","title":"Success"},"id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Id","description":"Our ``whatsapp_templates.id``"},"template_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Template Id","description":"Meta's template ID"},"name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Name"},"language":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Language"},"status":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Status","description":"Template status as reported by Meta. Unchanged library templates come back ``approved`` immediately."},"category":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Category"},"already_existed":{"type":"boolean","title":"Already Existed","description":"True when the tenant already had this template (no-op)","default":false},"error":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Error"},"error_code":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Error Code"},"error_subcode":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Error Subcode"},"error_is_retryable":{"type":"boolean","title":"Error Is Retryable","default":false},"fbtrace_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Fbtrace Id"}},"type":"object","required":["success"],"title":"AdoptLibraryTemplateResponse"},"AgentAnalyticsDay":{"properties":{"date":{"type":"string","title":"Date","description":"Day in the workspace timezone, YYYY-MM-DD"},"turns":{"type":"integer","title":"Turns","default":0},"handoffs":{"type":"integer","title":"Handoffs","default":0},"web_searches":{"type":"integer","title":"Web Searches","default":0},"live_reads":{"type":"integer","title":"Live Reads","default":0},"kb_zero_hit_turns":{"type":"integer","title":"Kb Zero Hit Turns","default":0},"grounding_rewrites":{"type":"integer","title":"Grounding Rewrites","default":0},"credits_billed":{"type":"integer","title":"Credits Billed","default":0}},"type":"object","required":["date"],"title":"AgentAnalyticsDay"},"AgentAnalyticsRates":{"properties":{"web_fallback_rate":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Web Fallback Rate","description":"web_turns / turns"},"kb_zero_hit_rate":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Kb Zero Hit Rate","description":"kb_zero_hit_turns / kb_search_turns"},"handoff_rate":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Handoff Rate","description":"handoffs / turns"},"grounding_rewrite_rate":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Grounding Rewrite Rate","description":"(grounding_rewrites + grounding_handoffs) / grounding_checked"},"resolved_pct":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Resolved Pct","description":"0..100, resolved_conversations / conversations; null below min_sample"},"handoff_pct":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Handoff Pct","description":"0..100, handoff_conversations / conversations; null below min_sample"}},"type":"object","title":"AgentAnalyticsRates"},"AgentAnalyticsResponse":{"properties":{"agent_id":{"type":"string","title":"Agent Id"},"days":{"type":"integer","title":"Days"},"from":{"type":"string","title":"From"},"to":{"type":"string","title":"To"},"timezone":{"type":"string","title":"Timezone","default":"UTC"},"truncated":{"type":"boolean","title":"Truncated","default":false},"min_sample":{"type":"integer","title":"Min Sample","default":20},"insufficient_data":{"type":"boolean","title":"Insufficient Data","default":false},"totals":{"$ref":"#/components/schemas/AgentAnalyticsTotals"},"rates":{"$ref":"#/components/schemas/AgentAnalyticsRates"},"credits_per_conversation":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Credits Per Conversation"},"daily":{"items":{"$ref":"#/components/schemas/AgentAnalyticsDay"},"type":"array","title":"Daily"}},"type":"object","required":["agent_id","days","from","to","totals","rates"],"title":"AgentAnalyticsResponse"},"AgentAnalyticsTotals":{"properties":{"turns":{"type":"integer","title":"Turns","default":0},"conversations":{"type":"integer","title":"Conversations","default":0},"answered":{"type":"integer","title":"Answered","default":0},"handoffs":{"type":"integer","title":"Handoffs","default":0},"fallbacks":{"type":"integer","title":"Fallbacks","default":0},"errors":{"type":"integer","title":"Errors","default":0},"kb_searches":{"type":"integer","title":"Kb Searches","default":0},"kb_zero_hit_turns":{"type":"integer","title":"Kb Zero Hit Turns","default":0},"kb_search_turns":{"type":"integer","title":"Kb Search Turns","default":0},"web_searches":{"type":"integer","title":"Web Searches","default":0},"web_turns":{"type":"integer","title":"Web Turns","default":0},"web_cache_hits":{"type":"integer","title":"Web Cache Hits","default":0},"live_reads":{"type":"integer","title":"Live Reads","default":0},"live_turns":{"type":"integer","title":"Live Turns","default":0},"enforced_lookups":{"type":"integer","title":"Enforced Lookups","default":0},"grounding_checked":{"type":"integer","title":"Grounding Checked","default":0},"grounding_rewrites":{"type":"integer","title":"Grounding Rewrites","default":0},"grounding_handoffs":{"type":"integer","title":"Grounding Handoffs","default":0},"credits_billed":{"type":"integer","title":"Credits Billed","default":0},"resolved_conversations":{"type":"integer","title":"Resolved Conversations","default":0},"handoff_conversations":{"type":"integer","title":"Handoff Conversations","default":0}},"type":"object","title":"AgentAnalyticsTotals"},"AgentBuilderAgentInput":{"properties":{"name":{"type":"string","maxLength":255,"minLength":1,"title":"Name"},"persona":{"anyOf":[{"type":"string","maxLength":15000},{"type":"null"}],"title":"Persona"},"instructions":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Instructions"},"model_tier":{"anyOf":[{"type":"string","enum":["standard","advanced"]},{"type":"null"}],"title":"Model Tier"},"base_language":{"anyOf":[{"type":"string","maxLength":16},{"type":"null"}],"title":"Base Language"},"language_mode":{"anyOf":[{"type":"string","enum":["auto","fixed"]},{"type":"null"}],"title":"Language Mode"},"default_language":{"anyOf":[{"type":"string","maxLength":16},{"type":"null"}],"title":"Default Language"},"supported_languages":{"items":{"type":"string"},"type":"array","maxItems":10,"title":"Supported Languages"},"tools_config":{"anyOf":[{"$ref":"#/components/schemas/ToolsConfig"},{"type":"null"}]},"handoff_config":{"anyOf":[{"$ref":"#/components/schemas/HandoffConfig"},{"type":"null"}]},"escalation_enabled":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Escalation Enabled"}},"type":"object","required":["name"],"title":"AgentBuilderAgentInput","description":"The (edited) ``AgentDraft.agent`` block."},"AgentBuilderCreateRequest":{"properties":{"build_id":{"type":"string","maxLength":36,"minLength":36,"title":"Build Id"},"agent":{"$ref":"#/components/schemas/AgentBuilderAgentInput"},"skills":{"items":{"$ref":"#/components/schemas/AgentBuilderSkillInput"},"type":"array","maxItems":7,"minItems":1,"title":"Skills"},"knowledge_sources":{"items":{"$ref":"#/components/schemas/AgentBuilderKnowledgeSourceInput"},"type":"array","maxItems":20,"title":"Knowledge Sources"},"live_sources":{"anyOf":[{"items":{"$ref":"#/components/schemas/AgentBuilderLiveSourceInput"},"type":"array","maxItems":5},{"type":"null"}],"title":"Live Sources"},"channel_ids":{"items":{"type":"string"},"type":"array","maxItems":40,"title":"Channel Ids"}},"additionalProperties":false,"type":"object","required":["build_id","agent","skills"],"title":"AgentBuilderCreateRequest"},"AgentBuilderEstimateResponse":{"properties":{"credits":{"type":"integer","title":"Credits","description":"AI credits charged for ONE successful build"},"price_euros":{"type":"string","title":"Price Euros","description":"credits x the plan's AI-credit rate, 2 decimals"},"ai_credit_rate_euros":{"type":"string","title":"Ai Credit Rate Euros","description":"The plan's price of one AI credit"},"billable":{"type":"boolean","title":"Billable","description":"Always true: every successful build is charged `credits` AI credits"},"charged_on":{"type":"string","title":"Charged On","description":"Failed builds and creating the agent are never charged","default":"successful_build"}},"type":"object","required":["credits","price_euros","ai_credit_rate_euros","billable"],"title":"AgentBuilderEstimateResponse"},"AgentBuilderGenerateRequest":{"properties":{"company_name":{"type":"string","maxLength":120,"minLength":1,"title":"Company Name"},"website_url":{"anyOf":[{"type":"string","maxLength":510},{"type":"null"}],"title":"Website Url","description":"Public company website. Its domain restricts web research; at most 3 pages on that domain are read through the SSRF-safe fetcher to propose live sources (events, opening hours, products)."},"goals":{"items":{"type":"string","enum":["answer_questions","qualify_leads","support","orders_shipping","collect_feedback","human_handoff"]},"type":"array","maxItems":6,"minItems":1,"title":"Goals"},"tone":{"type":"string","enum":["friendly","professional","casual","formal","enthusiastic"],"title":"Tone"},"base_language":{"anyOf":[{"type":"string","maxLength":16},{"type":"null"}],"title":"Base Language","description":"The agent's base language (BCP-47, e.g. 'de'). Required unless the legacy `languages` is sent."},"language_mode":{"type":"string","enum":["auto","fixed"],"title":"Language Mode","description":"auto = reply in the customer's language (auto-detect); fixed = always reply in base_language","default":"auto"},"languages":{"anyOf":[{"items":{"type":"string"},"type":"array","maxItems":10},{"type":"null"}],"title":"Languages","description":"DEPRECATED (old clients): languages[0] is used as base_language when base_language is not set"},"channel_ids":{"items":{"type":"string"},"type":"array","maxItems":40,"title":"Channel Ids","description":"The workspace's channels the agent will answer on (0-20)"},"notes":{"anyOf":[{"type":"string","maxLength":2000},{"type":"null"}],"title":"Notes"},"ui_language":{"anyOf":[{"type":"string","maxLength":16},{"type":"null"}],"title":"Ui Language","description":"Language the skills are written in (default: base_language)"},"request_id":{"anyOf":[{"type":"string","maxLength":64,"minLength":8,"pattern":"^[A-Za-z0-9_.:-]+$"},{"type":"null"}],"title":"Request Id","description":"Client idempotency key; a retry with the same key returns the same job and is never charged twice"}},"additionalProperties":false,"type":"object","required":["company_name","goals","tone"],"title":"AgentBuilderGenerateRequest"},"AgentBuilderJobError":{"properties":{"code":{"type":"string","title":"Code","description":"invalid_request | ai_unavailable | ai_error | interrupted (all: nothing was charged) | failed_after_charge (charged, draft lost) | charge_unknown (interrupted; charge could not be verified, poll again)"},"message":{"type":"string","title":"Message"},"credits_charged":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Credits Charged","description":"Set with failed_after_charge (credits charged for this build); otherwise null"}},"type":"object","required":["code","message"],"title":"AgentBuilderJobError"},"AgentBuilderJobResponse":{"properties":{"job_id":{"type":"string","title":"Job Id"},"status":{"type":"string","title":"Status","description":"queued | running | succeeded | failed"},"stage":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Stage","description":"researching | drafting | checking (only while running)"},"result":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Result","description":"AgentDraft when status=succeeded (build_id, agent, skills, knowledge_sources, live_sources, research, warnings, credits_charged). credits_charged is null if the ledger could not confirm the charge"},"error":{"anyOf":[{"$ref":"#/components/schemas/AgentBuilderJobError"},{"type":"null"}],"description":"Set when status=failed. Nothing was charged unless code=failed_after_charge (charge_unknown: not yet verified)"},"request_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Request Id"},"created_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Created At"},"updated_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Updated At"}},"type":"object","required":["job_id","status"],"title":"AgentBuilderJobResponse"},"AgentBuilderKnowledgeSourceInput":{"properties":{"source_type":{"type":"string","enum":["kb_folder","kb_all","web_search","faq"],"title":"Source Type"},"kb_folder_id":{"anyOf":[{"type":"string","maxLength":36},{"type":"null"}],"title":"Kb Folder Id"},"allowed_domains":{"anyOf":[{"items":{"type":"string"},"type":"array","maxItems":50},{"type":"null"}],"title":"Allowed Domains"},"is_enabled":{"type":"boolean","title":"Is Enabled","default":true}},"type":"object","required":["source_type"],"title":"AgentBuilderKnowledgeSourceInput"},"AgentBuilderLiveSourceInput":{"properties":{"url":{"type":"string","maxLength":2048,"minLength":1,"title":"Url"},"match":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Match"},"label":{"type":"string","maxLength":130,"minLength":1,"title":"Label"},"description":{"anyOf":[{"type":"string","maxLength":550},{"type":"null"}],"title":"Description"},"ttl_minutes":{"anyOf":[{},{"type":"null"}],"title":"Ttl Minutes"},"fetcher":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Fetcher"},"scope":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Scope"},"skill_slugs":{"items":{"type":"string"},"type":"array","maxItems":14,"title":"Skill Slugs"}},"type":"object","required":["url","label"],"title":"AgentBuilderLiveSourceInput","description":"One proposed live source the user kept (``AgentDraft.live_sources[]``).\n\nThe config keys are validated exactly like ``POST .../knowledge-sources``\n(``validate_live_source_config``); the SSRF/DNS check, the per-agent cap and\nthe unique label run in the commit service. ``skill_slugs`` = draft skills\nthat read it (``settings.skill_sources``). UI-only / draft-only keys\n(``kind``, ``live_mode``, ``key``, ``selected``) are ignored."},"AgentBuilderSkillInput":{"properties":{"key":{"type":"string","maxLength":64,"minLength":1,"title":"Key"},"slug":{"anyOf":[{"type":"string","maxLength":64},{"type":"null"}],"title":"Slug"},"title":{"type":"string","maxLength":255,"minLength":1,"title":"Title"},"description":{"type":"string","maxLength":1000,"minLength":1,"title":"Description"},"mode":{"anyOf":[{"type":"string","enum":["always_on","model_selected","flow_only"]},{"type":"null"}],"title":"Mode"},"body":{"type":"string","minLength":1,"title":"Body"},"attachment_instruction":{"anyOf":[{"$ref":"#/components/schemas/SkillAttachmentInstruction"},{"type":"null"}]}},"type":"object","required":["key","title","description","body"],"title":"AgentBuilderSkillInput","description":"One selected (possibly edited) draft skill. ``key`` matches the draft."},"AgentConfigDocument":{"properties":{"instructions":{"anyOf":[{"type":"string","maxLength":20000},{"type":"null"}],"title":"Instructions"},"model_tier":{"anyOf":[{"type":"string","enum":["standard","advanced"]},{"type":"null"}],"title":"Model Tier"},"tools_config":{"anyOf":[{"$ref":"#/components/schemas/ToolsConfig"},{"type":"null"}]},"handoff_config":{"anyOf":[{"$ref":"#/components/schemas/HandoffConfig"},{"type":"null"}]},"max_tool_calls":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Max Tool Calls"},"turn_timeout_ms":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Turn Timeout Ms"},"settings":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Settings"},"channel_actions":{"anyOf":[{"$ref":"#/components/schemas/ChannelActionsInput"},{"type":"null"}]},"persona":{"anyOf":[{"type":"string","maxLength":20000},{"type":"null"}],"title":"Persona"},"display_name":{"anyOf":[{"type":"string","maxLength":255},{"type":"null"}],"title":"Display Name"},"greeting_message":{"anyOf":[{"type":"string","maxLength":2000},{"type":"null"}],"title":"Greeting Message"},"ai_disclosure_message":{"anyOf":[{"type":"string","maxLength":2000},{"type":"null"}],"title":"Ai Disclosure Message"},"ai_disclosure_include_escalation":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Ai Disclosure Include Escalation"},"language_settings":{"anyOf":[{"$ref":"#/components/schemas/LanguageSettings"},{"type":"null"}]},"escalation":{"anyOf":[{"$ref":"#/components/schemas/EscalationSettings"},{"type":"null"}]},"source_footer_enabled":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Source Footer Enabled"},"source_footer_mode":{"anyOf":[{"type":"string","enum":["urls_only","urls_and_kb"]},{"type":"null"}],"title":"Source Footer Mode"},"source_link_params":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Source Link Params"},"name":{"anyOf":[{"type":"string","maxLength":255},{"type":"null"}],"title":"Name"},"avatar_url":{"anyOf":[{"type":"string","maxLength":500},{"type":"null"}],"title":"Avatar Url"},"greeting_buttons_enabled":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Greeting Buttons Enabled"},"greeting_human_button_caption":{"anyOf":[{"type":"string","maxLength":64},{"type":"null"}],"title":"Greeting Human Button Caption"},"greeting_continue_button_caption":{"anyOf":[{"type":"string","maxLength":64},{"type":"null"}],"title":"Greeting Continue Button Caption"},"greeting_continue_ack_message":{"anyOf":[{"type":"string","maxLength":1000},{"type":"null"}],"title":"Greeting Continue Ack Message"},"max_credits_per_conversation":{"anyOf":[{"type":"integer","maximum":10000.0,"minimum":1.0},{"type":"null"}],"title":"Max Credits Per Conversation"},"skills":{"anyOf":[{"items":{"$ref":"#/components/schemas/AgentSkillLinkInput"},"type":"array","maxItems":50},{"type":"null"}],"title":"Skills"},"knowledge_sources":{"anyOf":[{"items":{"$ref":"#/components/schemas/DraftKnowledgeSource"},"type":"array","maxItems":20},{"type":"null"}],"title":"Knowledge Sources"},"activation_rules":{"anyOf":[{"items":{"$ref":"#/components/schemas/DraftActivationRule"},"type":"array","maxItems":50},{"type":"null"}],"title":"Activation Rules"}},"additionalProperties":false,"type":"object","title":"AgentConfigDocument","description":"Everything the agent editor edits (contract section 2). ``is_active`` and\nbot priority are NOT part of it (they apply instantly)."},"AgentCreate":{"properties":{"instructions":{"anyOf":[{"type":"string","maxLength":20000},{"type":"null"}],"title":"Instructions"},"model_tier":{"anyOf":[{"type":"string","enum":["standard","advanced"]},{"type":"null"}],"title":"Model Tier"},"tools_config":{"anyOf":[{"$ref":"#/components/schemas/ToolsConfig"},{"type":"null"}]},"handoff_config":{"anyOf":[{"$ref":"#/components/schemas/HandoffConfig"},{"type":"null"}]},"max_tool_calls":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Max Tool Calls"},"turn_timeout_ms":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Turn Timeout Ms"},"settings":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Settings"},"channel_actions":{"anyOf":[{"$ref":"#/components/schemas/ChannelActionsInput"},{"type":"null"}]},"persona":{"anyOf":[{"type":"string","maxLength":20000},{"type":"null"}],"title":"Persona"},"display_name":{"anyOf":[{"type":"string","maxLength":255},{"type":"null"}],"title":"Display Name"},"greeting_message":{"anyOf":[{"type":"string","maxLength":2000},{"type":"null"}],"title":"Greeting Message"},"ai_disclosure_message":{"anyOf":[{"type":"string","maxLength":2000},{"type":"null"}],"title":"Ai Disclosure Message"},"ai_disclosure_include_escalation":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Ai Disclosure Include Escalation"},"language_settings":{"anyOf":[{"$ref":"#/components/schemas/LanguageSettings"},{"type":"null"}]},"escalation":{"anyOf":[{"$ref":"#/components/schemas/EscalationSettings"},{"type":"null"}]},"source_footer_enabled":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Source Footer Enabled"},"source_footer_mode":{"anyOf":[{"type":"string","enum":["urls_only","urls_and_kb"]},{"type":"null"}],"title":"Source Footer Mode"},"source_link_params":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Source Link Params"},"name":{"type":"string","maxLength":255,"minLength":1,"title":"Name"},"is_active":{"type":"boolean","title":"Is Active","default":true},"publish":{"type":"boolean","title":"Publish","default":true},"skills":{"anyOf":[{"items":{"$ref":"#/components/schemas/AgentSkillLinkInput"},"type":"array","maxItems":50},{"type":"null"}],"title":"Skills"},"knowledge_sources":{"anyOf":[{"items":{"$ref":"#/components/schemas/KnowledgeSourceCreate"},"type":"array","maxItems":20},{"type":"null"}],"title":"Knowledge Sources"}},"additionalProperties":false,"type":"object","required":["name"],"title":"AgentCreate"},"AgentDraftChange":{"properties":{"section":{"type":"string","title":"Section"},"fields":{"items":{"type":"string"},"type":"array","title":"Fields"}},"type":"object","required":["section"],"title":"AgentDraftChange"},"AgentDraftIssue":{"properties":{"section":{"type":"string","title":"Section"},"field":{"type":"string","title":"Field"},"code":{"type":"string","title":"Code"}},"type":"object","required":["section","field","code"],"title":"AgentDraftIssue"},"AgentDraftPut":{"properties":{"document":{"$ref":"#/components/schemas/AgentConfigDocument"}},"additionalProperties":false,"type":"object","required":["document"],"title":"AgentDraftPut"},"AgentDraftResponse":{"properties":{"agent_id":{"type":"string","title":"Agent Id"},"bot_id":{"type":"string","title":"Bot Id"},"document":{"additionalProperties":true,"type":"object","title":"Document"},"live_document":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Live Document"},"publish_state":{"type":"string","enum":["draft","published"],"title":"Publish State","default":"published"},"status":{"anyOf":[{"type":"string","enum":["draft","paused","not_connected","live"]},{"type":"null"}],"title":"Status"},"flow_targeted":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Flow Targeted"},"has_unpublished_changes":{"type":"boolean","title":"Has Unpublished Changes","default":false},"draft_updated_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Draft Updated At"},"draft_updated_by_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Draft Updated By Name"},"published_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Published At"},"published_by_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Published By Name"},"changes":{"items":{"$ref":"#/components/schemas/AgentDraftChange"},"type":"array","title":"Changes"},"validation":{"$ref":"#/components/schemas/AgentDraftValidation"},"avatar_display_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Avatar Display Url"},"widget_connections":{"items":{"$ref":"#/components/schemas/AgentWidgetConnection"},"type":"array","title":"Widget Connections"}},"type":"object","required":["agent_id","bot_id","document"],"title":"AgentDraftResponse"},"AgentDraftValidation":{"properties":{"errors":{"items":{"$ref":"#/components/schemas/AgentDraftIssue"},"type":"array","title":"Errors"},"warnings":{"items":{"$ref":"#/components/schemas/AgentDraftIssue"},"type":"array","title":"Warnings"}},"type":"object","title":"AgentDraftValidation"},"AgentFeedbackDay":{"properties":{"up":{"type":"integer","title":"Up","default":0},"down":{"type":"integer","title":"Down","default":0},"wrong":{"type":"integer","title":"Wrong","default":0},"date":{"type":"string","title":"Date","description":"UTC day, YYYY-MM-DD"}},"type":"object","required":["date"],"title":"AgentFeedbackDay"},"AgentFeedbackItem":{"properties":{"rating_id":{"type":"string","title":"Rating Id"},"ai_answer_event_id":{"type":"string","title":"Ai Answer Event Id"},"verdict":{"type":"string","title":"Verdict","description":"down | wrong"},"reason_category":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Reason Category"},"comment":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Comment"},"better_answer":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Better Answer","description":"The rater's correction"},"rated_by_user_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Rated By User Id"},"rated_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Rated At"},"conversation_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Conversation Id"},"message_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Message Id"},"question":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Question","description":"The customer message the agent answered"},"answer":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Answer","description":"The agent's reply"},"skills":{"items":{"$ref":"#/components/schemas/AgentFeedbackSkillRef"},"type":"array","title":"Skills"},"model_tier":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Model Tier"},"model":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Model"},"turn_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Turn Id","description":"Turn trace id (GET /automation/agents/{id}/turns)"},"correction_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Correction Id","description":"KB correction opened from this rating"},"correction_status":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Correction Status"},"contact_hidden":{"type":"boolean","title":"Contact Hidden","description":"Caller cannot open this conversation: conversation_id, message_id, question, answer are null","default":false}},"type":"object","required":["rating_id","ai_answer_event_id","verdict"],"title":"AgentFeedbackItem"},"AgentFeedbackResponse":{"properties":{"agent_id":{"type":"string","title":"Agent Id"},"days":{"type":"integer","title":"Days"},"skill":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Skill"},"totals":{"$ref":"#/components/schemas/AgentFeedbackTotals"},"by_skill":{"items":{"$ref":"#/components/schemas/AgentFeedbackSkill"},"type":"array","title":"By Skill"},"series":{"items":{"$ref":"#/components/schemas/AgentFeedbackDay"},"type":"array","title":"Series"},"recent":{"items":{"$ref":"#/components/schemas/AgentFeedbackItem"},"type":"array","title":"Recent","description":"Newest 👎 / wrong ratings"},"truncated":{"type":"boolean","title":"Truncated","default":false}},"type":"object","required":["agent_id","days","totals"],"title":"AgentFeedbackResponse"},"AgentFeedbackSkill":{"properties":{"up":{"type":"integer","title":"Up","default":0},"down":{"type":"integer","title":"Down","default":0},"wrong":{"type":"integer","title":"Wrong","default":0},"slug":{"type":"string","title":"Slug","description":"Skill slug; \"__none__\" = answers with no skill loaded"},"title":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Title"},"versions":{"items":{"type":"integer"},"type":"array","title":"Versions","description":"Skill versions seen in rated answers"},"total":{"type":"integer","title":"Total","default":0}},"type":"object","required":["slug"],"title":"AgentFeedbackSkill"},"AgentFeedbackSkillRef":{"properties":{"slug":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Slug"},"title":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Title"},"version":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Version"}},"type":"object","title":"AgentFeedbackSkillRef"},"AgentFeedbackTotals":{"properties":{"up":{"type":"integer","title":"Up","default":0},"down":{"type":"integer","title":"Down","default":0},"wrong":{"type":"integer","title":"Wrong","default":0},"rated":{"type":"integer","title":"Rated","description":"Rated answers in the window (up + down + wrong)","default":0},"answers":{"type":"integer","title":"Answers","description":"Rateable answers the agent sent in the window","default":0}},"type":"object","title":"AgentFeedbackTotals"},"AgentInsightsGoal":{"properties":{"skill_id":{"type":"string","title":"Skill Id"},"skill_title":{"type":"string","title":"Skill Title"},"goal_label":{"type":"string","title":"Goal Label"},"started":{"type":"integer","title":"Started","default":0},"completed":{"type":"integer","title":"Completed","default":0},"completion_pct":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Completion Pct"}},"type":"object","required":["skill_id","skill_title","goal_label"],"title":"AgentInsightsGoal"},"AgentInsightsKbGaps":{"properties":{"pending_count":{"type":"integer","title":"Pending Count","default":0}},"type":"object","title":"AgentInsightsKbGaps"},"AgentInsightsOverviewResponse":{"properties":{"range_days":{"type":"integer","title":"Range Days"},"goals":{"items":{"$ref":"#/components/schemas/AgentInsightsGoal"},"type":"array","title":"Goals"},"skills_by_usage":{"items":{"$ref":"#/components/schemas/AgentInsightsSkillUsage"},"type":"array","title":"Skills By Usage"},"kb_gaps":{"$ref":"#/components/schemas/AgentInsightsKbGaps"},"truncated":{"type":"boolean","title":"Truncated","default":false}},"type":"object","required":["range_days"],"title":"AgentInsightsOverviewResponse"},"AgentInsightsSkillUsage":{"properties":{"skill_id":{"type":"string","title":"Skill Id"},"title":{"type":"string","title":"Title"},"turns":{"type":"integer","title":"Turns","default":0},"agents":{"type":"integer","title":"Agents","default":0}},"type":"object","required":["skill_id","title"],"title":"AgentInsightsSkillUsage"},"AgentPerformance":{"properties":{"user_id":{"type":"string","title":"User Id","description":"Agent user ID"},"user_name":{"type":"string","title":"User Name","description":"Agent display name"},"user_email":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"User Email","description":"Agent email"},"conversation_count":{"type":"integer","title":"Conversation Count","description":"Number of conversations handled"},"message_count":{"type":"integer","title":"Message Count","description":"Number of outbound messages sent"}},"type":"object","required":["user_id","user_name","conversation_count","message_count"],"title":"AgentPerformance","description":"Performance metrics for a single agent"},"AgentResponse":{"properties":{"id":{"type":"string","title":"Id"},"bot_id":{"type":"string","title":"Bot Id"},"tenant_id":{"type":"string","title":"Tenant Id"},"name":{"type":"string","title":"Name"},"is_active":{"type":"boolean","title":"Is Active"},"persona":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Persona"},"display_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Display Name"},"avatar_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Avatar Url"},"greeting_message":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Greeting Message"},"ai_disclosure_message":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Ai Disclosure Message"},"ai_disclosure_include_escalation":{"type":"boolean","title":"Ai Disclosure Include Escalation","default":true},"instructions":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Instructions"},"model_tier":{"type":"string","title":"Model Tier","default":"standard"},"language_settings":{"$ref":"#/components/schemas/LanguageSettingsResponse"},"escalation":{"$ref":"#/components/schemas/EscalationSettingsResponse"},"source_footer_enabled":{"type":"boolean","title":"Source Footer Enabled","default":false},"source_footer_mode":{"type":"string","title":"Source Footer Mode","default":"urls_only"},"source_link_params":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Source Link Params"},"tools_config":{"additionalProperties":true,"type":"object","title":"Tools Config"},"handoff_config":{"additionalProperties":true,"type":"object","title":"Handoff Config"},"max_tool_calls":{"type":"integer","title":"Max Tool Calls"},"turn_timeout_ms":{"type":"integer","title":"Turn Timeout Ms"},"settings":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Settings"},"channel_actions":{"$ref":"#/components/schemas/ChannelActionsResponse"},"context":{"additionalProperties":true,"type":"object","title":"Context"},"skills":{"items":{"$ref":"#/components/schemas/AgentSkillResponse"},"type":"array","title":"Skills"},"knowledge_sources":{"items":{"$ref":"#/components/schemas/KnowledgeSourceResponse"},"type":"array","title":"Knowledge Sources"},"created_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Created At"},"updated_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Updated At"},"publish_state":{"type":"string","enum":["draft","published"],"title":"Publish State","default":"published"},"has_unpublished_changes":{"type":"boolean","title":"Has Unpublished Changes","default":false},"draft_updated_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Draft Updated At"},"published_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Published At"}},"type":"object","required":["id","bot_id","tenant_id","name","is_active","language_settings","escalation","max_tool_calls","turn_timeout_ms"],"title":"AgentResponse"},"AgentSkillLinkInput":{"properties":{"skill_id":{"type":"string","maxLength":36,"minLength":1,"title":"Skill Id"},"mode":{"anyOf":[{"type":"string","enum":["always_on","model_selected","flow_only"]},{"type":"null"}],"title":"Mode"},"pinned_version_id":{"anyOf":[{"type":"string","maxLength":36},{"type":"null"}],"title":"Pinned Version Id"},"sort_order":{"anyOf":[{"type":"integer","maximum":10000.0,"minimum":0.0},{"type":"null"}],"title":"Sort Order"},"sources":{"anyOf":[{"$ref":"#/components/schemas/SkillSourcesOverride"},{"type":"null"}]}},"additionalProperties":false,"type":"object","required":["skill_id"],"title":"AgentSkillLinkInput"},"AgentSkillResponse":{"properties":{"skill_id":{"type":"string","title":"Skill Id"},"slug":{"type":"string","title":"Slug"},"title":{"type":"string","title":"Title"},"mode":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Mode"},"effective_mode":{"type":"string","title":"Effective Mode"},"pinned_version_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Pinned Version Id"},"current_version":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Current Version"},"pinned_version":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Pinned Version"},"is_archived":{"type":"boolean","title":"Is Archived","default":false},"sort_order":{"type":"integer","title":"Sort Order","default":0},"sources":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Sources"}},"type":"object","required":["skill_id","slug","title","effective_mode"],"title":"AgentSkillResponse"},"AgentSkillsReplace":{"properties":{"skills":{"items":{"$ref":"#/components/schemas/AgentSkillLinkInput"},"type":"array","maxItems":50,"title":"Skills"}},"type":"object","title":"AgentSkillsReplace"},"AgentSuggestionResponse":{"properties":{"suggestion_id":{"type":"string","title":"Suggestion Id","description":"Unique ID to track this suggestion"},"ai_answer_event_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Ai Answer Event Id","description":"AI answer event id (Managed FAQ telemetry). Pass it to POST /ai-answers/{event_id}/rate and to /suggestions/{id}/accept. NULL when telemetry recording failed — never treat that as an error."},"answer":{"type":"string","title":"Answer","description":"AI-generated suggested response"},"sources":{"items":{"$ref":"#/components/schemas/SuggestionSource"},"type":"array","title":"Sources","description":"Source documents used"},"citations":{"items":{"additionalProperties":true,"type":"object"},"type":"array","title":"Citations","description":"In-text citations"},"query":{"type":"string","title":"Query","description":"Query text used for search"},"confidence":{"type":"number","maximum":1.0,"minimum":0.0,"title":"Confidence","description":"Confidence score 0-1"},"content_format":{"type":"string","title":"Content Format","description":"Format of `answer`: same literal values as the per-message content_format flag (\"markdown\" | \"plain\"). Always \"markdown\" here — this text is canonical GitHub-flavored markdown AI output. The frontend should tag the resulting message-create send with content_format=\"markdown\" so the send-side renderer converts it to the target channel's dialect.","default":"markdown"}},"type":"object","required":["suggestion_id","answer","query","confidence"],"title":"AgentSuggestionResponse","description":"Response from AI agent suggestion generation.","example":{"answer":"Based on our documentation, you can reset your password by...","citations":[],"confidence":0.85,"query":"how do I reset my password","sources":[{"relevance_score":0.95,"title":"Password Reset","type":"faq"}],"suggestion_id":"550e8400-e29b-41d4-a716-446655440000"}},"AgentTestHistoryItem":{"properties":{"role":{"type":"string","enum":["user","agent"],"title":"Role"},"text":{"type":"string","maxLength":4000,"title":"Text"}},"type":"object","required":["role","text"],"title":"AgentTestHistoryItem"},"AgentTestRequest":{"properties":{"message":{"type":"string","maxLength":4000,"title":"Message","default":""},"history":{"items":{"$ref":"#/components/schemas/AgentTestHistoryItem"},"type":"array","maxItems":50,"title":"History"},"state":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"State"},"skill_slug":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Skill Slug"},"channel_type":{"anyOf":[{"type":"string","maxLength":32},{"type":"null"}],"title":"Channel Type"},"task":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Task"},"attachment_ids":{"items":{"type":"string"},"type":"array","maxItems":5,"title":"Attachment Ids"},"config":{"type":"string","enum":["draft","live"],"title":"Config","default":"live"}},"type":"object","title":"AgentTestRequest"},"AgentTestResponse":{"properties":{"replies":{"items":{"additionalProperties":true,"type":"object"},"type":"array","title":"Replies"},"outcome":{"type":"string","title":"Outcome"},"outputs":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Outputs"},"handoff":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Handoff"},"engine_trace":{"items":{"additionalProperties":true,"type":"object"},"type":"array","title":"Engine Trace"},"credits":{"additionalProperties":true,"type":"object","title":"Credits"},"state":{"additionalProperties":true,"type":"object","title":"State"},"turn_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Turn Id"},"model":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Model"},"router":{"anyOf":[{"$ref":"#/components/schemas/AgentTraceRouter"},{"type":"null"}]},"skills_loaded":{"items":{"$ref":"#/components/schemas/AgentTraceSkill"},"type":"array","title":"Skills Loaded"},"tool_calls":{"items":{"$ref":"#/components/schemas/AgentTraceToolCall"},"type":"array","title":"Tool Calls"},"link_fixes":{"items":{"$ref":"#/components/schemas/AgentTraceLinkFix"},"type":"array","title":"Link Fixes"},"attachments":{"items":{"$ref":"#/components/schemas/AgentTraceAttachment"},"type":"array","title":"Attachments"},"checks":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Checks","description":"What was checked on this turn: grounding{verdict,claims_checked,unsupported_count,llm_used,latency_ms,reason?}, web{mode,searches,cache_hits,enforced}, live{mode,reads,cached_reads,enforced,source_ids}, kb{searches,zero_hit}, skill_overrides?, credit_cap{limit,used,reached}. Same shape as the turn log's router_result._turn."},"sources":{"items":{"additionalProperties":true,"type":"object"},"type":"array","title":"Sources"},"duration_ms":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Duration Ms"},"goal_progress":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Goal Progress"},"config":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Config"}},"type":"object","required":["outcome"],"title":"AgentTestResponse"},"AgentTraceAttachment":{"properties":{"step":{"type":"string","title":"Step","default":"attachment"},"kind":{"type":"string","title":"Kind","description":"image | pdf"},"filename":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Filename"},"mime":{"type":"string","title":"Mime","default":""},"status":{"type":"string","enum":["read","skipped"],"title":"Status"},"reason":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Reason","description":"null when read; else images_off | documents_off | unsupported_type | per_message_limit | per_conversation_limit | too_large | unreadable | download_failed | model_failed"},"pages_read":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Pages Read"},"pages_total":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Pages Total"},"truncated":{"type":"boolean","title":"Truncated","default":false},"excerpt":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Excerpt","description":"PDFs: first ~200 characters of the read pages"},"credits":{"type":"integer","title":"Credits","description":"AI credits this file costs (would cost, in the test console)","default":0},"summary":{"anyOf":[{"type":"string","enum":["ok","failed"]},{"type":"null"}],"title":"Summary","description":"Read files: ok when the file summary was made, failed when not; null when skipped"}},"type":"object","required":["kind","status"],"title":"AgentTraceAttachment","description":"One customer file the turn saw (read or skipped)."},"AgentTraceLinkFix":{"properties":{"original":{"type":"string","title":"Original"},"action":{"type":"string","enum":["repaired","removed"],"title":"Action"},"replacement":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Replacement"}},"type":"object","required":["original","action"],"title":"AgentTraceLinkFix"},"AgentTraceRouter":{"properties":{"skills":{"items":{"type":"string"},"type":"array","title":"Skills"},"language":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Language"},"needs_kb":{"type":"boolean","title":"Needs Kb","default":false},"needs_web":{"type":"boolean","title":"Needs Web","default":false},"latency_ms":{"type":"integer","title":"Latency Ms","default":0},"model":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Model"}},"type":"object","title":"AgentTraceRouter","description":"Pre-router decision for the turn (null when the router did not run)."},"AgentTraceSkill":{"properties":{"slug":{"type":"string","title":"Slug"},"version":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Version"},"source":{"type":"string","enum":["always","router","tool"],"title":"Source"}},"type":"object","required":["slug","source"],"title":"AgentTraceSkill"},"AgentTraceToolCall":{"properties":{"name":{"type":"string","title":"Name"},"status":{"type":"string","enum":["ok","error","empty"],"title":"Status"},"credits":{"type":"integer","title":"Credits","default":0},"latency_ms":{"type":"integer","title":"Latency Ms","default":0},"results":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Results"},"via":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Via","description":"auto | enforced: lookup the runtime made itself (absent when the model called the tool)"},"cache_hit":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Cache Hit","description":"true when a web search was served from the cache"},"source_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Source Id","description":"read_live_source: the live source read"},"fetcher":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Fetcher","description":"read_live_source: fetcher used"},"cached":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Cached","description":"read_live_source: page served within the source's TTL"}},"type":"object","required":["name","status"],"title":"AgentTraceToolCall"},"AgentTurnResponse":{"properties":{"id":{"type":"string","title":"Id"},"agent_id":{"type":"string","title":"Agent Id"},"bot_id":{"type":"string","title":"Bot Id"},"conversation_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Conversation Id"},"session_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Session Id"},"flow_run_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Flow Run Id"},"inbound_message_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Inbound Message Id"},"surface":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Surface"},"model":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Model"},"outcome":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Outcome"},"router_result":{"anyOf":[{},{"type":"null"}],"title":"Router Result"},"skills_loaded":{"anyOf":[{},{"type":"null"}],"title":"Skills Loaded"},"tool_calls":{"anyOf":[{},{"type":"null"}],"title":"Tool Calls"},"provenance":{"anyOf":[{},{"type":"null"}],"title":"Provenance"},"reply_message_ids":{"anyOf":[{},{"type":"null"}],"title":"Reply Message Ids"},"handoff_reason":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Handoff Reason"},"error":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Error"},"latency_ms":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Latency Ms"},"tokens_in":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Tokens In"},"tokens_out":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Tokens Out"},"internal_cost_usd":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Internal Cost Usd"},"credits_billed":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Credits Billed"},"created_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Created At"},"sources":{"items":{"additionalProperties":true,"type":"object"},"type":"array","title":"Sources"},"duration_ms":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Duration Ms"},"goal_progress":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Goal Progress"},"credits":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Credits"},"contact_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Contact Name"},"channel_type":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Channel Type"},"contact_hidden":{"type":"boolean","title":"Contact Hidden","default":false},"conversation_key":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Conversation Key"}},"type":"object","required":["id","agent_id","bot_id"],"title":"AgentTurnResponse","description":"Audit row. ``model_contents`` (raw model turns) is intentionally omitted."},"AgentUpdate":{"properties":{"instructions":{"anyOf":[{"type":"string","maxLength":20000},{"type":"null"}],"title":"Instructions"},"model_tier":{"anyOf":[{"type":"string","enum":["standard","advanced"]},{"type":"null"}],"title":"Model Tier"},"tools_config":{"anyOf":[{"$ref":"#/components/schemas/ToolsConfig"},{"type":"null"}]},"handoff_config":{"anyOf":[{"$ref":"#/components/schemas/HandoffConfig"},{"type":"null"}]},"max_tool_calls":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Max Tool Calls"},"turn_timeout_ms":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Turn Timeout Ms"},"settings":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Settings"},"channel_actions":{"anyOf":[{"$ref":"#/components/schemas/ChannelActionsInput"},{"type":"null"}]},"persona":{"anyOf":[{"type":"string","maxLength":20000},{"type":"null"}],"title":"Persona"},"display_name":{"anyOf":[{"type":"string","maxLength":255},{"type":"null"}],"title":"Display Name"},"greeting_message":{"anyOf":[{"type":"string","maxLength":2000},{"type":"null"}],"title":"Greeting Message"},"ai_disclosure_message":{"anyOf":[{"type":"string","maxLength":2000},{"type":"null"}],"title":"Ai Disclosure Message"},"ai_disclosure_include_escalation":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Ai Disclosure Include Escalation"},"language_settings":{"anyOf":[{"$ref":"#/components/schemas/LanguageSettings"},{"type":"null"}]},"escalation":{"anyOf":[{"$ref":"#/components/schemas/EscalationSettings"},{"type":"null"}]},"source_footer_enabled":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Source Footer Enabled"},"source_footer_mode":{"anyOf":[{"type":"string","enum":["urls_only","urls_and_kb"]},{"type":"null"}],"title":"Source Footer Mode"},"source_link_params":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Source Link Params"},"name":{"anyOf":[{"type":"string","maxLength":255,"minLength":1},{"type":"null"}],"title":"Name"},"is_active":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Is Active"}},"additionalProperties":false,"type":"object","title":"AgentUpdate"},"AgentWidgetConnection":{"properties":{"widget_id":{"type":"string","title":"Widget Id"},"name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Name"},"widget_type":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Widget Type"}},"type":"object","required":["widget_id"],"title":"AgentWidgetConnection"},"AiIssue":{"properties":{"code":{"type":"string","title":"Code"},"path":{"type":"string","title":"Path"},"message":{"type":"string","title":"Message"},"severity":{"type":"string","title":"Severity"}},"type":"object","required":["code","path","message","severity"],"title":"AiIssue"},"AlertDestinationCreate":{"properties":{"channel":{"type":"string","enum":["email","in_app","sms","slack","whatsapp"],"title":"Channel"},"scope":{"type":"string","enum":["account","tenant"],"title":"Scope","default":"account"},"config":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Config","description":"Channel payload, e.g. {\"email\": \"ops@acme.com\"} or {\"phone\": \"+491701234567\"}."},"is_enabled":{"type":"boolean","title":"Is Enabled","default":true}},"type":"object","required":["channel"],"title":"AlertDestinationCreate"},"AlertDestinationResponse":{"properties":{"id":{"type":"string","title":"Id"},"billing_account_id":{"type":"string","title":"Billing Account Id"},"tenant_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Tenant Id"},"scope":{"type":"string","enum":["account","tenant"],"title":"Scope"},"channel":{"type":"string","title":"Channel"},"is_enabled":{"type":"boolean","title":"Is Enabled"},"config":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Config"},"verified_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Verified At"},"needs_attention":{"type":"boolean","title":"Needs Attention","default":false},"last_error":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Last Error"},"last_used_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Last Used At"},"created_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Created At"}},"type":"object","required":["id","billing_account_id","scope","channel","is_enabled"],"title":"AlertDestinationResponse"},"AlertDestinationUpdate":{"properties":{"config":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Config"},"is_enabled":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Is Enabled"}},"type":"object","title":"AlertDestinationUpdate"},"AlertEventResponse":{"properties":{"id":{"type":"string","title":"Id"},"alert_type":{"type":"string","title":"Alert Type"},"severity":{"type":"string","title":"Severity"},"entity_type":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Entity Type"},"entity_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Entity Id"},"title":{"type":"string","title":"Title"},"status":{"type":"string","title":"Status"},"created_at":{"type":"string","format":"date-time","title":"Created At"},"dispatched_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Dispatched At"}},"type":"object","required":["id","alert_type","severity","title","status","created_at"],"title":"AlertEventResponse"},"AlertHistoryResponse":{"properties":{"items":{"items":{"$ref":"#/components/schemas/AlertEventResponse"},"type":"array","title":"Items"},"pagination":{"$ref":"#/components/schemas/PaginationMeta"}},"type":"object","required":["items","pagination"],"title":"AlertHistoryResponse"},"AlertSettingsBody":{"properties":{"mode":{"type":"string","enum":["all","by_severity","by_type"],"title":"Mode","default":"all"},"severity_matrix":{"anyOf":[{"additionalProperties":{"items":{"type":"string"},"type":"array"},"type":"object"},{"type":"null"}],"title":"Severity Matrix","description":"severity → list of channels, e.g. {\"warning\": [\"email\",\"in_app\"], \"severe\": [\"email\",\"sms\"]}. None = catalog defaults."},"type_overrides":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Type Overrides","description":"Per-alert-type channel overrides. None = none."},"email_always_on":{"type":"boolean","title":"Email Always On","default":true},"monthly_paid_cap_euros":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Monthly Paid Cap Euros","description":"Monthly paid-alert spend cap in EUR. None = uncapped.","default":20.0}},"type":"object","title":"AlertSettingsBody","description":"PUT body — the winning-scope settings row (all-or-nothing override)."},"AlertSettingsLayeredResponse":{"properties":{"effective_scope":{"type":"string","enum":["account","tenant","implicit"],"title":"Effective Scope"},"effective":{"anyOf":[{"$ref":"#/components/schemas/AlertSettingsResponse"},{"type":"null"}]},"account":{"anyOf":[{"$ref":"#/components/schemas/AlertSettingsResponse"},{"type":"null"}]},"tenant":{"anyOf":[{"$ref":"#/components/schemas/AlertSettingsResponse"},{"type":"null"}]},"destinations":{"items":{"$ref":"#/components/schemas/AlertDestinationResponse"},"type":"array","title":"Destinations","default":[]},"paid_channels_available":{"type":"boolean","title":"Paid Channels Available","default":true},"paid_channels_reason":{"type":"string","title":"Paid Channels Reason","default":"ok"},"sms_allowed_countries":{"items":{"type":"string"},"type":"array","title":"Sms Allowed Countries","default":[]}},"type":"object","required":["effective_scope"],"title":"AlertSettingsLayeredResponse","description":"3-layer view: what applies (effective) + both stored layers."},"AlertSettingsResponse":{"properties":{"mode":{"type":"string","enum":["all","by_severity","by_type"],"title":"Mode","default":"all"},"severity_matrix":{"anyOf":[{"additionalProperties":{"items":{"type":"string"},"type":"array"},"type":"object"},{"type":"null"}],"title":"Severity Matrix","description":"severity → list of channels, e.g. {\"warning\": [\"email\",\"in_app\"], \"severe\": [\"email\",\"sms\"]}. None = catalog defaults."},"type_overrides":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Type Overrides","description":"Per-alert-type channel overrides. None = none."},"email_always_on":{"type":"boolean","title":"Email Always On","default":true},"monthly_paid_cap_euros":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Monthly Paid Cap Euros","description":"Monthly paid-alert spend cap in EUR. None = uncapped.","default":20.0},"id":{"type":"string","title":"Id"},"billing_account_id":{"type":"string","title":"Billing Account Id"},"tenant_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Tenant Id"},"scope":{"type":"string","enum":["account","tenant"],"title":"Scope"}},"type":"object","required":["id","billing_account_id","scope"],"title":"AlertSettingsResponse"},"AlphanumericSenderRequest":{"properties":{"sender_id":{"type":"string","maxLength":11,"minLength":1,"title":"Sender Id"}},"type":"object","required":["sender_id"],"title":"AlphanumericSenderRequest"},"AlphanumericSenderResponse":{"properties":{"channel_id":{"type":"string","title":"Channel Id"},"sender_id":{"type":"string","title":"Sender Id"}},"type":"object","required":["channel_id","sender_id"],"title":"AlphanumericSenderResponse"},"ApiTokenCreate":{"properties":{"name":{"type":"string","maxLength":100,"minLength":3,"title":"Name","description":"Human-readable name for this token"},"scopes":{"items":{"type":"string"},"type":"array","minItems":1,"title":"Scopes","description":"Scopes granted to this token (must be subset of user's scopes)"},"expires_in_days":{"anyOf":[{"type":"integer","maximum":365.0,"minimum":1.0},{"type":"null"}],"title":"Expires In Days","description":"Token expiration in days (default: 90)"}},"type":"object","required":["name","scopes"],"title":"ApiTokenCreate","description":"Schema for creating a new API token.\n\nImportant: User can only grant scopes they have permission for.\nDefault expiration: 90 days, max: 365 days.","example":{"expires_in_days":90,"name":"CI/CD Pipeline Token","scopes":["campaigns:write","email:read","contacts:read"]}},"ApiTokenCreated":{"properties":{"id":{"type":"string","title":"Id","description":"Unique identifier (UUID)"},"tenant_id":{"type":"string","title":"Tenant Id","description":"Tenant this token belongs to"},"user_id":{"type":"string","title":"User Id","description":"User who created this token"},"name":{"type":"string","title":"Name","description":"Human-readable name for this token"},"token":{"type":"string","title":"Token","description":"Plaintext token - shown only once!"},"scopes":{"items":{"type":"string"},"type":"array","title":"Scopes","description":"Scopes granted to this token"},"created_at":{"type":"string","format":"date-time","title":"Created At","description":"When the token was created"},"expires_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Expires At","description":"When the token expires"},"warning":{"type":"string","title":"Warning","description":"Security warning","default":"Save this token now - it will not be shown again"}},"type":"object","required":["id","tenant_id","user_id","name","token","created_at"],"title":"ApiTokenCreated","description":"Response when token is first created.\n\nIMPORTANT: The plaintext token is returned ONLY ONCE here.\nSave it securely - you won't be able to retrieve it again.\n\nToken format: s7_api_<32_random_hex_chars>","example":{"created_at":"2025-01-01T00:00:00Z","expires_at":"2025-04-01T00:00:00Z","id":"t1a2b3c4-d5e6-f7g8-h9i0-j1k2l3m4n5o6","name":"CI/CD Pipeline Token","scopes":["campaigns:write","email:read","contacts:read"],"tenant_id":"tenant-123","token":"s7_api_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6q7r8s9t0u1v2w3x4y5z6","user_id":"user-456","warning":"Save this token now - it will not be shown again"}},"ApiTokenListResponse":{"properties":{"items":{"items":{"$ref":"#/components/schemas/ApiTokenResponse"},"type":"array","title":"Items","description":"List of items for this page"},"pagination":{"$ref":"#/components/schemas/PaginationMeta","description":"Pagination metadata"}},"type":"object","required":["items","pagination"],"title":"ApiTokenListResponse","description":"Paginated list of API tokens.\n\nExample response:\n```json\n{\n    \"items\": [...],\n    \"pagination\": {\n        \"total\": 5,\n        \"page\": 1,\n        \"page_size\": 20,\n        \"total_pages\": 1,\n        \"has_next\": false,\n        \"has_prev\": false\n    }\n}\n```"},"ApiTokenResponse":{"properties":{"id":{"type":"string","title":"Id","description":"Unique identifier (UUID)"},"tenant_id":{"type":"string","title":"Tenant Id","description":"Tenant this token belongs to"},"user_id":{"type":"string","title":"User Id","description":"User who created this token"},"name":{"type":"string","title":"Name","description":"Human-readable name for this token"},"scopes":{"items":{"type":"string"},"type":"array","title":"Scopes","description":"Scopes granted to this token"},"last_used_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Last Used At","description":"When the token was last used"},"created_at":{"type":"string","format":"date-time","title":"Created At","description":"When the token was created"},"expires_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Expires At","description":"When the token expires"},"is_active":{"type":"boolean","title":"Is Active","description":"Whether the token is active"}},"type":"object","required":["id","tenant_id","user_id","name","created_at","is_active"],"title":"ApiTokenResponse","description":"API token response (plaintext token NOT included).\n\nThis is the standard response for listing and viewing tokens.\nThe plaintext token is only shown once at creation time.","example":{"created_at":"2025-01-01T00:00:00Z","expires_at":"2025-04-01T00:00:00Z","id":"t1a2b3c4-d5e6-f7g8-h9i0-j1k2l3m4n5o6","is_active":true,"last_used_at":"2025-01-15T10:30:00Z","name":"CI/CD Pipeline Token","scopes":["campaigns:write","email:read","contacts:read"],"tenant_id":"tenant-123","user_id":"user-456"}},"ApiTokenUpdate":{"properties":{"name":{"anyOf":[{"type":"string","maxLength":100,"minLength":3},{"type":"null"}],"title":"Name","description":"Human-readable name for this token"},"is_active":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Is Active","description":"Set to false to revoke token, true to reactivate"}},"type":"object","title":"ApiTokenUpdate","description":"Schema for updating API token metadata.\n\nCan update:\n- name: Change the human-readable name\n- is_active: Revoke (false) or reactivate (true) the token\n\nCannot update (create new token instead):\n- scopes\n- expires_at","example":{"is_active":true,"name":"Updated CI/CD Token"}},"ArchiveEmailProviderResponse":{"properties":{"success":{"type":"boolean","title":"Success","description":"Whether the archive operation succeeded"},"message":{"type":"string","title":"Message","description":"Status message"},"id":{"type":"string","title":"Id","description":"ID of the archived provider"}},"type":"object","required":["success","message","id"],"title":"ArchiveEmailProviderResponse","description":"Schema for archive operation response","example":{"id":"prov-123","message":"Email provider archived successfully","success":true}},"ArchiveIntegrationResponse":{"properties":{"success":{"type":"boolean","title":"Success"},"integration_id":{"type":"string","title":"Integration Id"},"message":{"type":"string","title":"Message"},"archived_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Archived At"}},"type":"object","required":["success","integration_id","message"],"title":"ArchiveIntegrationResponse","description":"Response from archive (soft-delete) operation"},"ArchiveResponse":{"properties":{"success":{"type":"boolean","title":"Success"},"message":{"type":"string","title":"Message"},"channel_id":{"type":"string","title":"Channel Id"}},"type":"object","required":["success","message","channel_id"],"title":"ArchiveResponse","description":"Response for channel archive operation"},"ArchivedEmailIntegrationResponse":{"properties":{"created_at":{"type":"string","title":"Created At","description":"When the resource was created (UTC)"},"updated_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Updated At","description":"When the resource was last updated (UTC)"},"id":{"type":"string","title":"Id","description":"Unique identifier (UUID)"},"tenant_id":{"type":"string","title":"Tenant Id","description":"Tenant this resource belongs to"},"name":{"type":"string","title":"Name"},"email_address":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Email Address"},"oauth_email":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Oauth Email"},"integration_type":{"type":"string","title":"Integration Type"},"connection_status":{"type":"string","title":"Connection Status","default":"disconnected"},"is_archived":{"type":"boolean","title":"Is Archived","default":true},"archived_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Archived At"},"archived_by_user_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Archived By User Id"},"ses_domain":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Ses Domain"},"proxy_address":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Proxy Address"}},"type":"object","required":["created_at","id","tenant_id","name","integration_type"],"title":"ArchivedEmailIntegrationResponse","description":"Response schema for archived email integration"},"ArchivedEmailProviderResponse":{"properties":{"id":{"type":"string","title":"Id"},"tenant_id":{"type":"string","title":"Tenant Id"},"provider_type":{"$ref":"#/components/schemas/ProviderType"},"name":{"type":"string","title":"Name"},"config":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Config","description":"Non-sensitive config (secrets cleared)"},"verified_domains":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Verified Domains","description":"List of verified domains from provider"},"is_active":{"type":"boolean","title":"Is Active"},"is_default":{"type":"boolean","title":"Is Default"},"webhook_configured":{"type":"boolean","title":"Webhook Configured","description":"Whether webhooks were configured","default":false},"created_at":{"type":"string","format":"date-time","title":"Created At"},"updated_at":{"type":"string","format":"date-time","title":"Updated At"},"archived_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Archived At","description":"When the provider was archived"},"archived_by_user_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Archived By User Id","description":"User who archived the provider"}},"type":"object","required":["id","tenant_id","provider_type","name","is_active","is_default","created_at","updated_at"],"title":"ArchivedEmailProviderResponse","description":"Schema for archived email provider configuration response (admin/audit)","example":{"archived_at":"2024-06-15T14:30:00Z","archived_by_user_id":"user-789","config":{"from_email":"marketing@example.com","from_name":"Example Company"},"created_at":"2024-01-01T12:00:00Z","id":"prov-123","is_active":false,"is_default":false,"name":"Marketing Emails (Archived)","provider_type":"sendgrid","tenant_id":"tenant-456","updated_at":"2024-06-15T14:30:00Z"}},"AttachmentAISummary-Input":{"properties":{"status":{"type":"string","enum":["ok","failed","pending"],"title":"Status","description":"`ok` summary ready, `pending` being generated, `failed` generation failed (retry allowed)."},"kind":{"type":"string","enum":["image","pdf"],"title":"Kind","description":"File kind that was summarized."},"text":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Text","description":"Full plain-text summary (untrusted customer-file content)."},"sections":{"items":{"$ref":"#/components/schemas/AttachmentAISummarySection"},"type":"array","title":"Sections","description":"Parsed sections; empty when the summary could not be split (show `text` instead)."},"pages_read":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Pages Read","description":"PDF only: pages read (max 10)."},"pages_total":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Pages Total","description":"PDF only: total pages in the document."},"truncated":{"type":"boolean","title":"Truncated","description":"PDF only: true when only the first pages were read.","default":false},"model":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Model","description":"Model that produced the summary."},"source":{"type":"string","enum":["agent","on_demand"],"title":"Source","description":"`agent` = produced while a Conversational Agent read the file; `on_demand` = requested by a team member/API."},"created_at":{"type":"string","format":"date-time","title":"Created At","description":"When the summary was created (UTC)."},"updated_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Updated At","description":"When the summary was last updated (UTC)."}},"type":"object","required":["status","kind","source","created_at"],"title":"AttachmentAISummary","description":"AI-generated summary of an inbound image/PDF attachment.\n\nThe text is derived from a CUSTOMER-SUPPLIED file: treat it as untrusted\nplain text (never render as HTML/markdown, never execute links)."},"AttachmentAISummary-Output":{"properties":{"status":{"type":"string","enum":["ok","failed","pending"],"title":"Status","description":"`ok` summary ready, `pending` being generated, `failed` generation failed (retry allowed)."},"kind":{"type":"string","enum":["image","pdf"],"title":"Kind","description":"File kind that was summarized."},"text":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Text","description":"Full plain-text summary (untrusted customer-file content)."},"sections":{"items":{"$ref":"#/components/schemas/AttachmentAISummarySection"},"type":"array","title":"Sections","description":"Parsed sections; empty when the summary could not be split (show `text` instead)."},"pages_read":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Pages Read","description":"PDF only: pages read (max 10)."},"pages_total":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Pages Total","description":"PDF only: total pages in the document."},"truncated":{"type":"boolean","title":"Truncated","description":"PDF only: true when only the first pages were read.","default":false},"model":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Model","description":"Model that produced the summary."},"source":{"type":"string","enum":["agent","on_demand"],"title":"Source","description":"`agent` = produced while a Conversational Agent read the file; `on_demand` = requested by a team member/API."},"created_at":{"type":"string","title":"Created At","description":"When the summary was created (UTC)."},"updated_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Updated At","description":"When the summary was last updated (UTC)."}},"type":"object","required":["status","kind","source","created_at"],"title":"AttachmentAISummary","description":"AI-generated summary of an inbound image/PDF attachment.\n\nThe text is derived from a CUSTOMER-SUPPLIED file: treat it as untrusted\nplain text (never render as HTML/markdown, never execute links)."},"AttachmentAISummarySection":{"properties":{"key":{"type":"string","enum":["description","text","summary","key_facts"],"title":"Key","description":"Section label: `description`/`text` (images), `summary`/`key_facts` (PDFs)."},"text":{"type":"string","title":"Text","description":"Plain-text section body (untrusted customer-file content)."}},"type":"object","required":["key","text"],"title":"AttachmentAISummarySection","description":"One labelled section of an AI file summary (plain text)."},"AttachmentFromUrlRequest":{"properties":{"url":{"type":"string","maxLength":2083,"minLength":1,"format":"uri","title":"Url","description":"Public http(s) URL to fetch. file://, gopher://, etc. are rejected. Hostnames resolving to private/loopback/link-local IPs are blocked (SSRF protection)."},"filename":{"anyOf":[{"type":"string","maxLength":500},{"type":"null"}],"title":"Filename","description":"Optional override for the stored filename. If omitted, the filename is derived from the URL path, falling back to a UUID-based name."}},"type":"object","required":["url"],"title":"AttachmentFromUrlRequest","description":"Request body for POST /api/v1/attachments/from-url.\n\nFetches a remote URL server-side, validates the bytes against the\nsame MIME / size rules as POST /upload, stores them in our private\nGCS bucket, and returns an attachment ID. Per-tenant URL dedup: the\nsame `url` will return the existing attachment without re-fetching."},"AttachmentInfo":{"properties":{"id":{"type":"string","title":"Id"},"filename":{"type":"string","title":"Filename"},"content_type":{"type":"string","title":"Content Type"},"file_size":{"type":"integer","title":"File Size","description":"Size in bytes"},"url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Url","description":"Signed GCS URL for download (frontend uses this)"},"download_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Download Url","description":"Signed GCS URL for download (alias for url)"},"content_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Content Id","description":"Content-ID for inline images"},"is_inline":{"type":"boolean","title":"Is Inline","description":"Whether this is an inline attachment","default":false},"attachment_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Attachment Id","description":"ID of the generic attachment record for this file. Use it with `GET/POST /api/v1/attachments/{attachment_id}/summary` (AI file summaries) — not `id`, which is the email-attachment id. May be null for files received before attachment linking was introduced."},"ai_summary":{"anyOf":[{"$ref":"#/components/schemas/AttachmentAISummary-Output"},{"type":"null"}],"description":"AI summary of this file, when one exists. Derived from a customer-supplied file: treat as untrusted plain text."},"ai_summary_available":{"type":"boolean","title":"Ai Summary Available","description":"True when an on-demand summary can be requested via `POST /api/v1/attachments/{attachment_id}/summary` (AI enabled, inbound image/PDF, no summary yet).","default":false},"ai_summary_credits":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Ai Summary Credits","description":"AI credits an on-demand summary costs (set only when `ai_summary_available` is true)."}},"type":"object","required":["id","filename","content_type","file_size"],"title":"AttachmentInfo","description":"Attachment information for email messages"},"AttachmentSummaryActionResponse-Input":{"properties":{"attachment_id":{"type":"string","title":"Attachment Id","description":"The attachment that was summarized"},"summary":{"anyOf":[{"$ref":"#/components/schemas/AttachmentAISummary-Input"},{"type":"null"}],"description":"The summary (status `ok`, or `pending` on 202)"},"credits_charged":{"type":"integer","title":"Credits Charged","description":"AI credits charged by THIS call: 2 (image) / 3 (PDF) for a new summary, else 0","default":0}},"type":"object","required":["attachment_id"],"title":"AttachmentSummaryActionResponse","description":"Response of ``POST /api/v1/attachments/{attachment_id}/summary``.\n\n``summary`` is the same object as ``ai_summary`` on message attachments.\nIts text comes from a CUSTOMER-SUPPLIED file: treat it as untrusted plain text."},"AttachmentSummaryActionResponse-Output":{"properties":{"attachment_id":{"type":"string","title":"Attachment Id","description":"The attachment that was summarized"},"summary":{"anyOf":[{"$ref":"#/components/schemas/AttachmentAISummary-Output"},{"type":"null"}],"description":"The summary (status `ok`, or `pending` on 202)"},"credits_charged":{"type":"integer","title":"Credits Charged","description":"AI credits charged by THIS call: 2 (image) / 3 (PDF) for a new summary, else 0","default":0}},"type":"object","required":["attachment_id"],"title":"AttachmentSummaryActionResponse","description":"Response of ``POST /api/v1/attachments/{attachment_id}/summary``.\n\n``summary`` is the same object as ``ai_summary`` on message attachments.\nIts text comes from a CUSTOMER-SUPPLIED file: treat it as untrusted plain text."},"AttachmentUploadResponse":{"properties":{"id":{"type":"string","title":"Id","description":"Unique attachment identifier"},"attachment_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Attachment Id","description":"Alias of `id` — preferred name for new callers. Persist this rather than the URL so future URL-form changes are transparent."},"filename":{"type":"string","title":"Filename","description":"Original filename"},"content_type":{"type":"string","title":"Content Type","description":"MIME type of the file"},"size":{"type":"integer","title":"Size","description":"File size in bytes"},"storage_path":{"type":"string","title":"Storage Path","description":"Internal storage path"},"download_url":{"type":"string","title":"Download Url","description":"Authenticated proxy URL for downloading the attachment. Same-origin path (e.g. `/api/v1/attachments/{id}/download`) the browser fetches with the user's session cookie. The underlying GCS bucket is private; this is NOT a public URL. Use this for in-app render / agent UI / bot flows."},"public_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Public Url","description":"Absolute, **unauthenticated** public URL for the attachment (e.g. `https://api.sendseven.com/api/v1/attachments/public/{tenant_id}/{attachment_id}/{filename}`). Use this only for outbound contexts where an external service must `GET` the bytes — WhatsApp template `image.link` (Meta caches on first send), third-party webhook payloads, etc. The URL acts as a capability: anyone who has it can fetch the bytes. Don't put it in places that don't need it."}},"type":"object","required":["id","filename","content_type","size","storage_path","download_url"],"title":"AttachmentUploadResponse","description":"Response for successful file upload.","example":{"attachment_id":"550e8400-e29b-41d4-a716-446655440000","content_type":"application/pdf","download_url":"/api/v1/attachments/550e8400-e29b-41d4-a716-446655440000/download","filename":"document.pdf","id":"550e8400-e29b-41d4-a716-446655440000","public_url":"https://api.sendseven.com/api/v1/attachments/public/abc123/550e8400-e29b-41d4-a716-446655440000/document.pdf","size":102400,"storage_path":"tenants/abc123/attachments/doc.pdf"}},"AttachmentUrlResponse":{"properties":{"url":{"type":"string","title":"Url","description":"Signed download URL"},"expires_in_minutes":{"type":"integer","title":"Expires In Minutes","description":"Minutes until URL expires"}},"type":"object","required":["url","expires_in_minutes"],"title":"AttachmentUrlResponse","description":"Response for signed URL generation.","example":{"expires_in_minutes":60,"url":"https://storage.googleapis.com/bucket/path?signature=..."}},"AutomationBotCreate":{"properties":{"name":{"type":"string","maxLength":255,"minLength":1,"title":"Name","description":"Bot display name"},"bot_type":{"$ref":"#/components/schemas/BotTypeEnum","description":"Type of bot","default":"support"},"is_active":{"type":"boolean","title":"Is Active","description":"Whether bot is active","default":true},"priority":{"type":"integer","minimum":0.0,"title":"Priority","description":"Bot priority (higher = evaluated first when matching context)","default":0},"personality_prompt":{"anyOf":[{"type":"string","maxLength":15000},{"type":"null"}],"title":"Personality Prompt","description":"Custom system prompt defining bot personality"},"greeting_message":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Greeting Message","description":"Initial greeting sent to users"},"escalation_keywords":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Escalation Keywords","description":"Keywords that trigger escalation (e.g., ['agent', 'human', 'help'])"},"display_name":{"anyOf":[{"type":"string","maxLength":100},{"type":"null"}],"title":"Display Name","description":"Customer-facing name shown to end users (e.g., 'Eva', 'Max'). Falls back to bot name."},"avatar_url":{"anyOf":[{"type":"string","maxLength":2048},{"type":"null"}],"title":"Avatar Url","description":"Avatar image URL for the bot"},"ai_disclosure_message":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Ai Disclosure Message","description":"Custom AI disclosure override. Use {name} as placeholder for bot display name."},"ai_disclosure_include_escalation":{"type":"boolean","title":"Ai Disclosure Include Escalation","description":"Include 'ask for human agent' mention in the AI disclosure","default":true},"is_kb_only_mode":{"type":"boolean","title":"Is Kb Only Mode","description":"Only answer from knowledge base, no general knowledge","default":true},"response_language":{"anyOf":[{"type":"string","maxLength":10},{"type":"null"}],"title":"Response Language","description":"ISO 639-1 language code (e.g., 'en', 'de'). null = follow the customer's language (auto-detect); ''/'auto' are stored as null."},"confidence_threshold":{"type":"number","maximum":1.0,"minimum":0.0,"title":"Confidence Threshold","description":"Minimum confidence score to provide answer (0.0-1.0)","default":0.7},"is_detailed_response":{"type":"boolean","title":"Is Detailed Response","description":"Allow longer, more detailed bot responses instead of concise chat-style messages","default":false},"use_full_conversation_context":{"type":"boolean","title":"Use Full Conversation Context","description":"Feed the full current conversation (bounded) into the KB query instead of only the last few messages. Single-conversation only.","default":false},"vision_enabled":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Vision Enabled","description":"Beta: let the bot read images customers send (NULL/false = off)."},"web_search_mode":{"anyOf":[{"$ref":"#/components/schemas/WebSearchModeEnum"},{"type":"null"}],"description":"Beta: live web-search trigger mode — 'off' (default/NULL), 'kb_fallback' (search only after KB returns no answer), or 'always'. Any other value is rejected 422."},"web_search_allowed_domains":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Web Search Allowed Domains","description":"Beta: restrict web search to these plain domains (injected as site: operators). Empty list = no restriction; NULL = off. Entries must be plain hosts (no scheme/path); normalized on write."},"web_search_required_keywords":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Web Search Required Keywords","description":"Beta: keywords appended to every web-search query (e.g. brand name)."},"web_search_max_per_conversation":{"anyOf":[{"type":"integer","minimum":0.0},{"type":"null"}],"title":"Web Search Max Per Conversation","description":"Beta: per-conversation cap on web searches (>=0). NULL = default cap."},"deep_answers_enabled":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Deep Answers Enabled","description":"Beta: allow deeper KB+web synthesis / reasoning bump (NULL/false = off)."},"auto_translate_messages":{"type":"boolean","title":"Auto Translate Messages","description":"Auto-translate greetings, buttons, and responses to user's detected language","default":true},"language_detection_strategy":{"anyOf":[{"type":"string","enum":["cascade","fixed"]},{"type":"null"}],"title":"Language Detection Strategy","description":"How this bot resolves the language it speaks. 'cascade' (default) resolves per conversation via the fallback chain (contact locale -> inferred channel locale -> response_language/base_language); 'fixed' always speaks base_language. Omit / null to keep the default -- NULL is stored and treated as 'cascade' by the runtime, so existing bots are unaffected. Any other value is rejected 422."},"escalation_enabled":{"type":"boolean","title":"Escalation Enabled","description":"When disabled, the bot will never escalate to a human agent regardless of confidence score","default":true},"offers_escalation":{"type":"boolean","title":"Offers Escalation","description":"Always offer option to talk to human","default":true},"max_failed_attempts":{"type":"integer","maximum":10.0,"minimum":0.0,"title":"Max Failed Attempts","description":"Low-confidence responses before auto-escalation. Use 0 for bot types where escalation does not apply (e.g. keyword bots).","default":2},"handoff_message":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Handoff Message","description":"Custom message when escalating to human agent (e.g., 'An agent will reply soon')"},"no_answer_message":{"anyOf":[{"type":"string","maxLength":2000},{"type":"null"}],"title":"No Answer Message","description":"Custom text sent when the knowledge base has no answer. Empty/NULL uses the built-in localized text. Must not mention a human agent -- it is also sent when escalation is disabled."},"escalation_offer_message":{"anyOf":[{"type":"string","maxLength":2000},{"type":"null"}],"title":"Escalation Offer Message","description":"Custom text appended (after one blank line) offering a human agent. Empty/NULL uses the built-in localized text."},"immediate_handoff_threshold":{"type":"number","maximum":1.0,"minimum":0.0,"title":"Immediate Handoff Threshold","description":"Confidence below this triggers immediate escalation offer (0.0-1.0)","default":0.3},"required_fields":{"anyOf":[{"items":{"$ref":"#/components/schemas/RequiredFieldConfig"},"type":"array"},{"type":"null"}],"title":"Required Fields","description":"Fields to collect from users"},"routing_config":{"anyOf":[{"$ref":"#/components/schemas/RoutingConfigSchema"},{"type":"null"}],"description":"Routing configuration (required when bot_type='router')"},"is_field_collection_mode":{"type":"boolean","title":"Is Field Collection Mode","description":"Enable field collection mode (auto-set for lead_qualification and appointment bots)","default":false},"calendly_event_type_uri":{"anyOf":[{"type":"string","maxLength":255},{"type":"null"}],"title":"Calendly Event Type Uri","description":"Calendly event type URI for booking appointments"},"offline_escalation_behavior":{"$ref":"#/components/schemas/OfflineEscalationBehaviorEnum","description":"Behavior when escalation fires but no agents online (live chat): leave_message, escalate_anyway, continue_bot","default":"leave_message"},"offline_message":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Offline Message","description":"Custom message shown in offline scenarios (live chat)"},"outside_hours_message":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Outside Hours Message","description":"Custom message for non-livechat channels outside business hours"},"is_escalation_schedule_enabled":{"type":"boolean","title":"Is Escalation Schedule Enabled","description":"Enable time-based escalation gating","default":false},"escalation_timezone":{"type":"string","title":"Escalation Timezone","description":"IANA timezone for escalation schedule (e.g., 'Europe/Berlin')","default":"UTC"},"escalation_schedule":{"anyOf":[{"items":{"$ref":"#/components/schemas/TimeRangeConfig"},"type":"array"},{"type":"null"}],"title":"Escalation Schedule","description":"Time ranges when human escalation is available"},"escalation_tag_id":{"anyOf":[{"type":"string","maxLength":36},{"type":"null"}],"title":"Escalation Tag Id","description":"Tag (skill) to route escalated conversations to"},"escalation_routing_strategy":{"anyOf":[{"$ref":"#/components/schemas/EscalationRoutingStrategyEnum"},{"type":"null"}],"description":"Assignment on handoff: null = keep unassigned; round_robin_all | least_busy_all = all eligible members (tag ignored + cleared); notify_only | round_robin | least_busy = by tag (requires escalation_tag_id)"},"escalation_fallback_minutes":{"anyOf":[{"type":"integer","maximum":1440.0,"minimum":1.0},{"type":"null"}],"title":"Escalation Fallback Minutes","description":"Unattended alert after N minutes if still unassigned / unanswered (any non-null strategy; off-hours the countdown starts at the next escalation-schedule opening)"},"max_email_replies":{"anyOf":[{"type":"integer","maximum":50.0,"minimum":1.0},{"type":"null"}],"title":"Max Email Replies","description":"Max email replies per conversation before handing off to agents (email_support bots)","default":1},"is_email_reply_mode":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Is Email Reply Mode","description":"When TRUE on an 'assistant' bot, the email auto-reply logic kicks in (equivalent to legacy bot_type='email_support'). Default FALSE.","default":false},"coexist_bot_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Coexist Bot Id","description":"ID of another bot to coexist with"},"coexist_mode":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Coexist Mode","description":"Coexistence mode: 'active_when_active' or 'active_when_inactive'"},"base_language":{"anyOf":[{"type":"string","maxLength":10},{"type":"null"}],"title":"Base Language","description":"Language the bot's messages are written in (ISO 639-1)","default":"en"},"translation_cache":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Translation Cache","description":"Cached translations keyed by language code"},"greeting_buttons_enabled":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Greeting Buttons Enabled","description":"Show 'Talk to Human' / 'Ask the bot' buttons under the greeting (NULL/false = off)"},"greeting_human_button_caption":{"anyOf":[{"type":"string","maxLength":64},{"type":"null"}],"title":"Greeting Human Button Caption","description":"Caption for the 'Talk to Human' button. NULL = localized default."},"greeting_continue_button_caption":{"anyOf":[{"type":"string","maxLength":64},{"type":"null"}],"title":"Greeting Continue Button Caption","description":"Caption for the 'Ask the bot' button. NULL = localized default."},"greeting_continue_ack_message":{"anyOf":[{"type":"string","maxLength":1000},{"type":"null"}],"title":"Greeting Continue Ack Message","description":"Acknowledgement message sent after the user taps 'Ask the bot'. NULL = localized default."},"source_footer_enabled":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Source Footer Enabled"},"source_footer_mode":{"anyOf":[{"type":"string","enum":["urls_only","urls_and_kb"]},{"type":"null"}],"title":"Source Footer Mode","description":"Which sources to list: 'urls_only' or 'urls_and_kb'. NULL = column default ('urls_only')."},"source_link_params":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Source Link Params","description":"Per-channel query params appended to source links, keyed by channel_type with an optional 'default' key. Shape: {channel_type: {param_name: template_string}}. NULL = no decoration."},"kb_live_refresh_mode":{"anyOf":[{"type":"string","enum":["off","always_live","cached_ttl","live_once"]},{"type":"null"}],"title":"Kb Live Refresh Mode","description":"KB live-refresh mode: 'off' | 'always_live' | 'cached_ttl' | 'live_once'. NULL = 'off'."},"kb_live_refresh_ttl_seconds":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Kb Live Refresh Ttl Seconds","description":"Cache TTL in seconds, only meaningful when kb_live_refresh_mode == 'cached_ttl'. NULL = service default."}},"additionalProperties":false,"type":"object","required":["name"],"title":"AutomationBotCreate","description":"Schema for creating a new automation bot"},"AutomationBotListResponse":{"properties":{"id":{"type":"string","title":"Id"},"tenant_id":{"type":"string","title":"Tenant Id"},"name":{"type":"string","title":"Name"},"bot_type":{"$ref":"#/components/schemas/BotTypeEnum"},"is_active":{"type":"boolean","title":"Is Active"},"priority":{"type":"integer","title":"Priority","default":0},"avatar_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Avatar Url"},"total_conversations":{"type":"integer","title":"Total Conversations","default":0},"total_escalations":{"type":"integer","title":"Total Escalations","default":0},"activation_rules_count":{"type":"integer","title":"Activation Rules Count","default":0},"created_at":{"type":"string","format":"date-time","title":"Created At"},"agent_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Agent Id"},"escalation_tag_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Escalation Tag Id"},"escalation_routing_strategy":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Escalation Routing Strategy"},"escalation_fallback_minutes":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Escalation Fallback Minutes"}},"type":"object","required":["id","tenant_id","name","bot_type","is_active","created_at"],"title":"AutomationBotListResponse","description":"Response schema for listing bots"},"AutomationBotResponse":{"properties":{"id":{"type":"string","title":"Id"},"tenant_id":{"type":"string","title":"Tenant Id"},"name":{"type":"string","title":"Name"},"bot_type":{"$ref":"#/components/schemas/BotTypeEnum"},"is_active":{"type":"boolean","title":"Is Active"},"priority":{"type":"integer","title":"Priority","default":0},"personality_prompt":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Personality Prompt"},"greeting_message":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Greeting Message"},"escalation_keywords":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Escalation Keywords"},"display_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Display Name"},"avatar_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Avatar Url"},"ai_disclosure_message":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Ai Disclosure Message"},"ai_disclosure_include_escalation":{"type":"boolean","title":"Ai Disclosure Include Escalation","default":true},"is_kb_only_mode":{"type":"boolean","title":"Is Kb Only Mode"},"response_language":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Response Language"},"confidence_threshold":{"type":"number","title":"Confidence Threshold"},"is_detailed_response":{"type":"boolean","title":"Is Detailed Response","default":false},"use_full_conversation_context":{"type":"boolean","title":"Use Full Conversation Context","default":false},"vision_enabled":{"type":"boolean","title":"Vision Enabled","default":false},"web_search_mode":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Web Search Mode","default":"off"},"web_search_allowed_domains":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Web Search Allowed Domains"},"web_search_required_keywords":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Web Search Required Keywords"},"web_search_max_per_conversation":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Web Search Max Per Conversation"},"deep_answers_enabled":{"type":"boolean","title":"Deep Answers Enabled","default":false},"auto_translate_messages":{"type":"boolean","title":"Auto Translate Messages","default":true},"language_detection_strategy":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Language Detection Strategy"},"escalation_enabled":{"type":"boolean","title":"Escalation Enabled","default":true},"offers_escalation":{"type":"boolean","title":"Offers Escalation"},"max_failed_attempts":{"type":"integer","title":"Max Failed Attempts"},"handoff_message":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Handoff Message"},"no_answer_message":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"No Answer Message"},"escalation_offer_message":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Escalation Offer Message"},"immediate_handoff_threshold":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Immediate Handoff Threshold"},"required_fields":{"anyOf":[{"items":{"additionalProperties":true,"type":"object"},"type":"array"},{"type":"null"}],"title":"Required Fields"},"routing_config":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Routing Config"},"is_field_collection_mode":{"type":"boolean","title":"Is Field Collection Mode","default":false},"calendly_event_type_uri":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Calendly Event Type Uri"},"offline_escalation_behavior":{"$ref":"#/components/schemas/OfflineEscalationBehaviorEnum","default":"leave_message"},"offline_message":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Offline Message"},"outside_hours_message":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Outside Hours Message"},"is_escalation_schedule_enabled":{"type":"boolean","title":"Is Escalation Schedule Enabled","default":false},"escalation_timezone":{"type":"string","title":"Escalation Timezone","default":"UTC"},"escalation_schedule":{"anyOf":[{"items":{"additionalProperties":true,"type":"object"},"type":"array"},{"type":"null"}],"title":"Escalation Schedule"},"escalation_tag_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Escalation Tag Id"},"escalation_routing_strategy":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Escalation Routing Strategy"},"escalation_fallback_minutes":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Escalation Fallback Minutes"},"max_email_replies":{"type":"integer","title":"Max Email Replies","default":1},"is_email_reply_mode":{"type":"boolean","title":"Is Email Reply Mode","default":false},"coexist_bot_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Coexist Bot Id"},"coexist_mode":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Coexist Mode"},"base_language":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Base Language","default":"en"},"translation_cache":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Translation Cache"},"greeting_buttons_enabled":{"type":"boolean","title":"Greeting Buttons Enabled","default":false},"greeting_human_button_caption":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Greeting Human Button Caption"},"greeting_continue_button_caption":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Greeting Continue Button Caption"},"greeting_continue_ack_message":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Greeting Continue Ack Message"},"source_footer_enabled":{"type":"boolean","title":"Source Footer Enabled","default":false},"source_footer_mode":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Source Footer Mode","default":"urls_only"},"source_link_params":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Source Link Params"},"kb_live_refresh_mode":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Kb Live Refresh Mode"},"kb_live_refresh_ttl_seconds":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Kb Live Refresh Ttl Seconds"},"total_conversations":{"type":"integer","title":"Total Conversations","default":0},"total_escalations":{"type":"integer","title":"Total Escalations","default":0},"created_at":{"type":"string","format":"date-time","title":"Created At"},"updated_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Updated At"},"activation_rules_count":{"type":"integer","title":"Activation Rules Count","default":0},"keyword_triggers_count":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Keyword Triggers Count","default":0}},"type":"object","required":["id","tenant_id","name","bot_type","is_active","is_kb_only_mode","confidence_threshold","offers_escalation","max_failed_attempts","created_at"],"title":"AutomationBotResponse","description":"Response schema for automation bot"},"AutomationBotUpdate":{"properties":{"name":{"anyOf":[{"type":"string","maxLength":255,"minLength":1},{"type":"null"}],"title":"Name"},"bot_type":{"anyOf":[{"$ref":"#/components/schemas/BotTypeEnum"},{"type":"null"}]},"is_active":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Is Active"},"priority":{"anyOf":[{"type":"integer","minimum":0.0},{"type":"null"}],"title":"Priority","description":"Bot priority"},"personality_prompt":{"anyOf":[{"type":"string","maxLength":15000},{"type":"null"}],"title":"Personality Prompt"},"greeting_message":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Greeting Message"},"escalation_keywords":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Escalation Keywords"},"display_name":{"anyOf":[{"type":"string","maxLength":100},{"type":"null"}],"title":"Display Name"},"avatar_url":{"anyOf":[{"type":"string","maxLength":2048},{"type":"null"}],"title":"Avatar Url"},"ai_disclosure_message":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Ai Disclosure Message"},"ai_disclosure_include_escalation":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Ai Disclosure Include Escalation"},"is_kb_only_mode":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Is Kb Only Mode"},"response_language":{"anyOf":[{"type":"string","maxLength":10},{"type":"null"}],"title":"Response Language"},"confidence_threshold":{"anyOf":[{"type":"number","maximum":1.0,"minimum":0.0},{"type":"null"}],"title":"Confidence Threshold"},"is_detailed_response":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Is Detailed Response"},"use_full_conversation_context":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Use Full Conversation Context"},"vision_enabled":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Vision Enabled"},"web_search_mode":{"anyOf":[{"$ref":"#/components/schemas/WebSearchModeEnum"},{"type":"null"}],"description":"Beta web-search mode: 'off' | 'kb_fallback' | 'always'. Omit to keep current."},"web_search_allowed_domains":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Web Search Allowed Domains","description":"Beta: plain domains for site:-restricted web search. Empty list = no restriction. Omit to keep current."},"web_search_required_keywords":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Web Search Required Keywords","description":"Beta: keywords appended to every web-search query. Omit to keep current."},"web_search_max_per_conversation":{"anyOf":[{"type":"integer","minimum":0.0},{"type":"null"}],"title":"Web Search Max Per Conversation","description":"Beta: per-conversation web-search cap (>=0). Omit to keep current."},"deep_answers_enabled":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Deep Answers Enabled"},"auto_translate_messages":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Auto Translate Messages"},"language_detection_strategy":{"anyOf":[{"type":"string","enum":["cascade","fixed"]},{"type":"null"}],"title":"Language Detection Strategy","description":"How this bot resolves the language (see AutomationBotCreate). 'cascade' | 'fixed'. Omit on PATCH to preserve the current value; any other value is rejected 422. Because NULL means 'cascade', this cannot be reset to NULL via PATCH -- send 'cascade' to restore the default behaviour."},"escalation_enabled":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Escalation Enabled"},"offers_escalation":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Offers Escalation"},"max_failed_attempts":{"anyOf":[{"type":"integer","maximum":10.0,"minimum":0.0},{"type":"null"}],"title":"Max Failed Attempts"},"handoff_message":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Handoff Message"},"no_answer_message":{"anyOf":[{"type":"string","maxLength":2000},{"type":"null"}],"title":"No Answer Message"},"escalation_offer_message":{"anyOf":[{"type":"string","maxLength":2000},{"type":"null"}],"title":"Escalation Offer Message"},"immediate_handoff_threshold":{"anyOf":[{"type":"number","maximum":1.0,"minimum":0.0},{"type":"null"}],"title":"Immediate Handoff Threshold"},"required_fields":{"anyOf":[{"items":{"$ref":"#/components/schemas/RequiredFieldConfig"},"type":"array"},{"type":"null"}],"title":"Required Fields"},"routing_config":{"anyOf":[{"$ref":"#/components/schemas/RoutingConfigSchema"},{"type":"null"}]},"is_field_collection_mode":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Is Field Collection Mode"},"calendly_event_type_uri":{"anyOf":[{"type":"string","maxLength":255},{"type":"null"}],"title":"Calendly Event Type Uri"},"offline_escalation_behavior":{"anyOf":[{"$ref":"#/components/schemas/OfflineEscalationBehaviorEnum"},{"type":"null"}]},"offline_message":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Offline Message"},"outside_hours_message":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Outside Hours Message"},"is_escalation_schedule_enabled":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Is Escalation Schedule Enabled"},"escalation_timezone":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Escalation Timezone"},"escalation_schedule":{"anyOf":[{"items":{"$ref":"#/components/schemas/TimeRangeConfig"},"type":"array"},{"type":"null"}],"title":"Escalation Schedule"},"escalation_tag_id":{"anyOf":[{"type":"string","maxLength":36},{"type":"null"}],"title":"Escalation Tag Id"},"escalation_routing_strategy":{"anyOf":[{"$ref":"#/components/schemas/EscalationRoutingStrategyEnum"},{"type":"null"}]},"escalation_fallback_minutes":{"anyOf":[{"type":"integer","maximum":1440.0,"minimum":1.0},{"type":"null"}],"title":"Escalation Fallback Minutes"},"max_email_replies":{"anyOf":[{"type":"integer","maximum":50.0,"minimum":1.0},{"type":"null"}],"title":"Max Email Replies"},"is_email_reply_mode":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Is Email Reply Mode"},"coexist_bot_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Coexist Bot Id"},"coexist_mode":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Coexist Mode"},"base_language":{"anyOf":[{"type":"string","maxLength":10},{"type":"null"}],"title":"Base Language"},"translation_cache":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Translation Cache"},"greeting_buttons_enabled":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Greeting Buttons Enabled"},"greeting_human_button_caption":{"anyOf":[{"type":"string","maxLength":64},{"type":"null"}],"title":"Greeting Human Button Caption","description":"Caption for the 'Talk to Human' button. Omit to keep current value; NULL = localized default."},"greeting_continue_button_caption":{"anyOf":[{"type":"string","maxLength":64},{"type":"null"}],"title":"Greeting Continue Button Caption","description":"Caption for the 'Ask the bot' button. Omit to keep current value; NULL = localized default."},"greeting_continue_ack_message":{"anyOf":[{"type":"string","maxLength":1000},{"type":"null"}],"title":"Greeting Continue Ack Message","description":"Acknowledgement message sent after tapping 'Ask the bot'. Omit to keep current value; NULL = localized default."},"source_footer_enabled":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Source Footer Enabled"},"source_footer_mode":{"anyOf":[{"type":"string","enum":["urls_only","urls_and_kb"]},{"type":"null"}],"title":"Source Footer Mode","description":"Which sources to list: 'urls_only' or 'urls_and_kb'. Omit to keep current value."},"source_link_params":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Source Link Params","description":"Per-channel query params appended to source links, keyed by channel_type with an optional 'default' key. Shape: {channel_type: {param_name: template_string}}. Omit to keep current value."},"kb_live_refresh_mode":{"anyOf":[{"type":"string","enum":["off","always_live","cached_ttl","live_once"]},{"type":"null"}],"title":"Kb Live Refresh Mode","description":"KB live-refresh mode: 'off' | 'always_live' | 'cached_ttl' | 'live_once'. Omit to keep current value."},"kb_live_refresh_ttl_seconds":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Kb Live Refresh Ttl Seconds","description":"Cache TTL in seconds, only meaningful when kb_live_refresh_mode == 'cached_ttl'. Omit to keep current value."}},"additionalProperties":false,"type":"object","title":"AutomationBotUpdate","description":"Schema for updating an automation bot"},"AvailableChannelResponse":{"properties":{"channel_id":{"type":"string","title":"Channel Id","description":"Channel UUID"},"channel_type":{"type":"string","title":"Channel Type","description":"Channel type (whatsapp, telegram, etc.)"},"channel_name":{"type":"string","title":"Channel Name","description":"User-friendly channel name"},"identifier":{"type":"string","title":"Identifier","description":"Contact's identifier on this channel"},"status":{"$ref":"#/components/schemas/ChannelStatus","description":"Channel availability status"},"window_status":{"anyOf":[{"$ref":"#/components/schemas/WhatsAppChannelWindowStatus"},{"type":"null"}],"description":"WhatsApp window status (WhatsApp channels only)"},"email_mailbox":{"anyOf":[{"$ref":"#/components/schemas/EmailMailboxInfo"},{"type":"null"}],"description":"Email mailbox info (email channels only, when using mailbox-based routing)"}},"type":"object","required":["channel_id","channel_type","channel_name","identifier","status"],"title":"AvailableChannelResponse","description":"A channel available to reach a contact"},"AvailableChannelsResponse":{"properties":{"contact_id":{"type":"string","title":"Contact Id","description":"Contact UUID"},"channels":{"items":{"$ref":"#/components/schemas/AvailableChannelResponse"},"type":"array","title":"Channels","description":"List of available channels"}},"type":"object","required":["contact_id"],"title":"AvailableChannelsResponse","description":"Response listing all channels available to reach a contact"},"AvailableEventsResponse":{"properties":{"events":{"items":{"$ref":"#/components/schemas/WebhookEventTypeSchema"},"type":"array","title":"Events","description":"List of available event types"},"categories":{"items":{"type":"string"},"type":"array","title":"Categories","description":"List of event categories"}},"type":"object","required":["events","categories"],"title":"AvailableEventsResponse","description":"List of available event types with categories","example":{"categories":["message","conversation","contact"],"events":[{"category":"message","description":"Triggered when a new message is received from a contact","event":"message.received"},{"category":"message","description":"Triggered when a message is sent to a contact","event":"message.sent"}]}},"AvailableFieldsResponse":{"properties":{"fields":{"items":{"additionalProperties":{"type":"string"},"type":"object"},"type":"array","title":"Fields","description":"Available fields for variable mapping","default":[{"value":"contact.first_name","label":"First Name","example":"John"},{"value":"contact.last_name","label":"Last Name","example":"Doe"},{"value":"contact.name","label":"Full Name","example":"John Doe"},{"value":"contact.email","label":"Email","example":"john@example.com"},{"value":"contact.phone","label":"Phone","example":"+1234567890"},{"value":"contact.company","label":"Company","example":"Acme Inc"},{"value":"agent.name","label":"Agent Name","example":"Support Agent"},{"value":"agent.nickname","label":"Agent Nickname","example":"Sam"},{"value":"custom","label":"Custom Value","example":"(enter manually)"}]}},"type":"object","title":"AvailableFieldsResponse","description":"Response with available contact fields for variable mapping"},"AvailableNumberResponse":{"properties":{"number":{"type":"string","title":"Number"},"country":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Country"},"monthly_price_eur":{"anyOf":[{"type":"string","pattern":"^(?!^[-+.]*$)[+-]?0*\\d*\\.?\\d*$"},{"type":"null"}],"title":"Monthly Price Eur"},"yearly_price_eur":{"anyOf":[{"type":"string","pattern":"^(?!^[-+.]*$)[+-]?0*\\d*\\.?\\d*$"},{"type":"null"}],"title":"Yearly Price Eur"},"features":{"items":{"type":"string"},"type":"array","title":"Features"}},"type":"object","required":["number"],"title":"AvailableNumberResponse","description":"One number offered for purchase.\n\nPrices are captured at list-time and are the wholesale seven.io quote\n(EUR net). Our markup is applied server-side at provision-time."},"AvailableNumbersListResponse":{"properties":{"items":{"items":{"$ref":"#/components/schemas/AvailableNumberResponse"},"type":"array","title":"Items"},"country":{"type":"string","title":"Country"},"cached_until":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Cached Until"}},"type":"object","required":["items","country"],"title":"AvailableNumbersListResponse"},"AvailableScopesResponse":{"properties":{"scopes":{"additionalProperties":{"items":{"$ref":"#/components/schemas/ScopeInfo"},"type":"array"},"type":"object","title":"Scopes","description":"Scopes grouped by resource (e.g., 'campaigns', 'contacts')"},"total":{"type":"integer","title":"Total","description":"Total number of available scopes"}},"type":"object","required":["scopes","total"],"title":"AvailableScopesResponse","description":"Response containing all available scopes in the system.\n\nGrouped by resource for easy UI rendering.","example":{"scopes":{"campaigns":[{"action":"read","description":"View campaigns","scope":"campaigns:read"},{"action":"create","description":"Create campaigns","scope":"campaigns:create"}],"contacts":[{"action":"read","description":"View contacts","scope":"contacts:read"}]},"total":78}},"AvailableTokenScopesResponse":{"properties":{"scopes":{"items":{"type":"string"},"type":"array","title":"Scopes","description":"Flat list of all grantable scope strings"},"grouped":{"additionalProperties":{"items":{"$ref":"#/components/schemas/TokenScopeInfo"},"type":"array"},"type":"object","title":"Grouped","description":"Scopes grouped by resource for UI display"},"total":{"type":"integer","title":"Total","description":"Total number of grantable scopes"}},"type":"object","required":["scopes","grouped","total"],"title":"AvailableTokenScopesResponse","description":"Response containing all scopes the user can grant to API tokens.\n\nThis expands wildcard scopes to show all granular scopes available.\nUser with '*:*' will see all 78 scopes.\nUser with 'campaigns:*' will see all campaigns:* scopes.","example":{"grouped":{"campaigns":[{"action":"read","description":"View campaigns","scope":"campaigns:read"},{"action":"create","description":"Create campaigns","scope":"campaigns:create"}],"contacts":[{"action":"read","description":"View contacts","scope":"contacts:read"}]},"scopes":["campaigns:read","campaigns:create","contacts:read"],"total":3}},"BalanceResponse":{"properties":{"balance_eur":{"type":"string","pattern":"^(?!^[-+.]*$)[+-]?0*\\d*\\.?\\d*$","title":"Balance Eur"},"auto_topup_enabled":{"type":"boolean","title":"Auto Topup Enabled","default":false},"auto_topup_threshold_cents":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Auto Topup Threshold Cents"},"auto_topup_amount_cents":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Auto Topup Amount Cents"},"last_checked_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Last Checked At"},"low_balance_warn_eur":{"type":"string","pattern":"^(?!^[-+.]*$)[+-]?0*\\d*\\.?\\d*$","title":"Low Balance Warn Eur","description":"Effective low-balance warning threshold in EUR: the per-workspace override when set, otherwise the platform default."},"low_balance_warn_is_default":{"type":"boolean","title":"Low Balance Warn Is Default","description":"True when no per-workspace override is set (platform default applies).","default":true}},"type":"object","required":["balance_eur","low_balance_warn_eur"],"title":"BalanceResponse","description":"Prepaid SMS balance + the tenant's balance-related settings.\n\n``low_balance_warn_eur`` is the EFFECTIVE low-balance warning threshold\n(per-tenant override if set, else the platform default);\n``low_balance_warn_is_default`` tells whether it is the platform default.\nChange it with ``PUT /sms/low-balance-threshold``."},"BillingSummaryResponse":{"properties":{"outbound_messages":{"$ref":"#/components/schemas/DirectionMessageSummary","description":"Outbound message counts matching billing aggregation"},"inbound_messages":{"$ref":"#/components/schemas/DirectionMessageSummary","description":"Inbound message counts matching billing aggregation"},"campaigns":{"$ref":"#/components/schemas/CampaignSummary","description":"Campaign counts matching billing aggregation"},"kb_queries":{"type":"integer","title":"Kb Queries","description":"Knowledge base query count","default":0},"period_start":{"type":"string","title":"Period Start","description":"Start of the reporting period (ISO format)"},"period_end":{"type":"string","title":"Period End","description":"End of the reporting period (ISO format)"}},"type":"object","required":["period_start","period_end"],"title":"BillingSummaryResponse","description":"Billing-aligned analytics summary matching the exact billing aggregation logic.\n\nReturned by GET /api/v1/analytics/billing-summary","example":{"campaigns":{"email_byok":2000,"email_managed":5000,"push":1500},"inbound_messages":{"by_platform":{"email":50,"instagram":15,"live_chat":5,"messenger":20,"sms":30,"telegram":60,"whatsapp":120},"total":300},"kb_queries":450,"outbound_messages":{"by_platform":{"browser_push":5,"email":40,"instagram":10,"live_chat":10,"messenger":15,"sms":20,"telegram":50,"whatsapp":100},"total":250},"period_end":"2026-03-08T12:00:00Z","period_start":"2026-03-01T00:00:00Z"}},"BlockSenderDirectRequest":{"properties":{"email_address":{"type":"string","format":"email","title":"Email Address","description":"Email address to block"},"reason":{"anyOf":[{"type":"string","maxLength":1000},{"type":"null"}],"title":"Reason","description":"Reason for blocking"},"hide_existing_messages":{"type":"boolean","title":"Hide Existing Messages","description":"Also hide all existing messages from this sender","default":false}},"type":"object","required":["email_address"],"title":"BlockSenderDirectRequest","description":"Request to directly block an email address"},"BlockSenderRequest":{"properties":{"reason":{"anyOf":[{"type":"string","maxLength":1000},{"type":"null"}],"title":"Reason","description":"Reason for blocking"},"hide_existing_messages":{"type":"boolean","title":"Hide Existing Messages","description":"Also hide all existing messages from this sender","default":false}},"type":"object","title":"BlockSenderRequest","description":"Request to block an email sender"},"BlockedSenderListResponse":{"properties":{"items":{"items":{"$ref":"#/components/schemas/BlockedSenderResponse"},"type":"array","title":"Items","description":"List of items for this page"},"pagination":{"$ref":"#/components/schemas/PaginationMeta","description":"Pagination metadata"}},"type":"object","required":["items","pagination"],"title":"BlockedSenderListResponse","description":"Paginated list of blocked senders"},"BlockedSenderResponse":{"properties":{"id":{"type":"string","title":"Id"},"email_address":{"type":"string","title":"Email Address"},"blocked_by_user_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Blocked By User Id"},"reason":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Reason"},"created_at":{"type":"string","format":"date-time","title":"Created At"}},"type":"object","required":["id","email_address","created_at"],"title":"BlockedSenderResponse","description":"Response schema for blocked sender"},"Body_import_email_contacts_api_v1_email_contacts_import_post":{"properties":{"file":{"type":"string","format":"binary","title":"File","description":"CSV or XLSX file (.xlsx: first sheet, first row = headers). Anchor columns (at least one per row): contact_id, email, phone, whatsapp_id, display_name, first_name, last_name. Supports contact_id (UUID match target), first_name, last_name, display_name, email, phone, whatsapp_id, languages, birthday, tags, linkedin, instagram_handle, facebook, homepage, custom_fields."},"column_mapping":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Column Mapping","description":"JSON string mapping target fields to CSV column names, e.g. {\"email\": \"E-Mail\", \"name\": \"Full Name\", \"phone\": \"Telefon\"}. Per-EAV custom-field targets use the key \"custom_field:<field_definition_id>\"."},"duplicate_mode":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Duplicate Mode","description":"Override the tenant's duplicate_contact_mode for this import. Values: auto_merge, allow_duplicates, dont_allow_duplicates, update_existing. Omit to use the tenant default."},"update_field_mode":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Update Field Mode","description":"Only used with duplicate_mode=update_existing. 'overwrite' (default) replaces standard + EAV custom fields from the CSV (blank cells never wipe); 'fill_empty' only populates empty fields. Values: overwrite, fill_empty."},"subscriptions":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Subscriptions","description":"JSON array of structured channel->list subscription targets, e.g. [{\"channel_type\":\"email\",\"list_id\":\"...\"},{\"channel_type\":\"whatsapp\",\"list_id\":\"...\"}]. A contact is subscribed to a target only if it has the channel's required method (email→email, sms→phone, whatsapp→whatsapp_id/phone); others are silently skipped. Supported channels: email, sms, whatsapp. Takes precedence over create_subscription. Requires optin_confirmed=true."},"delimiter":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Delimiter","description":"CSV delimiter override. One of: ',' ';' tab '|'. Omit to auto-sniff."},"quotechar":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Quotechar","description":"CSV quote character override (default '\"'). Omit to auto-detect."},"default_phone_region":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Default Phone Region","description":"ISO-3166 alpha-2 region (e.g. 'DE') used to auto-prefix phone numbers lacking a '+' to E.164 (national '0175…' → '+49175…'). Omit to keep the lenient digit-prepend behaviour."},"apply_tag_ids":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Apply Tag Ids","description":"Post-import action: JSON array of tag IDs (e.g. [\"id1\",\"id2\"]) applied to EVERY contact this import creates OR updates. Tag IDs must belong to the tenant (404 otherwise)."},"apply_custom_field":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Apply Custom Field","description":"Post-import action: JSON object {\"field_id\": \"...\", \"value\": ...} — the custom-field value is set (overwrite) on EVERY contact this import creates OR updates. The field must be an active custom field of the tenant; SELECT/MULTISELECT values are options-validated, DATE/DATETIME must be ISO."},"identifiers_verified":{"type":"boolean","title":"Identifiers Verified","description":"Treat the imported phone numbers, email addresses and WhatsApp IDs as verified (you have confirmed the contacts control them). Stored with verified_source='import'. Default false: imported identifiers are unverified claims. Not available to OAuth apps.","default":false}},"type":"object","required":["file"],"title":"Body_import_email_contacts_api_v1_email_contacts_import_post"},"Body_sniff_csv_api_v1_email_contacts_sniff_post":{"properties":{"file":{"type":"string","format":"binary","title":"File","description":"CSV or XLSX file to inspect for delimiter/quotechar/headers"}},"type":"object","required":["file"],"title":"Body_sniff_csv_api_v1_email_contacts_sniff_post"},"Body_upload_agent_avatar_api_v1_automation_agents__agent_id__avatar_post":{"properties":{"file":{"type":"string","format":"binary","title":"File"}},"type":"object","required":["file"],"title":"Body_upload_agent_avatar_api_v1_automation_agents__agent_id__avatar_post"},"Body_upload_attachment_api_v1_attachments_upload_post":{"properties":{"file":{"type":"string","format":"binary","title":"File"}},"type":"object","required":["file"],"title":"Body_upload_attachment_api_v1_attachments_upload_post"},"Body_upload_bot_avatar_api_v1_automation_bots__bot_id__avatar_post":{"properties":{"file":{"type":"string","format":"binary","title":"File","description":"Avatar image file (PNG/JPG, max 2MB)"}},"type":"object","required":["file"],"title":"Body_upload_bot_avatar_api_v1_automation_bots__bot_id__avatar_post"},"Body_upload_channel_profile_photo_api_v1_channels__channel_id__profile_photo_post":{"properties":{"file":{"type":"string","format":"binary","title":"File"}},"type":"object","required":["file"],"title":"Body_upload_channel_profile_photo_api_v1_channels__channel_id__profile_photo_post"},"Body_upload_contact_avatar_api_v1_contacts__contact_id__avatar_post":{"properties":{"file":{"type":"string","format":"binary","title":"File"}},"type":"object","required":["file"],"title":"Body_upload_contact_avatar_api_v1_contacts__contact_id__avatar_post"},"Body_upload_document_file_api_v1_knowledge_base_documents_upload_post":{"properties":{"file":{"type":"string","format":"binary","title":"File"},"title":{"type":"string","title":"Title"},"document_type":{"type":"string","title":"Document Type","default":"uploaded_doc"},"folder_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Folder Id"}},"type":"object","required":["file","title"],"title":"Body_upload_document_file_api_v1_knowledge_base_documents_upload_post"},"Body_upload_keyword_trigger_media_api_v1_automation_bots__bot_id__keyword_triggers_upload_post":{"properties":{"file":{"type":"string","format":"binary","title":"File"}},"type":"object","required":["file"],"title":"Body_upload_keyword_trigger_media_api_v1_automation_bots__bot_id__keyword_triggers_upload_post"},"Body_upload_multiple_documents_api_v1_knowledge_base_documents_upload_multiple_post":{"properties":{"files":{"items":{"type":"string","format":"binary"},"type":"array","title":"Files","description":"Files to upload (max 200 files)"},"document_type":{"type":"string","title":"Document Type","default":"uploaded_doc"},"folder_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Folder Id"}},"type":"object","required":["files"],"title":"Body_upload_multiple_documents_api_v1_knowledge_base_documents_upload_multiple_post"},"Body_upload_template_media_api_v1_whatsapp_templates_upload_media_post":{"properties":{"channel_id":{"type":"string","title":"Channel Id"},"media_type":{"type":"string","title":"Media Type"},"file":{"type":"string","format":"binary","title":"File"}},"type":"object","required":["channel_id","media_type","file"],"title":"Body_upload_template_media_api_v1_whatsapp_templates_upload_media_post"},"Body_upload_user_avatar_api_v1_users_me_avatar_post":{"properties":{"file":{"type":"string","format":"binary","title":"File","description":"Avatar image file (PNG/JPG, max 2MB)"}},"type":"object","required":["file"],"title":"Body_upload_user_avatar_api_v1_users_me_avatar_post"},"BotActivationRuleCreate":{"properties":{"channel_ids":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Channel Ids","description":"Specific channel IDs to activate for (preferred over channel_types)"},"channel_types":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Channel Types","description":"[DEPRECATED] Channel types to activate for. Use channel_ids instead."},"widget_ids":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Widget Ids","description":"Specific widget IDs to activate for"},"is_schedule_enabled":{"type":"boolean","title":"Is Schedule Enabled","description":"Enable time-based activation","default":false},"timezone":{"type":"string","title":"Timezone","description":"IANA timezone for schedule (e.g., 'Europe/Berlin')","default":"UTC"},"time_ranges":{"anyOf":[{"items":{"$ref":"#/components/schemas/TimeRangeConfig"},"type":"array"},{"type":"null"}],"title":"Time Ranges","description":"Time ranges when bot should be active"},"requires_no_agents_online":{"type":"boolean","title":"Requires No Agents Online","description":"Only activate when no human agents are online","default":false},"recipient_email_addresses":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Recipient Email Addresses","description":"Only activate for emails sent TO these addresses (e.g., ['support@example.com'])"},"priority":{"type":"integer","maximum":100.0,"minimum":0.0,"title":"Priority","description":"Rule priority (higher = evaluated first)","default":0},"is_active":{"type":"boolean","title":"Is Active","description":"Whether rule is active","default":true}},"additionalProperties":false,"type":"object","title":"BotActivationRuleCreate","description":"Schema for creating an activation rule"},"BotActivationRuleResponse":{"properties":{"id":{"type":"string","title":"Id"},"bot_id":{"type":"string","title":"Bot Id"},"tenant_id":{"type":"string","title":"Tenant Id"},"channel_ids":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Channel Ids"},"channel_types":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Channel Types"},"widget_ids":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Widget Ids"},"is_schedule_enabled":{"type":"boolean","title":"Is Schedule Enabled"},"timezone":{"type":"string","title":"Timezone"},"time_ranges":{"anyOf":[{"items":{"additionalProperties":true,"type":"object"},"type":"array"},{"type":"null"}],"title":"Time Ranges"},"requires_no_agents_online":{"type":"boolean","title":"Requires No Agents Online"},"recipient_email_addresses":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Recipient Email Addresses"},"priority":{"type":"integer","title":"Priority"},"is_active":{"type":"boolean","title":"Is Active"},"created_at":{"type":"string","format":"date-time","title":"Created At"},"updated_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Updated At"}},"type":"object","required":["id","bot_id","tenant_id","is_schedule_enabled","timezone","requires_no_agents_online","priority","is_active","created_at"],"title":"BotActivationRuleResponse","description":"Response schema for activation rule"},"BotActivationRuleUpdate":{"properties":{"channel_ids":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Channel Ids"},"channel_types":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Channel Types"},"widget_ids":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Widget Ids"},"is_schedule_enabled":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Is Schedule Enabled"},"timezone":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Timezone"},"time_ranges":{"anyOf":[{"items":{"$ref":"#/components/schemas/TimeRangeConfig"},"type":"array"},{"type":"null"}],"title":"Time Ranges"},"requires_no_agents_online":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Requires No Agents Online"},"recipient_email_addresses":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Recipient Email Addresses"},"priority":{"anyOf":[{"type":"integer","maximum":100.0,"minimum":0.0},{"type":"null"}],"title":"Priority"},"is_active":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Is Active"}},"additionalProperties":false,"type":"object","title":"BotActivationRuleUpdate","description":"Schema for updating an activation rule"},"BotAnalyticsResponse":{"properties":{"bot_id":{"type":"string","title":"Bot Id"},"bot_name":{"type":"string","title":"Bot Name"},"period_start":{"type":"string","format":"date-time","title":"Period Start"},"period_end":{"type":"string","format":"date-time","title":"Period End"},"total_sessions":{"type":"integer","title":"Total Sessions","default":0},"active_sessions":{"type":"integer","title":"Active Sessions","default":0},"completed_sessions":{"type":"integer","title":"Completed Sessions","default":0},"escalated_sessions":{"type":"integer","title":"Escalated Sessions","default":0},"abandoned_sessions":{"type":"integer","title":"Abandoned Sessions","default":0},"resolution_rate":{"type":"number","title":"Resolution Rate","default":0.0},"escalation_rate":{"type":"number","title":"Escalation Rate","default":0.0},"avg_messages_per_session":{"type":"number","title":"Avg Messages Per Session","default":0.0},"avg_confidence_score":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Avg Confidence Score"},"conversations":{"type":"integer","title":"Conversations","default":0},"resolved_conversations":{"type":"integer","title":"Resolved Conversations","default":0},"handoff_conversations":{"type":"integer","title":"Handoff Conversations","default":0},"resolved_pct":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Resolved Pct"},"handoff_pct":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Handoff Pct"},"insufficient_data":{"type":"boolean","title":"Insufficient Data","default":true},"min_sample":{"type":"integer","title":"Min Sample","default":20},"total_input_tokens":{"type":"integer","title":"Total Input Tokens","default":0},"total_output_tokens":{"type":"integer","title":"Total Output Tokens","default":0},"escalation_reasons":{"additionalProperties":{"type":"integer"},"type":"object","title":"Escalation Reasons"}},"type":"object","required":["bot_id","bot_name","period_start","period_end"],"title":"BotAnalyticsResponse","description":"Analytics overview for a bot"},"BotAnalyticsSummary":{"properties":{"tenant_id":{"type":"string","title":"Tenant Id"},"period_start":{"type":"string","format":"date-time","title":"Period Start"},"period_end":{"type":"string","format":"date-time","title":"Period End"},"period_days":{"type":"integer","title":"Period Days","default":30},"total_bots":{"type":"integer","title":"Total Bots","default":0},"active_bots":{"type":"integer","title":"Active Bots","default":0},"total_sessions":{"type":"integer","title":"Total Sessions","default":0},"total_completions":{"type":"integer","title":"Total Completions","default":0},"total_escalations":{"type":"integer","title":"Total Escalations","default":0},"total_abandonments":{"type":"integer","title":"Total Abandonments","default":0},"total_messages":{"type":"integer","title":"Total Messages","default":0},"overall_resolution_rate":{"type":"number","title":"Overall Resolution Rate","default":0.0},"overall_escalation_rate":{"type":"number","title":"Overall Escalation Rate","default":0.0},"avg_messages_per_session":{"type":"number","title":"Avg Messages Per Session","default":0.0},"total_conversations":{"type":"integer","title":"Total Conversations","default":0},"total_resolved_conversations":{"type":"integer","title":"Total Resolved Conversations","default":0},"total_handoff_conversations":{"type":"integer","title":"Total Handoff Conversations","default":0},"resolved_pct":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Resolved Pct"},"handoff_pct":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Handoff Pct"},"insufficient_data":{"type":"boolean","title":"Insufficient Data","default":true},"min_sample":{"type":"integer","title":"Min Sample","default":20},"bot_breakdown":{"items":{"$ref":"#/components/schemas/BotBreakdownItem"},"type":"array","title":"Bot Breakdown"}},"type":"object","required":["tenant_id","period_start","period_end"],"title":"BotAnalyticsSummary","description":"Summary analytics across all bots"},"BotBreakdownItem":{"properties":{"bot_id":{"type":"string","title":"Bot Id"},"bot_name":{"type":"string","title":"Bot Name"},"bot_type":{"type":"string","title":"Bot Type","default":"support"},"is_active":{"type":"boolean","title":"Is Active","default":true},"sessions":{"type":"integer","title":"Sessions","default":0},"escalations":{"type":"integer","title":"Escalations","default":0},"resolution_rate":{"type":"number","title":"Resolution Rate","default":0.0},"conversations":{"type":"integer","title":"Conversations","default":0},"resolved_conversations":{"type":"integer","title":"Resolved Conversations","default":0},"handoff_conversations":{"type":"integer","title":"Handoff Conversations","default":0},"resolved_pct":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Resolved Pct"},"handoff_pct":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Handoff Pct"},"insufficient_data":{"type":"boolean","title":"Insufficient Data","default":true}},"type":"object","required":["bot_id","bot_name"],"title":"BotBreakdownItem","description":"Per-bot analytics breakdown"},"BotConversationSessionListResponse":{"properties":{"id":{"type":"string","title":"Id"},"conversation_id":{"type":"string","title":"Conversation Id"},"bot_id":{"type":"string","title":"Bot Id"},"bot_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Bot Name"},"status":{"$ref":"#/components/schemas/SessionStatusEnum"},"message_count":{"type":"integer","title":"Message Count"},"escalation_reason":{"anyOf":[{"$ref":"#/components/schemas/EscalationReasonEnum"},{"type":"null"}]},"started_at":{"type":"string","format":"date-time","title":"Started At"},"ended_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Ended At"}},"type":"object","required":["id","conversation_id","bot_id","status","message_count","started_at"],"title":"BotConversationSessionListResponse","description":"Response schema for listing sessions"},"BotConversationSessionResponse":{"properties":{"id":{"type":"string","title":"Id"},"conversation_id":{"type":"string","title":"Conversation Id"},"bot_id":{"type":"string","title":"Bot Id"},"tenant_id":{"type":"string","title":"Tenant Id"},"status":{"$ref":"#/components/schemas/SessionStatusEnum"},"message_count":{"type":"integer","title":"Message Count"},"low_confidence_count":{"type":"integer","title":"Low Confidence Count"},"collected_fields":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Collected Fields"},"pending_fields":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Pending Fields"},"escalation_reason":{"anyOf":[{"$ref":"#/components/schemas/EscalationReasonEnum"},{"type":"null"}]},"escalated_to_user_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Escalated To User Id"},"started_at":{"type":"string","format":"date-time","title":"Started At"},"ended_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Ended At"},"last_interaction_at":{"type":"string","format":"date-time","title":"Last Interaction At"}},"type":"object","required":["id","conversation_id","bot_id","tenant_id","status","message_count","low_confidence_count","started_at","last_interaction_at"],"title":"BotConversationSessionResponse","description":"Response schema for bot conversation session"},"BotInteractionLogResponse":{"properties":{"id":{"type":"string","title":"Id"},"session_id":{"type":"string","title":"Session Id"},"bot_id":{"type":"string","title":"Bot Id"},"tenant_id":{"type":"string","title":"Tenant Id"},"message_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Message Id"},"user_input":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"User Input"},"bot_response":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Bot Response"},"confidence_score":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Confidence Score"},"kb_sources_used":{"anyOf":[{"items":{"additionalProperties":true,"type":"object"},"type":"array"},{"type":"null"}],"title":"Kb Sources Used"},"input_tokens":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Input Tokens"},"output_tokens":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Output Tokens"},"model_used":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Model Used"},"created_at":{"type":"string","format":"date-time","title":"Created At"}},"type":"object","required":["id","session_id","bot_id","tenant_id","created_at"],"title":"BotInteractionLogResponse","description":"Response schema for bot interaction log"},"BotKBFolderConfig":{"properties":{"folder_id":{"type":"string","title":"Folder Id","description":"Folder ID"},"include_subfolders":{"type":"boolean","title":"Include Subfolders","description":"Whether to include documents from child folders","default":true}},"type":"object","required":["folder_id"],"title":"BotKBFolderConfig","description":"Configuration for a bot's access to a KB folder.","example":{"folder_id":"folder-uuid-123","include_subfolders":true}},"BotKBFolderResponse":{"properties":{"id":{"type":"string","title":"Id","description":"Assignment ID"},"bot_id":{"type":"string","title":"Bot Id","description":"Bot ID"},"folder_id":{"type":"string","title":"Folder Id","description":"Folder ID"},"folder_name":{"type":"string","title":"Folder Name","description":"Folder name"},"folder_path":{"type":"string","title":"Folder Path","description":"Full folder path (e.g., 'Websites/docs.example.com')"},"include_subfolders":{"type":"boolean","title":"Include Subfolders","description":"Whether subfolders are included"}},"type":"object","required":["id","bot_id","folder_id","folder_name","folder_path","include_subfolders"],"title":"BotKBFolderResponse","description":"Response for a single bot-folder assignment.","example":{"bot_id":"bot-uuid-456","folder_id":"folder-uuid-789","folder_name":"docs.example.com","folder_path":"Websites/docs.example.com","id":"link-uuid-123","include_subfolders":true}},"BotKBFoldersRequest":{"properties":{"folders":{"items":{"$ref":"#/components/schemas/BotKBFolderConfig"},"type":"array","title":"Folders","description":"List of folder configurations for the bot"}},"type":"object","required":["folders"],"title":"BotKBFoldersRequest","description":"Request to set a bot's KB folder configuration.","example":{"folders":[{"folder_id":"folder-uuid-1","include_subfolders":true},{"folder_id":"folder-uuid-2","include_subfolders":false}]}},"BotKBFoldersResponse":{"properties":{"bot_id":{"type":"string","title":"Bot Id","description":"Bot ID"},"folders":{"items":{"$ref":"#/components/schemas/BotKBFolderResponse"},"type":"array","title":"Folders","description":"List of folder assignments"},"total":{"type":"integer","title":"Total","description":"Total number of assignments","default":0}},"type":"object","required":["bot_id"],"title":"BotKBFoldersResponse","description":"Response for a bot's KB folder configuration.","example":{"bot_id":"bot-uuid-456","folders":[{"bot_id":"bot-uuid-456","folder_id":"folder-uuid-789","folder_name":"Product Documentation","folder_path":"Product Documentation","id":"link-uuid-123","include_subfolders":true}],"total":1}},"BotReorderRequest":{"properties":{"bot_ids":{"items":{"type":"string"},"type":"array","minItems":1,"title":"Bot Ids","description":"List of bot IDs in desired priority order (first = highest priority)"}},"additionalProperties":false,"type":"object","required":["bot_ids"],"title":"BotReorderRequest","description":"Request schema for reordering bots"},"BotReorderResponse":{"properties":{"success":{"type":"boolean","title":"Success","default":true},"updated_count":{"type":"integer","title":"Updated Count","description":"Number of bots updated"},"bots":{"items":{"$ref":"#/components/schemas/AutomationBotListResponse"},"type":"array","title":"Bots","description":"Updated bots in new priority order"}},"type":"object","required":["updated_count","bots"],"title":"BotReorderResponse","description":"Response schema for reorder operation"},"BotTypeEnum":{"type":"string","enum":["assistant","support","router","sales","lead_qualification","faq","appointment","keyword","email_support","agent"],"title":"BotTypeEnum","description":"Type of automation bot.\n\n'assistant' is the preferred value for new bots (Phase 2 consolidation).\nAll legacy values remain valid for backwards compatibility."},"BounceAnalyticsCategoryCount":{"properties":{"category":{"type":"string","title":"Category"},"count":{"type":"integer","title":"Count"}},"type":"object","required":["category","count"],"title":"BounceAnalyticsCategoryCount","description":"Bounce count by category"},"BounceAnalyticsDomainCount":{"properties":{"domain":{"type":"string","title":"Domain"},"count":{"type":"integer","title":"Count"}},"type":"object","required":["domain","count"],"title":"BounceAnalyticsDomainCount","description":"Bounce count by domain"},"BounceAnalyticsResponse":{"properties":{"total_bounces":{"type":"integer","title":"Total Bounces"},"hard_bounces":{"type":"integer","title":"Hard Bounces"},"soft_bounces":{"type":"integer","title":"Soft Bounces"},"hard_bounce_rate":{"type":"number","title":"Hard Bounce Rate"},"soft_bounce_rate":{"type":"number","title":"Soft Bounce Rate"},"top_categories":{"items":{"$ref":"#/components/schemas/BounceAnalyticsCategoryCount"},"type":"array","title":"Top Categories"},"top_domains":{"items":{"$ref":"#/components/schemas/BounceAnalyticsDomainCount"},"type":"array","title":"Top Domains"},"period_days":{"type":"integer","title":"Period Days"}},"type":"object","required":["total_bounces","hard_bounces","soft_bounces","hard_bounce_rate","soft_bounce_rate","top_categories","top_domains","period_days"],"title":"BounceAnalyticsResponse","description":"Bounce analytics response"},"BounceSuppressionListResponse":{"properties":{"items":{"items":{"$ref":"#/components/schemas/BounceSuppressionResponse"},"type":"array","title":"Items"},"total":{"type":"integer","title":"Total"},"limit":{"type":"integer","title":"Limit"},"offset":{"type":"integer","title":"Offset"}},"type":"object","required":["items","total","limit","offset"],"title":"BounceSuppressionListResponse","description":"Paginated list of suppressions"},"BounceSuppressionResponse":{"properties":{"id":{"type":"string","title":"Id"},"email":{"type":"string","title":"Email"},"bounce_type":{"type":"string","title":"Bounce Type"},"bounce_category":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Bounce Category"},"reason":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Reason"},"bounce_count":{"type":"integer","title":"Bounce Count"},"first_bounced_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"First Bounced At"},"last_bounced_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Last Bounced At"}},"type":"object","required":["id","email","bounce_type","bounce_count"],"title":"BounceSuppressionResponse","description":"Response for single suppression entry"},"BrandColors":{"properties":{"primary":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Primary","description":"Primary brand color (hex code)"},"secondary":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Secondary","description":"Secondary brand color (hex code)"},"accent":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Accent","description":"Accent color (hex code)"}},"type":"object","title":"BrandColors","description":"Brand color configuration"},"BrowserPushContent":{"properties":{"channel_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Channel Id"},"title":{"type":"string","title":"Title"},"text":{"type":"string","title":"Text"},"url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Url"},"image_attachment_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Image Attachment Id"}},"type":"object","required":["title","text"],"title":"BrowserPushContent","description":"Browser push notification content."},"BulkActionExecuteRequest":{"properties":{"selection":{"$ref":"#/components/schemas/BulkActionSelection"},"actions":{"items":{"anyOf":[{"$ref":"#/components/schemas/SetFieldAction"},{"$ref":"#/components/schemas/SetCustomFieldAction"},{"$ref":"#/components/schemas/AddTagsAction"},{"$ref":"#/components/schemas/RemoveTagsAction"},{"$ref":"#/components/schemas/AddToListAction"},{"$ref":"#/components/schemas/SetBlockedAction"}]},"type":"array","maxItems":20,"minItems":1,"title":"Actions","description":"Ordered list of actions, executed in order per contact"},"confirmation_token":{"type":"string","title":"Confirmation Token","description":"Signed token from POST /contacts/bulk-actions/preview"}},"type":"object","required":["selection","actions","confirmation_token"],"title":"BulkActionExecuteRequest"},"BulkActionExecuteResponse":{"properties":{"bulk_action_id":{"type":"string","title":"Bulk Action Id"},"status":{"type":"string","title":"Status","description":"pending|processing|completed|failed"},"poll_url":{"type":"string","title":"Poll Url","description":"GET this URL to poll job status"}},"type":"object","required":["bulk_action_id","status","poll_url"],"title":"BulkActionExecuteResponse"},"BulkActionPreviewRequest":{"properties":{"selection":{"$ref":"#/components/schemas/BulkActionSelection"},"actions":{"items":{"anyOf":[{"$ref":"#/components/schemas/SetFieldAction"},{"$ref":"#/components/schemas/SetCustomFieldAction"},{"$ref":"#/components/schemas/AddTagsAction"},{"$ref":"#/components/schemas/RemoveTagsAction"},{"$ref":"#/components/schemas/AddToListAction"},{"$ref":"#/components/schemas/SetBlockedAction"}]},"type":"array","maxItems":20,"minItems":1,"title":"Actions","description":"Ordered list of actions, executed in order per contact"}},"type":"object","required":["selection","actions"],"title":"BulkActionPreviewRequest"},"BulkActionPreviewResponse":{"properties":{"affected_count":{"type":"integer","title":"Affected Count","description":"Contacts currently matching the selection"},"sample":{"items":{"$ref":"#/components/schemas/BulkActionSampleContact"},"type":"array","title":"Sample","description":"Up to 5 matching contacts for the confirmation dialog"},"confirmation_token":{"type":"string","title":"Confirmation Token","description":"Pass back verbatim to /execute together with the SAME selection+actions"},"expires_in_seconds":{"type":"integer","title":"Expires In Seconds","description":"Token TTL (seconds)"}},"type":"object","required":["affected_count","sample","confirmation_token","expires_in_seconds"],"title":"BulkActionPreviewResponse"},"BulkActionSampleContact":{"properties":{"id":{"type":"string","title":"Id"},"display_name":{"type":"string","title":"Display Name"}},"type":"object","required":["id","display_name"],"title":"BulkActionSampleContact"},"BulkActionSelection":{"properties":{"mode":{"type":"string","enum":["ids","filter"],"title":"Mode"},"ids":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Ids","description":"Contact ids (mode='ids')"},"filter":{"anyOf":[{"$ref":"#/components/schemas/ContactFilterParams"},{"type":"null"}],"description":"GET /contacts filter params (mode='filter')"}},"type":"object","required":["mode"],"title":"BulkActionSelection","description":"How the target contacts are selected.\n\n- ``mode='ids'``     → explicit list of contact UUIDs (``ids`` required).\n- ``mode='filter'``  → all contacts matching ``filter`` (GET /contacts\n  semantics). ``filter`` may be ``{}`` to target ALL contacts."},"BulkActionStatusResponse":{"properties":{"id":{"type":"string","title":"Id"},"tenant_id":{"type":"string","title":"Tenant Id"},"user_id":{"type":"string","title":"User Id"},"status":{"type":"string","title":"Status","description":"pending|processing|completed|failed"},"affected_count":{"type":"integer","title":"Affected Count","description":"Contacts actually CHANGED so far (running total while processing; final total when completed)"},"selection":{"additionalProperties":true,"type":"object","title":"Selection"},"actions":{"items":{"additionalProperties":true,"type":"object"},"type":"array","title":"Actions"},"error":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Error"},"created_at":{"type":"string","title":"Created At"},"started_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Started At"},"completed_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Completed At"}},"type":"object","required":["id","tenant_id","user_id","status","affected_count","selection","actions","created_at"],"title":"BulkActionStatusResponse","description":"One bulk action run (poll endpoint + history items)."},"BulkCloseRequest":{"properties":{"conversation_ids":{"items":{"type":"string"},"type":"array","maxItems":100,"minItems":1,"title":"Conversation Ids","description":"List of conversation UUIDs to close"},"reason":{"anyOf":[{"type":"string","maxLength":500},{"type":"null"}],"title":"Reason","description":"Optional close reason (e.g., 'spam', 'duplicate')"}},"type":"object","required":["conversation_ids"],"title":"BulkCloseRequest","description":"Request body for bulk closing multiple conversations","example":{"conversation_ids":["c1a2b3c4-d5e6-f7g8-h9i0-j1k2l3m4n5o6","d2b3c4d5-e6f7-g8h9-i0j1-k2l3m4n5o6p7"],"reason":"spam"}},"BulkCloseResponse":{"properties":{"success_count":{"type":"integer","title":"Success Count","description":"Number of successfully closed conversations"},"failed_count":{"type":"integer","title":"Failed Count","description":"Number of failed closures"},"failed_ids":{"items":{"type":"string"},"type":"array","title":"Failed Ids","description":"IDs of conversations that failed to close","default":[]}},"type":"object","required":["success_count","failed_count"],"title":"BulkCloseResponse","description":"Response for bulk close operation","example":{"failed_count":2,"failed_ids":["c1a2b3c4-d5e6-f7g8-h9i0-j1k2l3m4n5o6","d2b3c4d5-e6f7-g8h9-i0j1-k2l3m4n5o6p7"],"success_count":8}},"BulkCreateLinksRequest":{"properties":{"urls":{"items":{"type":"string"},"type":"array","title":"Urls","description":"List of original URLs"},"feature_type":{"$ref":"#/components/schemas/FeatureType","description":"Feature using these tracked links"},"feature_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Feature Id","description":"ID of campaign/conversation/etc."},"expires_in_days":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Expires In Days","description":"Days until expiration (default: 365)"}},"type":"object","required":["urls","feature_type"],"title":"BulkCreateLinksRequest","description":"Request to create multiple tracked links"},"BulkCreateLinksResponse":{"properties":{"url_mapping":{"additionalProperties":{"type":"string"},"type":"object","title":"Url Mapping","description":"Mapping of original URLs to short URLs"},"created_count":{"type":"integer","title":"Created Count","description":"Number of links created"}},"type":"object","required":["url_mapping","created_count"],"title":"BulkCreateLinksResponse","description":"Response for bulk create links"},"BulkDeleteContactsRequest":{"properties":{"contact_ids":{"items":{"type":"string"},"type":"array","maxItems":100,"minItems":1,"title":"Contact Ids"}},"type":"object","required":["contact_ids"],"title":"BulkDeleteContactsRequest"},"BulkDeleteContactsResponse":{"properties":{"success_count":{"type":"integer","title":"Success Count"},"failed_count":{"type":"integer","title":"Failed Count"},"failed_ids":{"items":{"type":"string"},"type":"array","title":"Failed Ids","default":[]}},"type":"object","required":["success_count","failed_count"],"title":"BulkDeleteContactsResponse"},"BulkDeleteQueuedItem":{"properties":{"document_id":{"type":"string","title":"Document Id","description":"Document ID that was queued for deletion"},"job_id":{"type":"string","title":"Job Id","description":"Async job ID assigned to this deletion"}},"type":"object","required":["document_id","job_id"],"title":"BulkDeleteQueuedItem","description":"One queued deletion produced by POST /documents/bulk-delete."},"BulkDeleteRequest":{"properties":{"document_ids":{"items":{"type":"string"},"type":"array","minItems":1,"title":"Document Ids","description":"List of document IDs to delete (all must belong to the caller's tenant)."}},"type":"object","required":["document_ids"],"title":"BulkDeleteRequest","description":"Request body for POST /documents/bulk-delete.\n\nNo size limit is enforced at the schema layer per product decision\n(large multi-select delete is an explicit user action and is safe\nbecause each row becomes an individual Pub/Sub message)."},"BulkDeleteResponse":{"properties":{"queued":{"items":{"$ref":"#/components/schemas/BulkDeleteQueuedItem"},"type":"array","title":"Queued","description":"Per-document job handles. Poll GET /jobs/{job_id} for progress."}},"type":"object","title":"BulkDeleteResponse","description":"Response body for POST /documents/bulk-delete."},"BulkMoveRequest":{"properties":{"document_ids":{"items":{"type":"string"},"type":"array","maxItems":100,"minItems":1,"title":"Document Ids","description":"List of document IDs to move (1-100)"},"folder_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Folder Id","description":"Target folder ID. Use null for root level."}},"type":"object","required":["document_ids","folder_id"],"title":"BulkMoveRequest","description":"Request to move multiple documents to a folder.","example":{"document_ids":["doc-uuid-1","doc-uuid-2","doc-uuid-3"],"folder_id":"folder-uuid-123"}},"BulkMoveResponse":{"properties":{"success_count":{"type":"integer","title":"Success Count","description":"Number of documents successfully moved in database"},"failed_count":{"type":"integer","title":"Failed Count","description":"Number of documents that failed to move"},"failed_ids":{"items":{"type":"string"},"type":"array","title":"Failed Ids","description":"IDs of documents that failed to move"},"corpus_move_count":{"type":"integer","title":"Corpus Move Count","description":"Number of documents with successful Vertex AI corpus moves. If less than success_count, some documents may not be AI-searchable in the new folder.","default":0},"warnings":{"items":{"type":"string"},"type":"array","title":"Warnings","description":"Warning messages for documents that were moved in database but had corpus issues"}},"type":"object","required":["success_count","failed_count"],"title":"BulkMoveResponse","description":"Response for bulk move operation.\n\nIncludes information about both database moves and Vertex AI corpus moves.\nWhen corpus_move_count < success_count, some documents may not be searchable\nvia AI in their new folder.","example":{"corpus_move_count":6,"failed_count":2,"failed_ids":["doc-uuid-3","doc-uuid-5"],"success_count":8,"warnings":["No content found for document 'Old Guide' - AI search may not work correctly in new folder","Document 'Legacy Doc' was not in an AI corpus - may not be searchable in new folder"]}},"BulkSuppressRequest":{"properties":{"emails":{"items":{"type":"string","format":"email"},"type":"array","maxItems":1000,"minItems":1,"title":"Emails"},"reason":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Reason","default":"manual"}},"type":"object","required":["emails"],"title":"BulkSuppressRequest","description":"Request to bulk suppress emails"},"BulkSuppressResponse":{"properties":{"added":{"type":"integer","title":"Added"},"skipped":{"type":"integer","title":"Skipped"},"total":{"type":"integer","title":"Total"}},"type":"object","required":["added","skipped","total"],"title":"BulkSuppressResponse","description":"Response for bulk suppress operation"},"BusinessHoursResponse":{"properties":{"configured":{"type":"boolean","title":"Configured"},"timezone":{"type":"string","title":"Timezone"},"time_ranges":{"items":{"$ref":"#/components/schemas/BusinessHoursTimeRange"},"type":"array","title":"Time Ranges"},"updated_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Updated At"}},"type":"object","required":["configured","timezone","time_ranges"],"title":"BusinessHoursResponse"},"BusinessHoursTimeRange":{"properties":{"days":{"items":{"type":"integer"},"type":"array","title":"Days"},"start":{"type":"string","title":"Start"},"end":{"type":"string","title":"End"}},"type":"object","required":["days","start","end"],"title":"BusinessHoursTimeRange"},"BusinessHoursUpdate":{"properties":{"timezone":{"type":"string","maxLength":50,"minLength":1,"title":"Timezone"},"time_ranges":{"items":{"additionalProperties":true,"type":"object"},"type":"array","maxItems":50,"minItems":1,"title":"Time Ranges"}},"type":"object","required":["timezone","time_ranges"],"title":"BusinessHoursUpdate"},"BusinessHoursUsageItem":{"properties":{"bot_id":{"type":"string","title":"Bot Id"},"agent_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Agent Id"},"name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Name"},"bot_type":{"type":"string","title":"Bot Type"},"uses":{"items":{"type":"string","enum":["routing","handoff"]},"type":"array","title":"Uses"},"is_active":{"type":"boolean","title":"Is Active"},"publish_state":{"type":"string","enum":["draft","published"],"title":"Publish State"}},"type":"object","required":["bot_id","bot_type","uses","is_active","publish_state"],"title":"BusinessHoursUsageItem"},"BusinessHoursUsageResponse":{"properties":{"items":{"items":{"$ref":"#/components/schemas/BusinessHoursUsageItem"},"type":"array","title":"Items"}},"type":"object","required":["items"],"title":"BusinessHoursUsageResponse"},"CTWAAdBreakdownItem":{"properties":{"source_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Source Id","description":"Ad/post identity — the grouping key for the breakdown"},"headline":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Headline","description":"Human-readable ad label (may be null if not captured)"},"source_type":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Source Type","description":"Source type, e.g. 'ad' or 'post' (FB vs IG); may be null"},"source_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Source Url","description":"Link to the originating ad/post (may be null)"},"count":{"type":"integer","title":"Count","description":"Number of conversations driven by this source in the range"}},"type":"object","required":["count"],"title":"CTWAAdBreakdownItem","description":"A single ad/post source ranked by the number of CTWA conversations it drove.\n\nPurely informational counts — no cost or ROI data."},"CTWAAdBreakdownResponse":{"properties":{"total_conversations":{"type":"integer","title":"Total Conversations","description":"Total CTWA conversations in the date range (sum of all ad counts)"},"has_data":{"type":"boolean","title":"Has Data","description":"True when the tenant has at least one CTWA conversation in the range. Convenience flag for conditional rendering; equals total_conversations > 0."},"ads":{"items":{"$ref":"#/components/schemas/CTWAAdBreakdownItem"},"type":"array","title":"Ads","description":"Ad/post sources ranked by conversation count, descending"},"period_start":{"type":"string","title":"Period Start","description":"Start of the reporting period (ISO format)"},"period_end":{"type":"string","title":"Period End","description":"End of the reporting period (ISO format)"}},"type":"object","required":["total_conversations","has_data","period_start","period_end"],"title":"CTWAAdBreakdownResponse","description":"Click-to-WhatsApp \"Conversations from Ads\" breakdown.\n\nReturned by GET /api/v1/analytics/ctwa/ad-breakdown\n\nPurely informational. The frontend uses ``total_conversations == 0``\n(equivalently ``has_data == False``) to hide the section entirely — no\nseparate presence-check endpoint is needed.","example":{"ads":[{"count":30,"headline":"Summer Sale — Chat with us on WhatsApp","source_id":"120210000000000000","source_type":"ad","source_url":"https://fb.me/ad/120210000000000000"},{"count":12,"source_id":"120210000000000001","source_type":"post"}],"has_data":true,"period_end":"2026-06-11T23:59:59.999999Z","period_start":"2026-05-12T00:00:00Z","total_conversations":42}},"CampaignBatchItem":{"properties":{"batch_number":{"type":"integer","title":"Batch Number"},"total":{"type":"integer","title":"Total","default":0},"pending":{"type":"integer","title":"Pending","default":0},"sent":{"type":"integer","title":"Sent","default":0},"delivered":{"type":"integer","title":"Delivered","default":0},"read":{"type":"integer","title":"Read","default":0},"failed":{"type":"integer","title":"Failed","default":0},"skipped":{"type":"integer","title":"Skipped","default":0}},"additionalProperties":true,"type":"object","required":["batch_number"],"title":"CampaignBatchItem","description":"Per-batch delivery counts (one row per batch number)."},"CampaignBatchListResponse":{"properties":{"items":{"items":{"$ref":"#/components/schemas/CampaignBatchItem"},"type":"array","title":"Items","description":"List of items for this page"},"pagination":{"$ref":"#/components/schemas/PaginationMeta","description":"Pagination metadata"},"remaining":{"type":"integer","title":"Remaining","description":"Recipients waiting for a later batch","default":0},"current_batch_number":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Current Batch Number","description":"Batch the campaign is on; null = not sent in batches"},"send_batch_size":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Send Batch Size","description":"Saved batch size; null = no batches"}},"type":"object","required":["items","pagination"],"title":"CampaignBatchListResponse","description":"``GET /campaigns/{id}/batches``: batches plus the waiting total."},"CampaignCreate":{"properties":{"name":{"type":"string","title":"Name"},"description":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Description"},"channel_content":{"anyOf":[{"$ref":"#/components/schemas/ChannelContent"},{"type":"null"}]},"message_text":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Message Text"},"message_type":{"type":"string","title":"Message Type","default":"text"},"attachment_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Attachment Id"},"target_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Target Url"},"browser_push_title":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Browser Push Title"},"list_ids":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"List Ids"},"audience_conditions":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Audience Conditions","description":"Audience Builder condition tree (same format as a dynamic list's `segment_conditions`). Must be a top-level `AND` whose children include at least one positive list membership: a `has_optin` condition (`operator: equals`, `value: true`, `list_id` of a static or newsletter list of this workspace) or a group of only such conditions (`OR` = any of the lists, `AND` = all of them). Other children narrow the audience down or exclude contacts (e.g. `has_optin` with `value: false`). When set, the campaign's lists are DERIVED from the membership lists and any `list_ids` sent are ignored. Errors: 422 `audience_list_membership_required`, `audience_membership_list_invalid`, or a validation error for a malformed tree. A tree with no conditions counts as null."},"channel_filter":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Channel Filter"},"selected_channel_ids":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Selected Channel Ids"},"scheduled_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Scheduled At"},"timezone":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Timezone"},"send_rate_limit":{"anyOf":[{"type":"integer"},{"type":"null"}],"maximum":60000.0,"minimum":1.0,"title":"Send Rate Limit","description":"Send speed in messages per minute (1-60000). Null = channel default speed."},"send_batch_size":{"anyOf":[{"type":"integer"},{"type":"null"}],"maximum":1000000.0,"minimum":1.0,"title":"Send Batch Size","description":"Recipients per batch (1-1,000,000). The campaign pauses after each batch until the next batch is sent. Null = no batches."},"should_skip_duplicates":{"type":"boolean","title":"Should Skip Duplicates","default":true},"duplicate_window_hours":{"type":"integer","title":"Duplicate Window Hours","default":24},"whatsapp_template_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Whatsapp Template Id"},"whatsapp_template_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Whatsapp Template Name"},"whatsapp_template_language":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Whatsapp Template Language","default":"en"},"whatsapp_template_variable_mapping":{"anyOf":[{"additionalProperties":{"type":"string"},"type":"object"},{"type":"null"}],"title":"Whatsapp Template Variable Mapping"},"send_split_config":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Send Split Config"},"sms_optout_link_enabled":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Sms Optout Link Enabled"},"acknowledge_warmup_warning":{"type":"boolean","title":"Acknowledge Warmup Warning","default":false}},"type":"object","required":["name"],"title":"CampaignCreate"},"CampaignEmailStats":{"properties":{"sent":{"type":"integer","title":"Sent","description":"Email campaign messages sent (billable)","default":0},"delivered":{"type":"integer","title":"Delivered","description":"Email campaign messages delivered","default":0},"opened":{"type":"integer","title":"Opened","description":"Email campaign opens","default":0},"clicked":{"type":"integer","title":"Clicked","description":"Email campaign clicks","default":0},"bounced":{"type":"integer","title":"Bounced","description":"Email campaign bounces","default":0},"complained":{"type":"integer","title":"Complained","description":"Email campaign spam complaints","default":0},"open_rate":{"type":"number","title":"Open Rate","description":"Open rate percentage (opened/sent)","default":0.0},"click_rate":{"type":"number","title":"Click Rate","description":"Click rate percentage (clicked/opened)","default":0.0},"bounce_rate":{"type":"number","title":"Bounce Rate","description":"Bounce rate percentage (bounced/sent)","default":0.0}},"type":"object","title":"CampaignEmailStats","description":"Email/newsletter CAMPAIGN stats.\n\nSourced from the `email_campaigns` table (EmailCampaign.*) -- the same\nsource billing invoices as EMAIL_CAMPAIGN_MANAGED / EMAIL_CAMPAIGN_BYOK."},"CampaignInfo":{"properties":{"campaign_id":{"type":"string","title":"Campaign Id","description":"Campaign UUID"},"campaign_name":{"type":"string","title":"Campaign Name","description":"Campaign display name"},"campaign_message_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Campaign Message Id","description":"CampaignMessage UUID for tracking"}},"type":"object","required":["campaign_id","campaign_name"],"title":"CampaignInfo","description":"Campaign metadata for messages sent via campaigns.\n\nWhen a message was sent as part of a campaign (newsletter/broadcast),\nthis object is populated so the frontend can display a campaign badge\nin the conversation timeline."},"CampaignPerformanceResponse":{"properties":{"total_campaigns":{"type":"integer","title":"Total Campaigns","description":"Total campaigns in the period","default":0},"completed_campaigns":{"type":"integer","title":"Completed Campaigns","description":"Completed campaigns in the period","default":0},"total_sent":{"type":"integer","title":"Total Sent","description":"Total messages sent across all campaigns","default":0},"total_delivered":{"type":"integer","title":"Total Delivered","description":"Total messages delivered","default":0},"total_read":{"type":"integer","title":"Total Read","description":"Total messages read","default":0},"total_failed":{"type":"integer","title":"Total Failed","description":"Total messages failed","default":0},"delivery_rate":{"type":"number","title":"Delivery Rate","description":"Delivery rate percentage (delivered/sent)","default":0.0},"read_rate":{"type":"number","title":"Read Rate","description":"Read rate percentage (read/delivered)","default":0.0},"email_sent":{"type":"integer","title":"Email Sent","description":"Total email campaign messages sent","default":0},"email_opened":{"type":"integer","title":"Email Opened","description":"Total email opens","default":0},"email_clicked":{"type":"integer","title":"Email Clicked","description":"Total email clicks","default":0},"email_bounced":{"type":"integer","title":"Email Bounced","description":"Total email bounces","default":0},"email_complained":{"type":"integer","title":"Email Complained","description":"Total email spam complaints","default":0},"email_open_rate":{"type":"number","title":"Email Open Rate","description":"Email open rate percentage (opened/sent)","default":0.0},"email_click_rate":{"type":"number","title":"Email Click Rate","description":"Email click rate percentage (clicked/opened)","default":0.0},"email_bounce_rate":{"type":"number","title":"Email Bounce Rate","description":"Email bounce rate percentage (bounced/sent)","default":0.0},"email":{"$ref":"#/components/schemas/EmailBreakdown","description":"Email volume split by single/transactional vs campaign, each aligned to invoiced usage"},"support":{"$ref":"#/components/schemas/SupportBreakdown","description":"Customer-support volume: bot vs human (rollup-sourced) + single/transactional emails"},"by_channel":{"items":{"$ref":"#/components/schemas/ChannelCampaignStats"},"type":"array","title":"By Channel","description":"Campaign stats by channel type"},"recent_campaigns":{"items":{"$ref":"#/components/schemas/RecentCampaign"},"type":"array","title":"Recent Campaigns","description":"Last 5 completed campaigns"},"period_start":{"type":"string","title":"Period Start","description":"Start of the reporting period (ISO format)"},"period_end":{"type":"string","title":"Period End","description":"End of the reporting period (ISO format)"}},"type":"object","required":["period_start","period_end"],"title":"CampaignPerformanceResponse","description":"Campaign performance analytics.\n\nReturned by GET /api/v1/analytics/campaigns/performance","example":{"by_channel":[{"channel_type":"email","delivered":1950,"failed":50,"sent":2000,"total_messages":2000}],"completed_campaigns":10,"delivery_rate":96.0,"email_bounce_rate":2.5,"email_bounced":50,"email_click_rate":25.0,"email_clicked":200,"email_complained":5,"email_open_rate":40.0,"email_opened":800,"email_sent":2000,"period_end":"2026-03-08T00:00:00Z","period_start":"2026-02-06T00:00:00Z","read_rate":66.67,"recent_campaigns":[{"completed_at":"2026-03-05T12:00:00Z","delivered_count":1480,"name":"March Newsletter","sent_count":1500}],"total_campaigns":12,"total_delivered":4800,"total_failed":200,"total_read":3200,"total_sent":5000}},"CampaignRecipientItem":{"properties":{"id":{"type":"string","title":"Id","description":"Per-recipient row id (campaign_messages.id / email_campaign_analytics.id)"},"contact_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Contact Id","description":"Contact id, if the recipient is a known contact"},"name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Name","description":"Contact display name"},"identifier":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Identifier","description":"Channel handle (messenger) or email address (email)"},"status":{"type":"string","title":"Status","description":"Raw per-recipient status value"},"error_reason":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Error Reason","description":"Failure/bounce reason for failed-group recipients"},"last_error_code":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Last Error Code","description":"Machine-readable reason code, when it is a documented customer-facing code. Currently `insufficient_rcs_balance` / `rcs_wallet_blocked` for RCS rows held until the RCS balance is topped up; null otherwise (always null for email)."},"sent_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Sent At"},"delivered_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Delivered At"},"failed_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Failed At"}},"type":"object","required":["id","status"],"title":"CampaignRecipientItem","description":"One recipient of a campaign, unified across channels.\n\n- ``identifier``: messenger = channel handle (recipient_id); email = email\n  address.\n- ``error_reason``: messenger = error_message; email = bounce_reason for\n  bounces, otherwise error_message.\n- ``failed_at``: messenger = failed_at column; email = derived from\n  bounced_at / complained_at / (updated_at for a plain failed row).\n- ``last_error_code``: messenger = ``last_error_code`` through a\n  customer-safe ALLOWLIST (RCS hold codes only, else null); email = null."},"CampaignRecipientListResponse":{"properties":{"items":{"items":{"$ref":"#/components/schemas/CampaignRecipientItem"},"type":"array","title":"Items","description":"List of items for this page"},"pagination":{"$ref":"#/components/schemas/PaginationMeta","description":"Pagination metadata"}},"type":"object","required":["items","pagination"],"title":"CampaignRecipientListResponse","description":"Paginated list of campaign recipients ({items, pagination})."},"CampaignResponse":{"properties":{"id":{"type":"string","title":"Id"},"tenant_id":{"type":"string","title":"Tenant Id"},"name":{"type":"string","title":"Name"},"description":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Description"},"channel_content":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Channel Content"},"message_text":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Message Text"},"message_type":{"type":"string","title":"Message Type"},"attachment_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Attachment Id"},"target_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Target Url"},"browser_push_title":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Browser Push Title"},"list_ids":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"List Ids"},"audience_conditions":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Audience Conditions"},"channel_filter":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Channel Filter"},"selected_channel_ids":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Selected Channel Ids"},"status":{"$ref":"#/components/schemas/CampaignStatus"},"throttle_reason":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Throttle Reason"},"scheduled_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Scheduled At"},"timezone":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Timezone"},"started_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Started At"},"completed_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Completed At"},"total_recipients":{"type":"integer","title":"Total Recipients"},"sent_count":{"type":"integer","title":"Sent Count"},"delivered_count":{"type":"integer","title":"Delivered Count"},"read_count":{"type":"integer","title":"Read Count"},"failed_count":{"type":"integer","title":"Failed Count"},"send_rate_limit":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Send Rate Limit"},"send_batch_size":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Send Batch Size"},"current_batch_number":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Current Batch Number"},"should_skip_duplicates":{"type":"boolean","title":"Should Skip Duplicates"},"duplicate_window_hours":{"type":"integer","title":"Duplicate Window Hours"},"whatsapp_template_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Whatsapp Template Id"},"whatsapp_template_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Whatsapp Template Name"},"whatsapp_template_language":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Whatsapp Template Language"},"whatsapp_template_variable_mapping":{"anyOf":[{"additionalProperties":{"type":"string"},"type":"object"},{"type":"null"}],"title":"Whatsapp Template Variable Mapping"},"created_by_user_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Created By User Id"},"created_at":{"type":"string","format":"date-time","title":"Created At"},"updated_at":{"type":"string","format":"date-time","title":"Updated At"},"send_split_config":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Send Split Config"},"send_split_state":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Send Split State"},"warmup_acknowledged_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Warmup Acknowledged At"},"warmup_acknowledged_by_user_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Warmup Acknowledged By User Id"},"warmup_assessment":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Warmup Assessment"},"sms_optout_link_enabled":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Sms Optout Link Enabled"}},"type":"object","required":["id","tenant_id","name","description","message_text","message_type","attachment_id","target_url","browser_push_title","channel_filter","status","scheduled_at","started_at","completed_at","total_recipients","sent_count","delivered_count","read_count","failed_count","send_rate_limit","should_skip_duplicates","duplicate_window_hours","created_by_user_id","created_at","updated_at"],"title":"CampaignResponse"},"CampaignSendNextBatchRequest":{"properties":{"batch_size":{"anyOf":[{"type":"integer"},{"type":"null"}],"maximum":1000000.0,"minimum":1.0,"title":"Batch Size","description":"Send this many recipients in the next batch (1-1,000,000). Also becomes the campaign's saved batch size. Omit to use the saved size."},"all_remaining":{"type":"boolean","title":"All Remaining","description":"Send every remaining recipient now (ignores batch_size).","default":false}},"type":"object","title":"CampaignSendNextBatchRequest","description":"Body for ``POST /campaigns/{id}/send-next-batch`` and\n``POST /email-campaigns/{id}/send-next-batch``.\n\nSame fields as :class:`SendNextBatchRequest`, but ``batch_size`` is NOT\nrange-checked by pydantic: both endpoints check 1..1,000,000 themselves\n(:func:`next_batch_size_out_of_range`) so an out-of-range value gets the\nstable 422 ``{\"code\": \"send_batch_size_invalid\", \"message\"}`` (the same\ncode as ``PUT /campaigns/{id}``) instead of a generic validation error.\n``SendNextBatchRequest`` keeps its pydantic bounds for any other user."},"CampaignStatus":{"type":"string","enum":["draft","in_review","approved","rejected","scheduled","sending","completed","paused","failed","cancelled","throttled"],"title":"CampaignStatus"},"CampaignSummary":{"properties":{"email_managed":{"type":"integer","title":"Email Managed","description":"Email campaigns via managed providers (SendGrid Managed, SES)","default":0},"email_byok":{"type":"integer","title":"Email Byok","description":"Email campaigns via BYOK providers (customer's own SendGrid, Mailgun)","default":0},"push":{"type":"integer","title":"Push","description":"Browser push campaign messages","default":0}},"type":"object","title":"CampaignSummary","description":"Campaign counts by type"},"CampaignUnsubscribeAttributionCounts":{"properties":{"exact":{"type":"integer","title":"Exact","description":"Opt-outs with certain campaign attribution (link)"},"heuristic":{"type":"integer","title":"Heuristic","description":"Opt-outs attributed by the last-campaign-on-channel heuristic (keyword)"}},"type":"object","required":["exact","heuristic"],"title":"CampaignUnsubscribeAttributionCounts","description":"Opt-out counts by attribution confidence (full result set, not page).\n\nRedundant-but-explicit summary so the UI can headline\n\"N confirmed, M approximate\" without re-deriving from method_counts. For\nthis endpoint exact == link and heuristic == keyword one-to-one."},"CampaignUnsubscribeCount":{"properties":{"total":{"type":"integer","title":"Total","description":"Total opt-outs attributed to this campaign"},"exact":{"type":"integer","title":"Exact","description":"Opt-outs with certain campaign attribution (link/manual/one-click)"},"estimated":{"type":"integer","title":"Estimated","description":"Opt-outs attributed by heuristic (keyword window match); always 0 for email"}},"type":"object","required":["total","exact","estimated"],"title":"CampaignUnsubscribeCount","description":"Opt-out counts for one campaign on an index page."},"CampaignUnsubscribeCountsResponse":{"properties":{"counts":{"additionalProperties":{"$ref":"#/components/schemas/CampaignUnsubscribeCount"},"type":"object","title":"Counts","description":"Map of campaign id -> {total, exact, estimated}. Ids with no opt-outs are omitted."}},"type":"object","title":"CampaignUnsubscribeCountsResponse","description":"Batch opt-out counts keyed by campaign id (missing id => 0)."},"CampaignUnsubscribePagination":{"properties":{"page":{"type":"integer","title":"Page"},"page_size":{"type":"integer","title":"Page Size"},"total":{"type":"integer","title":"Total"}},"type":"object","required":["page","page_size","total"],"title":"CampaignUnsubscribePagination","description":"Pagination envelope for the opt-outs list."},"CampaignUpdate":{"properties":{"name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Name"},"description":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Description"},"channel_content":{"anyOf":[{"$ref":"#/components/schemas/ChannelContent"},{"type":"null"}]},"message_text":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Message Text"},"message_type":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Message Type"},"attachment_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Attachment Id"},"target_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Target Url"},"browser_push_title":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Browser Push Title"},"list_ids":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"List Ids"},"audience_conditions":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Audience Conditions","description":"Audience Builder condition tree (same format as a dynamic list's `segment_conditions`). Must be a top-level `AND` whose children include at least one positive list membership: a `has_optin` condition (`operator: equals`, `value: true`, `list_id` of a static or newsletter list of this workspace) or a group of only such conditions (`OR` = any of the lists, `AND` = all of them). Other children narrow the audience down or exclude contacts (e.g. `has_optin` with `value: false`). When set, the campaign's lists are DERIVED from the membership lists and any `list_ids` sent are ignored. Errors: 422 `audience_list_membership_required`, `audience_membership_list_invalid`, or a validation error for a malformed tree. A tree with no conditions counts as null. Omit to leave unchanged; null clears it (the campaign keeps its current lists)."},"channel_filter":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Channel Filter"},"selected_channel_ids":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Selected Channel Ids"},"scheduled_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Scheduled At"},"timezone":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Timezone"},"send_rate_limit":{"anyOf":[{"type":"integer"},{"type":"null"}],"maximum":60000.0,"minimum":1.0,"title":"Send Rate Limit","description":"Send speed in messages per minute (1-60000). Null clears it (channel default speed)."},"send_batch_size":{"anyOf":[{"type":"integer"},{"type":"null"}],"maximum":1000000.0,"minimum":1.0,"title":"Send Batch Size","description":"Recipients per batch (1-1,000,000). Null clears it (no further batches)."},"should_skip_duplicates":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Should Skip Duplicates"},"duplicate_window_hours":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Duplicate Window Hours"},"whatsapp_template_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Whatsapp Template Id"},"whatsapp_template_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Whatsapp Template Name"},"whatsapp_template_language":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Whatsapp Template Language"},"whatsapp_template_variable_mapping":{"anyOf":[{"additionalProperties":{"type":"string"},"type":"object"},{"type":"null"}],"title":"Whatsapp Template Variable Mapping"},"send_split_config":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Send Split Config"},"sms_optout_link_enabled":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Sms Optout Link Enabled"}},"type":"object","title":"CampaignUpdate"},"CannedResponseCreate":{"properties":{"title":{"type":"string","maxLength":255,"minLength":1,"title":"Title","description":"Display title for the canned response"},"content":{"type":"string","minLength":1,"title":"Content","description":"The actual response text"},"shortcut":{"anyOf":[{"type":"string","maxLength":50},{"type":"null"}],"title":"Shortcut","description":"Keyboard shortcut (e.g., 'greeting'). Stored lowercase."},"category":{"anyOf":[{"type":"string","maxLength":100},{"type":"null"}],"title":"Category","description":"Category for organization (e.g., 'Greetings', 'Support')"},"is_shared":{"type":"boolean","title":"Is Shared","description":"If true, visible to all team members. If false, only visible to creator.","default":false}},"type":"object","required":["title","content"],"title":"CannedResponseCreate","description":"Schema for creating a new canned response","example":{"category":"Greetings","content":"Hello! Thank you for contacting us. How can I help you today?","is_shared":true,"shortcut":"greeting","title":"Standard Greeting"}},"CannedResponseListResponse":{"properties":{"items":{"items":{"$ref":"#/components/schemas/CannedResponseResponse"},"type":"array","title":"Items","description":"List of items for this page"},"pagination":{"$ref":"#/components/schemas/PaginationMeta","description":"Pagination metadata"}},"type":"object","required":["items","pagination"],"title":"CannedResponseListResponse","description":"Paginated list of canned responses.\n\nExample response:\n```json\n{\n    \"items\": [...],\n    \"pagination\": {\n        \"total\": 25,\n        \"page\": 1,\n        \"page_size\": 20,\n        \"total_pages\": 2,\n        \"has_next\": true,\n        \"has_prev\": false\n    }\n}\n```"},"CannedResponseResponse":{"properties":{"created_at":{"type":"string","title":"Created At","description":"When the resource was created (UTC)"},"updated_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Updated At","description":"When the resource was last updated (UTC)"},"id":{"type":"string","title":"Id","description":"Unique identifier (UUID)"},"tenant_id":{"type":"string","title":"Tenant Id","description":"Tenant this resource belongs to"},"title":{"type":"string","title":"Title","description":"Display title"},"shortcut":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Shortcut","description":"Keyboard shortcut (lowercase)"},"content":{"type":"string","title":"Content","description":"Response text"},"category":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Category","description":"Category for organization"},"is_shared":{"type":"boolean","title":"Is Shared","description":"Whether visible to all team members"},"created_by_user_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Created By User Id","description":"ID of user who created this response"}},"type":"object","required":["created_at","id","tenant_id","title","content","is_shared"],"title":"CannedResponseResponse","description":"Full canned response in API responses.\n\nIncludes all fields plus ownership info.","example":{"category":"Greetings","content":"Hello! Thank you for contacting us. How can I help you today?","created_at":"2025-01-15T10:30:00Z","created_by_user_id":"user-456","id":"cr-a1b2c3d4-e5f6-7890-abcd-ef1234567890","is_shared":true,"shortcut":"greeting","tenant_id":"tenant-123","title":"Standard Greeting","updated_at":"2025-01-15T10:30:00Z"}},"CannedResponseUpdate":{"properties":{"title":{"anyOf":[{"type":"string","maxLength":255,"minLength":1},{"type":"null"}],"title":"Title","description":"Display title"},"content":{"anyOf":[{"type":"string","minLength":1},{"type":"null"}],"title":"Content","description":"Response text"},"shortcut":{"anyOf":[{"type":"string","maxLength":50},{"type":"null"}],"title":"Shortcut","description":"Keyboard shortcut"},"category":{"anyOf":[{"type":"string","maxLength":100},{"type":"null"}],"title":"Category","description":"Category"},"is_shared":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Is Shared","description":"Visibility setting"}},"type":"object","title":"CannedResponseUpdate","description":"Schema for updating a canned response (all fields optional)","example":{"content":"Hi there! Thanks for reaching out. How may I assist you?","title":"Updated Greeting"}},"CarouselCardButton":{"properties":{"type":{"type":"string","title":"Type","description":"Button type (e.g., 'url', 'quick_reply')"},"index":{"type":"integer","minimum":0.0,"title":"Index","description":"Button index within the card (0 or 1)","default":0},"parameters":{"items":{"additionalProperties":true,"type":"object"},"type":"array","title":"Parameters","description":"Button parameters (e.g., dynamic URL suffix)","default":[]}},"type":"object","required":["type"],"title":"CarouselCardButton","description":"Button for a WhatsApp carousel card"},"CarouselCardHeader":{"properties":{"type":{"type":"string","enum":["image","video"],"title":"Type","description":"Header type: 'image' or 'video'"},"media_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Media Id","description":"WhatsApp media ID for uploaded media"},"url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Url","description":"Direct URL to the media file"}},"type":"object","required":["type"],"title":"CarouselCardHeader","description":"Header for a carousel card (image or video)"},"CarouselCardRequest":{"properties":{"components":{"items":{"$ref":"#/components/schemas/TemplateComponentRequest"},"type":"array","title":"Components","description":"The card's components: one IMAGE/VIDEO HEADER, one BODY, and an optional BUTTONS component (<=2 buttons)."}},"type":"object","required":["components"],"title":"CarouselCardRequest","description":"A single carousel card within a CAROUSEL component.\n\nA card mirrors a mini-template: its ``components`` list holds exactly one\nHEADER (format IMAGE or VIDEO), exactly one BODY, and an optional BUTTONS\ncomponent. This is shape-only — structural rules (card count, header\nformat, cross-card consistency) are enforced at the route/service layer.\n\nSecurity hardening (API DoS): the mutual recursion between this model and\n``TemplateComponentRequest`` (``components`` -> ``cards`` -> ``components``)\nis capped here fail-closed. An inner card component may ONLY be\nHEADER/BODY/BUTTONS — never a nested CAROUSEL, and never carry its own\n``cards`` — and each BODY is length-bounded. This runs as a ``mode=\"before\"``\nvalidator so a hostile deeply-nested payload is rejected BEFORE Pydantic\nrecurses into (and fully parses) the inner components."},"CarouselsCapability":{"properties":{"supported":{"type":"boolean","title":"Supported","default":true},"max_cards":{"type":"integer","title":"Max Cards"},"requires_template":{"type":"boolean","title":"Requires Template","default":false}},"type":"object","required":["max_cards"],"title":"CarouselsCapability","description":"Carousels capability details"},"CatalogueConnectRequest":{"properties":{"channel_id":{"type":"string","title":"Channel Id","description":"The WhatsApp channel to attach the catalogue to"},"code":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Code","description":"Authorization CODE from the FBLB catalogue dialog. Exchanged server-side for an access token. Provide this OR access_token."},"access_token":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Access Token","description":"A catalogue access token already exchanged client-side. Provide this OR code."},"catalog_id":{"anyOf":[{"type":"string","maxLength":32,"pattern":"^\\d+$"},{"type":"null"}],"title":"Catalog Id","description":"Optional explicit catalogue id (e.g. copied from Meta Business Suite, or forwarded from the dialog's session_info). Guaranteed unblock for Coexistence numbers, where no Graph edge auto-returns the linked catalogue id. When supplied it is VERIFIED server-side (GET /{catalog_id}/products) before persisting and may be sent alone (without 'code'/'access_token'), since the channel's own WhatsApp token can read a Business-Manager catalogue. When omitted, the id is auto-resolved from the token's granular catalog_management scope. Must be a numeric Meta id."}},"type":"object","required":["channel_id"],"title":"CatalogueConnectRequest","description":"Consume the catalogue token/code returned by the FBLB dialog."},"CatalogueConnectResponse":{"properties":{"success":{"type":"boolean","title":"Success"},"catalog_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Catalog Id","description":"Resolved Meta catalogue id"},"same_catalog":{"type":"boolean","title":"Same Catalog","description":"True when the connected catalogue matches the catalogue Meta has linked to the WABA. False is a non-blocking warning (read still works, but outbound product messages reference the WABA-linked catalogue).","default":true},"warning":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Warning","description":"Human-readable caveat when same_catalog is False"}},"type":"object","required":["success"],"title":"CatalogueConnectResponse"},"CatalogueDisconnectResponse":{"properties":{"success":{"type":"boolean","title":"Success"},"id":{"type":"string","title":"Id","description":"The channel id the catalogue was disconnected from"}},"type":"object","required":["success","id"],"title":"CatalogueDisconnectResponse"},"CatalogueProductItem":{"properties":{"id":{"type":"string","title":"Id"},"product_retailer_id":{"type":"string","title":"Product Retailer Id"},"name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Name"},"description":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Description"},"image_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Image Url"},"url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Url"},"price_raw":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Price Raw"},"price":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Price"},"currency":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Currency"},"availability":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Availability"},"inventory":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Inventory"},"synced_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Synced At"}},"type":"object","required":["id","product_retailer_id"],"title":"CatalogueProductItem"},"CatalogueProductListResponse":{"properties":{"items":{"items":{"$ref":"#/components/schemas/CatalogueProductItem"},"type":"array","title":"Items"},"pagination":{"$ref":"#/components/schemas/CatalogueProductPagination"}},"type":"object","required":["items","pagination"],"title":"CatalogueProductListResponse"},"CatalogueProductPagination":{"properties":{"total":{"type":"integer","title":"Total"},"page":{"type":"integer","title":"Page"},"page_size":{"type":"integer","title":"Page Size"},"total_pages":{"type":"integer","title":"Total Pages"},"has_next":{"type":"boolean","title":"Has Next"},"has_prev":{"type":"boolean","title":"Has Prev"}},"type":"object","required":["total","page","page_size","total_pages","has_next","has_prev"],"title":"CatalogueProductPagination"},"CatalogueStatusResponse":{"properties":{"beta_enabled":{"type":"boolean","title":"Beta Enabled","description":"Whether the WhatsApp Catalogue beta is enabled for this tenant (the `whatsapp_catalogue` feature flag). The frontend reads THIS field to decide whether to show the catalogue UI."},"connected":{"type":"boolean","title":"Connected","description":"Whether a catalogue is connected on this channel"},"catalog_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Catalog Id"},"product_count":{"type":"integer","title":"Product Count","default":0},"last_synced_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Last Synced At","description":"ISO-8601 timestamp of the last successful sync (UTC, 'Z')"},"can_send":{"type":"boolean","title":"Can Send","description":"Whether outbound product messages are permitted. False on Coexistence numbers (fail-closed)."},"degrade_reason":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Degrade Reason","description":"Why the catalogue is degraded, if at all: null | 'coexistence_send_blocked' | 'app_managed' | 'not_resolvable'."},"config_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Config Id","description":"FBLB catalogue config id for the (re)connect dialog"},"app_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"App Id","description":"Facebook App ID for the dialog"}},"type":"object","required":["beta_enabled","connected","can_send"],"title":"CatalogueStatusResponse"},"CatalogueSyncResponse":{"properties":{"synced":{"type":"integer","title":"Synced","description":"Number of products upserted from Meta"},"degrade_reason":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Degrade Reason","description":"null | 'app_managed' | 'not_resolvable'"}},"type":"object","required":["synced"],"title":"CatalogueSyncResponse"},"ChannelActionsInput":{"properties":{"mark_as_read":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Mark As Read"},"typing_indicator":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Typing Indicator"}},"additionalProperties":false,"type":"object","title":"ChannelActionsInput","description":"Channel behaviour toggles of an agent (partial: omitted keys keep their value).\n\n``mark_as_read``: send a read receipt for each inbound message the agent\nhandles (where the channel supports it). ``typing_indicator``: show\n\"typing...\" while the agent prepares its reply (where supported)."},"ChannelActionsResponse":{"properties":{"mark_as_read":{"type":"boolean","title":"Mark As Read","default":false},"typing_indicator":{"type":"boolean","title":"Typing Indicator","default":false}},"type":"object","title":"ChannelActionsResponse","description":"Resolved toggles (always both keys; agents created before the feature resolve to false)."},"ChannelCampaignStats":{"properties":{"channel_type":{"type":"string","title":"Channel Type","description":"Channel type"},"sent":{"type":"integer","title":"Sent","description":"Messages sent","default":0},"delivered":{"type":"integer","title":"Delivered","description":"Messages delivered","default":0},"failed":{"type":"integer","title":"Failed","description":"Messages failed","default":0},"total_messages":{"type":"integer","title":"Total Messages","description":"Total campaign messages","default":0}},"type":"object","required":["channel_type"],"title":"ChannelCampaignStats","description":"Campaign message stats for a single channel type"},"ChannelCapabilities":{"properties":{"channel_id":{"type":"string","title":"Channel Id"},"channel_type":{"type":"string","title":"Channel Type"},"channel_name":{"type":"string","title":"Channel Name"},"features":{"$ref":"#/components/schemas/ChannelFeatures"},"reactions":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Reactions"},"reply":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Reply"},"sender_actions":{"anyOf":[{"$ref":"#/components/schemas/SenderActionSupportInfo"},{"type":"null"}],"description":"Which sender actions (read receipts, typing indicators) this channel supports."}},"type":"object","required":["channel_id","channel_type","channel_name","features"],"title":"ChannelCapabilities","description":"Response schema for channel capabilities endpoint","example":{"channel_id":"a1b2c3d4-5678-9abc-def0-1234567890ab","channel_name":"Main WhatsApp Business","channel_type":"whatsapp","features":{"audio":true,"carousels":{"max_cards":10,"requires_template":true,"supported":true},"contacts":true,"documents":true,"images":true,"interactive_buttons":{"max_buttons":3,"supported":true},"interactive_lists":{"max_rows_per_section":10,"max_sections":10,"supported":true},"location":true,"proxy":true,"quick_replies":false,"reactions":true,"stickers":true,"templates":true,"text":true,"videos":true}}},"ChannelChainEntry":{"properties":{"channel_type":{"type":"string","title":"Channel Type"},"channel_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Channel Id"}},"additionalProperties":false,"type":"object","required":["channel_type"],"title":"ChannelChainEntry","description":"One entry in a Send node's ``channel_chain``.\n\nThe wire shape historically was ``list[str]`` — each item a channel\ntype (\"whatsapp\", \"email\", ...). It now widens to accept EITHER a\nbare string OR an object pinning a specific Channel row::\n\n    # Legacy bare-string form (still supported):\n    [\"whatsapp\", \"email\"]\n\n    # New object form — pin to a specific WhatsApp / Messenger page:\n    [\n      {\"channel_type\": \"whatsapp\", \"channel_id\": \"<uuid>\"},\n      {\"channel_type\": \"email\"},  # channel_id null → tenant-default\n    ]\n\nBare strings are normalized to ``ChannelChainEntry(channel_type=s,\nchannel_id=None)`` via ``normalize_channel_chain`` so service-layer\ncode (``send_node.py``, ``observability``, etc.) sees a single shape."},"ChannelCommentCapabilitiesResponse":{"properties":{"beta_enabled":{"type":"boolean","title":"Beta Enabled","description":"Whether the tenant is enrolled in the `social_comments_beta` feature flag. Capabilities are reported regardless, so the UI can tell 'not enrolled' apart from 'enrolled but needs a reconnect'."},"items":{"items":{"$ref":"#/components/schemas/ChannelCommentCapability"},"type":"array","title":"Items"}},"type":"object","required":["beta_enabled","items"],"title":"ChannelCommentCapabilitiesResponse","description":"Capability probe across the tenant's comment-capable channels."},"ChannelCommentCapability":{"properties":{"channel_id":{"type":"string","title":"Channel Id"},"channel_type":{"type":"string","title":"Channel Type"},"channel_name":{"type":"string","title":"Channel Name"},"identifier":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Identifier"},"comments_supported":{"type":"boolean","title":"Comments Supported","description":"Whether this channel TYPE can do comments -> private reply at all (Instagram and Facebook Messenger only). Independent of this particular connection's state."},"can_receive_comments":{"type":"boolean","title":"Can Receive Comments","description":"Verified-true only. Instagram: the `instagram_business_manage_comments` scope is recorded as granted. Messenger: the Page is live-confirmed subscribed to the `feed` webhook field."},"reconnect_required":{"type":"boolean","title":"Reconnect Required","description":"The user must walk the OAuth dialog again — a missing permission cannot be added to a live connection."},"resubscribe_required":{"type":"boolean","title":"Resubscribe Required","description":"No permission is missing; only the Page's webhook subscription needs fixing, which does not require the user."},"reason":{"type":"string","title":"Reason","description":"Stable machine code: `ok`, `channel_type_not_supported`, `channel_inactive`, `scope_granted_unknown`, `scope_missing`, `feed_not_subscribed`, `subscription_unknown`, `missing_credentials`."},"detail":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Detail","description":"Human-readable explanation (English; UI should translate by `reason`)."},"missing_scopes":{"items":{"type":"string"},"type":"array","title":"Missing Scopes","description":"Permissions that must be granted on reconnect, if any."},"granted_scopes_known":{"type":"boolean","title":"Granted Scopes Known","description":"False when the channel has no recorded permission list at all — the 'connected before we tracked this' case, which is why the answer is a fail-closed no rather than a yes.","default":false},"checked_at":{"type":"string","format":"date-time","title":"Checked At"}},"type":"object","required":["channel_id","channel_type","channel_name","comments_supported","can_receive_comments","reconnect_required","resubscribe_required","reason","checked_at"],"title":"ChannelCommentCapability","description":"Whether ONE channel could actually receive comments today.\n\nFAIL-CLOSED CONTRACT: ``can_receive_comments`` is ``true`` only when we\npositively verified the requirement. Anything unknown — a channel connected\nbefore we recorded granted scopes, a Graph read that failed — reports\n``false`` with a ``reason``, never an optimistic guess."},"ChannelConnectAuditLogListResponse":{"properties":{"items":{"items":{"$ref":"#/components/schemas/ChannelConnectAuditLogResponse"},"type":"array","title":"Items","description":"List of items for this page"},"pagination":{"$ref":"#/components/schemas/PaginationMeta","description":"Pagination metadata"}},"type":"object","required":["items","pagination"],"title":"ChannelConnectAuditLogListResponse","description":"Paginated list of audit log entries."},"ChannelConnectAuditLogResponse":{"properties":{"id":{"type":"string","title":"Id","description":"Unique identifier (UUID)"},"token_id":{"type":"string","title":"Token Id","description":"Token ID"},"event_type":{"type":"string","title":"Event Type","description":"Event type"},"channel_type":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Channel Type","description":"Channel type (for connection events)"},"channel_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Channel Id","description":"Connected channel ID (on success)"},"ip_address":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Ip Address","description":"Client IP address"},"error_code":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Error Code","description":"Error code (for failures)"},"error_message":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Error Message","description":"Error message (for failures)"},"created_at":{"type":"string","title":"Created At","description":"Event timestamp"}},"type":"object","required":["id","token_id","event_type","created_at"],"title":"ChannelConnectAuditLogResponse","description":"Response schema for audit log entries."},"ChannelConnectDelegateRequest":{"properties":{"email":{"type":"string","format":"email","title":"Email","description":"Address of the colleague/agency who will do the connect"},"channel_types":{"items":{"type":"string"},"type":"array","maxItems":8,"minItems":1,"title":"Channel Types","description":"Channel types the link may connect — normally a single platform. The consolidated email option expands to its provider sub-types."},"recipient_name":{"anyOf":[{"type":"string","maxLength":100},{"type":"null"}],"title":"Recipient Name","description":"Recipient display name used in the email salutation"},"note":{"anyOf":[{"type":"string","maxLength":500},{"type":"null"}],"title":"Note","description":"Optional message from the requester, shown in the email"},"expires_in_hours":{"type":"integer","maximum":168.0,"minimum":1.0,"title":"Expires In Hours","description":"Link validity in hours (1-168, default 48)","default":48}},"type":"object","required":["email","channel_types"],"title":"ChannelConnectDelegateRequest","description":"Request to hand a channel connect over to somebody else by email.\n\nUsed by the channel setup wizard when the person doing the setup does not\nhave the platform access the connect requires (e.g. they are not an admin of\nthe Meta Business portfolio). Creates a scoped, short-lived connect token and\nmails the link to the colleague or agency who does have that access.\n\nDeliberately narrower than ``ChannelConnectTokenCreate``: the caller picks\nneither ``max_uses`` nor the use window, because this is a one-person,\none-channel hand-over rather than a reusable partner link.","example":{"channel_types":["whatsapp"],"email":"it@example.com","expires_in_hours":48,"note":"You're the admin on our Meta Business account - could you connect this?","recipient_name":"Sam"}},"ChannelConnectDelegateResponse":{"properties":{"success":{"type":"boolean","title":"Success","default":true},"token_id":{"type":"string","title":"Token Id","description":"Id of the created connect token"},"connect_url":{"type":"string","title":"Connect Url","description":"Tokenised connect URL"},"expires_at":{"type":"string","title":"Expires At","description":"When the link stops working"},"email_sent":{"type":"boolean","title":"Email Sent","description":"False when the link was created but the email failed to send"},"sent_to":{"type":"string","title":"Sent To","description":"Address the link was mailed to"}},"type":"object","required":["token_id","connect_url","expires_at","email_sent","sent_to"],"title":"ChannelConnectDelegateResponse","description":"Result of a hand-over request.\n\n``connect_url`` is returned even when ``email_sent`` is false so the UI can\nalways fall back to \"copy the link and send it yourself\" — the token exists\neither way and silently losing it would force the user to start over."},"ChannelConnectTokenCreate":{"properties":{"name":{"anyOf":[{"type":"string","maxLength":100},{"type":"null"}],"title":"Name","description":"User-friendly name for this token (e.g., 'Acme Corp Setup')"},"allowed_channel_types":{"items":{"type":"string"},"type":"array","minItems":1,"title":"Allowed Channel Types","description":"Channel types allowed to connect: telegram, whatsapp, instagram, messenger, gmail, smtp_imap, proxy, sendgrid_byok, mailgun_byok, sendgrid_managed, sms"},"expires_in_hours":{"type":"integer","maximum":168.0,"minimum":1.0,"title":"Expires In Hours","description":"Token validity in hours (1-168, default 24)","default":24},"max_uses":{"type":"integer","maximum":100.0,"minimum":1.0,"title":"Max Uses","description":"Maximum number of successful connections (1-100, default 10)","default":10},"use_window_minutes":{"type":"integer","maximum":1440.0,"minimum":5.0,"title":"Use Window Minutes","description":"Grace period in minutes after first use (5-1440, default 30)","default":30},"allowed_channel_modes":{"anyOf":[{"additionalProperties":{"items":{"type":"string"},"type":"array"},"type":"object"},{"type":"null"}],"title":"Allowed Channel Modes","description":"Optional per-channel connect-mode restrictions. Shape: {\"instagram\": [\"messaging\",\"social\"], \"messenger\": [\"business_messaging\",\"business_social\",\"personal_social\"], \"whatsapp\": [\"classic\",\"coexistence\"]}. An ABSENT channel key (or a null value) means ALL modes are allowed for that channel (backwards compatible). A channel key MUST list at least one mode — to allow every mode, omit the key rather than passing []. Enforcement is the intersection of these modes with the tenant's feature gating, fail-closed."},"partner_name":{"anyOf":[{"type":"string","maxLength":100},{"type":"null"}],"title":"Partner Name","description":"Partner/client name to display on connect page"},"partner_redirect_url":{"anyOf":[{"type":"string","maxLength":500},{"type":"null"}],"title":"Partner Redirect Url","description":"URL to redirect to after successful connection"}},"type":"object","required":["allowed_channel_types"],"title":"ChannelConnectTokenCreate","description":"Request schema for creating a new channel connect token.\n\nAllows admins to generate secure, time-limited tokens that\npartners can use to connect messaging channels.","example":{"allowed_channel_types":["telegram","whatsapp"],"expires_in_hours":48,"max_uses":5,"name":"Acme Corp Channel Setup","partner_name":"Acme Corporation","partner_redirect_url":"https://acme.com/setup-complete","use_window_minutes":30}},"ChannelConnectTokenCreatedResponse":{"properties":{"id":{"type":"string","title":"Id","description":"Unique identifier (UUID)"},"name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Name","description":"Token name"},"token_prefix":{"type":"string","title":"Token Prefix","description":"Token prefix for display (e.g., 's7_cc_abc12345')"},"allowed_channel_types":{"items":{"type":"string"},"type":"array","title":"Allowed Channel Types","description":"Allowed channel types"},"allowed_channel_modes":{"anyOf":[{"additionalProperties":{"items":{"type":"string"},"type":"array"},"type":"object"},{"type":"null"}],"title":"Allowed Channel Modes","description":"Per-channel connect-mode restrictions as authored by the admin. Null (or an absent channel key) means all modes are allowed for that channel."},"partner_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Partner Name","description":"Partner name"},"partner_redirect_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Partner Redirect Url","description":"Redirect URL after connection"},"connect_url":{"type":"string","title":"Connect Url","description":"Full URL for the public connect page"},"max_uses":{"type":"integer","title":"Max Uses","description":"Maximum allowed uses"},"current_uses":{"type":"integer","title":"Current Uses","description":"Current number of successful connections"},"use_window_minutes":{"type":"integer","title":"Use Window Minutes","description":"Grace period after first use"},"first_used_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"First Used At","description":"When token was first used"},"expires_at":{"type":"string","title":"Expires At","description":"Token expiration time"},"is_revoked":{"type":"boolean","title":"Is Revoked","description":"Whether token was manually revoked"},"is_locked":{"type":"boolean","title":"Is Locked","description":"Whether token was locked due to failures"},"is_valid":{"type":"boolean","title":"Is Valid","description":"Whether token is currently usable"},"remaining_uses":{"type":"integer","title":"Remaining Uses","description":"Number of remaining uses"},"created_at":{"type":"string","title":"Created At","description":"When token was created"},"revoked_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Revoked At","description":"When token was revoked"},"created_by_user_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Created By User Id","description":"User who created the token"},"token":{"type":"string","title":"Token","description":"Raw token (shown only once!) - format: s7_cc_xxxxx..."},"warning":{"type":"string","title":"Warning","description":"Security warning","default":"Save this token now - it will not be shown again"}},"type":"object","required":["id","token_prefix","connect_url","max_uses","current_uses","use_window_minutes","expires_at","is_revoked","is_locked","is_valid","remaining_uses","created_at","token"],"title":"ChannelConnectTokenCreatedResponse","description":"Response when creating a new token.\n\nIMPORTANT: The raw token is returned ONLY ONCE here.\nSave it securely - it cannot be retrieved again.","example":{"allowed_channel_types":["telegram","whatsapp"],"connect_url":"https://app.sendseven.com/connect/s7_cc_a1b2c3d4e5f6g7h8...","created_at":"2026-01-15T10:30:00Z","created_by_user_id":"user-123","current_uses":0,"expires_at":"2026-01-17T10:30:00Z","id":"t1a2b3c4-d5e6-f7g8-h9i0-j1k2l3m4n5o6","is_locked":false,"is_revoked":false,"is_valid":true,"max_uses":5,"name":"Acme Corp Channel Setup","partner_name":"Acme Corporation","partner_redirect_url":"https://acme.com/setup-complete","remaining_uses":5,"token":"s7_cc_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6q7r8s9t0u1v2w3x4y5z6a1b2c3d4e5f6","token_prefix":"s7_cc_a1b2c3d4","use_window_minutes":30,"warning":"Save this token now - it will not be shown again"}},"ChannelConnectTokenListResponse":{"properties":{"items":{"items":{"$ref":"#/components/schemas/ChannelConnectTokenResponse"},"type":"array","title":"Items","description":"List of items for this page"},"pagination":{"$ref":"#/components/schemas/PaginationMeta","description":"Pagination metadata"}},"type":"object","required":["items","pagination"],"title":"ChannelConnectTokenListResponse","description":"Paginated list of channel connect tokens."},"ChannelConnectTokenResponse":{"properties":{"id":{"type":"string","title":"Id","description":"Unique identifier (UUID)"},"name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Name","description":"Token name"},"token_prefix":{"type":"string","title":"Token Prefix","description":"Token prefix for display (e.g., 's7_cc_abc12345')"},"allowed_channel_types":{"items":{"type":"string"},"type":"array","title":"Allowed Channel Types","description":"Allowed channel types"},"allowed_channel_modes":{"anyOf":[{"additionalProperties":{"items":{"type":"string"},"type":"array"},"type":"object"},{"type":"null"}],"title":"Allowed Channel Modes","description":"Per-channel connect-mode restrictions as authored by the admin. Null (or an absent channel key) means all modes are allowed for that channel."},"partner_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Partner Name","description":"Partner name"},"partner_redirect_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Partner Redirect Url","description":"Redirect URL after connection"},"connect_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Connect Url","description":"Full connect URL for sharing"},"max_uses":{"type":"integer","title":"Max Uses","description":"Maximum allowed uses"},"current_uses":{"type":"integer","title":"Current Uses","description":"Current number of successful connections"},"use_window_minutes":{"type":"integer","title":"Use Window Minutes","description":"Grace period after first use"},"first_used_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"First Used At","description":"When token was first used"},"expires_at":{"type":"string","title":"Expires At","description":"Token expiration time"},"is_revoked":{"type":"boolean","title":"Is Revoked","description":"Whether token was manually revoked"},"is_locked":{"type":"boolean","title":"Is Locked","description":"Whether token was locked due to failures"},"is_valid":{"type":"boolean","title":"Is Valid","description":"Whether token is currently usable"},"remaining_uses":{"type":"integer","title":"Remaining Uses","description":"Number of remaining uses"},"created_at":{"type":"string","title":"Created At","description":"When token was created"},"revoked_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Revoked At","description":"When token was revoked"},"created_by_user_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Created By User Id","description":"User who created the token"}},"type":"object","required":["id","token_prefix","max_uses","current_uses","use_window_minutes","expires_at","is_revoked","is_locked","is_valid","remaining_uses","created_at"],"title":"ChannelConnectTokenResponse","description":"Standard response for channel connect tokens.\n\nDoes NOT include the raw token - that's only shown once at creation.","example":{"allowed_channel_types":["telegram","whatsapp"],"created_at":"2026-01-15T10:30:00Z","created_by_user_id":"user-123","current_uses":2,"expires_at":"2026-01-17T10:30:00Z","first_used_at":"2026-01-15T10:30:00Z","id":"t1a2b3c4-d5e6-f7g8-h9i0-j1k2l3m4n5o6","is_locked":false,"is_revoked":false,"is_valid":true,"max_uses":5,"name":"Acme Corp Channel Setup","partner_name":"Acme Corporation","partner_redirect_url":"https://acme.com/setup-complete","remaining_uses":3,"token_prefix":"s7_cc_a1b2c3d4","use_window_minutes":30}},"ChannelConnectTokenRevoke":{"properties":{"reason":{"anyOf":[{"type":"string","maxLength":500},{"type":"null"}],"title":"Reason","description":"Reason for revocation (for audit purposes)"}},"type":"object","title":"ChannelConnectTokenRevoke","description":"Request schema for revoking a token."},"ChannelContent":{"properties":{"whatsapp":{"anyOf":[{"$ref":"#/components/schemas/WhatsAppContent"},{"type":"null"}]},"telegram":{"anyOf":[{"$ref":"#/components/schemas/TelegramContent"},{"type":"null"}]},"messenger":{"anyOf":[{"$ref":"#/components/schemas/MessengerContent"},{"type":"null"}]},"sms":{"anyOf":[{"$ref":"#/components/schemas/SmsContent"},{"type":"null"}]},"browser_push":{"anyOf":[{"$ref":"#/components/schemas/BrowserPushContent"},{"type":"null"}]},"rcs":{"anyOf":[{"$ref":"#/components/schemas/RcsContent"},{"type":"null"}]}},"type":"object","title":"ChannelContent","description":"Per-channel content for campaigns. Each key is optional; include only\nthe channels the campaign targets."},"ChannelCreate":{"properties":{"channel_type":{"$ref":"#/components/schemas/ChannelType"},"name":{"type":"string","title":"Name"},"identifier":{"type":"string","title":"Identifier"},"config":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Config"}},"type":"object","required":["channel_type","name","identifier"],"title":"ChannelCreate","description":"Schema for creating a channel","example":{"channel_type":"whatsapp","config":{"phone_number_id":"123456789012345","waba_id":"987654321098765"},"identifier":"491521558086","name":"Main WhatsApp Business"}},"ChannelDeeplinkInfo":{"properties":{"channel_id":{"type":"string","title":"Channel Id"},"channel_name":{"type":"string","title":"Channel Name"},"channel_type":{"type":"string","title":"Channel Type"},"url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Url"},"code":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Code"},"manual":{"type":"boolean","title":"Manual","default":false},"instructions":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Instructions"}},"type":"object","required":["channel_id","channel_name","channel_type"],"title":"ChannelDeeplinkInfo","description":"Deeplink info for a single channel."},"ChannelFeatures":{"properties":{"text":{"type":"boolean","title":"Text","default":false},"images":{"type":"boolean","title":"Images","default":false},"videos":{"type":"boolean","title":"Videos","default":false},"audio":{"type":"boolean","title":"Audio","default":false},"voice":{"type":"boolean","title":"Voice","default":false},"documents":{"type":"boolean","title":"Documents","default":false},"stickers":{"type":"boolean","title":"Stickers","default":false},"location":{"type":"boolean","title":"Location","default":false},"contacts":{"type":"boolean","title":"Contacts","default":false},"reactions":{"type":"boolean","title":"Reactions","default":false},"supports_reply":{"type":"boolean","title":"Supports Reply","default":false},"interactive_buttons":{"anyOf":[{"type":"boolean"},{"$ref":"#/components/schemas/InteractiveButtonsCapability"}],"title":"Interactive Buttons","default":false},"interactive_lists":{"anyOf":[{"type":"boolean"},{"$ref":"#/components/schemas/InteractiveListsCapability"}],"title":"Interactive Lists","default":false},"carousels":{"anyOf":[{"type":"boolean"},{"$ref":"#/components/schemas/CarouselsCapability"}],"title":"Carousels","default":false},"templates":{"type":"boolean","title":"Templates","default":false},"quick_replies":{"anyOf":[{"type":"boolean"},{"$ref":"#/components/schemas/QuickRepliesCapability"}],"title":"Quick Replies","default":false},"proxy":{"type":"boolean","title":"Proxy","default":false},"max_text_length":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Max Text Length"},"max_media_caption_length":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Max Media Caption Length"}},"type":"object","title":"ChannelFeatures","description":"Feature matrix for a channel"},"ChannelHealthItem":{"properties":{"id":{"type":"string","title":"Id"},"name":{"type":"string","title":"Name"},"channel_type":{"type":"string","title":"Channel Type"},"integration_type":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Integration Type"},"connection_status":{"type":"string","title":"Connection Status"},"last_error":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Last Error"},"last_error_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Last Error At"},"email_address":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Email Address"}},"type":"object","required":["id","name","channel_type","connection_status"],"title":"ChannelHealthItem","description":"Health status of a single channel/integration"},"ChannelHealthResponse":{"properties":{"has_errors":{"type":"boolean","title":"Has Errors"},"error_count":{"type":"integer","title":"Error Count"},"integrations":{"items":{"$ref":"#/components/schemas/ChannelHealthItem"},"type":"array","title":"Integrations"}},"type":"object","required":["has_errors","error_count","integrations"],"title":"ChannelHealthResponse","description":"Response with health status of all channel integrations"},"ChannelMediaSpec":{"properties":{"max_image_size_mb":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Max Image Size Mb"},"max_video_size_mb":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Max Video Size Mb"},"max_audio_size_mb":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Max Audio Size Mb"},"max_document_size_mb":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Max Document Size Mb"},"supported_image_types":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Supported Image Types"},"supported_video_types":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Supported Video Types"},"supported_audio_types":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Supported Audio Types"},"supported_document_types":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Supported Document Types"}},"type":"object","title":"ChannelMediaSpec","description":"Media specifications for a single channel."},"ChannelMediaSpecsResponse":{"properties":{"whatsapp":{"anyOf":[{"$ref":"#/components/schemas/ChannelMediaSpec"},{"type":"null"}]},"telegram":{"anyOf":[{"$ref":"#/components/schemas/ChannelMediaSpec"},{"type":"null"}]},"messenger":{"anyOf":[{"$ref":"#/components/schemas/ChannelMediaSpec"},{"type":"null"}]},"sms":{"anyOf":[{"$ref":"#/components/schemas/ChannelMediaSpec"},{"type":"null"}]},"browser_push":{"anyOf":[{"$ref":"#/components/schemas/ChannelMediaSpec"},{"type":"null"}]},"rcs":{"anyOf":[{"$ref":"#/components/schemas/ChannelMediaSpec"},{"type":"null"}]}},"type":"object","title":"ChannelMediaSpecsResponse","description":"Per-channel media specifications for campaign attachments."},"ChannelMessageCount":{"properties":{"channel_type":{"type":"string","title":"Channel Type","description":"Channel type (whatsapp, telegram, etc.)"},"message_count":{"type":"integer","title":"Message Count","description":"Number of messages for this channel"}},"type":"object","required":["channel_type","message_count"],"title":"ChannelMessageCount","description":"Message count for a single channel type"},"ChannelPerformanceItem":{"properties":{"channel":{"type":"string","title":"Channel"},"sent":{"type":"integer","title":"Sent"},"delivered_pct":{"type":"number","title":"Delivered Pct"},"open_pct":{"type":"number","title":"Open Pct"},"reply_pct":{"type":"number","title":"Reply Pct"}},"type":"object","required":["channel","sent","delivered_pct","open_pct","reply_pct"],"title":"ChannelPerformanceItem","description":"Per-channel stats row."},"ChannelPerformanceResponse":{"properties":{"items":{"items":{"$ref":"#/components/schemas/ChannelPerformanceItem"},"type":"array","title":"Items"}},"type":"object","required":["items"],"title":"ChannelPerformanceResponse","description":"Body of ``GET /flows/{id}/observability/channels``."},"ChannelPriorityUpdate":{"properties":{"channel_priority":{"items":{"type":"string"},"type":"array","title":"Channel Priority"}},"type":"object","required":["channel_priority"],"title":"ChannelPriorityUpdate"},"ChannelProfileResponse":{"properties":{"channel_id":{"type":"string","title":"Channel Id"},"channel_type":{"type":"string","title":"Channel Type"},"channel_name":{"type":"string","title":"Channel Name"},"whatsapp":{"anyOf":[{"$ref":"#/components/schemas/WhatsAppProfileResponse"},{"type":"null"}]},"telegram":{"anyOf":[{"$ref":"#/components/schemas/TelegramProfileResponse"},{"type":"null"}]}},"type":"object","required":["channel_id","channel_type","channel_name"],"title":"ChannelProfileResponse","description":"Unified response schema for channel profile"},"ChannelProfileUpdateRequest":{"properties":{"about":{"anyOf":[{"type":"string","maxLength":139},{"type":"null"}],"title":"About","description":"[WhatsApp] Short description shown in profile"},"address":{"anyOf":[{"type":"string","maxLength":256},{"type":"null"}],"title":"Address","description":"[WhatsApp] Business address"},"email":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Email","description":"[WhatsApp] Business contact email"},"websites":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Websites","description":"[WhatsApp] List of website URLs (max 2)"},"vertical":{"anyOf":[{"$ref":"#/components/schemas/WhatsAppVertical"},{"type":"null"}],"description":"[WhatsApp] Business category"},"description":{"anyOf":[{"type":"string","maxLength":512},{"type":"null"}],"title":"Description","description":"Full description (max 512 chars)"},"name":{"anyOf":[{"type":"string","maxLength":64},{"type":"null"}],"title":"Name","description":"[Telegram] Bot display name"},"short_description":{"anyOf":[{"type":"string","maxLength":120},{"type":"null"}],"title":"Short Description","description":"[Telegram] Short description shown in chat list"},"commands":{"anyOf":[{"items":{"$ref":"#/components/schemas/TelegramBotCommand"},"type":"array"},{"type":"null"}],"title":"Commands","description":"[Telegram] List of bot commands"}},"type":"object","title":"ChannelProfileUpdateRequest","description":"Unified request schema for updating channel profile.\n\nInclude only the fields you want to update. Fields set to None are ignored.\n\nFor WhatsApp channels:\n- about, address, description, email, websites, vertical\n\nFor Telegram channels:\n- name, description, short_description, commands"},"ChannelProfileUpdateResponse":{"properties":{"success":{"type":"boolean","title":"Success"},"message":{"type":"string","title":"Message"},"updated_fields":{"items":{"type":"string"},"type":"array","title":"Updated Fields","default":[]},"errors":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Errors"}},"type":"object","required":["success","message"],"title":"ChannelProfileUpdateResponse","description":"Response schema for profile update"},"ChannelResolutionTime":{"properties":{"channel_type":{"type":"string","title":"Channel Type","description":"Channel type"},"count":{"type":"integer","title":"Count","description":"Number of closed conversations"},"avg_hours":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Avg Hours","description":"Average resolution time in hours"}},"type":"object","required":["channel_type","count"],"title":"ChannelResolutionTime","description":"Resolution time metrics for a single channel"},"ChannelResponse":{"properties":{"id":{"type":"string","title":"Id"},"tenant_id":{"type":"string","title":"Tenant Id"},"channel_type":{"$ref":"#/components/schemas/ChannelType"},"name":{"type":"string","title":"Name"},"identifier":{"type":"string","title":"Identifier"},"phone_number_formatted":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Phone Number Formatted"},"is_active":{"type":"boolean","title":"Is Active"},"is_verified":{"type":"boolean","title":"Is Verified","default":false},"is_coexistence":{"type":"boolean","title":"Is Coexistence","default":false},"created_at":{"type":"string","format":"date-time","title":"Created At"},"activation_status":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Activation Status","description":"ES/Coexistence activation lifecycle. `null` = ACTIVE (legacy / healthy channels). `AVAILABLE` = the channel row exists but setup is not complete (see `setup_pending_reason`); such a channel is `is_active=false` and MUST NOT be rendered as connected."},"setup_pending_reason":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Setup Pending Reason","description":"Non-null when the channel row exists but setup is INCOMPLETE and needs customer action. `coexistence_setup_incomplete` = the WhatsApp number must finish Coexistence setup on the phone (keep it active on the WhatsApp Business App, re-run connect, and complete Meta's dialog incl. the QR scan + on-handset confirmation). `null` for every fully-set-up channel. A channel with this set is NOT connected and must not render as such."},"avatar_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Avatar Url","description":"Profile picture of the connected account on this channel (Facebook Page / Instagram account). Null when unavailable or not captured for this channel type. Meta CDN URLs expire — do not persist client-side."},"rcs_agent_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Rcs Agent Id","description":"[RCS] Google RBM agent id, stored BARE (no '@rbm.goog' suffix). Normalisation happens on write via a model @validates hook."},"rcs_partner_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Rcs Partner Id","description":"[RCS] Google RBM partner account id."},"rcs_launch_state":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Rcs Launch State","description":"[RCS] Free-form brand-verification + per-carrier launch status. Absent/empty means NOTHING is launched (fail closed): pre-launch an agent can only reach allowlisted tester devices."},"rcs_message_traffic_type":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Rcs Message Traffic Type","description":"[RCS] The channel's DEFAULT `messageTrafficType` (`AUTHENTICATION` | `TRANSACTION` | `PROMOTION` | `SERVICEREQUEST` | `ACKNOWLEDGEMENT`), or `null` when none is set. A German regulatory declaration about the nature of the message (UWG / telecoms), NOT a billing tier and not a delivery-speed setting. `null` means unset -- the adapter then falls back to `SERVICEREQUEST` -- and is what an archived channel reads, since this value is deliberately not preserved across archiving. Returned canonicalised, i.e. exactly the value that will be sent to Google; a stored value outside the five enum values reads as `null` because that is what the adapter will make of it. Set it with `PUT /channels/{id}/rcs/credentials`.","example":"TRANSACTION"},"rcs_pricing_mode":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Rcs Pricing Mode","description":"[RCS] How this RCS agent is billed (read-only; set by SendSeven): `per_message` = each message is billed and counts towards your messages; `mau` = billed once per monthly active recipient, and its messages do NOT count as messages sent. `null` on every non-RCS channel.","example":"per_message"}},"type":"object","required":["id","tenant_id","channel_type","name","identifier","is_active","created_at"],"title":"ChannelResponse","description":"Schema for channel response.\n\nSECURITY: The 'config' field is intentionally excluded. It contains\nAPI tokens, secrets, and credentials that must NEVER be returned via API.\nUse 'identifier' for display purposes (phone number, bot username, page ID).\n\nFor WhatsApp channels:\n- ``identifier`` returns a purely numeric phone number (no '+' or spaces)\n- ``phone_number_formatted`` returns the human-readable formatted number\n\nFor RCS channels, three NON-SECRET columns are surfaced so the channel UI\ncan render the agent identity and the launch lifecycle straight off the\n``GET /channels`` row. The two secret RCS columns\n(``rcs_service_account_json`` -- one key that can send as ANY of our\nagents -- and ``rcs_client_token``) are deliberately absent and MUST NEVER\nbe added here or to any other read schema.\n\n``rcs_message_traffic_type`` is the ONE ``config`` key this schema exposes,\nand it arrives via the ``Channel.rcs_message_traffic_type`` **property** --\nnot by relaxing the config exclusion. Do not \"simplify\" that into returning\n``config`` (or a filtered copy of it): the exclusion above is the reason a\nrotated ``access_token`` has never come back out of this endpoint, and a\npassthrough is how the next new secret key would.","example":{"channel_type":"whatsapp","created_at":"2026-01-15T10:30:00Z","id":"a1b2c3d4-5678-9abc-def0-1234567890ab","identifier":"491521558086","is_active":true,"is_verified":true,"name":"Main WhatsApp Business","phone_number_formatted":"+49 1521 558086","tenant_id":"t1e2n3a4-5678-9abc-def0-1234567890ab"}},"ChannelResponseTime":{"properties":{"channel_type":{"type":"string","title":"Channel Type","description":"Channel type"},"conversation_count":{"type":"integer","title":"Conversation Count","description":"Number of conversations with responses"},"avg_response_minutes":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Avg Response Minutes","description":"Average response time in minutes"}},"type":"object","required":["channel_type","conversation_count"],"title":"ChannelResponseTime","description":"Response time metrics for a single channel"},"ChannelSenderActionSupport":{"properties":{"read":{"type":"boolean","title":"Read","description":"The channel can show the contact that their message was read."},"typing":{"type":"boolean","title":"Typing","description":"The channel can show the contact a typing indicator."},"typing_off":{"type":"boolean","title":"Typing Off","description":"The typing indicator can be cleared explicitly. When false, `state: \"off\"` is accepted as a no-op and the indicator ends on its own (timeout or your next reply)."},"typing_ttl_seconds":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Typing Ttl Seconds","description":"How long the platform shows the typing indicator after one call, in seconds. `null` means it stays until cleared or until you reply."},"typing_requires_message":{"type":"boolean","title":"Typing Requires Message","description":"The platform ties the typing indicator to a received message. If you do not pass `message_id`, the latest inbound message of the conversation is used."},"channel_type":{"type":"string","title":"Channel Type","description":"Channel type, e.g. `whatsapp`."}},"type":"object","required":["read","typing","typing_off","typing_requires_message","channel_type"],"title":"ChannelSenderActionSupport","description":"Sender-action support for one channel type.","example":{"channel_type":"whatsapp","read":true,"typing":true,"typing_off":false,"typing_requires_message":true,"typing_ttl_seconds":25}},"ChannelStats":{"properties":{"channel_type":{"$ref":"#/components/schemas/SubscriptionChannelType"},"total":{"type":"integer","title":"Total","default":0},"subscribed":{"type":"integer","title":"Subscribed","default":0},"pending":{"type":"integer","title":"Pending","default":0},"unsubscribed":{"type":"integer","title":"Unsubscribed","default":0},"bounced":{"type":"integer","title":"Bounced","default":0},"complained":{"type":"integer","title":"Complained","default":0}},"type":"object","required":["channel_type"],"title":"ChannelStats","description":"Statistics for a single channel","example":{"bounced":15,"channel_type":"email","complained":5,"pending":50,"subscribed":850,"total":1000,"unsubscribed":80}},"ChannelStatus":{"type":"string","enum":["available","template_required","unavailable"],"title":"ChannelStatus","description":"Status of a channel for a contact"},"ChannelStatusBreakdown":{"properties":{"channel_type":{"type":"string","title":"Channel Type","description":"Channel type (email, sms, whatsapp, etc.)"},"subscribed":{"type":"integer","title":"Subscribed","description":"Active subscriptions","default":0},"pending":{"type":"integer","title":"Pending","description":"Pending confirmations","default":0},"unsubscribed":{"type":"integer","title":"Unsubscribed","description":"Opted-out subscriptions","default":0},"bounced":{"type":"integer","title":"Bounced","description":"Bounced subscriptions","default":0},"complained":{"type":"integer","title":"Complained","description":"Spam-complained subscriptions","default":0},"total":{"type":"integer","title":"Total","description":"Total subscriptions for this channel","default":0}},"type":"object","required":["channel_type"],"title":"ChannelStatusBreakdown","description":"Subscription counts for a single channel type"},"ChannelSwitchRequest":{"properties":{"channel_ids":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Channel Ids"}},"type":"object","title":"ChannelSwitchRequest","description":"Request to generate channel switch deeplinks.\n\nEither ``channel_ids`` (specific channels) or no filter (all switchable channels)."},"ChannelSwitchResponse":{"properties":{"token":{"type":"string","title":"Token"},"deeplinks":{"items":{"$ref":"#/components/schemas/ChannelDeeplinkInfo"},"type":"array","title":"Deeplinks"},"expires_at":{"type":"string","format":"date-time","title":"Expires At"},"share_token_id":{"type":"string","title":"Share Token Id"}},"type":"object","required":["token","deeplinks","expires_at","share_token_id"],"title":"ChannelSwitchResponse","description":"Response with channel switch token and deeplinks."},"ChannelType":{"type":"string","enum":["whatsapp","telegram","messenger","instagram","sms","viber","live_chat","browser_push","email","rcs"],"title":"ChannelType","description":"Every messaging channel the platform supports."},"ChannelTypeInfo":{"properties":{"value":{"type":"string","title":"Value"},"display_name":{"type":"string","title":"Display Name"},"is_available":{"type":"boolean","title":"Is Available"}},"type":"object","required":["value","display_name","is_available"],"title":"ChannelTypeInfo","description":"Information about a channel type"},"ChannelTypesResponse":{"properties":{"available":{"items":{"type":"string"},"type":"array","title":"Available"},"coming_soon":{"items":{"type":"string"},"type":"array","title":"Coming Soon"},"all_types":{"items":{"$ref":"#/components/schemas/ChannelTypeInfo"},"type":"array","title":"All Types"}},"type":"object","required":["available","coming_soon","all_types"],"title":"ChannelTypesResponse","description":"Response with all channel types and their availability"},"ChannelUpdate":{"properties":{"name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Name"},"identifier":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Identifier"},"config":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Config"},"is_active":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Is Active"}},"type":"object","title":"ChannelUpdate","description":"Schema for updating a channel","example":{"is_active":true,"name":"Updated WhatsApp Business"}},"CheckAliasResponse":{"properties":{"alias":{"type":"string","title":"Alias","description":"The alias that was checked"},"available":{"type":"boolean","title":"Available","description":"Whether the alias is available"}},"type":"object","required":["alias","available"],"title":"CheckAliasResponse","description":"Response from checking alias availability"},"CheckExistingConversationRequest":{"properties":{"contact_id":{"type":"string","title":"Contact Id","description":"Contact UUID to check"},"channel_id":{"type":"string","title":"Channel Id","description":"Channel UUID to check"}},"type":"object","required":["contact_id","channel_id"],"title":"CheckExistingConversationRequest","description":"Request to check for existing conversations without creating one.\n\nThis is the first phase of the two-phase conversation creation flow.\nUse this to check if there are existing open or closed conversations\nbefore deciding to create a new one or reopen an existing one.","example":{"channel_id":"channel-uuid","contact_id":"contact-uuid"}},"CheckExistingConversationResponse":{"properties":{"has_existing_open":{"type":"boolean","title":"Has Existing Open","description":"True if an open or assigned conversation exists"},"existing_open_conversation_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Existing Open Conversation Id","description":"ID of the existing open conversation (if has_existing_open=True)"},"has_existing_closed":{"type":"boolean","title":"Has Existing Closed","description":"True if a closed conversation exists (when no open conversation)"},"existing_closed_info":{"anyOf":[{"$ref":"#/components/schemas/ExistingConversationInfo"},{"type":"null"}],"description":"Details about the existing closed conversation"},"channel_type":{"type":"string","title":"Channel Type","description":"Channel type"},"window_status":{"anyOf":[{"$ref":"#/components/schemas/WhatsAppChannelWindowStatus"},{"type":"null"}],"description":"WhatsApp window status (WhatsApp channels only)"}},"type":"object","required":["has_existing_open","has_existing_closed","channel_type"],"title":"CheckExistingConversationResponse","description":"Response from checking for existing conversations.\n\nProvides information about existing open and closed conversations\nso the UI can decide whether to:\n- Use the existing open conversation\n- Reopen the closed conversation\n- Create a new conversation"},"ClickDetail":{"properties":{"clicked_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Clicked At"},"device_type":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Device Type"},"ip_address":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Ip Address"}},"type":"object","title":"ClickDetail","description":"A single click event"},"CloneWaFlowRequest":{"properties":{"name":{"anyOf":[{"type":"string","maxLength":200},{"type":"null"}],"title":"Name"},"target_tenant_id":{"anyOf":[{"type":"string","maxLength":36},{"type":"null"}],"title":"Target Tenant Id","description":"Workspace of the same billing account (default: this one)"},"target_channel_ids":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Target Channel Ids","description":"Push a draft to these numbers after copying"}},"type":"object","title":"CloneWaFlowRequest"},"CloseConversationRequest":{"properties":{"notes":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Notes","description":"Optional closure notes"},"summarize":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Summarize","description":"Control AI summarization (only used when tenant setting is 'ask')"}},"type":"object","title":"CloseConversationRequest","description":"Request body for closing a conversation","example":{"notes":"Customer issue resolved","summarize":true}},"CoexistenceStatusResponse":{"properties":{"channel_id":{"type":"string","title":"Channel Id"},"is_coexistence":{"type":"boolean","title":"Is Coexistence"},"is_on_biz_app":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Is On Biz App"},"platform_type":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Platform Type"},"coexistence_onboarded_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Coexistence Onboarded At"},"history_sync_status":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"History Sync Status","description":"One of: not_started, requested, in_progress, completed, declined, failed, unavailable (Meta's one-time 24h-from-onboarding sync window closed before history could be requested)"},"history_sync_progress":{"anyOf":[{"type":"integer","maximum":100.0,"minimum":0.0},{"type":"null"}],"title":"History Sync Progress"},"history_sync_phase":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"History Sync Phase"},"contacts_sync_status":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Contacts Sync Status","description":"One of: not_started, requested, completed, failed, unavailable (24h sync window closed before the address book could be requested)"},"disconnected_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Disconnected At"},"disconnection_reason":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Disconnection Reason"},"reconnected_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Reconnected At"}},"type":"object","required":["channel_id","is_coexistence"],"title":"CoexistenceStatusResponse","description":"Polling payload for the frontend Coexistence syncing screen.\n\nAll fields except ``channel_id`` and ``is_coexistence`` may be null on\nfresh / non-coexistence channels. Datetimes are timezone-aware UTC."},"CommentAndPrivateReplyRequest":{"properties":{"public_text":{"anyOf":[{"type":"string","maxLength":2200},{"type":"null"}],"title":"Public Text","description":"Public reply body, posted under the comment (text only). Omit to send only a private DM."},"private_text":{"anyOf":[{"type":"string","maxLength":1000},{"type":"null"}],"title":"Private Text","description":"Private DM body — or the button template's body when `buttons` is set. Omit to post only a public reply."},"channel_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Channel Id","description":"The Instagram or Facebook Messenger channel whose post was commented on. Optional when the comment is stored in your workspace (the stored channel wins); **required** otherwise."},"buttons":{"anyOf":[{"items":{"$ref":"#/components/schemas/PrivateReplyButton"},"type":"array","maxItems":3},{"type":"null"}],"title":"Buttons","description":"Up to 3 buttons on the **private DM** (sent as a button template). Cannot be combined with `image_url`. Ignored unless `private_text` is set."},"quick_replies":{"anyOf":[{"items":{"$ref":"#/components/schemas/PrivateReplyQuickReply"},"type":"array","maxItems":13},{"type":"null"}],"title":"Quick Replies","description":"**DEPRECATED / IGNORED.** Comment private replies are buttons-only since 2026-08-13. Accepted for backwards compatibility (no 422), but any chips are silently ignored and never sent to Meta — use `buttons` instead."},"image_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Image Url","description":"**Facebook Messenger only.** Image URL on the **private DM**. Rejected on Instagram. Cannot be combined with `buttons`. Ignored unless `private_text` is set."}},"type":"object","title":"CommentAndPrivateReplyRequest","description":"Body of ``POST /comments/{comment_id}/reply`` — post a public reply\nand/or send a private DM in ONE call.\n\nAt least one of ``public_text`` / ``private_text`` must be provided. The\nprivate-reply fields (``buttons`` / ``quick_replies`` / ``image_url``) reuse\nthe exact shapes of :class:`PrivateReplyRequest` and only apply to the\nprivate DM part."},"CommentAndPrivateReplyResponse":{"properties":{"status":{"type":"string","title":"Status","description":"`sent` (all requested parts sent) or `partial` (public reply sent, private DM failed)."},"public":{"anyOf":[{"$ref":"#/components/schemas/PublicReplyResponse"},{"type":"null"}],"description":"The public reply, when one was requested and posted."},"private":{"anyOf":[{"$ref":"#/components/schemas/PrivateReplyResponse"},{"type":"null"}],"description":"The private DM, when one was requested and sent."},"private_error":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Private Error","description":"Set only on `partial`: why the private DM failed after the public reply already went out."},"private_error_type":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Private Error Type","description":"Set only on `partial`: the exception class name of the private-DM failure (e.g. `PrivateReplyAlreadySentError`), so the UI can label it without parsing the message."}},"type":"object","required":["status"],"title":"CommentAndPrivateReplyResponse","description":"Result of a combined send.\n\n``status`` is ``sent`` when everything the caller asked for went out, and\n``partial`` in the ONE partial case the service tolerates: the public reply\nwas posted but the follow-up private DM then failed (the public reply is\nalready durable, so it is not rolled back). ``private_error`` /\n``private_error_type`` describe that failure. Every OTHER failure mode is a\nhard HTTP error, never a partial."},"CompanyProfileResponse":{"properties":{"company_legal_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Company Legal Name","description":"Legal business name"},"company_address_line1":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Company Address Line1","description":"Physical address line 1 (required for CAN-SPAM)"},"company_address_line2":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Company Address Line2","description":"Physical address line 2 (suite, floor, etc.)"},"company_city":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Company City","description":"City"},"company_state_province":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Company State Province","description":"State/Province/Region"},"company_postal_code":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Company Postal Code","description":"Postal/ZIP code"},"company_country":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Company Country","description":"Country"},"company_email":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Company Email","description":"Company contact email"},"company_phone":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Company Phone","description":"Company contact phone"},"company_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Company Name","description":"Display name (used in templates and UI)"},"company_domain":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Company Domain","description":"Company domain (e.g., example.com)"},"brand_colors":{"anyOf":[{"$ref":"#/components/schemas/BrandColors"},{"type":"null"}],"description":"Brand color palette"},"logo_variants":{"anyOf":[{"$ref":"#/components/schemas/LogoVariants"},{"type":"null"}],"description":"Logo files at different sizes"},"is_complete_for_email":{"type":"boolean","title":"Is Complete For Email","description":"Whether company profile has all required fields for CAN-SPAM compliance","default":false}},"type":"object","title":"CompanyProfileResponse","description":"Company profile and branding information (GET response)"},"CompanyProfileUpdate":{"properties":{"company_legal_name":{"anyOf":[{"type":"string","maxLength":255},{"type":"null"}],"title":"Company Legal Name","description":"Legal business name"},"company_address_line1":{"anyOf":[{"type":"string","maxLength":255},{"type":"null"}],"title":"Company Address Line1","description":"Physical address line 1"},"company_address_line2":{"anyOf":[{"type":"string","maxLength":255},{"type":"null"}],"title":"Company Address Line2","description":"Physical address line 2"},"company_city":{"anyOf":[{"type":"string","maxLength":100},{"type":"null"}],"title":"Company City","description":"City"},"company_state_province":{"anyOf":[{"type":"string","maxLength":100},{"type":"null"}],"title":"Company State Province","description":"State/Province/Region"},"company_postal_code":{"anyOf":[{"type":"string","maxLength":20},{"type":"null"}],"title":"Company Postal Code","description":"Postal/ZIP code"},"company_country":{"anyOf":[{"type":"string","maxLength":100},{"type":"null"}],"title":"Company Country","description":"Country"},"company_email":{"anyOf":[{"type":"string","maxLength":255},{"type":"null"}],"title":"Company Email","description":"Company contact email"},"company_phone":{"anyOf":[{"type":"string","maxLength":50},{"type":"null"}],"title":"Company Phone","description":"Company contact phone"},"company_name":{"anyOf":[{"type":"string","maxLength":255},{"type":"null"}],"title":"Company Name","description":"Display name"},"company_domain":{"anyOf":[{"type":"string","maxLength":255},{"type":"null"}],"title":"Company Domain","description":"Company domain"},"brand_colors":{"anyOf":[{"$ref":"#/components/schemas/BrandColors"},{"type":"null"}],"description":"Brand color palette"}},"additionalProperties":false,"type":"object","title":"CompanyProfileUpdate","description":"Update company profile and branding (PATCH request)"},"CompileRequest":{"properties":{"builder_doc":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Builder Doc"},"version_id":{"anyOf":[{"type":"string","maxLength":36},{"type":"null"}],"title":"Version Id"}},"type":"object","title":"CompileRequest"},"ComplianceFooterValidationResponse":{"properties":{"is_valid":{"type":"boolean","title":"Is Valid","description":"Whether the footer HTML is valid"},"errors":{"items":{"type":"string"},"type":"array","title":"Errors","description":"List of validation errors"},"warnings":{"items":{"type":"string"},"type":"array","title":"Warnings","description":"List of validation warnings"},"sanitized_html":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Sanitized Html","description":"Sanitized HTML after validation"}},"type":"object","required":["is_valid"],"title":"ComplianceFooterValidationResponse","description":"Response from validating compliance footer HTML","example":{"errors":[],"is_valid":true,"sanitized_html":"<p>Footer HTML with {{unsubscribe_link}}</p>","warnings":["Removed <style> tag"]}},"ConfigureWebhooksResponse":{"properties":{"success":{"type":"boolean","title":"Success","description":"Whether webhook configuration succeeded"},"webhook_url":{"type":"string","title":"Webhook Url","description":"The configured webhook URL"},"message":{"type":"string","title":"Message","description":"Status message"},"current_settings":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Current Settings","description":"Current webhook settings from SendGrid"}},"type":"object","required":["success","webhook_url","message"],"title":"ConfigureWebhooksResponse","description":"Response from configuring SendGrid webhooks","example":{"current_settings":{"enabled":true,"url":"https://api.example.com/webhooks/sendgrid/email"},"message":"SendGrid event webhooks configured successfully","success":true,"webhook_url":"https://api.example.com/webhooks/sendgrid/email?tenant_id=xxx&config_id=yyy"}},"ConfirmOptInResponse":{"properties":{"success":{"type":"boolean","title":"Success"},"message":{"type":"string","title":"Message"},"subscription":{"anyOf":[{"$ref":"#/components/schemas/ConfirmOptInSubscriptionSummary"},{"type":"null"}]}},"type":"object","required":["success","message"],"title":"ConfirmOptInResponse","description":"Schema for opt-in confirmation response (public endpoint)","example":{"message":"Subscription confirmed successfully","subscription":{"channel_type":"email","opt_in_confirmed_at":"2026-10-06T10:15:00Z","status":"subscribed"},"success":true}},"ConfirmOptInSubscriptionSummary":{"properties":{"status":{"$ref":"#/components/schemas/SubscriptionStatus"},"channel_type":{"$ref":"#/components/schemas/SubscriptionChannelType"},"opt_in_confirmed_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Opt In Confirmed At"}},"type":"object","required":["status","channel_type"],"title":"ConfirmOptInSubscriptionSummary","description":"What the PUBLIC (unauthenticated) confirm endpoint may reveal about the\nconfirmed subscription. Deliberately minimal: no ids, IP address, user\nagent, tokens or the rest of the record. Never widen this to\n``SubscriptionResponse``."},"ConnectionTestResponse":{"properties":{"integration_id":{"type":"string","title":"Integration Id"},"success":{"type":"boolean","title":"Success"},"status":{"type":"string","title":"Status"},"message":{"type":"string","title":"Message"},"smtp":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Smtp"},"imap":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Imap"}},"type":"object","required":["integration_id","success","status","message"],"title":"ConnectionTestResponse","description":"Response from connection test operation"},"ConnectionType":{"type":"string","enum":["sendgrid","sendgrid_managed","mailgun","ses","gmail","smtp_imap","ses_managed","proxy"],"title":"ConnectionType","description":"Type of email connection"},"ContactAddress":{"properties":{"street":{"anyOf":[{"type":"string","maxLength":255},{"type":"null"}],"title":"Street","description":"Street address"},"city":{"anyOf":[{"type":"string","maxLength":100},{"type":"null"}],"title":"City","description":"City"},"state":{"anyOf":[{"type":"string","maxLength":100},{"type":"null"}],"title":"State","description":"State/Province"},"zip":{"anyOf":[{"type":"string","maxLength":20},{"type":"null"}],"title":"Zip","description":"ZIP/Postal code"},"country":{"anyOf":[{"type":"string","maxLength":100},{"type":"null"}],"title":"Country","description":"Country name"},"country_code":{"anyOf":[{"type":"string","maxLength":3},{"type":"null"}],"title":"Country Code","description":"Two-letter country code"},"type":{"anyOf":[{"type":"string","enum":["HOME","WORK"]},{"type":"null"}],"title":"Type","description":"Address type"}},"type":"object","title":"ContactAddress","description":"Contact postal address"},"ContactAttachmentItem":{"properties":{"attachment_id":{"type":"string","title":"Attachment Id","description":"Attachment UUID"},"filename":{"type":"string","title":"Filename","description":"Original filename"},"content_type":{"type":"string","title":"Content Type","description":"MIME type"},"category":{"type":"string","enum":["image","video","audio","voice","file"],"title":"Category","description":"Media category derived from content_type + message meta. `voice` = audio flagged as a voice note (meta.is_voice); `file` = documents / anything non-AV."},"file_size":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"File Size","description":"Size in bytes"},"width":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Width","description":"Pixel width (image/video)"},"height":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Height","description":"Pixel height (image/video)"},"duration":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Duration","description":"Duration in seconds (audio/video)"},"url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Url","description":"Short-lived signed view URL (60 min); null if signing failed"},"thumbnail_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Thumbnail Url","description":"Short-lived signed thumbnail URL; null when no thumbnail exists"},"direction":{"type":"string","enum":["inbound","outbound"],"title":"Direction","description":"Message direction the attachment was exchanged on"},"created_at":{"type":"string","format":"date-time","title":"Created At","description":"Message time (when exchanged)"},"conversation_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Conversation Id","description":"Conversation the attachment belongs to (for jump-to)"},"message_id":{"type":"string","title":"Message Id","description":"Message the attachment belongs to"},"source":{"type":"string","enum":["message","email"],"title":"Source","description":"Where the file came from. `message` = chat/media message attachment (incl. interactive/template header media); `email` = an email file attachment (`message_id` is the email's message; one item per file, so an email with N files yields N items).","default":"message"}},"type":"object","required":["attachment_id","filename","content_type","category","direction","created_at","message_id"],"title":"ContactAttachmentItem","description":"A single media item exchanged with a contact.\n\nOne item per file: a message's primary/featured attachment, its\ninteractive/template header media, or each file attachment of an email\nmessage (``source=\"email\"``). Powers the WhatsApp-profile-style media gallery on the\ncontact-detail page. Covers ALL media categories including documents.\n\n``url`` and ``thumbnail_url`` are short-lived (60 min) signed view URLs\ninto the private media bucket — never raw bucket URLs. Regenerate by\nre-fetching a page once they expire."},"ContactAttachmentsResponse":{"properties":{"items":{"items":{"$ref":"#/components/schemas/ContactAttachmentItem"},"type":"array","title":"Items","description":"Media items, newest first"},"next_cursor":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Next Cursor","description":"Opaque cursor for the next (older) page; null when no more"},"has_more":{"type":"boolean","title":"Has More","description":"Whether older media exist beyond this page","default":false}},"type":"object","required":["items"],"title":"ContactAttachmentsResponse","description":"Keyset-paginated, newest-first list of ALL media exchanged with a\ncontact across every conversation.\n\nPagination is keyset/cursor only (DESC by ``(created_at, message_id)``).\n``next_cursor`` is the opaque cursor of the LAST returned item when\n``has_more`` is true, otherwise null."},"ContactAvatarResponse":{"properties":{"success":{"type":"boolean","title":"Success","default":true},"contact_id":{"type":"string","title":"Contact Id"},"avatar_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Avatar Url","description":"Permanent URL of the stored picture, or null after removal. Always points at our own proxy — never at a platform CDN, whose signed URLs expire within days."},"avatar_is_custom":{"type":"boolean","title":"Avatar Is Custom","description":"True while a manually uploaded picture is in place; platform profile-picture syncing is suppressed until it is removed.","default":false}},"type":"object","required":["contact_id"],"title":"ContactAvatarResponse","description":"Result of uploading or removing a contact's profile picture."},"ContactCard":{"properties":{"name":{"$ref":"#/components/schemas/ContactName","description":"Contact name (formatted_name is required)"},"phones":{"anyOf":[{"items":{"$ref":"#/components/schemas/ContactPhone"},"type":"array","maxItems":20},{"type":"null"}],"title":"Phones","description":"Phone numbers (up to 20)"},"emails":{"anyOf":[{"items":{"$ref":"#/components/schemas/ContactEmail"},"type":"array","maxItems":20},{"type":"null"}],"title":"Emails","description":"Email addresses (up to 20)"},"addresses":{"anyOf":[{"items":{"$ref":"#/components/schemas/ContactAddress"},"type":"array","maxItems":20},{"type":"null"}],"title":"Addresses","description":"Postal addresses (up to 20)"},"org":{"anyOf":[{"$ref":"#/components/schemas/ContactOrg"},{"type":"null"}],"description":"Organization/company information"},"urls":{"anyOf":[{"items":{"$ref":"#/components/schemas/ContactUrl"},"type":"array","maxItems":20},{"type":"null"}],"title":"Urls","description":"Website URLs (up to 20)"},"birthday":{"anyOf":[{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$"},{"type":"null"}],"title":"Birthday","description":"Birthday in YYYY-MM-DD format"}},"type":"object","required":["name"],"title":"ContactCard","description":"A single contact card to be sent via WhatsApp.\n\nAt minimum, the contact must have a formatted_name in the name field.\nOptionally include phones, emails, addresses, and organization info."},"ContactChannelTimelineResponse":{"properties":{"items":{"items":{"$ref":"#/components/schemas/MessageResponse"},"type":"array","title":"Items","description":"Messages from prior conversations on this channel, newest first"},"next_cursor":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Next Cursor","description":"Opaque cursor for the next (older) page; null when no more"},"has_more":{"type":"boolean","title":"Has More","description":"Whether older messages exist beyond this page","default":false}},"type":"object","required":["items"],"title":"ContactChannelTimelineResponse","description":"Keyset-paginated stream of a contact's messages on a SINGLE channel,\ndrawn from PRIOR conversations only.\n\nPowers the \"previous messages on this channel\" timeline shown next to an\nopen conversation. Reuses the standard ``MessageResponse`` item shape\n(each item already carries ``conversation_id``) so the frontend renders\nthem with the existing message bubble — no new bubble type needed.\n\nPagination is keyset/cursor only (DESC by created_at, id). ``next_cursor``\nis the opaque cursor of the LAST returned item when ``has_more`` is true,\notherwise null."},"ContactCreate":{"properties":{"name":{"anyOf":[{"type":"string","maxLength":255},{"type":"null"}],"title":"Name","description":"Contact's full name"},"first_name":{"anyOf":[{"type":"string","maxLength":100},{"type":"null"}],"title":"First Name","description":"Contact's first/given name"},"last_name":{"anyOf":[{"type":"string","maxLength":100},{"type":"null"}],"title":"Last Name","description":"Contact's last/family name"},"phone":{"anyOf":[{"type":"string","maxLength":50},{"type":"null"}],"title":"Phone","description":"Phone number with country code (E.164, e.g. +491754633940)"},"email":{"anyOf":[{"type":"string","format":"email"},{"type":"null"}],"title":"Email","description":"Email address"},"languages":{"anyOf":[{"type":"string","maxLength":255},{"type":"null"}],"title":"Languages","description":"Comma-separated ISO 639-1 language codes (e.g. 'en,de,fr'). Auto-inferred from phone country if omitted."},"birthday":{"anyOf":[{"type":"string","format":"date"},{"type":"null"}],"title":"Birthday","description":"Contact's date of birth (ISO 8601 YYYY-MM-DD)"},"contact_methods":{"anyOf":[{"items":{"$ref":"#/components/schemas/ContactMethodCreate"},"type":"array"},{"type":"null"}],"title":"Contact Methods","description":"Contact methods to create alongside the contact"},"duplicate_mode":{"anyOf":[{"$ref":"#/components/schemas/DuplicateMode"},{"type":"null"}],"description":"Optional per-request override for duplicate-contact handling. If omitted, the tenant's ``duplicate_contact_mode`` setting applies. Values: auto_merge, allow_duplicates, dont_allow_duplicates."},"custom_fields":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Custom Fields","description":"Custom field values keyed by custom field definition ID (preferred, stable across renames) or by the field `key`. Each value is validated and coerced to the field's type (see GET /custom-fields). On create, null / \"\" / [] entries are ignored. In auto_merge mode the given values overwrite the matched contact's values. Unknown or inactive fields are rejected with 422 `unknown_field` and nothing is written."}},"type":"object","title":"ContactCreate","description":"Schema for creating a new contact","example":{"birthday":"1990-04-23","contact_methods":[{"method_type":"whatsapp_id","value":"1234567890"}],"email":"john@example.com","languages":"en,de","name":"John Doe","phone":"+1234567890"}},"ContactDeleteResponse":{"properties":{"success":{"type":"boolean","title":"Success","description":"Whether the operation succeeded","default":true},"message":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Message","description":"Optional success message"},"details":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Details","description":"Details of deleted/anonymized data"}},"type":"object","title":"ContactDeleteResponse","description":"Response for contact deletion with details","example":{"details":{"conversations_anonymized":2,"custom_fields_removed":5,"subscriptions_removed":3},"message":"Contact deleted successfully","success":true}},"ContactEmail":{"properties":{"email":{"type":"string","maxLength":255,"minLength":1,"title":"Email","description":"Email address"},"type":{"anyOf":[{"type":"string","enum":["HOME","WORK"]},{"type":"null"}],"title":"Type","description":"Email type"}},"type":"object","required":["email"],"title":"ContactEmail","description":"Contact email address"},"ContactExportStartResponse":{"properties":{"job_id":{"type":"string","title":"Job Id","description":"PubSubJob id; use to poll status"},"status":{"type":"string","title":"Status","description":"Initial job status","default":"queued"},"message":{"type":"string","title":"Message","description":"Human-readable status message"},"poll_url":{"type":"string","title":"Poll Url","description":"Relative URL to poll for progress"},"download_url":{"type":"string","title":"Download Url","description":"Relative URL that serves the finished file (302 to a short-lived signed URL). Returns 409 until the job is completed."}},"type":"object","required":["job_id","message","poll_url","download_url"],"title":"ContactExportStartResponse","description":"Returned by POST /contacts/export (202 Accepted).\n\nThe export runs in a background task; poll ``poll_url`` until ``status``\nis ``completed``, then download via ``download_url``."},"ContactExportStatusResponse":{"properties":{"job_id":{"type":"string","title":"Job Id"},"status":{"type":"string","title":"Status","description":"queued | processing | completed | failed | cancelled"},"message":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Message","description":"Human-readable status message"},"progress_percent":{"type":"integer","title":"Progress Percent","description":"0-100","default":0},"total_items":{"type":"integer","title":"Total Items","description":"Total contacts matched by the filters","default":0},"processed_items":{"type":"integer","title":"Processed Items","description":"Contacts written so far","default":0},"format":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Format","description":"csv | xlsx (once known)"},"filename":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Filename","description":"Download filename; present once status == completed"},"total_contacts":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Total Contacts","description":"Contacts in the finished file; present when completed"},"columns":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Columns","description":"Ordered export column headers; present when completed"},"download_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Download Url","description":"Relative download URL (302 to a signed URL); present when completed"},"error_message":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Error Message","description":"Present when status == failed"}},"type":"object","required":["job_id","status"],"title":"ContactExportStatusResponse","description":"Progress + final result for an async contact export job."},"ContactFieldValue":{"properties":{"field_id":{"type":"string","title":"Field Id","description":"Custom field definition ID"},"field_key":{"type":"string","title":"Field Key","description":"Field key"},"field_name":{"type":"string","title":"Field Name","description":"Field display name"},"field_type":{"$ref":"#/components/schemas/FieldType","description":"Field data type"},"value":{"title":"Value","description":"Current value"}},"type":"object","required":["field_id","field_key","field_name","field_type"],"title":"ContactFieldValue","description":"Response schema for a single contact's custom field value.","example":{"field_id":"cf-12345678","field_key":"company_size","field_name":"Company Size","field_type":"select","value":"51-200"}},"ContactFieldValuesResponse":{"properties":{"contact_id":{"type":"string","title":"Contact Id","description":"Contact ID"},"fields":{"items":{"$ref":"#/components/schemas/ContactFieldValue"},"type":"array","title":"Fields","description":"List of custom field values"}},"type":"object","required":["contact_id"],"title":"ContactFieldValuesResponse","description":"Response schema for all custom field values of a contact.","example":{"contact_id":"ct-12345678","fields":[{"field_id":"cf-12345678","field_key":"company_size","field_name":"Company Size","field_type":"select","value":"51-200"}]}},"ContactFilterParams":{"properties":{"search":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Search","description":"Search by name, phone, email, or IDs (same as GET /contacts)"},"include_live_chat":{"type":"boolean","title":"Include Live Chat","description":"Include live chat contacts (same as GET /contacts)","default":false},"custom_field_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Custom Field Id","description":"custom_field_definitions.id used for filtering (same as GET /contacts)"},"custom_field_value":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Custom Field Value","description":"Custom-field filter value (same semantics as GET /contacts)"},"custom_field_value_to":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Custom Field Value To","description":"Upper bound for custom_field_op='between' (same as GET /contacts)"},"custom_field_op":{"anyOf":[{"type":"string","enum":["eq","contains","gt","lt","gte","lte","between"]},{"type":"null"}],"title":"Custom Field Op","description":"Custom-field comparison operator (same as GET /contacts)"},"list_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"List Id","description":"lists.id — filter by list subscription (same as GET /contacts). Must belong to the tenant (422 if not)."},"list_subscription_status":{"anyOf":[{"type":"string","enum":["subscribed","pending","unsubscribed","bounced","complained","any","not_subscribed"]},{"type":"null"}],"title":"List Subscription Status","description":"Subscription status to match for list_id; defaults to 'subscribed'. 'any' = a row in ANY status, 'not_subscribed' = no 'subscribed' row (includes contacts with no row at all). Requires list_id (422)."},"list_subscription_channel":{"anyOf":[{"$ref":"#/components/schemas/SubscriptionChannelType"},{"type":"null"}],"description":"Optionally narrow the list filter to one subscription channel (same as GET /contacts). Requires list_id (422)."},"is_blocked":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Is Blocked","description":"Blocked-state filter (same as GET /contacts): true = only blocked contacts, false = only unblocked, null = all."}},"type":"object","title":"ContactFilterParams","description":"Filter selection — EXACTLY the filter parameters of ``GET /contacts``.\n\nSemantics (search across name/phone/email/id/contact-method values,\nlive-chat visibility, server-side custom-field operator resolution,\nlist-subscription EXISTS/NOT EXISTS) are shared with ``GET /contacts`` via\n``_resolve_custom_field_filter`` + ``_resolve_list_subscription_filter`` +\n``ContactService.list_contacts_paginated``."},"ContactHistoryResponse":{"properties":{"items":{"items":{"oneOf":[{"$ref":"#/components/schemas/ConversationEvent"},{"$ref":"#/components/schemas/FlowRunEvent"},{"$ref":"#/components/schemas/FlowMessageEvent"},{"$ref":"#/components/schemas/MessageEvent"},{"$ref":"#/components/schemas/SchedulerEvent"},{"$ref":"#/components/schemas/TicketSummaryEvent"}],"discriminator":{"propertyName":"event_type","mapping":{"conversation":"#/components/schemas/ConversationEvent","flow_message":"#/components/schemas/FlowMessageEvent","flow_run":"#/components/schemas/FlowRunEvent","message":"#/components/schemas/MessageEvent","scheduler":"#/components/schemas/SchedulerEvent","ticket_summary":"#/components/schemas/TicketSummaryEvent"}}},"type":"array","title":"Items","description":"Merged, time-descending timeline for this page."},"next_cursor":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Next Cursor","description":"Opaque cursor for the next page. ``null`` when ``has_more`` is False."},"has_more":{"type":"boolean","title":"Has More","description":"Whether more rows exist after this page.","default":false}},"type":"object","required":["items"],"title":"ContactHistoryResponse","description":"Envelope for a single page of the contact-history timeline.\n\nThe FE renders ``items`` directly (each item is one of the three\nevent variants) and treats ``next_cursor`` / ``has_more`` exactly\nlike every other cursor-paginated endpoint in the API."},"ContactIntegrationLink":{"properties":{"provider":{"type":"string","title":"Provider","description":"Stable provider key, e.g. 'hubspot'"},"provider_label":{"type":"string","title":"Provider Label","description":"Human-readable provider name"},"external_id":{"type":"string","title":"External Id","description":"The contact's id in the external system"},"url":{"type":"string","title":"Url","description":"Deep-link to the contact's record in the provider"},"status":{"type":"string","title":"Status","description":"Link state, e.g. 'linked'. Reserved for future states.","default":"linked"},"last_synced_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Last Synced At","description":"ISO 8601 (Z) timestamp of last sync, or null"}},"type":"object","required":["provider","provider_label","external_id","url"],"title":"ContactIntegrationLink","description":"One external-system link for a contact.\n\n``provider`` is the stable machine key (``hubspot``, ``salesforce`` …);\n``provider_label`` is the human display name. ``url`` deep-links straight\nto the contact's record in that system."},"ContactIntegrationsResponse":{"properties":{"items":{"items":{"$ref":"#/components/schemas/ContactIntegrationLink"},"type":"array","title":"Items"}},"type":"object","title":"ContactIntegrationsResponse","description":"Aggregated external links for a single contact."},"ContactLinkActivityResponse":{"properties":{"contact_id":{"type":"string","title":"Contact Id"},"total_clicks":{"type":"integer","title":"Total Clicks","description":"Total number of link clicks"},"unique_links":{"type":"integer","title":"Unique Links","description":"Number of unique links"},"device_breakdown":{"additionalProperties":{"type":"integer"},"type":"object","title":"Device Breakdown","description":"Click counts by device type (desktop, mobile, tablet, other)"},"links":{"items":{"$ref":"#/components/schemas/ContactLinkItem"},"type":"array","title":"Links","description":"Links associated with this contact, with click stats"}},"type":"object","required":["contact_id","total_clicks","unique_links","device_breakdown","links"],"title":"ContactLinkActivityResponse","description":"Complete link activity for a specific contact.\n\nShows all links created FOR this contact (personalized links)\nand links clicked BY this contact, with click statistics."},"ContactLinkItem":{"properties":{"tracked_link_id":{"type":"string","title":"Tracked Link Id"},"short_code":{"type":"string","title":"Short Code"},"short_url":{"type":"string","title":"Short Url"},"original_url":{"type":"string","title":"Original Url"},"feature_type":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Feature Type"},"feature_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Feature Id"},"label":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Label"},"created_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Created At"},"expires_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Expires At"},"is_active":{"type":"boolean","title":"Is Active","default":true},"click_count":{"type":"integer","title":"Click Count","description":"Total clicks on this link","default":0},"first_click_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"First Click At","description":"Timestamp of first click"},"last_click_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Last Click At","description":"Timestamp of most recent click"},"clicks":{"items":{"$ref":"#/components/schemas/ClickDetail"},"type":"array","title":"Clicks","description":"Recent click details (max 10)"}},"type":"object","required":["tracked_link_id","short_code","short_url","original_url"],"title":"ContactLinkItem","description":"A tracked link associated with a contact"},"ContactListMembership":{"properties":{"list_id":{"type":"string","title":"List Id"},"list_name":{"type":"string","title":"List Name"},"list_type":{"type":"string","title":"List Type"},"list_slug":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"List Slug"},"list_description":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"List Description"},"channel_type":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Channel Type"},"subscription_status":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Subscription Status"},"subscribed_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Subscribed At"},"membership_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Membership Id"}},"type":"object","required":["list_id","list_name","list_type"],"title":"ContactListMembership","description":"A list/subscription the contact belongs to"},"ContactListMembershipsResponse":{"properties":{"contact_id":{"type":"string","title":"Contact Id"},"contact_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Contact Name"},"total_memberships":{"type":"integer","title":"Total Memberships"},"lists":{"items":{"$ref":"#/components/schemas/ContactListMembership"},"type":"array","title":"Lists"}},"type":"object","required":["contact_id","total_memberships","lists"],"title":"ContactListMembershipsResponse","description":"Response for contact's list memberships","example":{"contact_id":"c123","contact_name":"John Doe","lists":[{"channel_type":"email","list_id":"list1","list_name":"Newsletter","list_type":"newsletter","membership_id":"sub123","subscribed_at":"2025-01-15T10:30:00Z","subscription_status":"subscribed"}],"total_memberships":2}},"ContactListResponse":{"properties":{"items":{"items":{"$ref":"#/components/schemas/ContactResponse"},"type":"array","title":"Items","description":"List of items for this page"},"pagination":{"$ref":"#/components/schemas/PaginationMeta","description":"Pagination metadata"}},"type":"object","required":["items","pagination"],"title":"ContactListResponse","description":"Paginated list of contacts.\n\nExample response:\n```json\n{\n    \"items\": [...],\n    \"pagination\": {\n        \"total\": 150,\n        \"page\": 1,\n        \"page_size\": 20,\n        \"total_pages\": 8,\n        \"has_next\": true,\n        \"has_prev\": false\n    }\n}\n```"},"ContactMergeSuggestionConfirmRequest":{"properties":{"confirmed":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Confirmed","description":"Must be `true` to confirm the merge. The merge cannot be undone."}},"type":"object","title":"ContactMergeSuggestionConfirmRequest","description":"Explicit confirmation for an irreversible merge.\n\n``confirmed`` must be the JSON boolean ``true``. Anything else (missing,\n``false``, ``\"true\"``, ``1``) is normalised to ``None`` here and rejected by\nthe endpoint with a 422 that never echoes the submitted value."},"ContactMergeSuggestionContactRef":{"properties":{"id":{"type":"string","title":"Id","description":"Contact ID"},"name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Name","description":"Display name, or null when the contact has none"},"avatar_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Avatar Url","description":"Avatar URL, or null"}},"type":"object","required":["id"],"title":"ContactMergeSuggestionContactRef","description":"Minimal contact summary shown next to a suggestion."},"ContactMergeSuggestionListResponse":{"properties":{"items":{"items":{"$ref":"#/components/schemas/ContactMergeSuggestionResponse"},"type":"array","title":"Items","description":"Suggestions on this page"},"pagination":{"$ref":"#/components/schemas/PaginationMeta","description":"Pagination metadata"}},"type":"object","required":["items","pagination"],"title":"ContactMergeSuggestionListResponse","description":"Paginated list of contact merge suggestions."},"ContactMergeSuggestionResponse":{"properties":{"id":{"type":"string","title":"Id","description":"Suggestion ID"},"status":{"type":"string","title":"Status","description":"`pending`, `merged`, `dismissed` or `obsolete`"},"origin":{"type":"string","title":"Origin","description":"Why the suggestion exists: `live_chat_claim`, `whatsapp_phone_reveal` or `channel_switch`"},"matched_method_type":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Matched Method Type","description":"Which identifier matched, e.g. `email`, `phone` or `contact_id`"},"matched_value":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Matched Value","description":"The value that matched. Entered by the visitor and NOT verified until an agent confirms."},"conversation_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Conversation Id","description":"Conversation the suggestion was raised in (the visitor's). Null when you cannot open that conversation."},"created_at":{"type":"string","title":"Created At","description":"When the suggestion was created (UTC)"},"resolved_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Resolved At","description":"When it was confirmed or dismissed (UTC)"},"resolved_by_user_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Resolved By User Id","description":"User who confirmed or dismissed it"},"verified":{"type":"boolean","title":"Verified","description":"True only after an agent confirmed the merge (`status` is `merged`)","default":false},"source_contact":{"anyOf":[{"$ref":"#/components/schemas/ContactMergeSuggestionContactRef"},{"type":"null"}],"description":"The unverified contact that is absorbed on confirm. Null on the matched contact's side when you cannot open the visitor's conversation."},"target_contact":{"anyOf":[{"$ref":"#/components/schemas/ContactMergeSuggestionContactRef"},{"type":"null"}],"description":"The existing matched contact, which survives a confirmed merge"},"survivor_contact_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Survivor Contact Id","description":"Set once merged: the surviving contact (the target)"},"perspective":{"anyOf":[{"type":"string","enum":["source","target"]},{"type":"null"}],"title":"Perspective","description":"Which side of the suggestion the listed conversation's contact is on: `source` (the visitor who entered the details) or `target` (the existing contact whose details were entered). Set on list items; null on the confirm and dismiss responses, which have no conversation context."}},"type":"object","required":["id","status","origin","created_at"],"title":"ContactMergeSuggestionResponse","description":"A proposed merge of an unverified contact into an existing contact."},"ContactMethodCreate":{"properties":{"method_type":{"type":"string","title":"Method Type","description":"Type: phone, email, whatsapp_id, whatsapp_bsuid, telegram_id, messenger_id, instagram_id, linkedin, homepage, facebook, instagram_handle, social_other"},"value":{"type":"string","maxLength":255,"title":"Value","description":"The external identifier value"},"channel_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Channel Id","description":"Required for messenger_id, instagram_id (page-scoped) and whatsapp_bsuid (business-portfolio-scoped)"},"display_name":{"anyOf":[{"type":"string","maxLength":255},{"type":"null"}],"title":"Display Name","description":"Display name like @username or custom label for social_other"},"phone_type":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Phone Type","description":"Phone line type (only meaningful when method_type=phone). One of: mobile, fixed_line, fixed_line_or_mobile, voip, unknown. Auto-inferred from value if omitted."},"is_primary":{"type":"boolean","title":"Is Primary","description":"Set as primary for this method type","default":false},"verified":{"type":"boolean","title":"Verified","description":"Mark this identifier as verified (you have confirmed the contact controls it). Recorded with verified_source='api'. Channel-native platform IDs (telegram_id, messenger_id, instagram_id, whatsapp_bsuid) are always verified.","default":false},"duplicate_mode":{"anyOf":[{"$ref":"#/components/schemas/DuplicateMode"},{"type":"null"}],"description":"Optional per-request override for duplicate-contact handling. If omitted, the tenant's ``duplicate_contact_mode`` setting applies. Values: auto_merge, allow_duplicates, dont_allow_duplicates."}},"type":"object","required":["method_type","value"],"title":"ContactMethodCreate","description":"Schema for creating a contact method"},"ContactMethodInfo":{"properties":{"id":{"type":"string","title":"Id"},"method_type":{"type":"string","title":"Method Type"},"value":{"type":"string","title":"Value"},"display_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Display Name"},"is_primary":{"type":"boolean","title":"Is Primary","default":false}},"type":"object","required":["id","method_type","value"],"title":"ContactMethodInfo","description":"Lightweight contact method for embedding in responses"},"ContactMethodResponse":{"properties":{"id":{"type":"string","title":"Id"},"contact_id":{"type":"string","title":"Contact Id"},"method_type":{"type":"string","title":"Method Type"},"value":{"type":"string","title":"Value"},"channel_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Channel Id"},"display_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Display Name"},"phone_type":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Phone Type"},"is_primary":{"type":"boolean","title":"Is Primary","default":false},"email_status":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Email Status"},"bounce_count":{"type":"integer","title":"Bounce Count","default":0},"last_bounce_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Last Bounce At"},"last_used_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Last Used At"},"created_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Created At"},"verified":{"type":"boolean","title":"Verified","description":"True when the identifier is verified: a channel-native platform ID, an identifier the contact has messaged you from, or one marked as verified manually, via the API, by an import or a contact token.","default":false},"verified_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Verified At","description":"When the identifier was verified (UTC)."},"verified_source":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Verified Source","description":"How it was verified: channel_native, inbound, manual, import, api, token or backfill. Null when unverified."}},"type":"object","required":["id","contact_id","method_type","value"],"title":"ContactMethodResponse","description":"Response schema for a contact method"},"ContactMethodUpdate":{"properties":{"method_type":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Method Type","description":"Optional: type hint for value normalization. When updating a whatsapp_id row, pass method_type='whatsapp_id' so the new value is normalized at the schema layer. Otherwise the service layer applies normalization based on the existing row's type."},"value":{"anyOf":[{"type":"string","maxLength":500},{"type":"null"}],"title":"Value","description":"Updated identifier value"},"display_name":{"anyOf":[{"type":"string","maxLength":255},{"type":"null"}],"title":"Display Name"},"phone_type":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Phone Type","description":"Override phone line type (only meaningful when method_type=phone). One of: mobile, fixed_line, fixed_line_or_mobile, voip, unknown."},"is_primary":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Is Primary"},"verified":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Verified","description":"true marks the identifier as verified (verified_source='api'); false clears it. Omit to keep it unchanged. Changing `value` without sending verified=true resets the method to unverified."}},"type":"object","title":"ContactMethodUpdate","description":"Schema for updating a contact method"},"ContactMethodVerificationUpdate":{"properties":{"verified":{"type":"boolean","title":"Verified","description":"true marks the identifier as verified, false marks it unverified."}},"type":"object","required":["verified"],"title":"ContactMethodVerificationUpdate","description":"Body of PATCH /contacts/{contact_id}/methods/{method_id}/verification."},"ContactName":{"properties":{"formatted_name":{"type":"string","maxLength":500,"minLength":1,"title":"Formatted Name","description":"Full formatted name (REQUIRED)"},"first_name":{"anyOf":[{"type":"string","maxLength":100},{"type":"null"}],"title":"First Name","description":"First name"},"last_name":{"anyOf":[{"type":"string","maxLength":100},{"type":"null"}],"title":"Last Name","description":"Last name"},"middle_name":{"anyOf":[{"type":"string","maxLength":100},{"type":"null"}],"title":"Middle Name","description":"Middle name"},"prefix":{"anyOf":[{"type":"string","maxLength":20},{"type":"null"}],"title":"Prefix","description":"Name prefix (e.g., Dr., Mr.)"},"suffix":{"anyOf":[{"type":"string","maxLength":20},{"type":"null"}],"title":"Suffix","description":"Name suffix (e.g., Jr., PhD)"}},"type":"object","required":["formatted_name"],"title":"ContactName","description":"Contact name information"},"ContactNoteListResponse":{"properties":{"items":{"items":{"$ref":"#/components/schemas/ContactNoteResponse"},"type":"array","title":"Items","description":"List of items for this page"},"pagination":{"$ref":"#/components/schemas/PaginationMeta","description":"Pagination metadata"}},"type":"object","required":["items","pagination"],"title":"ContactNoteListResponse","description":"Paginated list of contact notes.\n\nExample response:\n```json\n{\n    \"items\": [...],\n    \"pagination\": {\n        \"total\": 10,\n        \"page\": 1,\n        \"page_size\": 20,\n        \"total_pages\": 1,\n        \"has_next\": false,\n        \"has_prev\": false\n    }\n}\n```"},"ContactNoteResponse":{"properties":{"created_at":{"type":"string","title":"Created At","description":"When the resource was created (UTC)"},"updated_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Updated At","description":"When the resource was last updated (UTC)"},"id":{"type":"string","title":"Id","description":"Unique identifier (UUID)"},"tenant_id":{"type":"string","title":"Tenant Id","description":"Tenant this resource belongs to"},"contact_id":{"type":"string","title":"Contact Id","description":"Associated contact ID"},"user_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"User Id","description":"ID of user who created the note (None for system-created notes)"},"content":{"type":"string","title":"Content","description":"Note content"},"user_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"User Name","description":"Name of user who created the note"},"user_email":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"User Email","description":"Email of user who created the note"}},"type":"object","required":["created_at","id","tenant_id","contact_id","content"],"title":"ContactNoteResponse","description":"Full note response for contact notes.\n\nSimilar to NoteResponse but with contact_id instead of conversation_id.","example":{"contact_id":"contact-uuid-789","content":"VIP customer - handle with priority.","created_at":"2025-01-15T10:30:00Z","id":"note-uuid-123","tenant_id":"tenant-uuid-456","updated_at":"2025-01-15T10:30:00Z","user_email":"jane@company.com","user_id":"user-uuid-012","user_name":"Jane Agent"}},"ContactOrg":{"properties":{"company":{"anyOf":[{"type":"string","maxLength":255},{"type":"null"}],"title":"Company","description":"Company name"},"department":{"anyOf":[{"type":"string","maxLength":255},{"type":"null"}],"title":"Department","description":"Department"},"title":{"anyOf":[{"type":"string","maxLength":255},{"type":"null"}],"title":"Title","description":"Job title"}},"type":"object","title":"ContactOrg","description":"Contact organization information"},"ContactPhone":{"properties":{"phone":{"type":"string","maxLength":25,"minLength":1,"title":"Phone","description":"Phone number in international format"},"type":{"anyOf":[{"type":"string","enum":["CELL","MAIN","IPHONE","HOME","WORK"]},{"type":"null"}],"title":"Type","description":"Phone number type"},"wa_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Wa Id","description":"WhatsApp ID if different from phone"}},"type":"object","required":["phone"],"title":"ContactPhone","description":"Contact phone number"},"ContactPinActionResponse":{"properties":{"success":{"type":"boolean","title":"Success","default":true},"id":{"type":"string","title":"Id"}},"type":"object","required":["id"],"title":"ContactPinActionResponse"},"ContactPinSet":{"properties":{"pin":{"type":"string","maxLength":12,"minLength":4,"title":"Pin","description":"4 to 12 digits"}},"additionalProperties":false,"type":"object","required":["pin"],"title":"ContactPinSet"},"ContactPinStatus":{"properties":{"is_set":{"type":"boolean","title":"Is Set"},"locked":{"type":"boolean","title":"Locked","description":"True while PIN checks are refused (temporary lock or escalated)"},"locked_until":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Locked Until","description":"When a temporary lock ends automatically (null when not locked or escalated)"},"escalated":{"type":"boolean","title":"Escalated","description":"Locked 3 times within 24 hours; stays locked until reset","default":false},"set_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Set At"}},"type":"object","required":["is_set","locked"],"title":"ContactPinStatus"},"ContactResponse":{"properties":{"name":{"anyOf":[{"type":"string","maxLength":255},{"type":"null"}],"title":"Name","description":"Contact's full name"},"first_name":{"anyOf":[{"type":"string","maxLength":100},{"type":"null"}],"title":"First Name","description":"Contact's first/given name"},"last_name":{"anyOf":[{"type":"string","maxLength":100},{"type":"null"}],"title":"Last Name","description":"Contact's last/family name"},"phone":{"anyOf":[{"type":"string","maxLength":50},{"type":"null"}],"title":"Phone","description":"Phone number with country code (E.164, e.g. +491754633940)"},"email":{"anyOf":[{"type":"string","format":"email"},{"type":"null"}],"title":"Email","description":"Email address"},"created_at":{"type":"string","title":"Created At","description":"When the resource was created (UTC)"},"updated_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Updated At","description":"When the resource was last updated (UTC)"},"id":{"type":"string","title":"Id","description":"Unique identifier (UUID)"},"tenant_id":{"type":"string","title":"Tenant Id","description":"Tenant this resource belongs to"},"avatar_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Avatar Url"},"avatar_is_custom":{"type":"boolean","title":"Avatar Is Custom","default":false},"language":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Language"},"languages":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Languages"},"birthday":{"anyOf":[{"type":"string","format":"date"},{"type":"null"}],"title":"Birthday"},"is_blocked":{"type":"boolean","title":"Is Blocked","default":false},"is_archived":{"type":"boolean","title":"Is Archived","default":false},"blocked_inbound_count":{"type":"integer","title":"Blocked Inbound Count","description":"Number of incoming messages dropped because the contact is blocked.","default":0},"last_blocked_inbound_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Last Blocked Inbound At","description":"When the most recent incoming message was dropped because of the block."},"source":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Source"},"last_contacted_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Last Contacted At"},"custom_fields":{"anyOf":[{"additionalProperties":{"$ref":"#/components/schemas/CustomFieldValue"},"type":"object"},{"type":"null"}],"title":"Custom Fields"},"tags":{"anyOf":[{"items":{"$ref":"#/components/schemas/TagInfo"},"type":"array"},{"type":"null"}],"title":"Tags"},"contact_methods":{"anyOf":[{"items":{"$ref":"#/components/schemas/ContactMethodResponse"},"type":"array"},{"type":"null"}],"title":"Contact Methods"},"subscriptions":{"anyOf":[{"items":{"$ref":"#/components/schemas/ContactSubscriptionSummary"},"type":"array"},{"type":"null"}],"title":"Subscriptions","description":"Active list subscriptions (status subscribed/pending). Only populated on GET /contacts with include=subscriptions; [] when the contact has none. Null otherwise."},"conversation_count":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Conversation Count","description":"Number of conversations with this contact. Only populated on GET /contacts with include=conversation_stats (0 when none)."},"last_conversation_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Last Conversation At","description":"Most recent conversation activity (MAX conversations.last_message_at). Only populated on GET /contacts with include=conversation_stats; null when the contact has no conversations."},"potential_duplicates":{"anyOf":[{"items":{"$ref":"#/components/schemas/DuplicateRef"},"type":"array"},{"type":"null"}],"title":"Potential Duplicates","description":"Populated when the request used allow_duplicates mode and a duplicate was detected. Empty/null otherwise."},"merged_from":{"anyOf":[{"$ref":"#/components/schemas/MergedFromInfo"},{"type":"null"}],"description":"Set when the requested ID was an old/merged UUID and we resolved it to the surviving contact via contact_aliases."}},"type":"object","required":["created_at","id","tenant_id"],"title":"ContactResponse","description":"Full contact response with all fields.\n\nChannel identifiers are now in the `contact_methods` list.\nIncludes custom fields and tags.","example":{"contact_methods":[{"id":"cm-123","is_primary":true,"method_type":"whatsapp_id","value":"1234567890"},{"id":"cm-456","is_primary":true,"method_type":"email","value":"john@example.com"}],"created_at":"2025-01-15T10:30:00Z","email":"john@example.com","id":"c1a2b3c4-d5e6-f7g8-h9i0-j1k2l3m4n5o6","is_blocked":false,"name":"John Doe","phone":"+1234567890","tags":[{"color":"#FFD700","id":"tag1","name":"VIP"}],"tenant_id":"t1a2b3c4-d5e6-f7g8-h9i0-j1k2l3m4n5o6","updated_at":"2025-01-15T14:45:00Z"}},"ContactSubscriptionSummary":{"properties":{"list_id":{"type":"string","title":"List Id"},"list_name":{"type":"string","title":"List Name"},"channel_type":{"type":"string","title":"Channel Type"},"status":{"type":"string","title":"Status","description":"'subscribed' or 'pending'"}},"type":"object","required":["list_id","list_name","channel_type","status"],"title":"ContactSubscriptionSummary","description":"Compact active list-subscription row embedded in GET /contacts items\nwhen ``include=subscriptions`` is requested. Only ``subscribed`` /\n``pending`` rows are ever surfaced."},"ContactSubscriptionsResponse":{"properties":{"contact_id":{"type":"string","title":"Contact Id"},"total":{"type":"integer","title":"Total"},"subscriptions":{"items":{"$ref":"#/components/schemas/SubscriptionResponse"},"type":"array","title":"Subscriptions"}},"type":"object","required":["contact_id","total","subscriptions"],"title":"ContactSubscriptionsResponse","description":"Schema for contact's subscriptions","example":{"contact_id":"contact-123","subscriptions":[],"total":5}},"ContactSummary":{"properties":{"id":{"type":"string","title":"Id"},"tenant_id":{"type":"string","title":"Tenant Id"},"name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Name"},"first_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"First Name"},"last_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Last Name"},"phone":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Phone"},"email":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Email"},"created_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Created At"},"methods":{"items":{"$ref":"#/components/schemas/DuplicateRefMethod"},"type":"array","title":"Methods"}},"type":"object","required":["id","tenant_id"],"title":"ContactSummary","description":"Compact contact representation for list contexts (duplicate groups)."},"ContactTokenContact":{"properties":{"contact_id":{"anyOf":[{"type":"string","maxLength":36},{"type":"null"}],"title":"Contact Id","description":"Existing contact id"},"external_id":{"anyOf":[{"type":"string","maxLength":255},{"type":"null"}],"title":"External Id","description":"Your user id. Mapped to the contact once and reused (same mapping as signed widget identity)."},"methods":{"anyOf":[{"items":{"$ref":"#/components/schemas/ContactTokenMethod"},"type":"array","maxItems":10},{"type":"null"}],"title":"Methods","description":"Contact methods (email / phone / whatsapp), each verified or a claim. Max 10."},"on_conflict":{"anyOf":[{"type":"string","maxLength":10},{"type":"null"}],"title":"On Conflict","description":"`error` (default): 409 `identifier_conflict` when a method belongs to another contact. `merge`: merge those contacts into this one (verified methods only; requires `contacts:update`)."},"email":{"anyOf":[{"type":"string","maxLength":320},{"type":"null"}],"title":"Email","description":"Legacy: email for this user (use `methods`)"},"phone":{"anyOf":[{"type":"string","maxLength":64},{"type":"null"}],"title":"Phone","description":"Legacy: phone number, E.164 preferred (use `methods`)"},"name":{"anyOf":[{"type":"string","maxLength":255},{"type":"null"}],"title":"Name","description":"Display name, used only when a contact is created"},"create_if_missing":{"type":"boolean","title":"Create If Missing","description":"Create the contact when nothing matches (requires `contacts:create`)","default":false}},"type":"object","title":"ContactTokenContact","description":"Who the token is for: ``contact_id``, or identifiers to resolve / create."},"ContactTokenCreate":{"properties":{"widget_id":{"type":"string","maxLength":36,"minLength":1,"title":"Widget Id","description":"Live chat widget the token is redeemable on"},"contact":{"$ref":"#/components/schemas/ContactTokenContact"},"expose":{"$ref":"#/components/schemas/ContactTokenExpose"},"context":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Context","description":"Any JSON object (max 4 KB serialized, 30 keys) shared with the chat as verified context. Never stored on the contact."},"save":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Save","description":"Fields written to the contact when the token is created (replaces the value). Keys: name, first_name, last_name, language, or active custom field keys. Requires `contacts:update` (or `contacts:create` for a contact created by this call)."},"save_if_empty":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Save If Empty","description":"Like `save`, but written only where the field is empty."},"ttl_seconds":{"type":"integer","title":"Ttl Seconds","description":"Redeem window in seconds (30-3600, default 600)","default":600},"grant_ttl_seconds":{"type":"integer","title":"Grant Ttl Seconds","description":"How long the context stays valid in the chat session after redeem (60-604800, default 86400)","default":86400},"max_redemptions":{"type":"integer","title":"Max Redemptions","description":"How often the token may be redeemed (page reloads, tabs), 1-50","default":10}},"type":"object","required":["widget_id","contact"],"title":"ContactTokenCreate","example":{"contact":{"create_if_missing":true,"external_id":"crm-123","methods":[{"type":"email","value":"anna@example.com","verified":true},{"type":"whatsapp","value":"+4917012345678","verified":false}],"name":"Anna Weber","on_conflict":"error"},"context":{"open_orders":2},"save":{"plan":"Scale"},"save_if_empty":{"company":"Acme GmbH"},"ttl_seconds":600,"widget_id":"3f6c1d2e-8a1b-4c5d-9e0f-123456789abc"}},"ContactTokenExpose":{"properties":{"standard_fields":{"items":{"type":"string"},"type":"array","maxItems":20,"title":"Standard Fields","description":"Standard fields shared with the chat: name, first_name, last_name, email, phone, language, country, timezone, company, created_at, channel"},"custom_fields":{"items":{"type":"string"},"type":"array","maxItems":50,"title":"Custom Fields","description":"Custom field keys shared with the chat (values are read when the token is redeemed)"}},"type":"object","title":"ContactTokenExpose"},"ContactTokenMethod":{"properties":{"type":{"type":"string","maxLength":20,"title":"Type","description":"`email`, `phone` or `whatsapp`"},"value":{"type":"string","maxLength":320,"minLength":1,"title":"Value","description":"The address / number (WhatsApp with or without a leading +)"},"verified":{"type":"boolean","title":"Verified","description":"true: you have verified this method; it is used to find the contact and stored as verified. false (default): attached as an unverified claim and never used to find the contact.","default":false}},"type":"object","required":["type","value"],"title":"ContactTokenMethod","description":"A contact method sent with the token (write-back model)."},"ContactTokenResponse":{"properties":{"id":{"type":"string","title":"Id","description":"Token id (use it to revoke)"},"token":{"type":"string","title":"Token","description":"The token. Returned ONLY in this response; pass it to the widget as `contactToken`."},"token_prefix":{"type":"string","title":"Token Prefix","description":"First characters of the token, for display/debugging"},"widget_id":{"type":"string","title":"Widget Id"},"contact_id":{"type":"string","title":"Contact Id"},"expires_at":{"type":"string","format":"date-time","title":"Expires At","description":"Redeem deadline"},"grant_expires_in":{"type":"integer","title":"Grant Expires In","description":"Seconds the context stays valid in a chat session after redeem"},"max_redemptions":{"type":"integer","title":"Max Redemptions"},"created":{"type":"boolean","title":"Created","description":"True when this call created the contact","default":false},"merged_contact_ids":{"items":{"type":"string"},"type":"array","title":"Merged Contact Ids","description":"Contacts merged into this one (`on_conflict: merge`)"},"skipped":{"items":{"additionalProperties":true,"type":"object"},"type":"array","title":"Skipped","description":"Methods not attached, e.g. `{type, value, reason: \"unverified_conflict\"}`"}},"type":"object","required":["id","token","token_prefix","widget_id","contact_id","expires_at","grant_expires_in","max_redemptions"],"title":"ContactTokenResponse"},"ContactTokenRevokeResponse":{"properties":{"success":{"type":"boolean","title":"Success","default":true},"id":{"type":"string","title":"Id","description":"Contact id"},"revoked":{"type":"integer","title":"Revoked","description":"Number of tokens revoked"}},"type":"object","required":["id","revoked"],"title":"ContactTokenRevokeResponse"},"ContactUpdate":{"properties":{"name":{"anyOf":[{"type":"string","maxLength":255},{"type":"null"}],"title":"Name"},"first_name":{"anyOf":[{"type":"string","maxLength":100},{"type":"null"}],"title":"First Name","description":"Contact's first/given name"},"last_name":{"anyOf":[{"type":"string","maxLength":100},{"type":"null"}],"title":"Last Name","description":"Contact's last/family name"},"phone":{"anyOf":[{"type":"string","maxLength":50},{"type":"null"}],"title":"Phone","description":"Phone number with country code (E.164, e.g. +491754633940)"},"email":{"anyOf":[{"type":"string","format":"email"},{"type":"null"}],"title":"Email"},"avatar_url":{"anyOf":[{"type":"string","maxLength":2048},{"type":"null"}],"title":"Avatar Url"},"languages":{"anyOf":[{"type":"string","maxLength":255},{"type":"null"}],"title":"Languages","description":"Comma-separated ISO 639-1 language codes (e.g. 'en,de,fr')."},"birthday":{"anyOf":[{"type":"string","format":"date"},{"type":"null"}],"title":"Birthday","description":"Contact's date of birth (ISO 8601 YYYY-MM-DD)"},"custom_fields":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Custom Fields","description":"Custom field values to MERGE, keyed by custom field definition ID (preferred) or field `key`. Only the given fields are touched; null / \"\" / [] clears that field. Omitting `custom_fields` (or sending null for the whole map) changes nothing. Validated per field_type; any invalid entry rejects the whole request with 422 and nothing is written."}},"type":"object","title":"ContactUpdate","description":"Schema for updating a contact (all fields optional)","example":{"birthday":"1990-04-23","email":"john.smith@example.com","languages":"en,de","name":"John Smith"}},"ContactUrl":{"properties":{"url":{"type":"string","maxLength":2000,"title":"Url","description":"URL"},"type":{"anyOf":[{"type":"string","enum":["HOME","WORK"]},{"type":"null"}],"title":"Type","description":"URL type"}},"type":"object","required":["url"],"title":"ContactUrl","description":"Contact URL/website"},"ContextEndpointCreate":{"properties":{"name":{"type":"string","maxLength":100,"minLength":1,"title":"Name","description":"Display name"},"url":{"type":"string","maxLength":2048,"minLength":8,"title":"Url","description":"HTTPS URL that receives the signed POST"},"timeout_ms":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Timeout Ms","description":"Request timeout in ms, clamped to 200..3000 (default 1500)"},"ttl_seconds":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Ttl Seconds","description":"Cache lifetime per contact in seconds, clamped to 0..3600 (default 300, 0 = no cache)"},"response_map":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Response Map","description":"Up to 30 entries `{\"out_key\": {\"path\": \"a.b.c\", \"label\": \"Plan\"}}`. Only mapped values reach the agent."},"is_enabled":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Is Enabled","description":"Disabled endpoints are never called","default":true}},"type":"object","required":["name","url"],"title":"ContextEndpointCreate"},"ContextEndpointListResponse":{"properties":{"items":{"items":{"$ref":"#/components/schemas/ContextEndpointResponse"},"type":"array","title":"Items"},"pagination":{"$ref":"#/components/schemas/PaginationMeta"}},"type":"object","required":["items","pagination"],"title":"ContextEndpointListResponse"},"ContextEndpointResponse":{"properties":{"id":{"type":"string","title":"Id"},"name":{"type":"string","title":"Name"},"url":{"type":"string","title":"Url"},"timeout_ms":{"type":"integer","title":"Timeout Ms"},"ttl_seconds":{"type":"integer","title":"Ttl Seconds"},"response_map":{"additionalProperties":true,"type":"object","title":"Response Map"},"is_enabled":{"type":"boolean","title":"Is Enabled"},"has_secret":{"type":"boolean","title":"Has Secret","description":"Whether a signing secret is stored and readable"},"secret_hint":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Secret Hint","description":"Last 4 characters of the signing secret"},"created_by":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Created By"},"created_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Created At"},"updated_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Updated At"}},"type":"object","required":["id","name","url","timeout_ms","ttl_seconds","is_enabled","has_secret"],"title":"ContextEndpointResponse"},"ContextEndpointTestRequest":{"properties":{"contact_id":{"type":"string","maxLength":36,"minLength":1,"title":"Contact Id","description":"Contact whose identifiers are sent"}},"type":"object","required":["contact_id"],"title":"ContextEndpointTestRequest"},"ContextEndpointTestResponse":{"properties":{"ok":{"type":"boolean","title":"Ok"},"error_code":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Error Code"},"data":{"additionalProperties":true,"type":"object","title":"Data","description":"Values after the response map (what the agent would see)"},"request_payload":{"additionalProperties":true,"type":"object","title":"Request Payload","description":"The JSON body that was sent"}},"type":"object","required":["ok"],"title":"ContextEndpointTestResponse"},"ContextEndpointUpdate":{"properties":{"name":{"anyOf":[{"type":"string","maxLength":100,"minLength":1},{"type":"null"}],"title":"Name"},"url":{"anyOf":[{"type":"string","maxLength":2048,"minLength":8},{"type":"null"}],"title":"Url"},"timeout_ms":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Timeout Ms"},"ttl_seconds":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Ttl Seconds"},"response_map":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Response Map"},"is_enabled":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Is Enabled"}},"type":"object","title":"ContextEndpointUpdate","description":"Partial update; omitted fields are kept. ``response_map: null`` clears it.\n\nChanging ``url`` needs ``contacts:read`` too and rotates the signing secret\n(the response then carries the new ``secret``)."},"ContextEndpointWithSecretResponse":{"properties":{"id":{"type":"string","title":"Id"},"name":{"type":"string","title":"Name"},"url":{"type":"string","title":"Url"},"timeout_ms":{"type":"integer","title":"Timeout Ms"},"ttl_seconds":{"type":"integer","title":"Ttl Seconds"},"response_map":{"additionalProperties":true,"type":"object","title":"Response Map"},"is_enabled":{"type":"boolean","title":"Is Enabled"},"has_secret":{"type":"boolean","title":"Has Secret","description":"Whether a signing secret is stored and readable"},"secret_hint":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Secret Hint","description":"Last 4 characters of the signing secret"},"created_by":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Created By"},"created_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Created At"},"updated_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Updated At"},"secret":{"type":"string","title":"Secret","description":"Signing secret (shown only once)"}},"type":"object","required":["id","name","url","timeout_ms","ttl_seconds","is_enabled","has_secret","secret"],"title":"ContextEndpointWithSecretResponse","description":"Create / rotate-secret / URL-changing update only. Store the secret on your server; it is not shown again."},"ConversationAssignmentBreakdown":{"properties":{"assigned":{"type":"integer","title":"Assigned","description":"Number of assigned conversations"},"unassigned":{"type":"integer","title":"Unassigned","description":"Number of unassigned conversations"}},"type":"object","required":["assigned","unassigned"],"title":"ConversationAssignmentBreakdown","description":"Conversation counts by assignment status"},"ConversationCreate":{"properties":{"contact_id":{"type":"string","title":"Contact Id","description":"Contact UUID"},"channel_id":{"type":"string","title":"Channel Id","description":"Channel UUID"},"subject":{"anyOf":[{"type":"string","maxLength":500},{"type":"null"}],"title":"Subject","description":"Conversation subject"},"contact_method_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Contact Method Id","description":"Optional. Bind the conversation to a SPECIFIC contact method (e.g. one of the contact's several email addresses / phone numbers) instead of resolving the address via the channel's primary method. Must belong to the same contact + tenant and be compatible with the channel's method type (a PHONE method is also accepted for a WhatsApp channel). When omitted, behavior is unchanged: the address is resolved via the channel's primary method downstream."}},"type":"object","required":["contact_id","channel_id"],"title":"ConversationCreate","description":"Schema for creating a conversation","example":{"channel_id":"ch1a2b3c4-d5e6-f7g8-h9i0-j1k2l3m4n5o6","contact_id":"c1a2b3c4-d5e6-f7g8-h9i0-j1k2l3m4n5o6","contact_method_id":"cm1a2b3c4-d5e6-f7g8-h9i0-j1k2l3m4n5o6","subject":"Support request"}},"ConversationDraftMarker":{"properties":{"snippet":{"type":"string","title":"Snippet","description":"Plain text, first ~140 chars, whitespace collapsed, HTML stripped","default":""},"mode":{"type":"string","title":"Mode","description":"'reply' or 'note'","default":"reply"},"has_attachments":{"type":"boolean","title":"Has Attachments","default":false},"updated_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Updated At"}},"type":"object","title":"ConversationDraftMarker","description":"Compact ``my_draft`` marker on ConversationResponse (list + detail)."},"ConversationEmailSummary":{"properties":{"conversation_id":{"type":"string","title":"Conversation Id"},"subject":{"type":"string","title":"Subject","default":""},"participant_email":{"type":"string","title":"Participant Email","default":""},"participant_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Participant Name"},"integration_email":{"type":"string","title":"Integration Email","default":""},"thread_count":{"type":"integer","title":"Thread Count","default":0},"total_attachments":{"type":"integer","title":"Total Attachments","default":0},"first_message_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"First Message At"},"last_message_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Last Message At"},"participants":{"items":{"$ref":"#/components/schemas/EmailAddress"},"type":"array","title":"Participants"},"cc_participants":{"items":{"$ref":"#/components/schemas/EmailAddress"},"type":"array","title":"Cc Participants"},"labels":{"items":{"type":"string"},"type":"array","title":"Labels"},"is_unread":{"type":"boolean","title":"Is Unread","default":false}},"type":"object","required":["conversation_id"],"title":"ConversationEmailSummary","description":"Summary of email information for conversation sidebar"},"ConversationEvent":{"properties":{"event_type":{"type":"string","const":"conversation","title":"Event Type","default":"conversation"},"id":{"type":"string","title":"Id","description":"Stable client-side key. Matches ``conversation_id``."},"occurred_at":{"type":"string","title":"Occurred At","description":"Chronological anchor (``conversations.created_at``). Used as the merge sort key."},"conversation_id":{"type":"string","title":"Conversation Id"},"subject":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Subject"},"status":{"$ref":"#/components/schemas/ConversationStatus"},"channel":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Channel","description":"Platform identifier of the channel (whatsapp, telegram, email, ...)."},"opened_at":{"type":"string","title":"Opened At","description":"When the conversation was opened (mirrors ``occurred_at``)."},"closed_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Closed At"},"last_message_at":{"type":"string","title":"Last Message At"},"last_message_preview":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Last Message Preview","description":"Plain-text preview of the most recent message (truncated to 140 chars)."},"unread_count":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Unread Count"},"assigned_user_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Assigned User Id"}},"type":"object","required":["id","occurred_at","conversation_id","status","opened_at","last_message_at"],"title":"ConversationEvent","description":"Timeline row representing a single conversation."},"ConversationListResponse":{"properties":{"items":{"items":{"$ref":"#/components/schemas/ConversationResponse"},"type":"array","title":"Items","description":"List of items for this page"},"pagination":{"$ref":"#/components/schemas/PaginationMeta","description":"Pagination metadata"}},"type":"object","required":["items","pagination"],"title":"ConversationListResponse","description":"Paginated list of conversations.\n\nExample response:\n```json\n{\n    \"items\": [...],\n    \"pagination\": {\n        \"total\": 150,\n        \"page\": 1,\n        \"page_size\": 20,\n        \"total_pages\": 8,\n        \"has_next\": true,\n        \"has_prev\": false\n    }\n}\n```"},"ConversationMergeRequest":{"properties":{"source_conversation_id":{"type":"string","title":"Source Conversation Id","description":"ID of the conversation to merge in and delete"}},"type":"object","required":["source_conversation_id"],"title":"ConversationMergeRequest","description":"Request body for merging a source conversation into this one.\n\nThe path conversation (``previous_id``) survives and is reopened;\nthe ``source_conversation_id`` is merged in and then deleted."},"ConversationResponse":{"properties":{"created_at":{"type":"string","title":"Created At","description":"When the resource was created (UTC)"},"updated_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Updated At","description":"When the resource was last updated (UTC)"},"id":{"type":"string","title":"Id","description":"Unique identifier (UUID)"},"tenant_id":{"type":"string","title":"Tenant Id","description":"Tenant this resource belongs to"},"contact_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Contact Id","description":"Contact UUID (can be None for some types)"},"channel_id":{"type":"string","title":"Channel Id","description":"Channel UUID"},"channel_type":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Channel Type","description":"Channel type: email, telegram, whatsapp, live_chat, etc."},"contact_method_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Contact Method Id","description":"Contact method used when conversation was created"},"active_contact_method_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Active Contact Method Id","description":"Current active contact method for replies"},"contact_method":{"anyOf":[{"$ref":"#/components/schemas/ContactMethodInfo"},{"type":"null"}],"description":"Contact method used when conversation was created"},"active_contact_method":{"anyOf":[{"$ref":"#/components/schemas/ContactMethodInfo"},{"type":"null"}],"description":"Current active contact method for replies"},"assigned_user_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Assigned User Id"},"assigned_user":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Assigned User","description":"Assigned user details (id, name, email, chat_nickname)"},"status":{"$ref":"#/components/schemas/ConversationStatus"},"subject":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Subject"},"last_message_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Last Message At"},"last_message":{"anyOf":[{"$ref":"#/components/schemas/LastMessagePreview"},{"type":"null"}]},"contact":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Contact","description":"Contact details"},"is_live_chat":{"type":"boolean","title":"Is Live Chat","description":"Is this from a live chat session","default":false},"is_email":{"type":"boolean","title":"Is Email","description":"Is this an email conversation","default":false},"needs_reply":{"type":"boolean","title":"Needs Reply","description":"True when last message is from customer","default":true},"last_customer_message_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Last Customer Message At"},"last_agent_reply_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Last Agent Reply At"},"tags":{"anyOf":[{"items":{"$ref":"#/components/schemas/TagInfo"},"type":"array"},{"type":"null"}],"title":"Tags"},"live_chat_session_status":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Live Chat Session Status","description":"Live chat session status: waiting, active, closed"},"live_chat_last_activity_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Live Chat Last Activity At","description":"Last activity timestamp from the live chat session"},"visitor_online":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Visitor Online","description":"Whether the live chat visitor is currently connected via WebSocket"},"visitor_widget_state":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Visitor Widget State","description":"Widget panel state: 'open' or 'minimized'"},"visitor_tab_state":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Visitor Tab State","description":"Browser tab visibility: 'visible' or 'hidden'"},"visitor_focused":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Visitor Focused","description":"Whether the chat input is currently focused (real-time only, may be null)"},"visitor_last_visible_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Visitor Last Visible At","description":"Last time visitor's tab was visible"},"visitor_last_focus_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Visitor Last Focus At","description":"Last time visitor focused the chat input"},"bot_session_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Bot Session Id","description":"Active bot session ID if bot is handling this conversation"},"active_flow_run":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Active Flow Run","description":"Currently active flow run on this conversation. Shape: ``{run_id, flow_id, flow_name, trigger_kind, trigger_class, started_at, current_node_id, current_node_display_name}``. ``trigger_kind`` is the raw flow trigger type (e.g. ``incoming_message``/``manual``/``schedule``) and ``trigger_class`` is the derived ``\"inbound\" | \"background\"`` bucket (background = manual/api_event/schedule); both are null when the trigger can't be resolved (frontend then fails open). None when no flow run is active for the contact+conversation."},"snoozed_until":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Snoozed Until","description":"When the snooze expires (timezone-aware ISO, 'Z' suffix). NULL when not snoozed. Conversation is currently snoozed when status=='open' AND this is in the future."},"snooze_reopen_on_message":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Snooze Reopen On Message","description":"Per-snooze choice: auto-reopen the conversation to the Open tab when an inbound customer message arrives while snoozed. Only meaningful while snoozed_until is set."},"closed_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Closed At","description":"When conversation was closed"},"closed_by_user_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Closed By User Id","description":"User who closed the conversation"},"ai_summary":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Ai Summary","description":"AI-generated conversation summary"},"user_intent":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"User Intent","description":"Why did the user contact us?"},"resolution_summary":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Resolution Summary","description":"How was it resolved?"},"sentiment_analysis":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Sentiment Analysis","description":"Customer sentiment progression"},"ai_tags":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Ai Tags","description":"AI-generated tags/categories"},"summarized_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Summarized At","description":"When AI summary was generated"},"my_draft":{"anyOf":[{"$ref":"#/components/schemas/ConversationDraftMarker"},{"type":"null"}],"description":"The requesting user's unsent draft: {snippet, mode, has_attachments, updated_at}; null when none."}},"type":"object","required":["created_at","id","tenant_id","channel_id","status"],"title":"ConversationResponse","description":"Full conversation response with all fields.\n\nIncludes contact info, assigned user, last message preview, and tags.","example":{"channel_id":"channel012","channel_type":"whatsapp","contact":{"id":"contact789","name":"John Doe","phone":"+1234567890"},"contact_id":"contact789","created_at":"2025-01-15T10:30:00Z","id":"conv123","is_email":false,"is_live_chat":false,"last_message":{"created_at":"2025-01-15T14:45:00Z","direction":"inbound","message_type":"text","text":"Hello, I need help"},"last_message_at":"2025-01-15T14:45:00Z","needs_reply":true,"status":"open","tags":[{"color":"#FFD700","id":"tag1","name":"VIP"}],"tenant_id":"tenant456","updated_at":"2025-01-15T14:45:00Z"}},"ConversationStatsResponse":{"properties":{"by_status":{"$ref":"#/components/schemas/ConversationStatusBreakdown","description":"PERIOD view: current status of conversations CREATED in the date range. For a live workload snapshot use current_by_status."},"current_by_status":{"$ref":"#/components/schemas/CurrentWorkloadBreakdown","description":"LIVE snapshot of conversation counts by current status, with the same visibility rules as the Conversations inbox (no date filter)"},"by_assignment":{"$ref":"#/components/schemas/ConversationAssignmentBreakdown"},"new_conversations":{"type":"integer","title":"New Conversations","description":"Number of conversations created in the date range","default":0},"resolved_conversations":{"type":"integer","title":"Resolved Conversations","description":"Number of conversations with RESOLVED status","default":0},"closed_conversations":{"type":"integer","title":"Closed Conversations","description":"Number of conversations with CLOSED status","default":0},"avg_duration_hours":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Avg Duration Hours","description":"Average conversation duration in hours (from creation to close)"},"avg_messages_per_conversation":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Avg Messages Per Conversation","description":"Average number of messages per conversation"},"period_start":{"type":"string","title":"Period Start","description":"Start of the reporting period (ISO format)"},"period_end":{"type":"string","title":"Period End","description":"End of the reporting period (ISO format)"}},"type":"object","required":["by_status","by_assignment","period_start","period_end"],"title":"ConversationStatsResponse","description":"Conversation statistics by status and assignment with derived insights.\n\nReturned by GET /api/v1/analytics/conversations/stats","example":{"avg_duration_hours":4.5,"avg_messages_per_conversation":8.2,"by_assignment":{"assigned":120,"unassigned":30},"by_status":{"closed":105,"open":45},"closed_conversations":105,"new_conversations":150,"period_end":"2026-01-18T00:00:00Z","period_start":"2026-01-01T00:00:00Z","resolved_conversations":35}},"ConversationStatus":{"type":"string","enum":["open","assigned","resolved","closed"],"title":"ConversationStatus","description":"Conversation status"},"ConversationStatusBreakdown":{"properties":{"open":{"type":"integer","title":"Open","description":"Number of open conversations"},"assigned":{"type":"integer","title":"Assigned","description":"Number of assigned conversations","default":0},"resolved":{"type":"integer","title":"Resolved","description":"Number of resolved conversations","default":0},"closed":{"type":"integer","title":"Closed","description":"Number of closed conversations"}},"type":"object","required":["open","closed"],"title":"ConversationStatusBreakdown","description":"Conversation counts by status"},"ConversationSummaryDetail":{"properties":{"conversation_id":{"type":"string","title":"Conversation Id","description":"Conversation the summary belongs to (echoes the path param)."},"user_intent":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"User Intent","description":"What the customer wanted — ``Conversation.user_intent``. NULL if not yet summarized."},"summary":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Summary","description":"Summary body — ``Conversation.ai_summary``. NULL if not yet summarized."},"resolution":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Resolution","description":"How it was resolved — ``Conversation.resolution_summary``. NULL if not yet summarized."},"summarized_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Summarized At","description":"When the summary was generated (``Conversation.summarized_at``). NULL if never summarized."}},"type":"object","required":["conversation_id"],"title":"ConversationSummaryDetail","description":"Full AI ticket summary for a single conversation.\n\nLazy-loaded by the contact-page timeline when a ``ticket_summary`` row\nis expanded — the timeline list itself only carries a 140-char\n``summary_preview`` (see\n:class:`app.schemas.contact_history.TicketSummaryEvent`) to keep the\nlist payload small. The three text sections are read directly off the\n``Conversation`` SHARD row (``user_intent`` / ``ai_summary`` /\n``resolution_summary``) written by\n:class:`app.services.conversation_summarization_service.ConversationSummarizationService`."},"ConversationUpdate":{"properties":{"status":{"anyOf":[{"$ref":"#/components/schemas/ConversationStatus"},{"type":"null"}]},"assigned_user_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Assigned User Id"},"subject":{"anyOf":[{"type":"string","maxLength":500},{"type":"null"}],"title":"Subject"},"notes":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Notes"}},"type":"object","title":"ConversationUpdate","description":"Schema for updating a conversation (all fields optional)","example":{"assigned_user_id":"u1a2b3c4-d5e6-f7g8-h9i0-j1k2l3m4n5o6","status":"assigned"}},"CostEstimateResponse":{"properties":{"recipient_count":{"type":"integer","title":"Recipient Count"},"cost_per_email":{"type":"number","title":"Cost Per Email","description":"Cost per email in cents"},"total_cost":{"type":"integer","title":"Total Cost","description":"Total cost in cents"},"total_cost_eur":{"type":"string","title":"Total Cost Eur","description":"Total cost formatted in EUR"},"current_balance":{"type":"integer","title":"Current Balance","description":"Current balance in cents"},"current_balance_eur":{"type":"string","title":"Current Balance Eur","description":"Current balance formatted in EUR"},"sufficient_balance":{"type":"boolean","title":"Sufficient Balance","description":"Whether balance is sufficient"},"cost_category":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Cost Category","description":"'email_campaign_managed' or 'email_campaign_byok'"},"sending_type":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Sending Type","description":"'managed' or 'byok'"},"unit_price_eur":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Unit Price Eur","description":"Price per email in EUR, 6 decimals"},"total_cost_eur_precise":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Total Cost Eur Precise","description":"Total in EUR, 6 decimals"},"plan_included_messages":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Plan Included Messages","description":"Monthly included messages of the plan (shared pool)"}},"type":"object","required":["recipient_count","cost_per_email","total_cost","total_cost_eur","current_balance","current_balance_eur","sufficient_balance"],"title":"CostEstimateResponse","description":"Response for campaign cost estimation","example":{"cost_per_email":0.08,"current_balance":5000,"current_balance_eur":"50.00","recipient_count":1000,"sufficient_balance":true,"total_cost":80,"total_cost_eur":"0.80"}},"CrawlDomainSummary":{"properties":{"id":{"type":"string","title":"Id","description":"History record ID"},"domain":{"type":"string","title":"Domain","description":"Domain name (e.g., docs.example.com)"},"base_url":{"type":"string","title":"Base Url","description":"Full base URL used for crawl"},"page_count":{"type":"integer","title":"Page Count","description":"Number of pages in stored list"},"crawled_at":{"type":"string","title":"Crawled At","description":"ISO datetime when crawl completed"},"days_since_crawl":{"type":"integer","title":"Days Since Crawl","description":"Days since last crawl","default":0},"is_eligible_for_recrawl":{"type":"boolean","title":"Is Eligible For Recrawl","description":"Whether this domain is eligible for bulk re-crawl (must be 14+ days old)","default":true}},"type":"object","required":["id","domain","base_url","page_count","crawled_at"],"title":"CrawlDomainSummary","description":"Summary of a previously crawled domain for re-crawl selection.","example":{"base_url":"https://docs.example.com","crawled_at":"2025-01-15T10:35:00Z","days_since_crawl":15,"domain":"docs.example.com","id":"hist-uuid-123","is_eligible_for_recrawl":true,"page_count":47}},"CrawlJobStatusResponse":{"properties":{"job_id":{"type":"string","title":"Job Id","description":"Unique job ID"},"status":{"type":"string","title":"Status","description":"Status: discovering, uploading, completed, failed"},"message":{"type":"string","title":"Message","description":"Human-readable status message"},"base_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Base Url","description":"Base URL being crawled"},"discovered_urls":{"anyOf":[{"items":{"additionalProperties":true,"type":"object"},"type":"array"},{"type":"null"}],"title":"Discovered Urls","description":"List of discovered URLs with url, title (optional), and priority"},"total_pages":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Total Pages","description":"Total pages discovered"},"discovered_count":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Discovered Count","description":"Real-time count of pages discovered so far (during crawl)"},"pages_crawled":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Pages Crawled","description":"Pages successfully crawled"},"pages_failed":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Pages Failed","description":"Pages that failed to crawl"},"documents_created":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Documents Created","description":"Documents created in KB"},"document_ids":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Document Ids","description":"IDs of created documents"},"crawl_statistics":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Crawl Statistics","description":"Detailed crawl stats"},"errors":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Errors","description":"List of error messages"},"blocked_pages":{"anyOf":[{"items":{"additionalProperties":true,"type":"object"},"type":"array"},{"type":"null"}],"title":"Blocked Pages","description":"Pages blocked by the target server (429 rate limited or 403 forbidden)"},"blocked_count":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Blocked Count","description":"Total number of pages blocked by the target server"},"finalize_phase":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Finalize Phase","description":"Current finalization phase: 'importing', 'cleaning_up', 'saving_history'. Only present after all pages are crawled and before job completes."},"rag_imported_files":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Rag Imported Files","description":"Number of files imported to RAG so far (during 'importing' phase)"},"rag_total_files":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Rag Total Files","description":"Total number of files to import to RAG (during 'importing' phase)"},"started_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Started At","description":"ISO datetime when job started"},"completed_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Completed At","description":"ISO datetime when job completed"},"updated_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Updated At","description":"ISO datetime of last update"}},"type":"object","required":["job_id","status","message"],"title":"CrawlJobStatusResponse","description":"Detailed status response for a website crawl job.\n\nPoll this endpoint to track async crawl progress.","example":{"base_url":"https://docs.example.com","completed_at":"2025-01-15T10:35:00Z","document_ids":["doc-1","doc-2"],"documents_created":24,"errors":["Failed to fetch /private-page: 403 Forbidden"],"job_id":"job-uuid-123","message":"Crawl completed successfully","pages_crawled":24,"pages_failed":1,"started_at":"2025-01-15T10:30:00Z","status":"completed","total_pages":25}},"CrawlLimitsResponse":{"properties":{"max_pages":{"type":"integer","title":"Max Pages","description":"Maximum pages a single crawl may request for this workspace"},"default_max_pages":{"type":"integer","title":"Default Max Pages","description":"Platform default limit (applies unless raised for this workspace)"},"is_custom":{"type":"boolean","title":"Is Custom","description":"True when SendSeven support set a workspace-specific limit"},"support_email":{"type":"string","title":"Support Email","description":"Contact address to request a higher limit"}},"type":"object","required":["max_pages","default_max_pages","is_custom","support_email"],"title":"CrawlLimitsResponse","description":"Effective website-crawl page limit for the current workspace.","example":{"default_max_pages":2000,"is_custom":false,"max_pages":2000,"support_email":"support@sendseven.com"}},"CrawlPreviewRequest":{"properties":{"url":{"type":"string","maxLength":2048,"minLength":1,"title":"Url","description":"Raw URL as typed by the user"}},"type":"object","required":["url"],"title":"CrawlPreviewRequest","description":"Request for the pre-crawl URL check (POST /documents/crawl/preview).\n\n``url`` is a plain string on purpose (NOT HttpUrl): the server owns the\nvalidation and always answers 200 with a structured ``error_code`` so the\nUI can show a friendly, translated message instead of a pydantic 422."},"CrawlPreviewResponse":{"properties":{"valid":{"type":"boolean","title":"Valid"},"error_code":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Error Code"},"error_message":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Error Message"},"input_url":{"type":"string","title":"Input Url"},"normalized_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Normalized Url"},"final_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Final Url"},"status_code":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Status Code"},"reachable":{"type":"boolean","title":"Reachable","default":false},"title":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Title"},"sitemap":{"$ref":"#/components/schemas/CrawlPreviewSitemap"},"estimated_pages":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Estimated Pages","description":"Sitemap page count when a sitemap was found, else null"},"max_pages_limit":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Max Pages Limit","description":"Your workspace's maximum pages per crawl. When estimated_pages exceeds it, only this many pages can be crawled; contact support@sendseven.com to raise it."}},"type":"object","required":["valid","input_url"],"title":"CrawlPreviewResponse","description":"Result of the pre-crawl URL sanity check. Always HTTP 200.\n\n``valid=false`` ⇒ ``error_code`` is one of: invalid_url, invalid_hostname,\ndns_unresolvable, blocked, unreachable, timeout, http_error."},"CrawlPreviewSitemap":{"properties":{"found":{"type":"boolean","title":"Found","description":"Whether a sitemap was found","default":false},"url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Url","description":"Sitemap URL that was used"},"is_index":{"type":"boolean","title":"Is Index","description":"True if the sitemap is a sitemap index (expanded up to a cap)","default":false},"page_count":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Page Count","description":"Number of URLs listed (null when no sitemap)"},"sample_urls":{"items":{"type":"string"},"type":"array","title":"Sample Urls","description":"Up to 5 example URLs from the sitemap"}},"type":"object","title":"CrawlPreviewSitemap","description":"Sitemap detection result for the crawl preview."},"CrawledDomainsResponse":{"properties":{"domains":{"items":{"$ref":"#/components/schemas/CrawlDomainSummary"},"type":"array","title":"Domains","description":"List of previously crawled domains with stored URL history"},"total":{"type":"integer","title":"Total","description":"Total number of crawled domains","default":0}},"type":"object","title":"CrawledDomainsResponse","description":"Response for listing previously crawled domains.","example":{"domains":[{"base_url":"https://docs.example.com","crawled_at":"2025-01-15T10:35:00Z","domain":"docs.example.com","id":"hist-uuid-123","page_count":47}],"total":1}},"CreateFlowRequest":{"properties":{"name":{"type":"string","maxLength":200,"minLength":1,"title":"Name"},"description":{"anyOf":[{"type":"string","maxLength":500},{"type":"null"}],"title":"Description"},"definition":{"additionalProperties":true,"type":"object","title":"Definition","description":"The full flow graph: ``trigger`` + ``nodes[]`` + ``edges[]`` + ``entry_node_id``. See ``fl01_create_flows.py`` migration docstring for the locked JSON shape. Hard-capped at 256 KB JSON-encoded; validated against the structural rules in ``FlowDefinitionValidator``."},"re_entry_policy":{"type":"string","title":"Re Entry Policy","description":"How to behave when the same contact is triggered again while a run is already in flight. MVP only enforces ``block`` (ignore the new trigger). ``restart`` and ``parallel`` are scaffolded for Phase 3 UX.","default":"block"},"default_language":{"anyOf":[{"type":"string","maxLength":10},{"type":"null"}],"title":"Default Language","description":"Per-flow fallback language used by the runtime when a contact has no resolvable locale. Typically set by the frontend to the operator's current UI language at create time. Backend defaults to ``'en'`` (DB server_default) if omitted. Must be one of the curated ~40 ``SUPPORTED_TRANSLATION_LOCALES``."},"manually_triggerable":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Manually Triggerable","description":"When True, the flow appears in the operator's manual-start picker and accepts ``POST /flows/{id}/runs`` requests. Automatically forced True when the trigger kind is ``manual``.","default":false},"auto_translate_messages":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Auto Translate Messages","description":"Per-flow opt-in for runtime LLM translation of outbound Send-node messages into the contact's preferred language.","default":false},"language_detection_strategy":{"anyOf":[{"type":"string","enum":["cascade","fixed"]},{"type":"null"}],"title":"Language Detection Strategy","description":"How this flow's runs resolve the language they speak to a contact. ``cascade`` (the default) resolves per run via the fallback chain (contact locale → inferred channel locale → ``default_language``). ``fixed`` always speaks ``default_language``, skipping the cascade. OMIT / ``null`` to keep the default — NULL is stored and treated as ``cascade`` by the runtime, so existing flows are unaffected. Any value other than ``cascade`` / ``fixed`` is rejected with 422."},"default_channel_chain":{"anyOf":[{"items":{"anyOf":[{"type":"string"},{"$ref":"#/components/schemas/ChannelChainEntry"}]},"type":"array"},{"type":"null"}],"title":"Default Channel Chain","description":"Flow-level channel fallback chain — May 2026 this is the SINGLE source of truth for channel order (per-node ``channel_chain`` has been removed; node configs only carry content). Bare channel-type strings (``[\"whatsapp\", \"live_chat\"]``) OR ``{channel_type, channel_id?}`` objects pinning a specific Channel row. When omitted / NULL on create, the server seeds from the tenant's active channels in a sensible order. A flow with any send/choice/collect_input node MUST have a non-empty chain to validate."},"stop_on_agent_reply":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Stop On Agent Reply","description":"When True (the default), the flow engine terminates any active run for the contact + conversation the moment a human agent sends an outbound message. Bot replies and flow-Send messages do NOT trip the hook — only operator-authored outbound messages do. Set to False for long-running drips whose cadence should be independent of agent activity.","default":true},"stop_on_conversation_closed":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Stop On Conversation Closed","description":"When True (the default), the flow engine terminates any active run scoped to the conversation the moment that conversation is closed. Only runs whose recorded ``context.conversation_id`` matches the closed conversation are canceled — flows without a recorded conversation are left untouched. Set to False for long-running re-engagement drips that intentionally outlive the original conversation.","default":true},"priority":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Priority","description":"Trigger-arbitration priority among INBOUND-EVENT-triggered (entry) flows. LOWER = higher priority (evaluated FIRST). When multiple entry flows match the same inbound event they start in ascending-priority order. Default 0. Background flows (manual/api_event/schedule) ignore this field.","default":0},"exclusive":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Exclusive","description":"When True, while a run of this entry-triggered flow is actively running on a conversation, NO other entry-triggered flow may start on that conversation (background manual/api/schedule flows are exempt). Default False preserves today's fire-all behaviour.","default":false}},"type":"object","required":["name","definition"],"title":"CreateFlowRequest","description":"Request body for ``POST /flows``."},"CreateScheduledRuleRequest":{"properties":{"name":{"type":"string","maxLength":200,"minLength":1,"title":"Name"},"description":{"anyOf":[{"type":"string","maxLength":500},{"type":"null"}],"title":"Description"},"kind":{"$ref":"#/components/schemas/ScheduledRuleKind","description":"birthday | recurring_cron | segment_blast"},"channel_type":{"$ref":"#/components/schemas/ChannelType"},"channel_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Channel Id","description":"Pinned channel UUID — REQUIRED for chat-app channel types (whatsapp, telegram, sms, messenger, instagram, rcs). The channel IS the sender for these. Must be NULL for ``channel_type=email`` (use ``email_mailbox_id`` instead)."},"email_mailbox_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Email Mailbox Id","description":"Pinned EmailMailbox UUID — REQUIRED for ``channel_type=email``. The mailbox is the sender (controls the From: address). Must be NULL for chat-app channel types."},"payload":{"additionalProperties":true,"type":"object","title":"Payload","description":"Free-form JSON payload. Same per-channel rules as ``scheduled_sends.payload``: WhatsApp REQUIRES ``{template_id, variables}``; email accepts EITHER free-text (``{subject, text|html}``) OR templated (``{template_id, variables?}``) — mutually exclusive; SMS/Telegram/Messenger/Instagram/RCS must NOT carry ``template_id`` but MAY carry ``attachments`` (a list of ≤20 attachment UUIDs owned by your workspace). Hard-capped at 64 KB."},"time_of_day_local":{"anyOf":[{"type":"string","maxLength":8},{"type":"null"}],"title":"Time Of Day Local","description":"Local clock time as \"HH:MM\" or \"HH:MM:SS\". Required for `birthday` rules. Optional for `recurring_cron` (the RRULE controls timing). Ignored for `segment_blast`."},"timezone":{"anyOf":[{"type":"string","maxLength":64},{"type":"null"}],"title":"Timezone","description":"IANA tz name (e.g. ``Europe/Berlin``). NULL semantics: birthday → contact-local TZ, recurring_cron/segment_blast → UTC."},"recurrence_rule":{"anyOf":[{"type":"string","maxLength":255},{"type":"null"}],"title":"Recurrence Rule","description":"iCalendar RRULE string. Required for `recurring_cron`; must be NULL for `birthday` and `segment_blast`."},"segment_filter":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Segment Filter","description":"Audience selector. Two accepted shapes:\n  * NEW: ``{\"match_op\": \"any\"|\"all\", \"rules\": [{\"kind\": \"tag\"|\"list\"|\"language\", \"op\": ..., \"values\": [...]}]}``\n  * LEGACY: ``{\"tags\": [...], \"tags_op\": \"any\"|\"all\"}`` (normalized by the engine).\nNULL for `birthday` (audience is implicit)."},"next_evaluation_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Next Evaluation At","description":"For `segment_blast`: the dispatch time. For other kinds the evaluator manages this cursor — leaving it NULL is fine."}},"type":"object","required":["name","kind","channel_type","payload"],"title":"CreateScheduledRuleRequest","description":"Request body for ``POST /scheduler/rules``."},"CreateScheduledSendRequest":{"properties":{"contact_id":{"type":"string","title":"Contact Id","description":"Contact UUID on the tenant's shard"},"channel_type":{"$ref":"#/components/schemas/ChannelType","description":"Canonical ChannelType (whatsapp, telegram, sms, email, ...)"},"channel_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Channel Id","description":"Pinned channel UUID — REQUIRED for chat-app channel types (whatsapp, telegram, sms, messenger, instagram, rcs). Each chat-app channel IS the sender (for RCS: the RBM agent). Leave NULL only for ``channel_type=email`` (use ``email_mailbox_id`` instead)."},"email_mailbox_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Email Mailbox Id","description":"Pinned EmailMailbox UUID — REQUIRED for ``channel_type=email``. The mailbox is the sender (controls the From: address and the underlying provider/integration). Must be NULL for chat-app channel types (use ``channel_id`` instead)."},"payload":{"additionalProperties":true,"type":"object","title":"Payload","description":"Free-form JSON payload. Per-channel rules:\n  - WhatsApp: REQUIRES ``{template_id, variables}`` — free text is rejected because the 24h messaging window cannot be guaranteed at schedule time.\n  - Email: EITHER ``{subject, text|html, include_signature?}`` (free-text) OR ``{template_id, variables?}`` (references an EmailContent template). Mutually exclusive.\n  - SMS / Telegram / Messenger / Instagram / RCS: rendered text payload (e.g. ``{\"text\": \"...\"}``), optionally with ``attachments`` — a list of attachment UUIDs (uploaded via ``POST /attachments/upload``) fanned out as one message per attachment at fire time. Max 20. Every id must belong to your workspace.\nHard-capped at 64 KB JSON-encoded."},"send_at":{"type":"string","format":"date-time","title":"Send At","description":"First-fire timestamp (timezone-aware UTC). Must be at least 1 minute in the future."},"recurrence_rule":{"anyOf":[{"type":"string","maxLength":255},{"type":"null"}],"title":"Recurrence Rule","description":"Optional iCalendar RRULE (e.g. ``FREQ=DAILY;BYHOUR=9``). When set, the row stays in ``scheduled`` after firing and ``next_run_at`` advances per the rule. Capped at 255 chars."},"timezone":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Timezone","description":"IANA timezone for recurrence interpretation (e.g. ``Europe/Berlin``). Defaults to UTC. Currently informational; recurrence math runs in UTC so this is preserved for display only."}},"type":"object","required":["contact_id","channel_type","payload","send_at"],"title":"CreateScheduledSendRequest","description":"Request body for ``POST /scheduler/sends``."},"CreateTemplateRequest":{"properties":{"channel_id":{"type":"string","title":"Channel Id","description":"WhatsApp channel ID"},"name":{"type":"string","maxLength":512,"minLength":1,"pattern":"^[a-z0-9_]+$","title":"Name","description":"Template name (lowercase, numbers, underscores only)"},"category":{"$ref":"#/components/schemas/TemplateCategory","description":"Template category"},"language":{"type":"string","title":"Language","description":"ISO language code (e.g., en, de, es)","default":"en"},"components":{"items":{"$ref":"#/components/schemas/TemplateComponentRequest"},"type":"array","title":"Components","description":"Template components"},"allow_category_change":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Allow Category Change","description":"Allow Meta to change template category if needed","default":true},"parameter_format":{"type":"string","enum":["positional","named"],"title":"Parameter Format","description":"Placeholder style for the template body. `positional` uses `{{1}}`, `{{2}}`, ...; `named` uses identifier placeholders like `{{first_name}}`. When `named`, supply `body_named_examples` instead of `example.body_text` on the BODY component.","default":"positional"},"body_named_examples":{"anyOf":[{"additionalProperties":{"type":"string"},"type":"object"},{"type":"null"}],"title":"Body Named Examples","description":"Example values for NAMED placeholders, keyed by placeholder name (e.g., `{\"first_name\": \"John\", \"order_id\": \"ORD-123\"}`). Required when `parameter_format='named'`. Ignored when `parameter_format='positional'` (use `example.body_text` on the BODY component instead)."},"message_send_ttl_seconds":{"anyOf":[{"type":"integer","maximum":900.0,"minimum":30.0},{"type":"null"}],"title":"Message Send Ttl Seconds","description":"AUTHENTICATION templates only. Time-to-live for the delivered code message (30-900 seconds). WhatsApp stops trying to deliver the message after this window. Ignored for other categories."},"default_header_media_url":{"anyOf":[{"type":"string","maxLength":500},{"type":"null"}],"title":"Default Header Media Url","description":"Default header media URL for IMAGE/VIDEO/DOCUMENT templates. Persisted alongside the template so the send path can fall back to it when callers don't pass `header_media_url`. Typically the public URL returned by `POST /upload-media`."},"default_header_document_filename":{"anyOf":[{"type":"string","maxLength":255},{"type":"null"}],"title":"Default Header Document Filename","description":"Default filename presented to the recipient for DOCUMENT-type headers. Persisted alongside the template so the send path can fall back to it when callers don't pass `header_document_filename`. Should include the correct file extension (e.g. 'invoice.pdf'). Only meaningful for DOCUMENT header templates."}},"type":"object","required":["channel_id","name","category","components"],"title":"CreateTemplateRequest","description":"Request to create a new WhatsApp template","example":{"allow_category_change":true,"category":"UTILITY","channel_id":"a1b2c3d4-5678-9abc-def0-1234567890ab","components":[{"format":"TEXT","text":"Order Update","type":"HEADER"},{"example":{"body_text":[["John","12345","March 5"]]},"text":"Hi {{1}}, your order #{{2}} has been shipped! Estimated delivery: {{3}}.","type":"BODY"},{"text":"Reply STOP to unsubscribe","type":"FOOTER"},{"buttons":[{"text":"Track Order","type":"URL","url":"https://example.com/track/{{1}}"}],"type":"BUTTONS"}],"language":"en","name":"order_confirmation","parameter_format":"positional"}},"CreateTemplateResponse":{"properties":{"success":{"type":"boolean","title":"Success"},"id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Id","description":"Local template ID"},"template_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Template Id","description":"Meta's template ID"},"status":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Status","description":"Template status (usually PENDING)"},"error":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Error"},"error_code":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Error Code","description":"Meta error.code"},"error_subcode":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Error Subcode","description":"Meta error.error_subcode (if present)"},"error_category":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Error Category","description":"Catalog category for the Meta error"},"error_is_retryable":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Error Is Retryable","description":"True when Meta reported a transient fault on their side (retry may succeed), False when the template was rejected and must be changed, None when unknown. Drives the endpoint's 503-vs-422 status."},"fbtrace_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Fbtrace Id","description":"Meta trace id (for Meta Direct Support)"},"meta_message":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Meta Message","description":"Meta error.message, verbatim"},"meta_details":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Meta Details","description":"Meta error.error_data.details, verbatim"},"meta_user_title":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Meta User Title","description":"Meta error.error_user_title, verbatim"},"meta_user_message":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Meta User Message","description":"Meta error.error_user_msg, verbatim"}},"type":"object","required":["success"],"title":"CreateTemplateResponse","description":"Response after creating a template"},"CreateTrackedLinkRequest":{"properties":{"original_url":{"type":"string","title":"Original Url","description":"Full destination URL"},"feature_type":{"$ref":"#/components/schemas/FeatureType","description":"Feature using this tracked link (defaults to OTHER)","default":"other"},"feature_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Feature Id","description":"ID of campaign/conversation/etc."},"contact_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Contact Id","description":"Intended recipient (optional)"},"metadata":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Metadata","description":"Additional context"},"expires_in_days":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Expires In Days","description":"Days until expiration (default: 365)"},"label":{"anyOf":[{"type":"string","maxLength":100},{"type":"null"}],"title":"Label","description":"User-friendly label for the link"},"tracking_mode":{"$ref":"#/components/schemas/TrackingMode","description":"Tracking granularity. 'shared' (default) = one link for all recipients. 'per_recipient' = template link cloned per recipient at send time; requires feature_type=REACH or EMAIL_CAMPAIGN and feature_id.","default":"shared"}},"type":"object","required":["original_url"],"title":"CreateTrackedLinkRequest","description":"Request to create a tracked link"},"CreateVersionRequest":{"properties":{"builder_doc":{"additionalProperties":true,"type":"object","title":"Builder Doc"},"change_note":{"anyOf":[{"type":"string","maxLength":500},{"type":"null"}],"title":"Change Note"},"restored_from_version_id":{"anyOf":[{"type":"string","maxLength":36},{"type":"null"}],"title":"Restored From Version Id"}},"type":"object","required":["builder_doc"],"title":"CreateVersionRequest"},"CreateWaFlowRequest":{"properties":{"name":{"type":"string","maxLength":200,"minLength":1,"title":"Name"},"categories":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Categories"},"template_key":{"anyOf":[{"type":"string","maxLength":64},{"type":"null"}],"title":"Template Key"},"language":{"anyOf":[{"type":"string","maxLength":16},{"type":"null"}],"title":"Language"},"builder_doc":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Builder Doc"},"placeholder_mapping":{"anyOf":[{"additionalProperties":{"anyOf":[{"type":"string"},{"type":"null"}]},"type":"object"},{"type":"null"}],"title":"Placeholder Mapping"},"origin":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Origin","description":"Only 'ai' (with builder_doc) is accepted"},"ai_generation_id":{"anyOf":[{"type":"string","maxLength":36},{"type":"null"}],"title":"Ai Generation Id"},"description":{"anyOf":[{"type":"string","maxLength":1000},{"type":"null"}],"title":"Description"}},"type":"object","required":["name"],"title":"CreateWaFlowRequest"},"CsvSniffResponse":{"properties":{"delimiter":{"type":"string","title":"Delimiter","description":"Detected delimiter, e.g. ',' ';' '\\t' '|'"},"quotechar":{"type":"string","title":"Quotechar","description":"Detected quote character, e.g. '\"'"},"headers":{"items":{"type":"string"},"type":"array","title":"Headers","description":"Parsed header column names (in order)"},"suggested_phone_region":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Suggested Phone Region","description":"Best-effort ISO alpha-2 region derived from the tenant's company_country, or null."}},"type":"object","required":["delimiter","quotechar"],"title":"CsvSniffResponse","description":"Result of sniffing a CSV file's dialect + headers.\n\nLets the frontend preview parser match the backend exactly (the FE\npreview was previously hardcoded to comma).","example":{"delimiter":";","headers":["E-Mail","Telefon","Vorname","Nachname"],"quotechar":"\"","suggested_phone_region":"DE"}},"CurrentUserResponse":{"properties":{"id":{"type":"string","title":"Id","description":"Unique identifier (UUID)"},"email":{"type":"string","title":"Email","description":"User email address"},"name":{"type":"string","title":"Name","description":"Auth0 synced name"},"display_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Display Name","description":"User-editable display name"},"avatar_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Avatar Url","description":"URL to user's avatar image"},"chat_nickname":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Chat Nickname","description":"Chat display nickname"},"live_chat_available":{"type":"boolean","title":"Live Chat Available","description":"Available for live chat assignment","default":true},"personal_phone":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Personal Phone","description":"Personal phone for receiving notifications (user-global)"},"light_theme":{"type":"string","title":"Light Theme","description":"Light mode theme","default":"sendseven-light"},"dark_theme":{"type":"string","title":"Dark Theme","description":"Dark mode theme","default":"sendseven-dark"},"preferred_language":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Preferred Language","description":"Preferred UI language code (e.g., en, de, fr)"},"preferred_translation_language":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Preferred Translation Language","description":"Preferred language for chat translations"},"auto_translate_enabled":{"type":"boolean","title":"Auto Translate Enabled","description":"Whether automatic translation of incoming messages is enabled","default":false},"shoutbox_hidden":{"type":"boolean","title":"Shoutbox Hidden","description":"Whether the team-chat Shoutbox widget is hidden/deactivated (user-global)","default":false},"spoken_languages":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Spoken Languages","description":"Comma-separated ISO 639-1 codes the user speaks (e.g., 'de,en')"},"aup_accepted_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Aup Accepted At","description":"When the user accepted the Acceptable Use Policy"},"aup_version":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Aup Version","description":"Version of the Acceptable Use Policy the user accepted"},"created_at":{"type":"string","format":"date-time","title":"Created At","description":"When the user was created"},"tenant_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Tenant Id","description":"Current tenant context"},"role":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Role","description":"DEPRECATED: Use RBAC system. Field removed from API responses."},"tenant_multi_agent_mode":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Tenant Multi Agent Mode","description":"Whether tenant uses multi-agent mode"},"phone":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Phone","description":"Agent's phone number for email signatures (per-tenant)"},"title":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Title","description":"Agent's job title for email signatures (per-tenant)"},"email_auto_signature_enabled":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Email Auto Signature Enabled","description":"Master toggle for auto-inserting the signature into the composer (per-tenant)"},"email_auto_signature_on_replies":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Email Auto Signature On Replies","description":"Auto-insert the signature when opening a reply composer (per-tenant)"},"email_auto_signature_on_new":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Email Auto Signature On New","description":"Auto-insert the signature when opening a new-email composer (per-tenant)"}},"type":"object","required":["id","email","name","created_at"],"title":"CurrentUserResponse","description":"Enhanced response for GET /users/me with tenant context.\n\nIncludes theme preferences and optional tenant-specific information.","example":{"avatar_url":"https://example.com/avatar.jpg","chat_nickname":"JohnD","created_at":"2025-01-15T10:30:00Z","dark_theme":"charcoal","display_name":"Johnny","email":"john@example.com","id":"u1a2b3c4-d5e6-f7g8-h9i0-j1k2l3m4n5o6","light_theme":"default","live_chat_available":true,"name":"John Doe","phone":"+1-555-0123","preferred_language":"en","role":"ADMIN","tenant_id":"t1a2b3c4-d5e6-f7g8-h9i0-j1k2l3m4n5o6","tenant_multi_agent_mode":true,"title":"Senior Support Agent"}},"CurrentWorkloadBreakdown":{"properties":{"open":{"type":"integer","title":"Open","description":"Conversations currently OPEN (not snoozed)","default":0},"assigned":{"type":"integer","title":"Assigned","description":"Conversations currently ASSIGNED (not snoozed)","default":0},"resolved":{"type":"integer","title":"Resolved","description":"Conversations currently RESOLVED","default":0},"closed":{"type":"integer","title":"Closed","description":"Conversations currently CLOSED","default":0},"snoozed":{"type":"integer","title":"Snoozed","description":"Open/assigned conversations currently snoozed","default":0}},"type":"object","title":"CurrentWorkloadBreakdown","description":"Live conversation counts by current status (NOT date-scoped).\n\nMirrors the Conversations inbox: bot/flow-test conversations excluded,\ncurrently-snoozed active work reported separately, closed live-chat\nsessions hidden. Used by the dashboard workload card."},"CustomFieldDefinitionCreate":{"properties":{"name":{"type":"string","maxLength":100,"minLength":1,"title":"Name","description":"Display name for the field"},"key":{"type":"string","maxLength":100,"minLength":1,"pattern":"^[a-z][a-z0-9_]*$","title":"Key","description":"Internal key (snake_case, starts with letter)"},"field_type":{"$ref":"#/components/schemas/FieldType","description":"Data type for the field"},"description":{"anyOf":[{"type":"string","maxLength":500},{"type":"null"}],"title":"Description","description":"Help text describing the field"},"options":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Options","description":"Options for SELECT/MULTISELECT field types"},"required":{"type":"boolean","title":"Required","description":"Whether this field is required","default":false},"default_value":{"anyOf":[{"type":"string","maxLength":1000},{"type":"null"}],"title":"Default Value","description":"Default value for new contacts"},"agent_visible":{"type":"boolean","title":"Agent Visible","description":"Whether agents can see this field","default":true},"agent_editable":{"type":"boolean","title":"Agent Editable","description":"Whether agents can edit this field","default":true},"user_editable":{"type":"boolean","title":"User Editable","description":"Whether contacts can edit this field (self-service)","default":false},"display_order":{"type":"integer","minimum":0.0,"title":"Display Order","description":"Display order in UI (lower = first)","default":0},"validation":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Validation","description":"Optional value constraints (type-applicable keys only): number → min/max; text/email/phone/url → min_length/max_length/pattern; date/datetime → date_min/date_max (YYYY-MM-DD or 'today'). Enforced on contact value writes."},"ai_hint":{"anyOf":[{"type":"string","maxLength":1000},{"type":"null"}],"title":"Ai Hint","description":"What this field means and how an AI agent should use or set it (max 1000 characters)."},"option_descriptions":{"anyOf":[{"additionalProperties":{"type":"string"},"type":"object"},{"type":"null"}],"title":"Option Descriptions","description":"select/multiselect only: {option: description} explaining each option to AI agents. Keys must be existing options; descriptions max 300 characters."},"ai_access":{"type":"string","enum":["none","read","read_write","fill_if_empty"],"title":"Ai Access","description":"What AI agents may do with this field: `none` (invisible, default), `read` (agents see it), `read_write` (agents see it and may update any value), `fill_if_empty` (agents see it and may only fill it while it is empty). `location` and `collection` fields allow only `none` or `read`.","default":"none"}},"type":"object","required":["name","key","field_type"],"title":"CustomFieldDefinitionCreate","description":"Schema for creating a custom field definition.\n\nCustom fields extend the contact model with tenant-specific attributes.\nField types determine validation and UI rendering.","example":{"agent_editable":true,"agent_visible":true,"description":"Number of employees","display_order":0,"field_type":"select","key":"company_size","name":"Company Size","options":["1-10","11-50","51-200","201-500","500+"],"required":false}},"CustomFieldDefinitionResponse":{"properties":{"id":{"type":"string","title":"Id","description":"Unique identifier (UUID)"},"tenant_id":{"type":"string","title":"Tenant Id","description":"Tenant this resource belongs to"},"name":{"type":"string","title":"Name","description":"Display name for the field"},"key":{"type":"string","title":"Key","description":"Internal key (snake_case)"},"field_type":{"$ref":"#/components/schemas/FieldType","description":"Data type for the field"},"description":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Description","description":"Help text"},"options":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Options","description":"Options for select types"},"required":{"type":"boolean","title":"Required","description":"Whether field is required"},"default_value":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Default Value","description":"Default value"},"agent_visible":{"type":"boolean","title":"Agent Visible","description":"Visible to agents"},"agent_editable":{"type":"boolean","title":"Agent Editable","description":"Editable by agents"},"user_editable":{"type":"boolean","title":"User Editable","description":"Editable by contacts"},"display_order":{"type":"integer","title":"Display Order","description":"Display order"},"is_active":{"type":"boolean","title":"Is Active","description":"Whether field is active"},"validation":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Validation","description":"Value constraints (min/max, min_length/max_length/pattern, date_min/date_max) or null"},"ai_hint":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Ai Hint","description":"Guidance for AI agents on what the field means and how to use it, or null"},"option_descriptions":{"anyOf":[{"additionalProperties":{"type":"string"},"type":"object"},{"type":"null"}],"title":"Option Descriptions","description":"{option: description} for select/multiselect fields, or null"},"ai_access":{"type":"string","enum":["none","read","read_write","fill_if_empty"],"title":"Ai Access","description":"What AI agents may do with this field: `none` (invisible, default), `read` (agents see it), `read_write` (agents see it and may update any value), `fill_if_empty` (agents see it and may only fill it while it is empty). `location` and `collection` fields allow only `none` or `read`.","default":"none"},"created_at":{"type":"string","format":"date-time","title":"Created At","description":"Creation timestamp"}},"type":"object","required":["id","tenant_id","name","key","field_type","required","agent_visible","agent_editable","user_editable","display_order","is_active","created_at"],"title":"CustomFieldDefinitionResponse","description":"Response schema for a custom field definition.\n\nIncludes all field configuration and metadata.","example":{"agent_editable":true,"agent_visible":true,"created_at":"2025-01-15T10:30:00Z","description":"Number of employees","display_order":0,"field_type":"select","id":"cf-12345678-abcd-efgh-ijkl-mnopqrstuvwx","is_active":true,"key":"company_size","name":"Company Size","options":["1-10","11-50","51-200","201-500","500+"],"required":false,"tenant_id":"tn-12345678-abcd-efgh-ijkl-mnopqrstuvwx","user_editable":false}},"CustomFieldDefinitionUpdate":{"properties":{"name":{"anyOf":[{"type":"string","maxLength":100,"minLength":1},{"type":"null"}],"title":"Name","description":"Display name for the field"},"description":{"anyOf":[{"type":"string","maxLength":500},{"type":"null"}],"title":"Description","description":"Help text describing the field"},"options":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Options","description":"Options for SELECT/MULTISELECT field types"},"required":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Required","description":"Whether this field is required"},"default_value":{"anyOf":[{"type":"string","maxLength":1000},{"type":"null"}],"title":"Default Value","description":"Default value for new contacts"},"agent_visible":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Agent Visible","description":"Whether agents can see this field"},"agent_editable":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Agent Editable","description":"Whether agents can edit this field"},"user_editable":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"User Editable","description":"Whether contacts can edit this field (self-service)"},"display_order":{"anyOf":[{"type":"integer","minimum":0.0},{"type":"null"}],"title":"Display Order","description":"Display order in UI (lower = first)"},"validation":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Validation","description":"Optional value constraints (type-applicable keys only): number → min/max; text/email/phone/url → min_length/max_length/pattern; date/datetime → date_min/date_max (YYYY-MM-DD or 'today'). Enforced on contact value writes. Send null to clear; omit to keep."},"ai_hint":{"anyOf":[{"type":"string","maxLength":1000},{"type":"null"}],"title":"Ai Hint","description":"What this field means and how an AI agent should use or set it (max 1000 characters). Send null to clear; omit to keep."},"option_descriptions":{"anyOf":[{"additionalProperties":{"type":"string"},"type":"object"},{"type":"null"}],"title":"Option Descriptions","description":"select/multiselect only: {option: description} for AI agents. Keys must be existing options (after this update); descriptions max 300 characters. Replaces the whole map; null clears; omit to keep."},"ai_access":{"anyOf":[{"type":"string","enum":["none","read","read_write","fill_if_empty"]},{"type":"null"}],"title":"Ai Access","description":"What AI agents may do with this field: `none` (invisible, default), `read` (agents see it), `read_write` (agents see it and may update any value), `fill_if_empty` (agents see it and may only fill it while it is empty). `location` and `collection` fields allow only `none` or `read`. Omit to keep; null resets to `none`."}},"type":"object","title":"CustomFieldDefinitionUpdate","description":"Schema for updating a custom field definition.\n\nNote: `key` and `field_type` cannot be changed after creation\nto preserve data integrity.","example":{"display_order":1,"name":"Updated Company Size","options":["1-10","11-50","51-200","201-1000","1000+"]}},"CustomFieldValue":{"properties":{"field_definition_id":{"type":"string","title":"Field Definition Id"},"name":{"type":"string","title":"Name"},"field_type":{"type":"string","title":"Field Type"},"value":{"type":"string","title":"Value"}},"type":"object","required":["field_definition_id","name","field_type","value"],"title":"CustomFieldValue","description":"Custom field value with metadata"},"DailyOptInPoint":{"properties":{"date":{"type":"string","title":"Date","description":"Date in YYYY-MM-DD format"},"channel_type":{"type":"string","title":"Channel Type","description":"Channel type"},"count":{"type":"integer","title":"Count","description":"Number of opt-ins on this date"}},"type":"object","required":["date","channel_type","count"],"title":"DailyOptInPoint","description":"Daily opt-in count for a channel"},"DailyOptOutPoint":{"properties":{"date":{"type":"string","title":"Date","description":"Date in YYYY-MM-DD format"},"channel_type":{"type":"string","title":"Channel Type","description":"Channel type"},"count":{"type":"integer","title":"Count","description":"Number of opt-outs on this date"}},"type":"object","required":["date","channel_type","count"],"title":"DailyOptOutPoint","description":"Daily opt-out count for a channel"},"DashboardMetricsResponse":{"properties":{"total_messages":{"type":"integer","title":"Total Messages","description":"Total number of messages in the period"},"inbound_messages":{"type":"integer","title":"Inbound Messages","description":"Number of inbound (customer) messages"},"outbound_messages":{"type":"integer","title":"Outbound Messages","description":"Number of outbound (agent) messages"},"total_conversations":{"type":"integer","title":"Total Conversations","description":"Total number of conversations"},"open_conversations":{"type":"integer","title":"Open Conversations","description":"Number of currently open conversations"},"closed_conversations":{"type":"integer","title":"Closed Conversations","description":"Number of closed conversations"},"total_contacts":{"type":"integer","title":"Total Contacts","description":"Total number of contacts"},"active_channels":{"type":"integer","title":"Active Channels","description":"Number of active channels"},"period_start":{"type":"string","title":"Period Start","description":"Start of the reporting period (ISO format)"},"period_end":{"type":"string","title":"Period End","description":"End of the reporting period (ISO format)"},"billing_messages_sent":{"type":"integer","title":"Billing Messages Sent","description":"Billing-account-wide billable messages sent for the current calendar month (matches /settings/billing 'Messages' line)","default":0},"billing_email_campaign_sends":{"type":"integer","title":"Billing Email Campaign Sends","description":"Billing-account-wide billable email campaign sends (managed + BYOK) for the current calendar month","default":0},"billing_kb_queries":{"type":"integer","title":"Billing Kb Queries","description":"Billing-account-wide billable KB queries for the current calendar month","default":0},"billing_period_start":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Billing Period Start","description":"Start of the billing (calendar month) window, ISO format"},"billing_period_end":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Billing Period End","description":"End of the billing window (now), ISO format"},"rcs_monthly_active_recipients":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Rcs Monthly Active Recipients","description":"This workspace's RCS monthly active recipients for the current month (Europe/Berlin) on agents billed per monthly active recipient. Null when the workspace has no such agent."},"rcs_mau_period":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Rcs Mau Period","description":"Month of rcs_monthly_active_recipients as YYYY-MM (Europe/Berlin). Null when rcs_monthly_active_recipients is null."}},"type":"object","required":["total_messages","inbound_messages","outbound_messages","total_conversations","open_conversations","closed_conversations","total_contacts","active_channels","period_start","period_end"],"title":"DashboardMetricsResponse","description":"Dashboard overview metrics response.\n\nReturned by GET /api/v1/analytics/dashboard","example":{"active_channels":3,"closed_conversations":105,"inbound_messages":800,"open_conversations":45,"outbound_messages":700,"period_end":"2026-01-18T00:00:00Z","period_start":"2026-01-01T00:00:00Z","total_contacts":500,"total_conversations":150,"total_messages":1500}},"DataSourceCreate":{"properties":{"name":{"type":"string","maxLength":128,"minLength":1,"title":"Name"},"source_type":{"type":"string","title":"Source Type","description":"'webhook' or 'internal'","default":"webhook"},"resolver_key":{"anyOf":[{"type":"string","maxLength":64},{"type":"null"}],"title":"Resolver Key"},"url":{"anyOf":[{"type":"string","maxLength":2048},{"type":"null"}],"title":"Url"},"headers":{"anyOf":[{},{"type":"null"}],"title":"Headers"},"secret":{"anyOf":[{},{"type":"null"}],"title":"Secret"},"timeout_ms":{"anyOf":[{"type":"integer","maximum":8000.0,"minimum":100.0},{"type":"null"}],"title":"Timeout Ms"},"is_active":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Is Active","default":true}},"type":"object","required":["name"],"title":"DataSourceCreate"},"DataSourceUpdate":{"properties":{"name":{"anyOf":[{"type":"string","maxLength":128,"minLength":1},{"type":"null"}],"title":"Name"},"resolver_key":{"anyOf":[{"type":"string","maxLength":64},{"type":"null"}],"title":"Resolver Key"},"url":{"anyOf":[{"type":"string","maxLength":2048},{"type":"null"}],"title":"Url"},"headers":{"anyOf":[{},{"type":"null"}],"title":"Headers"},"secret":{"anyOf":[{},{"type":"null"}],"title":"Secret"},"timeout_ms":{"anyOf":[{"type":"integer","maximum":8000.0,"minimum":100.0},{"type":"null"}],"title":"Timeout Ms"},"is_active":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Is Active"}},"type":"object","title":"DataSourceUpdate"},"DeleteResponse":{"properties":{"success":{"type":"boolean","title":"Success","description":"Whether deletion succeeded","default":true},"id":{"type":"string","title":"Id","description":"ID of deleted resource"},"message":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Message","default":"Resource deleted successfully"}},"type":"object","required":["id"],"title":"DeleteResponse","description":"Response for resource deletion"},"DeleteSenderResponse":{"properties":{"success":{"type":"boolean","title":"Success"},"channel_id":{"type":"string","title":"Channel Id"},"released":{"type":"boolean","title":"Released","default":false}},"type":"object","required":["success","channel_id"],"title":"DeleteSenderResponse"},"DeleteTrackedLinkResponse":{"properties":{"message":{"type":"string","title":"Message","description":"Success message"}},"type":"object","required":["message"],"title":"DeleteTrackedLinkResponse","description":"Response from deleting a tracked link"},"DeliveryStatus":{"type":"string","enum":["pending","success","failed","retrying","dead_letter","queued"],"title":"DeliveryStatus","description":"Status of webhook delivery attempt.\n\nMUST stay in lockstep with ``app.models.webhook_delivery.DeliveryStatus``:\nthe API layer reads the MODEL enum off the ORM row and validates it against\nTHIS enum, so a member missing here is a hard 500 on\n``GET /webhook-endpoints/{id}/deliveries`` (Sentry BACKEND-1F7)."},"DestinationVerifyRequest":{"properties":{"code":{"type":"string","maxLength":8,"minLength":4,"title":"Code"}},"type":"object","required":["code"],"title":"DestinationVerifyRequest"},"DirectionMessageSummary":{"properties":{"total":{"type":"integer","title":"Total","description":"Total message count","default":0},"by_platform":{"additionalProperties":{"type":"integer"},"type":"object","title":"By Platform","description":"Breakdown by platform"}},"type":"object","title":"DirectionMessageSummary","description":"Message summary with total and platform breakdown"},"DnsRecordResponse":{"properties":{"id":{"type":"string","title":"Id"},"record_type":{"type":"string","title":"Record Type"},"record_name":{"type":"string","title":"Record Name"},"record_value":{"type":"string","title":"Record Value"},"is_verified":{"type":"boolean","title":"Is Verified"},"last_check_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Last Check At"},"type":{"type":"string","title":"Type","description":"Alias of `record_type` (CNAME, TXT, MX).","readOnly":true},"name":{"type":"string","title":"Name","description":"Alias of `record_name` (the DNS host to create).","readOnly":true},"value":{"type":"string","title":"Value","description":"Alias of `record_value` (the DNS record content).","readOnly":true}},"type":"object","required":["id","record_type","record_name","record_value","is_verified","type","name","value"],"title":"DnsRecordResponse","description":"DNS record for domain verification (stored row).\n\nSerialised with both `record_type`/`record_name`/`record_value` and the\n`type`/`name`/`value` aliases."},"DnsVerificationCheckResponse":{"properties":{"integration_id":{"type":"string","title":"Integration Id"},"status":{"type":"string","title":"Status"},"message":{"type":"string","title":"Message"}},"type":"object","required":["integration_id","status","message"],"title":"DnsVerificationCheckResponse","description":"Response from DNS verification check"},"DocumentMoveRequest":{"properties":{"folder_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Folder Id","description":"Target folder ID. Use null to move to root (unorganized)."}},"type":"object","required":["folder_id"],"title":"DocumentMoveRequest","description":"Request to move a single document to a folder.","example":{"folder_id":"folder-uuid-123"}},"DocumentMoveResponse":{"properties":{"id":{"type":"string","title":"Id","description":"Document ID"},"tenant_id":{"type":"string","title":"Tenant Id","description":"Tenant ID"},"document_type":{"type":"string","title":"Document Type","description":"Document type"},"title":{"type":"string","title":"Title","description":"Document title"},"source_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Source Url","description":"Source URL if applicable"},"folder_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Folder Id","description":"New folder ID"},"gemini_file_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Gemini File Name","description":"RAG file reference"},"gemini_store_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Gemini Store Name","description":"Vertex AI RAG Corpus name"},"uploaded_by_user_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Uploaded By User Id","description":"User who uploaded"},"created_at":{"type":"string","title":"Created At","description":"ISO datetime when created"},"updated_at":{"type":"string","title":"Updated At","description":"ISO datetime when last updated"},"corpus_moved":{"type":"boolean","title":"Corpus Moved","description":"Whether the Vertex AI corpus was updated. If false, AI search may not work correctly.","default":true},"warning":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Warning","description":"Warning message if corpus move had issues"}},"type":"object","required":["id","tenant_id","document_type","title","created_at","updated_at"],"title":"DocumentMoveResponse","description":"Response for single document move operation.\n\nIncludes document data and information about corpus move status.","example":{"corpus_moved":true,"created_at":"2025-01-15T10:30:00Z","document_type":"faq","folder_id":"folder-uuid-789","gemini_file_name":"files/abc123","gemini_store_name":"ragCorpora/tenant_abc_folder_xyz","id":"doc-uuid-123","tenant_id":"tenant-uuid-456","title":"How to reset your password","updated_at":"2025-01-15T10:30:00Z","uploaded_by_user_id":"auth0|user123"}},"DomainDnsRecordResponse":{"properties":{"type":{"type":"string","title":"Type"},"name":{"type":"string","title":"Name"},"value":{"type":"string","title":"Value"},"purpose":{"type":"string","title":"Purpose"},"record_type":{"type":"string","title":"Record Type","description":"Alias of `type`.","readOnly":true},"record_name":{"type":"string","title":"Record Name","description":"Alias of `name`.","readOnly":true},"record_value":{"type":"string","title":"Record Value","description":"Alias of `value`.","readOnly":true}},"type":"object","required":["type","name","value","purpose","record_type","record_name","record_value"],"title":"DomainDnsRecordResponse","description":"DNS record for domain verification (add-domain / domain-status).\n\nSerialised with both `type`/`name`/`value` and the\n`record_type`/`record_name`/`record_value` aliases used by the create\nresponse and GET /dns-records."},"DomainVerificationRequest":{"properties":{"domain":{"type":"string","title":"Domain","description":"Domain to verify (e.g., 'acme.com')"}},"type":"object","required":["domain"],"title":"DomainVerificationRequest","description":"Request to verify a customer domain"},"DomainVerificationResponse":{"properties":{"domain":{"type":"string","title":"Domain"},"verification_status":{"type":"string","title":"Verification Status"},"dkim_status":{"type":"string","title":"Dkim Status","default":"UNKNOWN"},"verified":{"type":"boolean","title":"Verified"},"dns_records":{"items":{"$ref":"#/components/schemas/DomainDnsRecordResponse"},"type":"array","title":"Dns Records"}},"type":"object","required":["domain","verification_status","verified"],"title":"DomainVerificationResponse","description":"Response with domain verification status and DNS records"},"DoubleOptInResendRequest":{"properties":{"email":{"type":"string","format":"email","title":"Email","description":"Email address to resend confirmation to"}},"type":"object","required":["email"],"title":"DoubleOptInResendRequest","description":"Request to resend confirmation email"},"DoubleOptInSendRequest":{"properties":{"email":{"type":"string","format":"email","title":"Email","description":"Email address to confirm"},"list_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"List Id","description":"Optional list ID to subscribe to"},"category":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Category","description":"Subscription category (newsletter, promotions, updates)","default":"newsletter"},"ip_address":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Ip Address","description":"IP address of requester for audit trail"},"user_agent":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"User Agent","description":"User agent for audit trail"}},"type":"object","required":["email"],"title":"DoubleOptInSendRequest","description":"Request to send double opt-in confirmation email"},"DoubleOptInSendResponse":{"properties":{"token_id":{"type":"string","title":"Token Id","description":"Unique token ID"},"message":{"type":"string","title":"Message","description":"Success message"},"expires_at":{"type":"string","format":"date-time","title":"Expires At","description":"When the token expires"},"email":{"type":"string","title":"Email","description":"Email address confirmation was sent to"}},"type":"object","required":["token_id","message","expires_at","email"],"title":"DoubleOptInSendResponse","description":"Response after sending confirmation email"},"DoubleOptInTemplateCopyRequest":{"properties":{"name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Name","description":"Override template name"},"description":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Description","description":"Override description"},"button_text":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Button Text","description":"Override button text"},"subject_line":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Subject Line","description":"Override subject line"}},"type":"object","title":"DoubleOptInTemplateCopyRequest","description":"Schema for copying a system template"},"DoubleOptInTemplateCreate":{"properties":{"name":{"type":"string","maxLength":255,"minLength":1,"title":"Name","description":"Template name"},"description":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Description","description":"Template description"},"button_text":{"type":"string","maxLength":100,"title":"Button Text","description":"CTA button text","default":"Confirm Subscription"},"subject_line":{"type":"string","maxLength":255,"title":"Subject Line","description":"Email subject line","default":"Please confirm your subscription"},"is_active":{"type":"boolean","title":"Is Active","description":"Whether template is active","default":true},"email_content_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Email Content Id","description":"ID of the EmailContent to use (required if not copying from system)"},"copy_from_system":{"type":"boolean","title":"Copy From System","description":"If true, copy from system template","default":false}},"type":"object","required":["name"],"title":"DoubleOptInTemplateCreate","description":"Schema for creating a double opt-in template"},"DoubleOptInTemplateResponse":{"properties":{"name":{"type":"string","maxLength":255,"minLength":1,"title":"Name","description":"Template name"},"description":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Description","description":"Template description"},"button_text":{"type":"string","maxLength":100,"title":"Button Text","description":"CTA button text","default":"Confirm Subscription"},"subject_line":{"type":"string","maxLength":255,"title":"Subject Line","description":"Email subject line","default":"Please confirm your subscription"},"is_active":{"type":"boolean","title":"Is Active","description":"Whether template is active","default":true},"id":{"type":"string","title":"Id"},"tenant_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Tenant Id"},"email_content_id":{"type":"string","title":"Email Content Id"},"is_system_template":{"type":"boolean","title":"Is System Template"},"created_at":{"type":"string","format":"date-time","title":"Created At"},"updated_at":{"type":"string","format":"date-time","title":"Updated At"}},"type":"object","required":["name","id","tenant_id","email_content_id","is_system_template","created_at","updated_at"],"title":"DoubleOptInTemplateResponse","description":"Schema for double opt-in template response"},"DoubleOptInTemplateUpdate":{"properties":{"name":{"anyOf":[{"type":"string","maxLength":255,"minLength":1},{"type":"null"}],"title":"Name"},"description":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Description"},"button_text":{"anyOf":[{"type":"string","maxLength":100},{"type":"null"}],"title":"Button Text"},"subject_line":{"anyOf":[{"type":"string","maxLength":255},{"type":"null"}],"title":"Subject Line"},"email_content_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Email Content Id"},"is_active":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Is Active"}},"type":"object","title":"DoubleOptInTemplateUpdate","description":"Schema for updating a double opt-in template"},"DraftActivationRule":{"properties":{"id":{"anyOf":[{"type":"string","maxLength":36},{"type":"null"}],"title":"Id"},"channel_ids":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Channel Ids"},"channel_types":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Channel Types"},"widget_ids":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Widget Ids"},"is_schedule_enabled":{"type":"boolean","title":"Is Schedule Enabled","default":false},"timezone":{"type":"string","maxLength":50,"minLength":1,"title":"Timezone","default":"UTC"},"time_ranges":{"anyOf":[{"items":{"$ref":"#/components/schemas/TimeRangeConfig"},"type":"array","maxItems":50},{"type":"null"}],"title":"Time Ranges"},"requires_no_agents_online":{"type":"boolean","title":"Requires No Agents Online","default":false},"is_active":{"type":"boolean","title":"Is Active","default":true},"schedule_preset":{"anyOf":[{"type":"string","enum":["always","business_hours","outside_business_hours","custom"]},{"type":"null"}],"title":"Schedule Preset"},"contact_tag_ids":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Contact Tag Ids"},"first_message_keywords":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"First Message Keywords"},"recipient_email_addresses":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Recipient Email Addresses"},"priority":{"type":"integer","maximum":100.0,"minimum":0.0,"title":"Priority","default":0}},"additionalProperties":false,"type":"object","title":"DraftActivationRule","description":"An activation rule (channel + schedule) inside the draft document."},"DraftKnowledgeSource":{"properties":{"source_type":{"type":"string","enum":["kb_folder","kb_all","web_search","faq","live_source"],"title":"Source Type"},"kb_folder_id":{"anyOf":[{"type":"string","maxLength":36},{"type":"null"}],"title":"Kb Folder Id"},"allowed_domains":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Allowed Domains"},"is_enabled":{"type":"boolean","title":"Is Enabled","default":true},"config":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Config"},"sort_order":{"anyOf":[{"type":"integer","maximum":10000.0,"minimum":0.0},{"type":"null"}],"title":"Sort Order"},"id":{"anyOf":[{"type":"string","maxLength":36},{"type":"null"}],"title":"Id"},"kb_folder_name":{"anyOf":[{"type":"string","maxLength":255},{"type":"null"}],"title":"Kb Folder Name"}},"additionalProperties":false,"type":"object","required":["source_type"],"title":"DraftKnowledgeSource","description":"A knowledge source inside the draft document. ``id`` is assigned by the\nserver on the first draft save and kept on publish (new rows reuse it)."},"DuplicateBotRequest":{"properties":{"name":{"anyOf":[{"type":"string","maxLength":255},{"type":"null"}],"title":"Name"}},"additionalProperties":false,"type":"object","title":"DuplicateBotRequest"},"DuplicateFlowRequest":{"properties":{"name":{"anyOf":[{"type":"string","maxLength":200,"minLength":1},{"type":"null"}],"title":"Name"}},"type":"object","title":"DuplicateFlowRequest","description":"Request body for ``POST /flows/{flow_id}/duplicate``.\n\nThe body is fully optional. When ``name`` is omitted (or the request\nhas no body at all) the duplicate's name defaults to\n``\"<original.name> (copy)\"``."},"DuplicateGroup":{"properties":{"signal":{"type":"string","title":"Signal","description":"One of: phone, email, platform_id, name"},"value":{"type":"string","title":"Value","description":"The shared value (e.g., a phone number, email, platform ID)"},"contacts":{"items":{"$ref":"#/components/schemas/ContactSummary"},"type":"array","title":"Contacts"}},"type":"object","required":["signal","value"],"title":"DuplicateGroup","description":"A single group of contacts sharing one signal value."},"DuplicateGroupListResponse":{"properties":{"items":{"items":{"$ref":"#/components/schemas/DuplicateGroup"},"type":"array","title":"Items","description":"List of items for this page"},"pagination":{"$ref":"#/components/schemas/PaginationMeta","description":"Pagination metadata"}},"type":"object","required":["items","pagination"],"title":"DuplicateGroupListResponse","description":"Paginated response for GET /contacts/duplicates."},"DuplicateMode":{"type":"string","enum":["auto_merge","allow_duplicates","dont_allow_duplicates"],"title":"DuplicateMode","description":"Per-request override for duplicate-contact handling.\n\nMirrors ``app.models.tenant.DuplicateContactMode``. Defined here as a\nPydantic-friendly enum so request bodies can be validated without\nimporting the SQLAlchemy model module."},"DuplicateRef":{"properties":{"id":{"type":"string","title":"Id"},"name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Name"},"phone":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Phone"},"email":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Email"},"methods":{"items":{"$ref":"#/components/schemas/DuplicateRefMethod"},"type":"array","title":"Methods"}},"type":"object","required":["id"],"title":"DuplicateRef","description":"Light-weight contact reference returned in duplicate-detection responses."},"DuplicateRefMethod":{"properties":{"method_type":{"type":"string","title":"Method Type"},"value":{"type":"string","title":"Value"},"channel_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Channel Id"},"is_primary":{"type":"boolean","title":"Is Primary","default":false}},"type":"object","required":["method_type","value"],"title":"DuplicateRefMethod","description":"Tiny method summary embedded in DuplicateRef. Not a full ContactMethodResponse."},"EffectiveSignatureResponse":{"properties":{"html":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Html","description":"Resolved HTML signature"},"text":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Text","description":"Resolved plain text signature"},"source":{"type":"string","title":"Source","description":"Source of the signature: 'user' or 'template' or 'none'"}},"type":"object","required":["source"],"title":"EffectiveSignatureResponse","description":"Response for get_effective_signature with fully resolved placeholders","example":{"html":"<div><p><strong>John Smith</strong></p></div>","source":"user","text":"John Smith"}},"EmailAddress":{"properties":{"email":{"type":"string","title":"Email"},"name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Name"}},"type":"object","required":["email"],"title":"EmailAddress","description":"Email address with optional display name"},"EmailBreakdown":{"properties":{"single":{"$ref":"#/components/schemas/SingleEmailStats"},"campaign":{"$ref":"#/components/schemas/CampaignEmailStats"}},"type":"object","title":"EmailBreakdown","description":"Email volume split by type, each aligned to how billing invoices it:\n`single` (transactional/1:1, from email_messages) vs `campaign`\n(newsletter, from email_campaigns). The two sources are disjoint -- a\ncampaign send never writes an email_messages row -- so they never\ndouble-count."},"EmailCampaignAnalyticsResponse":{"properties":{"campaign_id":{"type":"string","title":"Campaign Id"},"email_campaign_id":{"type":"string","title":"Email Campaign Id"},"total_recipients":{"type":"integer","title":"Total Recipients","default":0},"sent_count":{"type":"integer","title":"Sent Count","default":0},"delivered_count":{"type":"integer","title":"Delivered Count","default":0},"opened_count":{"type":"integer","title":"Opened Count","default":0},"clicked_count":{"type":"integer","title":"Clicked Count","default":0},"bounced_count":{"type":"integer","title":"Bounced Count","default":0},"complained_count":{"type":"integer","title":"Complained Count","default":0},"failed_count":{"type":"integer","title":"Failed Count","default":0},"held_count":{"type":"integer","title":"Held Count","default":0},"delivery_rate":{"type":"number","title":"Delivery Rate","default":0.0},"open_rate":{"type":"number","title":"Open Rate","default":0.0},"click_rate":{"type":"number","title":"Click Rate","default":0.0},"click_to_open_rate":{"type":"number","title":"Click To Open Rate","default":0.0},"bounce_rate":{"type":"number","title":"Bounce Rate","default":0.0},"complaint_rate":{"type":"number","title":"Complaint Rate","default":0.0},"sends_by_hour":{"additionalProperties":{"type":"integer"},"type":"object","title":"Sends By Hour"},"opens_by_hour":{"additionalProperties":{"type":"integer"},"type":"object","title":"Opens By Hour"},"clicks_by_hour":{"additionalProperties":{"type":"integer"},"type":"object","title":"Clicks By Hour"},"top_links":{"items":{"additionalProperties":true,"type":"object"},"type":"array","title":"Top Links"}},"type":"object","required":["campaign_id","email_campaign_id"],"title":"EmailCampaignAnalyticsResponse","description":"Detailed campaign analytics response","example":{"bounce_rate":2.0,"bounced_count":20,"campaign_id":"campaign-789","click_rate":12.2,"click_to_open_rate":26.7,"clicked_count":120,"clicks_by_hour":{"12":50,"13":70},"complained_count":2,"complaint_rate":0.2,"delivered_count":980,"delivery_rate":98.0,"email_campaign_id":"ecampaign-123","open_rate":45.9,"opened_count":450,"opens_by_hour":{"12":200,"13":250},"sends_by_hour":{"12":500,"13":500},"sent_count":1000,"top_links":[{"clicks":80,"unique_clicks":65,"url":"https://example.com/page"},{"clicks":40,"unique_clicks":38,"url":"https://example.com/other"}],"total_recipients":1000}},"EmailCampaignCostQuote":{"properties":{"recipient_count":{"type":"integer","title":"Recipient Count","description":"Emails priced"},"cost_category":{"type":"string","title":"Cost Category","description":"'email_campaign_managed' or 'email_campaign_byok'"},"sending_type":{"type":"string","title":"Sending Type","description":"'managed' (SendSeven-managed sending) or 'byok' (own provider / SES integration)"},"unit_price_eur":{"type":"string","title":"Unit Price Eur","description":"Price per email in EUR, 6 decimals (e.g. '0.008000')"},"cost_per_email":{"type":"number","title":"Cost Per Email","description":"Price per email in EUR cents"},"total_cost":{"type":"integer","title":"Total Cost","description":"Total in EUR cents, rounded up"},"total_cost_eur":{"type":"string","title":"Total Cost Eur","description":"Total in EUR, 2 decimals"},"total_cost_eur_precise":{"type":"string","title":"Total Cost Eur Precise","description":"Total in EUR, 6 decimals (for '< EUR 0.01' display)"},"plan_included_messages":{"type":"integer","title":"Plan Included Messages","description":"Monthly included messages of the plan (shared pool)"}},"type":"object","required":["recipient_count","cost_category","sending_type","unit_price_eur","cost_per_email","total_cost","total_cost_eur","total_cost_eur_precise","plan_included_messages"],"title":"EmailCampaignCostQuote","description":"Real price of an email campaign send, from the tenant's pricing model.\n\nList price before the plan's included message pool: sends that fit in the\nmonthly included messages are not charged extra."},"EmailCampaignCreate":{"properties":{"name":{"type":"string","maxLength":255,"minLength":1,"title":"Name","description":"Campaign name"},"mailbox_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Mailbox Id","description":"Email mailbox to send from. Auto-resolves from_email, from_name, and sending method. When provided, from_email/from_name/provider/integration fields become optional."},"email_provider_config_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Email Provider Config Id","description":"Email provider to use (SendGrid/Mailgun)"},"email_integration_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Email Integration Id","description":"SES email integration to use (Type 4)"},"email_template_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Email Template Id","description":"Email template (LEGACY - for backward compatibility)"},"email_content_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Email Content Id","description":"Email content (NEW - 3-tier system with layouts)"},"subject_line":{"type":"string","maxLength":255,"minLength":1,"title":"Subject Line","description":"Email subject"},"preview_text":{"anyOf":[{"type":"string","maxLength":150},{"type":"null"}],"title":"Preview Text","description":"Email preview text"},"from_name":{"anyOf":[{"type":"string","maxLength":100,"minLength":1},{"type":"null"}],"title":"From Name","description":"From name (optional when mailbox_id is set)"},"from_email":{"anyOf":[{"type":"string","format":"email"},{"type":"null"}],"title":"From Email","description":"From email address (optional when mailbox_id is set)"},"reply_to_email":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Reply To Email","description":"Reply-to email address"},"subscription_categories":{"anyOf":[{"items":{"type":"string"},"type":"array","maxItems":100},{"type":"null"}],"title":"Subscription Categories","description":"Target specific subscription categories (max 100)"},"list_ids":{"anyOf":[{"items":{"type":"string"},"type":"array","maxItems":100},{"type":"null"}],"title":"List Ids","description":"Target specific lists (max 100; must belong to this workspace)"},"audience_conditions":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Audience Conditions","description":"Audience Builder condition tree (same format as a dynamic list's `segment_conditions`). Must be a top-level `AND` whose children include at least one positive list membership: a `has_optin` condition (`operator: equals`, `value: true`, `list_id` of a static or newsletter list of this workspace) or a group of only such conditions (`OR` = any of the lists, `AND` = all of them). Other children narrow the audience down or exclude contacts (e.g. `has_optin` with `value: false`). When set, the campaign's lists are DERIVED from the membership lists and any `list_ids` sent are ignored. Errors: 422 `audience_list_membership_required`, `audience_membership_list_invalid`, or a validation error for a malformed tree. A tree with no conditions counts as null."},"personalization_enabled":{"type":"boolean","title":"Personalization Enabled","description":"Enable personalization","default":false},"tracking_enabled":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Tracking Enabled","description":"Track email OPENS. Omit to inherit the tenant's track_opens_default (defaults True)."},"track_clicks":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Track Clicks","description":"Track link CLICKS. Omit to inherit the tenant's track_clicks_default (defaults True)."},"scheduled_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Scheduled At","description":"Schedule for future send"},"status":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Status","description":"Campaign status","default":"draft"},"send_rate_limit":{"anyOf":[{"type":"integer"},{"type":"null"}],"maximum":60000.0,"minimum":1.0,"title":"Send Rate Limit","description":"Send speed in emails per minute (1-60000). Null = default speed."},"send_batch_size":{"anyOf":[{"type":"integer"},{"type":"null"}],"maximum":1000000.0,"minimum":1.0,"title":"Send Batch Size","description":"Recipients per batch (1-1000000). The campaign pauses after each batch until the next batch is sent. Null = no batches. Out of range -> 422 send_batch_size_invalid."}},"type":"object","required":["name","subject_line"],"title":"EmailCampaignCreate","description":"Schema for creating an email campaign","example":{"list_ids":["list-789"],"mailbox_id":"mailbox-uuid","name":"November Newsletter","personalization_enabled":true,"preview_text":"Check out what's new this month","reply_to_email":"support@example.com","subject_line":"Your November Updates","subscription_categories":["newsletter"],"track_clicks":true,"tracking_enabled":true}},"EmailCampaignDetailedAnalyticsResponse":{"properties":{"campaign_id":{"type":"string","title":"Campaign Id","description":"Campaign ID"},"device_breakdown":{"additionalProperties":{"type":"integer"},"type":"object","title":"Device Breakdown","description":"Device type breakdown from native click User-Agents. Keys are one of 'Mobile' / 'Tablet' / 'Desktop' / 'Unknown'; values are the number of UNIQUE RECIPIENTS that clicked from that device type (a recipient who used multiple device types is counted once per bucket)."},"link_performance":{"items":{"$ref":"#/components/schemas/LinkPerformance"},"type":"array","title":"Link Performance","description":"Top 20 links by clicks"},"total_tracked_links":{"type":"integer","title":"Total Tracked Links","description":"Total tracked links count","default":0}},"type":"object","required":["campaign_id"],"title":"EmailCampaignDetailedAnalyticsResponse","description":"Detailed campaign analytics with device breakdown and link performance.\n\nContains advanced metrics including link performance with CTR per link.","example":{"campaign_id":"ecampaign-123","device_breakdown":{"Desktop":30,"Mobile":42,"Tablet":3,"Unknown":1},"link_performance":[{"click_through_rate":12.5,"clicks":120,"first_click_at":"2024-01-01T12:00:00","last_click_at":"2024-01-02T14:30:00","short_url":"https://example.com/l/abc123","url":"https://example.com/page"}],"total_tracked_links":5}},"EmailCampaignListItem":{"properties":{"id":{"type":"string","title":"Id"},"tenant_id":{"type":"string","title":"Tenant Id"},"campaign_id":{"type":"string","title":"Campaign Id"},"subject_line":{"type":"string","title":"Subject Line"},"from_name":{"type":"string","title":"From Name"},"from_email":{"type":"string","title":"From Email"},"sent_count":{"type":"integer","title":"Sent Count","default":0},"delivered_count":{"type":"integer","title":"Delivered Count","default":0},"opened_count":{"type":"integer","title":"Opened Count","default":0},"clicked_count":{"type":"integer","title":"Clicked Count","default":0},"recipient_count":{"type":"integer","title":"Recipient Count","description":"Estimated recipients (draft/scheduled) or actual sent count","default":0},"campaign_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Campaign Name"},"campaign_status":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Campaign Status"},"scheduled_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Scheduled At"},"timezone":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Timezone"},"created_at":{"type":"string","format":"date-time","title":"Created At"}},"type":"object","required":["id","tenant_id","campaign_id","subject_line","from_name","from_email","created_at"],"title":"EmailCampaignListItem","description":"Lightweight email campaign for list views.\n\nContains only essential fields for listing campaigns."},"EmailCampaignListResponse":{"properties":{"items":{"items":{"$ref":"#/components/schemas/EmailCampaignListItem"},"type":"array","title":"Items","description":"List of items for this page"},"pagination":{"$ref":"#/components/schemas/PaginationMeta","description":"Pagination metadata"}},"type":"object","required":["items","pagination"],"title":"EmailCampaignListResponse","description":"Paginated list of email campaigns.\n\nExample response:\n```json\n{\n    \"items\": [...],\n    \"pagination\": {\n        \"total\": 50,\n        \"page\": 1,\n        \"page_size\": 20,\n        \"total_pages\": 3,\n        \"has_next\": true,\n        \"has_prev\": false\n    }\n}\n```"},"EmailCampaignResponse":{"properties":{"created_at":{"type":"string","title":"Created At","description":"When the resource was created (UTC)"},"updated_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Updated At","description":"When the resource was last updated (UTC)"},"id":{"type":"string","title":"Id","description":"Unique identifier (UUID)"},"tenant_id":{"type":"string","title":"Tenant Id","description":"Tenant this resource belongs to"},"campaign_id":{"type":"string","title":"Campaign Id","description":"Base campaign ID"},"email_provider_config_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Email Provider Config Id"},"email_integration_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Email Integration Id"},"sending_method":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Sending Method","description":"'provider', 'ses_integration', or 'none'"},"email_template_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Email Template Id"},"email_content_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Email Content Id"},"subject_line":{"type":"string","title":"Subject Line"},"preview_text":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Preview Text"},"from_name":{"type":"string","title":"From Name"},"from_email":{"type":"string","title":"From Email"},"reply_to_email":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Reply To Email"},"subscription_categories":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Subscription Categories"},"list_ids":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"List Ids","description":"List IDs targeted by this campaign"},"audience_conditions":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Audience Conditions","description":"Audience Builder condition tree; null = the campaign targets its saved lists"},"personalization_enabled":{"type":"boolean","title":"Personalization Enabled","default":false},"tracking_enabled":{"type":"boolean","title":"Tracking Enabled","default":true},"track_clicks":{"type":"boolean","title":"Track Clicks","default":true},"sent_count":{"type":"integer","title":"Sent Count","default":0},"delivered_count":{"type":"integer","title":"Delivered Count","default":0},"opened_count":{"type":"integer","title":"Opened Count","default":0},"clicked_count":{"type":"integer","title":"Clicked Count","default":0},"bounced_count":{"type":"integer","title":"Bounced Count","default":0},"complained_count":{"type":"integer","title":"Complained Count","default":0},"failed_count":{"type":"integer","title":"Failed Count","description":"Number of recipients whose send permanently failed","default":0},"recipient_count":{"type":"integer","title":"Recipient Count","description":"Estimated recipients (draft/scheduled) or actual sent count","default":0},"campaign_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Campaign Name"},"campaign_status":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Campaign Status"},"scheduled_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Scheduled At"},"timezone":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Timezone","description":"IANA timezone for scheduled_at display (e.g. 'Europe/Berlin')"},"throttle_reason":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Throttle Reason"},"send_rate_limit":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Send Rate Limit"},"send_batch_size":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Send Batch Size"},"current_batch_number":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Current Batch Number"}},"type":"object","required":["created_at","id","tenant_id","campaign_id","subject_line","from_name","from_email"],"title":"EmailCampaignResponse","description":"Full email campaign response with all fields.\n\nInherits id, tenant_id from TenantResourceMixin and created_at, updated_at from TimestampMixin.","example":{"bounced_count":20,"campaign_id":"campaign-789","campaign_name":"November Newsletter","campaign_status":"scheduled","clicked_count":120,"complained_count":2,"created_at":"2024-01-01T12:00:00Z","delivered_count":980,"email_provider_config_id":"prov-123","from_email":"newsletter@example.com","from_name":"Company Team","id":"ecampaign-123","opened_count":450,"personalization_enabled":true,"preview_text":"Check out what's new","reply_to_email":"support@example.com","scheduled_at":"2026-03-01T13:00:00Z","sending_method":"provider","sent_count":1000,"subject_line":"Your November Updates","subscription_categories":["newsletter"],"tenant_id":"tenant-456","timezone":"Europe/Berlin","track_clicks":true,"tracking_enabled":true,"updated_at":"2024-01-01T12:00:00Z"}},"EmailCampaignUpdate":{"properties":{"name":{"anyOf":[{"type":"string","maxLength":255,"minLength":1},{"type":"null"}],"title":"Name"},"subject_line":{"anyOf":[{"type":"string","maxLength":255,"minLength":1},{"type":"null"}],"title":"Subject Line"},"preview_text":{"anyOf":[{"type":"string","maxLength":150},{"type":"null"}],"title":"Preview Text"},"from_name":{"anyOf":[{"type":"string","maxLength":100,"minLength":1},{"type":"null"}],"title":"From Name"},"from_email":{"anyOf":[{"type":"string","format":"email"},{"type":"null"}],"title":"From Email"},"reply_to_email":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Reply To Email"},"email_provider_config_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Email Provider Config Id"},"email_integration_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Email Integration Id"},"email_template_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Email Template Id"},"email_content_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Email Content Id"},"subscription_categories":{"anyOf":[{"items":{"type":"string"},"type":"array","maxItems":100},{"type":"null"}],"title":"Subscription Categories"},"list_ids":{"anyOf":[{"items":{"type":"string"},"type":"array","maxItems":100},{"type":"null"}],"title":"List Ids","description":"Target specific lists (max 100; must belong to this workspace)"},"audience_conditions":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Audience Conditions","description":"Audience Builder condition tree (same format as a dynamic list's `segment_conditions`). Must be a top-level `AND` whose children include at least one positive list membership: a `has_optin` condition (`operator: equals`, `value: true`, `list_id` of a static or newsletter list of this workspace) or a group of only such conditions (`OR` = any of the lists, `AND` = all of them). Other children narrow the audience down or exclude contacts (e.g. `has_optin` with `value: false`). When set, the campaign's lists are DERIVED from the membership lists and any `list_ids` sent are ignored. Errors: 422 `audience_list_membership_required`, `audience_membership_list_invalid`, or a validation error for a malformed tree. A tree with no conditions counts as null. Omit to leave unchanged; null clears it (the campaign keeps its current lists)."},"personalization_enabled":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Personalization Enabled"},"tracking_enabled":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Tracking Enabled","description":"Track email OPENS"},"track_clicks":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Track Clicks","description":"Track link CLICKS"},"scheduled_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Scheduled At"},"send_rate_limit":{"anyOf":[{"type":"integer"},{"type":"null"}],"maximum":60000.0,"minimum":1.0,"title":"Send Rate Limit","description":"Send speed in emails per minute (1-60000). Null clears it."},"send_batch_size":{"anyOf":[{"type":"integer"},{"type":"null"}],"maximum":1000000.0,"minimum":1.0,"title":"Send Batch Size","description":"Recipients per batch (1-1000000). Null clears it (no further batches). Out of range -> 422 send_batch_size_invalid."}},"type":"object","title":"EmailCampaignUpdate","description":"Schema for updating email campaign (before sending)"},"EmailComposeFromTemplateRequest":{"properties":{"email_content_id":{"type":"string","title":"Email Content Id","description":"ID of the EmailContent template to use"},"variable_overrides":{"anyOf":[{"additionalProperties":{"type":"string"},"type":"object"},{"type":"null"}],"title":"Variable Overrides","description":"Override template variable values before compilation"},"cc_emails":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Cc Emails","description":"CC recipients"},"bcc_emails":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Bcc Emails","description":"BCC recipients"},"include_signature":{"type":"boolean","title":"Include Signature","description":"Whether to include email signature","default":true}},"type":"object","required":["email_content_id"],"title":"EmailComposeFromTemplateRequest","description":"Request body for composing an email from an EmailContent template"},"EmailComposeRequest":{"properties":{"subject":{"type":"string","maxLength":998,"minLength":1,"title":"Subject","description":"Email subject"},"text_body":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Text Body","description":"Plain text body"},"html_body":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Html Body","description":"HTML body (for rich text)"},"to_email":{"anyOf":[{"type":"string","format":"email"},{"type":"null"}],"title":"To Email","description":"Explicit recipient address. Honored over the contact's primary email; falls back to the contact's primary email when omitted."},"cc_emails":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Cc Emails","description":"CC recipients"},"bcc_emails":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Bcc Emails","description":"BCC recipients"},"attachment_ids":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Attachment Ids","description":"IDs of attachments to include"},"sender_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Sender Id","description":"Override the connection used to send this email. Must be one of the ids returned by GET /api/v1/inboxes/{id}/sender-options (or, when composing inside an existing conversation, /api/v1/conversations/{id}/sender-options)."}},"type":"object","required":["subject"],"title":"EmailComposeRequest","description":"Request body for composing a new outbound email (not a reply)"},"EmailConnectionListResponse":{"properties":{"connections":{"items":{"$ref":"#/components/schemas/EmailConnectionResponse"},"type":"array","title":"Connections","description":"List of email connections"},"total":{"type":"integer","title":"Total","description":"Total number of connections"}},"type":"object","required":["connections","total"],"title":"EmailConnectionListResponse","description":"Response schema for listing email connections"},"EmailConnectionResponse":{"properties":{"id":{"type":"string","title":"Id","description":"Unique identifier"},"tenant_id":{"type":"string","title":"Tenant Id","description":"Tenant ID"},"name":{"type":"string","title":"Name","description":"User-friendly name"},"source_type":{"$ref":"#/components/schemas/SourceType","description":"Which table this connection came from (provider or integration)"},"connection_type":{"$ref":"#/components/schemas/ConnectionType","description":"Type of connection (sendgrid, mailgun, gmail, smtp_imap, ses_managed, proxy)"},"is_active":{"type":"boolean","title":"Is Active","description":"Whether the connection is active"},"connection_status":{"type":"string","title":"Connection Status","description":"Connection status (connected, disconnected, error, pending_verification, pending_dns)"},"can_send_campaigns":{"type":"boolean","title":"Can Send Campaigns","description":"Whether this connection can send bulk email campaigns"},"can_receive_support":{"type":"boolean","title":"Can Receive Support","description":"Whether this connection can receive inbound support emails"},"can_reply_support":{"type":"boolean","title":"Can Reply Support","description":"Whether this connection can send replies to support emails"},"is_campaign_provider":{"type":"boolean","title":"Is Campaign Provider","description":"Whether this is the active campaign provider for the tenant"},"is_support_default":{"type":"boolean","title":"Is Support Default","description":"Whether this is the default support integration (used for outbound support emails)","default":false},"outbound_mailbox_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Outbound Mailbox Id","description":"ID of the EmailMailbox used to send proxy replies (new outbound selector)"},"outbound_mailbox_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Outbound Mailbox Name","description":"Display name of the linked outbound mailbox for proxy integrations"},"outbound_provider_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Outbound Provider Id","description":"DEPRECATED: backward-compat ID of linked outbound provider for proxy integrations. Prefer outbound_mailbox_id."},"outbound_provider_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Outbound Provider Name","description":"DEPRECATED: backward-compat name of linked outbound provider for proxy integrations. Prefer outbound_mailbox_name."},"proxy_forward_to":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Proxy Forward To","description":"Your email address where proxy emails are forwarded to"},"accepted_aliases":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Accepted Aliases","description":"Additional email aliases that are accepted"},"recipient_validation_mode":{"type":"string","title":"Recipient Validation Mode","description":"Recipient validation mode: 'any' (accept all), 'strict' (only proxy_forward_to), 'whitelist' (proxy_forward_to + accepted_aliases)","default":"any"},"has_received_email":{"type":"boolean","title":"Has Received Email","description":"Whether this proxy has received at least one email (forwarding confirmed)","default":false},"proxy_address":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Proxy Address","description":"Inbound proxy receive-address (the @proxy.sendseven.com hash) for PROXY connections. This is the address customers forward TO; it is NEVER a sendable/from address."},"email_address":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Email Address","description":"Primary, real, sendable email address for this connection. For PROXY connections this is the configured sending/identity address, NOT the inbound proxy hash (see proxy_address)."},"verified_domains":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Verified Domains","description":"List of verified domains (for SendGrid/Mailgun)"},"key_status":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Key Status","description":"BYOK API-key health for provider connections: {valid: bool|null, error: str|null, checked_at: iso}. valid=false -> render a 'reconnect key' banner. Absent for non-BYOK/integration connections."},"last_error":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Last Error","description":"Last error message if any"},"created_at":{"type":"string","format":"date-time","title":"Created At","description":"When the connection was created"},"updated_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Updated At","description":"When the connection was last updated"}},"type":"object","required":["id","tenant_id","name","source_type","connection_type","is_active","connection_status","can_send_campaigns","can_receive_support","can_reply_support","is_campaign_provider","created_at"],"title":"EmailConnectionResponse","description":"Unified response schema for email connections.\n\nThis schema combines data from both email_provider_configs and email_integrations\ntables into a single unified view.","example":{"can_receive_support":false,"can_reply_support":false,"can_send_campaigns":true,"connection_status":"connected","connection_type":"sendgrid","created_at":"2024-01-01T12:00:00Z","email_address":"marketing@example.com","id":"prov-123-abc","is_active":true,"is_campaign_provider":true,"name":"Marketing SendGrid","source_type":"provider","tenant_id":"tenant-456","updated_at":"2024-01-02T12:00:00Z","verified_domains":["example.com"]}},"EmailContactImportSummary":{"properties":{"total":{"type":"integer","title":"Total","description":"Total rows in CSV"},"imported":{"type":"integer","title":"Imported","description":"Successfully imported contacts"},"duplicates":{"type":"integer","title":"Duplicates","description":"Duplicate contacts skipped"},"invalid_rows":{"type":"integer","title":"Invalid Rows","description":"Invalid rows (empty or bad email format)"},"errors":{"items":{"type":"string"},"type":"array","title":"Errors","description":"Error messages"},"merged_count":{"type":"integer","title":"Merged Count","description":"Rows that auto-merged into an existing contact (auto_merge mode or live_chat-source resurrection). Counted separately from imported (which represents NEW contacts created).","default":0},"duplicate_blocked_count":{"type":"integer","title":"Duplicate Blocked Count","description":"Rows blocked because the duplicate_mode was dont_allow_duplicates and the row matched an existing contact. The row was NOT imported and NOT merged.","default":0},"updated_count":{"type":"integer","title":"Updated Count","description":"Rows that updated an existing matched contact under duplicate_mode=update_existing (standard + EAV custom fields). Counted separately from imported (new contacts) and merged.","default":0},"skipped_field_values":{"type":"integer","title":"Skipped Field Values","description":"Per-field custom-field values skipped during parse because they failed SELECT/MULTISELECT option validation or ISO date validation. The owning row was still imported. See errors[] for the per-value warnings.","default":0},"subscriptions_created":{"type":"integer","title":"Subscriptions Created","description":"Total ContactSubscription rows created across all channel targets.","default":0},"subscriptions_skipped":{"additionalProperties":{"type":"integer"},"type":"object","title":"Subscriptions Skipped","description":"Per-channel count of subscription targets skipped because the contact lacked the required method (e.g. {\"whatsapp\": 3})."},"created":{"type":"integer","title":"Created","description":"New contacts created (same value as 'imported'; explicit name for the created/matched breakdown).","default":0},"matched_by_id":{"type":"integer","title":"Matched By Id","description":"Rows matched to an existing contact via the contact_id UUID column and updated in place.","default":0},"matched_by_contact_method":{"type":"integer","title":"Matched By Contact Method","description":"Rows matched to an existing contact by email/phone/whatsapp_id and updated in place (duplicate_mode=update_existing).","default":0},"unknown_contact_id_rows":{"type":"integer","title":"Unknown Contact Id Rows","description":"Rows carrying a contact_id UUID that does not exist in this workspace. These rows are ERRORS (listed in errors[]) and never create a contact.","default":0},"post_actions":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Post Actions","description":"Result of the optional post-import actions (apply_tag_ids / apply_custom_field), e.g. {\"tagged_contacts\": 12, \"custom_field_set_contacts\": 12, \"tag_ids\": [...], \"field_id\": \"...\"}. Null when no action was requested."}},"type":"object","required":["total","imported","duplicates","invalid_rows"],"title":"EmailContactImportSummary","description":"Schema for import results summary","example":{"duplicate_blocked_count":0,"duplicates":10,"errors":["Row 15: Invalid email format","Row 23: Empty row - no email, name, or phone"],"imported":85,"invalid_rows":5,"merged_count":0,"total":100}},"EmailContactListResponse":{"properties":{"contacts":{"items":{"$ref":"#/components/schemas/EmailContactResponse"},"type":"array","title":"Contacts"},"total":{"type":"integer","title":"Total"},"limit":{"type":"integer","title":"Limit"},"offset":{"type":"integer","title":"Offset"}},"type":"object","required":["contacts","total","limit","offset"],"title":"EmailContactListResponse","description":"Schema for paginated email contact list","example":{"contacts":[],"limit":50,"offset":0,"total":150}},"EmailContactResponse":{"properties":{"id":{"type":"string","title":"Id"},"tenant_id":{"type":"string","title":"Tenant Id"},"contact_id":{"type":"string","title":"Contact Id"},"email":{"type":"string","title":"Email"},"name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Name"},"phone":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Phone"},"subscription_status":{"$ref":"#/components/schemas/SubscriptionStatusEnum"},"subscription_category":{"type":"string","title":"Subscription Category"},"opt_in_method":{"$ref":"#/components/schemas/OptInMethodEnum"},"opt_in_confirmed_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Opt In Confirmed At"},"unsubscribed_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Unsubscribed At"},"created_at":{"type":"string","format":"date-time","title":"Created At"},"updated_at":{"type":"string","format":"date-time","title":"Updated At"}},"type":"object","required":["id","tenant_id","contact_id","email","subscription_status","subscription_category","opt_in_method","created_at","updated_at"],"title":"EmailContactResponse","description":"Schema for email contact details","example":{"contact_id":"770e8400-e29b-41d4-a716-446655440002","created_at":"2025-01-10T09:00:00Z","email":"customer@example.com","id":"550e8400-e29b-41d4-a716-446655440000","name":"John Doe","opt_in_confirmed_at":"2025-01-15T10:30:00Z","opt_in_method":"import","phone":"+1234567890","subscription_category":"newsletter","subscription_status":"subscribed","tenant_id":"660e8400-e29b-41d4-a716-446655440001","updated_at":"2025-01-15T10:30:00Z"}},"EmailContactUpdate":{"properties":{"name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Name"},"phone":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Phone"},"subscription_status":{"anyOf":[{"$ref":"#/components/schemas/SubscriptionStatusEnum"},{"type":"null"}]},"subscription_category":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Subscription Category"},"unsubscribe_reason":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Unsubscribe Reason"}},"type":"object","title":"EmailContactUpdate","description":"Schema for updating email contact","example":{"name":"Jane Doe","subscription_status":"unsubscribed","unsubscribe_reason":"No longer interested"}},"EmailContentCreate":{"properties":{"name":{"type":"string","maxLength":255,"minLength":1,"title":"Name","description":"Content name"},"description":{"anyOf":[{"type":"string","maxLength":1000},{"type":"null"}],"title":"Description","description":"Content description"},"content_type":{"$ref":"#/components/schemas/EmailContentTypeEnum","description":"Content type: 'html' for MJML-based or 'plain_text' for text-only templates","default":"html"},"template_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Template Id","description":"ID of email template used as base (analytics only, HTML type only)"},"subject_line":{"anyOf":[{"type":"string","maxLength":255},{"type":"null"}],"title":"Subject Line","description":"Subject line with {{variable}} support"},"preview_text":{"anyOf":[{"type":"string","maxLength":150},{"type":"null"}],"title":"Preview Text","description":"Email preview text (appears in inbox)"},"from_name":{"anyOf":[{"type":"string","maxLength":100},{"type":"null"}],"title":"From Name","description":"Default sender name"},"from_email":{"anyOf":[{"type":"string","maxLength":255},{"type":"null"}],"title":"From Email","description":"Default sender email"},"editor_type":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Editor Type","description":"Editor type: 'stripo' (default for new content) or 'mjml' (legacy GrapesJS)","default":"stripo"},"mjml_source":{"anyOf":[{"type":"string","minLength":1},{"type":"null"}],"title":"Mjml Source","description":"Full MJML content with {{variable}} placeholders (required for html type with mjml editor)"},"stripo_design_json":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Stripo Design Json","description":"Stripo editor design JSON for re-opening designs (stripo editor type only)"},"compiled_html":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Compiled Html","description":"Pre-compiled HTML from Stripo export (stripo editor type only)"},"text_body":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Text Body","description":"Plain text template content with {{variable}} placeholders (required for plain_text type)"},"variable_values":{"anyOf":[{"additionalProperties":{"type":"string"},"type":"object"},{"type":"null"}],"title":"Variable Values","description":"Template variable values (not contact/company/system)"},"footer_mode":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Footer Mode","description":"Compliance footer override for this email: `inherit` (default — use the account setting), `both` (unsubscribe + manage preferences), `unsubscribe_only`, or `none` (no visible footer, for transactional mail). RFC 8058 List-Unsubscribe headers are sent regardless."},"ai_generated":{"type":"boolean","title":"Ai Generated","description":"Whether content was AI-generated","default":false},"ai_prompt":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Ai Prompt","description":"AI prompt used for generation"},"ai_model_used":{"anyOf":[{"type":"string","maxLength":100},{"type":"null"}],"title":"Ai Model Used","description":"AI model used"},"ai_generation_cost_cents":{"anyOf":[{"type":"integer","minimum":0.0},{"type":"null"}],"title":"Ai Generation Cost Cents","description":"AI generation cost in cents"}},"type":"object","required":["name"],"title":"EmailContentCreate","description":"Schema for creating email content with variable system.\n\nSupports two content types:\n- **html** (default): Full MJML content with visual editor support\n- **plain_text**: Plain text template with {{variable}} placeholders\n\nThe variable system supports 4 types of variables:\n1. Template variables: Custom content (e.g., {{hero_title}}, {{cta_url}})\n   - Filled via variable_values field or form mode\n2. Contact variables: Auto-filled from contact data (e.g., {{contact.name}})\n   - Replaced at send time with actual contact data\n3. Company variables: Auto-filled from company data (e.g., {{company.name}})\n   - Replaced at send time with company info\n4. System variables: Auto-filled by platform (e.g., {{system.year}})\n   - Replaced at send time with system values\n\nVariable naming convention: lowercase_with_underscores\nExamples: hero_title, contact.name (valid), heroTitle, hero-title (invalid)","examples":[{"ai_generated":false,"content_type":"html","description":"Welcome email for new subscribers (HTML)","mjml_source":"<mjml><mj-body><mj-text>Hello {{contact.name}}!</mj-text></mj-body></mjml>","name":"Welcome Email - HTML","preview_text":"Get started with your new account","subject_line":"Welcome to {{company.name}}, {{contact.name}}!","variable_values":{"cta_url":"https://example.com/get-started","hero_title":"Welcome to Our Platform!"}},{"ai_generated":false,"content_type":"plain_text","description":"Welcome email for new subscribers (plain text)","name":"Welcome Email - Plain Text","subject_line":"Welcome to {{company.name}}, {{contact.name}}!","text_body":"Hello {{contact.name}},\n\nWelcome to {{company.name}}!\n\nBest regards,\nThe Team"}]},"EmailContentDuplicate":{"properties":{"new_name":{"type":"string","maxLength":255,"minLength":1,"title":"New Name","description":"Name for the duplicated content"}},"type":"object","required":["new_name"],"title":"EmailContentDuplicate","description":"Schema for duplicating an email content"},"EmailContentListItem":{"properties":{"id":{"type":"string","title":"Id","description":"Unique identifier (UUID)"},"tenant_id":{"type":"string","title":"Tenant Id","description":"Tenant this resource belongs to"},"name":{"type":"string","title":"Name","description":"Content name"},"description":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Description","description":"Content description"},"content_type":{"type":"string","title":"Content Type","description":"Content type: 'html' or 'plain_text'","default":"html"},"editor_type":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Editor Type","description":"Editor type: 'stripo' or 'mjml'","default":"stripo"},"template_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Template Id","description":"Email template ID"},"subject_line":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Subject Line","description":"Email subject line"},"preview_text":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Preview Text","description":"Preview text"},"from_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"From Name","description":"Sender name"},"from_email":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"From Email","description":"Sender email"},"ai_generated":{"type":"boolean","title":"Ai Generated","description":"AI-generated flag","default":false},"is_system":{"type":"boolean","title":"Is System","description":"System template flag","default":false},"is_hidden":{"type":"boolean","title":"Is Hidden","description":"Hidden/archived flag","default":false},"is_valid":{"type":"boolean","title":"Is Valid","description":"Validation status"},"version":{"type":"integer","title":"Version","description":"Content version"},"created_at":{"type":"string","format":"date-time","title":"Created At","description":"Creation timestamp"},"updated_at":{"type":"string","format":"date-time","title":"Updated At","description":"Last update timestamp"},"layout_template_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Layout Template Name","description":"Template name"}},"type":"object","required":["id","tenant_id","name","is_valid","version","created_at","updated_at"],"title":"EmailContentListItem","description":"Email content list item (without full MJML source or text_body)"},"EmailContentPreviewRequest":{"properties":{"variables":{"additionalProperties":true,"type":"object","title":"Variables","description":"Test variables to render (e.g., {'contact': {'name': 'John'}})"}},"type":"object","title":"EmailContentPreviewRequest","description":"Schema for previewing email content with test variables","example":{"variables":{"company":{"name":"Acme Corp"},"contact":{"email":"john@example.com","name":"John Doe"},"cta_url":"https://example.com/get-started"}}},"EmailContentPreviewResponse":{"properties":{"html_content":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Html Content","description":"Compiled and rendered HTML (html type)"},"text_content":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Text Content","description":"Rendered plain text (plain_text type)"},"subject_line":{"type":"string","title":"Subject Line","description":"Rendered subject line"},"variables_used":{"items":{"type":"string"},"type":"array","title":"Variables Used","description":"Variables found in content"},"content_type":{"type":"string","title":"Content Type","description":"Content type of the previewed content","default":"html"}},"type":"object","required":["subject_line","variables_used"],"title":"EmailContentPreviewResponse","description":"Schema for email content preview response","example":{"content_type":"html","html_content":"<html><body><h1>Welcome John Doe!</h1></body></html>","subject_line":"Welcome to Acme Corp, John Doe!","variables_used":["contact.name","company.name","cta_url"]}},"EmailContentResponse":{"properties":{"id":{"type":"string","title":"Id","description":"Unique identifier (UUID)"},"tenant_id":{"type":"string","title":"Tenant Id","description":"Tenant this resource belongs to"},"name":{"type":"string","title":"Name","description":"Content name"},"description":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Description","description":"Content description"},"content_type":{"type":"string","title":"Content Type","description":"Content type: 'html' or 'plain_text'","default":"html"},"template_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Template Id","description":"Email template ID"},"subject_line":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Subject Line","description":"Email subject line"},"preview_text":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Preview Text","description":"Preview text"},"from_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"From Name","description":"Sender name"},"from_email":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"From Email","description":"Sender email"},"editor_type":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Editor Type","description":"Editor type: 'stripo' or 'mjml' (legacy)","default":"stripo"},"mjml_source":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Mjml Source","description":"Full MJML content (legacy, html type only)"},"compiled_html":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Compiled Html","description":"Compiled HTML content from Stripo or MJML compilation"},"stripo_design_json":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Stripo Design Json","description":"Stripo editor design JSON for re-opening designs"},"text_body":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Text Body","description":"Plain text template content (plain_text type only)"},"variable_values":{"anyOf":[{"additionalProperties":{"type":"string"},"type":"object"},{"type":"null"}],"title":"Variable Values","description":"Variable values"},"variables_used":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Variables Used","description":"Extracted variables"},"footer_mode":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Footer Mode","description":"Compliance footer override for this email: `inherit` (default — use the account setting), `both` (unsubscribe + manage preferences), `unsubscribe_only`, or `none` (no visible footer, for transactional mail). RFC 8058 List-Unsubscribe headers are sent regardless."},"ai_generated":{"type":"boolean","title":"Ai Generated","description":"AI-generated flag","default":false},"ai_prompt":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Ai Prompt","description":"AI prompt"},"ai_model_used":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Ai Model Used","description":"AI model"},"ai_generation_cost_cents":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Ai Generation Cost Cents","description":"AI cost"},"is_system":{"type":"boolean","title":"Is System","description":"System template flag","default":false},"is_valid":{"type":"boolean","title":"Is Valid","description":"Validation status"},"validation_errors":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Validation Errors","description":"Validation errors"},"version":{"type":"integer","title":"Version","description":"Content version"},"created_by":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Created By","description":"Creator user ID"},"created_at":{"type":"string","format":"date-time","title":"Created At","description":"Creation timestamp"},"updated_at":{"type":"string","format":"date-time","title":"Updated At","description":"Last update timestamp"},"layout_template":{"anyOf":[{"$ref":"#/components/schemas/LayoutTemplateInfo"},{"type":"null"}],"description":"Layout template info"}},"type":"object","required":["id","tenant_id","name","is_valid","version","created_at","updated_at"],"title":"EmailContentResponse","description":"Full email content response with variable system","example":{"ai_generated":false,"content_type":"html","created_at":"2025-11-17T10:00:00Z","created_by":"auth0|user123","description":"Welcome email for new subscribers","id":"content-123","is_system":false,"is_valid":true,"layout_template":{"category":"newsletter","id":"layout-789","name":"Professional Layout"},"mjml_source":"<mjml><mj-body><mj-text>Hello {{hero_title}}</mj-text></mj-body></mjml>","name":"Welcome Email - November 2025","preview_text":"Get started with your new account","subject_line":"Welcome to {{company.name}}, {{contact.name}}!","template_id":"template-789","tenant_id":"tenant-456","updated_at":"2025-11-17T10:00:00Z","variable_values":{"cta_url":"https://example.com/get-started","hero_title":"Welcome to Our Platform!"},"variables_used":["hero_title","contact.name","company.name","cta_url"],"version":1}},"EmailContentTypeEnum":{"type":"string","enum":["html","plain_text"],"title":"EmailContentTypeEnum","description":"Content type for email templates"},"EmailContentUpdate":{"properties":{"name":{"anyOf":[{"type":"string","maxLength":255,"minLength":1},{"type":"null"}],"title":"Name"},"description":{"anyOf":[{"type":"string","maxLength":1000},{"type":"null"}],"title":"Description"},"template_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Template Id"},"subject_line":{"anyOf":[{"type":"string","maxLength":255},{"type":"null"}],"title":"Subject Line"},"preview_text":{"anyOf":[{"type":"string","maxLength":150},{"type":"null"}],"title":"Preview Text"},"from_name":{"anyOf":[{"type":"string","maxLength":100},{"type":"null"}],"title":"From Name"},"from_email":{"anyOf":[{"type":"string","maxLength":255},{"type":"null"}],"title":"From Email"},"editor_type":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Editor Type","description":"Editor type: 'stripo' or 'mjml'"},"mjml_source":{"anyOf":[{"type":"string","minLength":1},{"type":"null"}],"title":"Mjml Source"},"stripo_design_json":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Stripo Design Json","description":"Stripo editor design JSON"},"compiled_html":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Compiled Html","description":"Pre-compiled HTML from Stripo export"},"text_body":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Text Body","description":"Plain text template content with {{variable}} placeholders"},"variable_values":{"anyOf":[{"additionalProperties":{"type":"string"},"type":"object"},{"type":"null"}],"title":"Variable Values"},"footer_mode":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Footer Mode","description":"Compliance footer override for this email: `inherit` (default — use the account setting), `both` (unsubscribe + manage preferences), `unsubscribe_only`, or `none` (no visible footer, for transactional mail). RFC 8058 List-Unsubscribe headers are sent regardless."}},"type":"object","title":"EmailContentUpdate","description":"Schema for updating email content with variable system.\n\nNote: content_type cannot be changed after creation.\nFor HTML type, update mjml_source. For plain_text type, update text_body.","example":{"mjml_source":"<mjml><mj-body><mj-text>Updated {{hero_title}}</mj-text></mj-body></mjml>","name":"Updated Content Name","subject_line":"New subject line with {{contact.name}}","variable_values":{"hero_title":"New Title"}}},"EmailIntegrationCreate":{"properties":{"name":{"type":"string","maxLength":255,"minLength":1,"title":"Name","description":"Integration name"},"email_address":{"type":"string","format":"email","title":"Email Address","description":"Email address for this integration"},"integration_type":{"$ref":"#/components/schemas/EmailIntegrationType","description":"Type of integration (gmail, smtp_imap, sendgrid, ses_managed, proxy). Mailgun is a BYOK email provider configured via the email-providers flow, not an integration type."},"smtp_config":{"anyOf":[{"$ref":"#/components/schemas/SmtpConfig"},{"type":"null"}]},"imap_config":{"anyOf":[{"$ref":"#/components/schemas/ImapConfig"},{"type":"null"}]},"ses_domain":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Ses Domain","description":"Domain for SES verification"},"ses_reputation_policy":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Ses Reputation Policy","description":"SES reputation policy","default":"standard"},"proxy_forward_to":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Proxy Forward To","description":"Email to forward proxy emails to"},"outbound_mailbox_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Outbound Mailbox Id","description":"ID of the EmailMailbox used to send proxy replies (must belong to the tenant and have can_send_support=True). This is the user-facing outbound selector for proxies."},"outbound_provider_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Outbound Provider Id","description":"DEPRECATED: dormant/backward-compat only. Use outbound_mailbox_id for proxy outbound. No longer used to drive proxy sending."},"recipient_validation_mode":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Recipient Validation Mode","description":"Recipient validation mode: 'any', 'strict', or 'whitelist'"},"accepted_aliases":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Accepted Aliases","description":"Additional email aliases that are accepted"},"default_signature":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Default Signature","description":"Default email signature"},"include_signature":{"type":"boolean","title":"Include Signature","description":"Include signature in outgoing emails","default":true},"initial_sync_hours":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Initial Sync Hours","description":"How many hours back to pull emails on first connection. Options: 0 (from now only), 24 (1 day), 72 (3 days), 168 (1 week), 336 (2 weeks), 720 (1 month)","default":24}},"type":"object","required":["name","email_address","integration_type"],"title":"EmailIntegrationCreate","description":"Create a new email integration"},"EmailIntegrationCreateResponse":{"properties":{"id":{"type":"string","title":"Id"},"tenant_id":{"type":"string","title":"Tenant Id"},"name":{"type":"string","title":"Name"},"email_address":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Email Address"},"oauth_email":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Oauth Email"},"integration_type":{"type":"string","title":"Integration Type"},"is_active":{"type":"boolean","title":"Is Active"},"connection_status":{"type":"string","title":"Connection Status"},"ses_tenant_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Ses Tenant Name"},"ses_configuration_set":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Ses Configuration Set"},"ses_domain":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Ses Domain"},"ses_reputation_policy":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Ses Reputation Policy"},"dns_verification_status":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Dns Verification Status"},"dns_records":{"items":{"$ref":"#/components/schemas/SesSetupDnsRecord"},"type":"array","title":"Dns Records"},"setup_message":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Setup Message"},"created_at":{"type":"string","format":"date-time","title":"Created At"}},"type":"object","required":["id","tenant_id","name","integration_type","is_active","connection_status","created_at"],"title":"EmailIntegrationCreateResponse","description":"Response for create integration - includes DNS records for SES"},"EmailIntegrationResponse":{"properties":{"created_at":{"type":"string","title":"Created At","description":"When the resource was created (UTC)"},"updated_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Updated At","description":"When the resource was last updated (UTC)"},"id":{"type":"string","title":"Id","description":"Unique identifier (UUID)"},"tenant_id":{"type":"string","title":"Tenant Id","description":"Tenant this resource belongs to"},"name":{"type":"string","title":"Name"},"email_address":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Email Address"},"oauth_email":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Oauth Email"},"integration_type":{"type":"string","title":"Integration Type"},"is_active":{"type":"boolean","title":"Is Active"},"connection_status":{"type":"string","title":"Connection Status"},"last_error":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Last Error"},"last_error_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Last Error At"},"smtp_host":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Smtp Host"},"smtp_port":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Smtp Port"},"smtp_username":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Smtp Username"},"smtp_use_tls":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Smtp Use Tls"},"imap_host":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Imap Host"},"imap_port":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Imap Port"},"imap_username":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Imap Username"},"imap_use_ssl":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Imap Use Ssl"},"ses_tenant_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Ses Tenant Name"},"ses_domain":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Ses Domain"},"ses_sending_status":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Ses Sending Status"},"ses_reputation_policy":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Ses Reputation Policy"},"dns_verification_status":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Dns Verification Status"},"proxy_address":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Proxy Address"},"proxy_forward_to":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Proxy Forward To"},"outbound_mailbox_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Outbound Mailbox Id"},"outbound_mailbox_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Outbound Mailbox Name"},"outbound_provider_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Outbound Provider Id"},"accepted_aliases":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Accepted Aliases"},"recipient_validation_mode":{"type":"string","title":"Recipient Validation Mode","default":"any"},"default_signature":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Default Signature"},"include_signature":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Include Signature"},"sync_enabled":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Sync Enabled"},"last_sync_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Last Sync At"},"initial_sync_hours":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Initial Sync Hours"}},"type":"object","required":["created_at","id","tenant_id","name","integration_type","is_active","connection_status"],"title":"EmailIntegrationResponse","description":"Response schema for email integration"},"EmailIntegrationType":{"type":"string","enum":["gmail","smtp_imap","sendgrid","ses_managed","proxy"],"title":"EmailIntegrationType","description":"Types of email integrations"},"EmailIntegrationUpdate":{"properties":{"name":{"anyOf":[{"type":"string","maxLength":255,"minLength":1},{"type":"null"}],"title":"Name"},"is_active":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Is Active"},"smtp_config":{"anyOf":[{"$ref":"#/components/schemas/SmtpConfig"},{"type":"null"}]},"imap_config":{"anyOf":[{"$ref":"#/components/schemas/ImapConfig"},{"type":"null"}]},"default_signature":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Default Signature"},"include_signature":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Include Signature"},"sync_enabled":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Sync Enabled"},"proxy_forward_to":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Proxy Forward To","description":"Your email address where proxy emails are forwarded to (e.g., support@yourcompany.com)"},"outbound_mailbox_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Outbound Mailbox Id","description":"ID of the EmailMailbox used to send proxy replies (must belong to the tenant and have can_send_support=True). This is the user-facing outbound selector for proxies."},"outbound_provider_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Outbound Provider Id","description":"DEPRECATED: dormant/backward-compat only. Use outbound_mailbox_id for proxy outbound. No longer used to drive proxy sending."},"accepted_aliases":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Accepted Aliases","description":"Additional email aliases that are accepted (e.g., dev@yourcompany.com, info@yourcompany.com)"},"recipient_validation_mode":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Recipient Validation Mode","description":"Recipient validation mode: 'any' (accept all), 'strict' (only proxy_forward_to), 'whitelist' (proxy_forward_to + accepted_aliases)"},"initial_sync_hours":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Initial Sync Hours","description":"How many hours back to pull emails on first connection. Options: 0 (from now only), 24 (1 day), 72 (3 days), 168 (1 week), 336 (2 weeks), 720 (1 month)"}},"type":"object","title":"EmailIntegrationUpdate","description":"Update an existing email integration"},"EmailLayoutPreviewResponse":{"properties":{"html":{"type":"string","title":"Html","description":"Compiled HTML from MJML source"},"has_compliance_footer":{"type":"boolean","title":"Has Compliance Footer","description":"Whether compliance footer was injected (promotional category)","default":false}},"type":"object","required":["html"],"title":"EmailLayoutPreviewResponse","description":"Response from previewing MJML template","example":{"has_compliance_footer":true,"html":"<html><body>...</body></html>"}},"EmailLayoutTemplateCreate":{"properties":{"name":{"type":"string","maxLength":255,"minLength":1,"title":"Name","description":"Email template name"},"description":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Description","description":"Template description"},"category":{"anyOf":[{"type":"string","maxLength":100},{"type":"null"}],"title":"Category","description":"Category (transactional, promotional, custom)"},"mjml_source":{"type":"string","minLength":1,"title":"Mjml Source","description":"MJML source with {{variable_name}} placeholders"},"color_palette":{"anyOf":[{"additionalProperties":{"type":"string"},"type":"object"},{"type":"null"}],"title":"Color Palette","description":"Color palette (e.g., {'primary': '#0066CC'})"},"font_family":{"anyOf":[{"type":"string","maxLength":100},{"type":"null"}],"title":"Font Family","description":"Font family (e.g., 'Arial, sans-serif')"},"thumbnail_url":{"anyOf":[{"type":"string","maxLength":500},{"type":"null"}],"title":"Thumbnail Url","description":"Template preview thumbnail URL"}},"type":"object","required":["name","mjml_source"],"title":"EmailLayoutTemplateCreate","description":"Schema for creating an email template with variable system","example":{"category":"promotional","color_palette":{"primary":"#0066CC","secondary":"#333333"},"description":"Clean template with variable placeholders for customization","font_family":"Arial, sans-serif","mjml_source":"<mjml><mj-body><mj-text>{{hero_title}}</mj-text><mj-button href='{{cta_url}}'>{{cta_text}}</mj-button></mj-body></mjml>","name":"Professional Newsletter Template","thumbnail_url":"https://storage.example.com/templates/preview.png"}},"EmailLayoutTemplateDuplicate":{"properties":{"new_name":{"type":"string","maxLength":255,"minLength":1,"title":"New Name","description":"Name for the duplicated template"}},"type":"object","required":["new_name"],"title":"EmailLayoutTemplateDuplicate","description":"Schema for duplicating an email template","example":{"new_name":"My Custom Template (Copy)"}},"EmailLayoutTemplateListResponse":{"properties":{"id":{"type":"string","title":"Id"},"tenant_id":{"type":"string","title":"Tenant Id"},"name":{"type":"string","title":"Name"},"description":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Description"},"category":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Category"},"thumbnail_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Thumbnail Url"},"is_system_template":{"type":"boolean","title":"Is System Template"},"is_active":{"type":"boolean","title":"Is Active","default":true},"is_hidden":{"type":"boolean","title":"Is Hidden","default":false},"version":{"type":"integer","title":"Version"},"created_at":{"type":"string","format":"date-time","title":"Created At"},"updated_at":{"type":"string","format":"date-time","title":"Updated At"}},"type":"object","required":["id","tenant_id","name","description","category","thumbnail_url","is_system_template","version","created_at","updated_at"],"title":"EmailLayoutTemplateListResponse","description":"Schema for email template list (without full content)"},"EmailLayoutTemplateResponse":{"properties":{"id":{"type":"string","title":"Id"},"tenant_id":{"type":"string","title":"Tenant Id"},"name":{"type":"string","title":"Name"},"description":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Description"},"category":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Category"},"mjml_source":{"type":"string","title":"Mjml Source"},"html_compiled":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Html Compiled"},"color_palette":{"anyOf":[{"additionalProperties":{"type":"string"},"type":"object"},{"type":"null"}],"title":"Color Palette"},"font_family":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Font Family"},"thumbnail_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Thumbnail Url"},"is_system_template":{"type":"boolean","title":"Is System Template"},"version":{"type":"integer","title":"Version"},"created_by":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Created By"},"created_at":{"type":"string","format":"date-time","title":"Created At"},"updated_at":{"type":"string","format":"date-time","title":"Updated At"}},"type":"object","required":["id","tenant_id","name","description","category","mjml_source","html_compiled","color_palette","font_family","thumbnail_url","is_system_template","version","created_by","created_at","updated_at"],"title":"EmailLayoutTemplateResponse","description":"Schema for email template response with variable system","example":{"category":"promotional","color_palette":{"primary":"#0066CC"},"created_at":"2025-11-17T10:00:00Z","created_by":"auth0|user123","description":"Clean template with variable placeholders","font_family":"Arial, sans-serif","html_compiled":"<html>...</html>","id":"template-123","is_system_template":false,"mjml_source":"<mjml><mj-body><mj-text>{{hero_title}}</mj-text><mj-button href='{{cta_url}}'>{{cta_text}}</mj-button></mj-body></mjml>","name":"Professional Newsletter Template","tenant_id":"tenant-456","thumbnail_url":"https://storage.example.com/templates/preview.png","updated_at":"2025-11-17T10:00:00Z","version":1}},"EmailLayoutTemplateUpdate":{"properties":{"name":{"anyOf":[{"type":"string","maxLength":255,"minLength":1},{"type":"null"}],"title":"Name"},"description":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Description"},"category":{"anyOf":[{"type":"string","maxLength":100},{"type":"null"}],"title":"Category"},"mjml_source":{"anyOf":[{"type":"string","minLength":1},{"type":"null"}],"title":"Mjml Source"},"color_palette":{"anyOf":[{"additionalProperties":{"type":"string"},"type":"object"},{"type":"null"}],"title":"Color Palette"},"font_family":{"anyOf":[{"type":"string","maxLength":100},{"type":"null"}],"title":"Font Family"},"thumbnail_url":{"anyOf":[{"type":"string","maxLength":500},{"type":"null"}],"title":"Thumbnail Url"}},"type":"object","title":"EmailLayoutTemplateUpdate","description":"Schema for updating an email template with variable system","example":{"category":"transactional","description":"Updated description","name":"Updated Template Name"}},"EmailMailboxCreate":{"properties":{"name":{"type":"string","maxLength":255,"minLength":1,"title":"Name","description":"Display name for the mailbox (e.g. 'Support', 'Sales')"},"display_name":{"anyOf":[{"type":"string","maxLength":255},{"type":"null"}],"title":"Display Name","description":"From name shown to email recipients. When set, overrides 'name' for sending. When null, 'name' is used."},"email_address":{"type":"string","format":"email","title":"Email Address","description":"Email address for this mailbox"},"email_integration_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Email Integration Id","description":"Link to an email integration (Gmail, SMTP, SES, Proxy)"},"email_provider_config_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Email Provider Config Id","description":"Link to an email provider config (SendGrid, Mailgun)"},"is_default":{"type":"boolean","title":"Is Default","description":"Whether this is the default mailbox for the tenant","default":false},"can_send_campaigns":{"type":"boolean","title":"Can Send Campaigns","description":"Whether this mailbox can be used for email campaigns","default":false},"can_send_support":{"type":"boolean","title":"Can Send Support","description":"Whether this mailbox can be used for support replies","default":true},"include_thread_history":{"type":"boolean","title":"Include Thread History","description":"Default for outbound replies from this mailbox: when True, the quoted prior-thread history block is appended beneath the reply. Can be overridden per-send.","default":true},"access_mode":{"type":"string","title":"Access Mode","description":"Who can use this mailbox: 'all' (any team member) or 'specific' (only designated users)","default":"all"},"allowed_user_ids":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Allowed User Ids","description":"User IDs allowed to use this mailbox (only when access_mode='specific')"}},"type":"object","required":["name","email_address"],"title":"EmailMailboxCreate","description":"Schema for creating a new email mailbox.","example":{"access_mode":"all","can_send_campaigns":false,"can_send_support":true,"email_address":"support@company.com","email_integration_id":"integration-uuid","is_default":true,"name":"Support"}},"EmailMailboxInfo":{"properties":{"id":{"type":"string","title":"Id","description":"Mailbox UUID"},"email_address":{"type":"string","title":"Email Address","description":"Mailbox email address"},"name":{"type":"string","title":"Name","description":"Mailbox display name"}},"type":"object","required":["id","email_address","name"],"title":"EmailMailboxInfo","description":"Compact mailbox info embedded in AvailableChannelResponse for email channels."},"EmailMailboxListResponse":{"properties":{"items":{"items":{"$ref":"#/components/schemas/EmailMailboxResponse"},"type":"array","title":"Items","description":"List of items for this page"},"pagination":{"$ref":"#/components/schemas/PaginationMeta","description":"Pagination metadata"}},"type":"object","required":["items","pagination"],"title":"EmailMailboxListResponse","description":"Paginated list of email mailboxes."},"EmailMailboxResponse":{"properties":{"created_at":{"type":"string","title":"Created At","description":"When the resource was created (UTC)"},"updated_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Updated At","description":"When the resource was last updated (UTC)"},"id":{"type":"string","title":"Id","description":"Unique identifier (UUID)"},"tenant_id":{"type":"string","title":"Tenant Id","description":"Tenant this resource belongs to"},"name":{"type":"string","title":"Name","description":"Display name"},"display_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Display Name","description":"From name shown to email recipients. Overrides 'name' for sending when set."},"email_address":{"type":"string","title":"Email Address","description":"Email address"},"email_integration_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Email Integration Id","description":"Linked email integration ID"},"email_provider_config_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Email Provider Config Id","description":"Linked email provider config ID"},"integration_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Integration Name","description":"Name of linked email integration"},"provider_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Provider Name","description":"Name of linked email provider config"},"is_active":{"type":"boolean","title":"Is Active","description":"Whether the mailbox is active"},"is_default":{"type":"boolean","title":"Is Default","description":"Whether this is the default mailbox"},"can_send_campaigns":{"type":"boolean","title":"Can Send Campaigns","description":"Whether this mailbox can be used for email campaigns","default":false},"can_send_support":{"type":"boolean","title":"Can Send Support","description":"Whether this mailbox can be used for support replies","default":true},"include_thread_history":{"type":"boolean","title":"Include Thread History","description":"Default for outbound replies: append the quoted prior-thread history block","default":true},"access_mode":{"type":"string","title":"Access Mode","description":"Who can use this mailbox: 'all' or 'specific'","default":"all"},"allowed_user_ids":{"items":{"type":"string"},"type":"array","title":"Allowed User Ids","description":"User IDs allowed to use this mailbox"}},"type":"object","required":["created_at","id","tenant_id","name","email_address","is_active","is_default"],"title":"EmailMailboxResponse","description":"Full email mailbox in API responses.","example":{"access_mode":"all","allowed_user_ids":[],"can_send_campaigns":false,"can_send_support":true,"created_at":"2026-02-20T10:00:00Z","email_address":"support@company.com","email_integration_id":"integration-uuid","id":"mailbox-uuid","integration_name":"Gmail - Support","is_active":true,"is_default":true,"name":"Support","tenant_id":"tenant-uuid","updated_at":"2026-02-20T10:00:00Z"}},"EmailMailboxUpdate":{"properties":{"name":{"anyOf":[{"type":"string","maxLength":255,"minLength":1},{"type":"null"}],"title":"Name","description":"Display name for the mailbox"},"display_name":{"anyOf":[{"type":"string","maxLength":255},{"type":"null"}],"title":"Display Name","description":"From name shown to email recipients. When set, overrides 'name' for sending. Set to empty string to clear."},"email_address":{"anyOf":[{"type":"string","format":"email"},{"type":"null"}],"title":"Email Address","description":"Email address for this mailbox"},"is_active":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Is Active","description":"Whether the mailbox is active"},"is_default":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Is Default","description":"Whether this is the default mailbox"},"can_send_campaigns":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Can Send Campaigns","description":"Whether this mailbox can be used for email campaigns"},"can_send_support":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Can Send Support","description":"Whether this mailbox can be used for support replies"},"include_thread_history":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Include Thread History","description":"Default for outbound replies from this mailbox: when True, the quoted prior-thread history block is appended beneath the reply."},"access_mode":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Access Mode","description":"Who can use this mailbox: 'all' or 'specific'"},"allowed_user_ids":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Allowed User Ids","description":"User IDs allowed to use this mailbox (only when access_mode='specific')"}},"type":"object","title":"EmailMailboxUpdate","description":"Schema for updating an email mailbox (all fields optional).","example":{"access_mode":"specific","allowed_user_ids":["user-uuid-1","user-uuid-2"],"can_send_campaigns":true,"is_default":true,"name":"Updated Support"}},"EmailMessageDetailResponse":{"properties":{"id":{"type":"string","title":"Id"},"message_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Message Id"},"thread_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Thread Id"},"direction":{"type":"string","title":"Direction"},"from_email":{"type":"string","title":"From Email"},"from_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"From Name"},"to_emails":{"items":{"$ref":"#/components/schemas/EmailAddress"},"type":"array","title":"To Emails"},"cc_emails":{"items":{"$ref":"#/components/schemas/EmailAddress"},"type":"array","title":"Cc Emails"},"bcc_emails":{"items":{"$ref":"#/components/schemas/EmailAddress"},"type":"array","title":"Bcc Emails"},"reply_to":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Reply To"},"subject":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Subject"},"text_body":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Text Body"},"html_body":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Html Body"},"raw_headers":{"anyOf":[{"additionalProperties":{"type":"string"},"type":"object"},{"type":"null"}],"title":"Raw Headers"},"attachments":{"items":{"$ref":"#/components/schemas/AttachmentInfo"},"type":"array","title":"Attachments"},"status":{"type":"string","title":"Status"},"error_message":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Error Message"},"sent_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Sent At"},"received_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Received At"},"created_at":{"type":"string","title":"Created At"}},"type":"object","required":["id","direction","from_email","status","created_at"],"title":"EmailMessageDetailResponse","description":"Detailed email message response including full HTML"},"EmailMessageResponse":{"properties":{"id":{"type":"string","title":"Id"},"direction":{"type":"string","title":"Direction"},"status":{"type":"string","title":"Status"},"from_email":{"type":"string","title":"From Email"},"from_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"From Name"},"subject":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Subject"},"is_hidden":{"type":"boolean","title":"Is Hidden","default":false},"created_at":{"type":"string","format":"date-time","title":"Created At"}},"type":"object","required":["id","direction","status","from_email","created_at"],"title":"EmailMessageResponse","description":"Response schema for email message"},"EmailProviderConfigCreate":{"properties":{"provider_type":{"$ref":"#/components/schemas/ProviderType","description":"Provider type (sendgrid, mailgun)"},"name":{"type":"string","maxLength":255,"minLength":1,"title":"Name","description":"User-friendly name"},"api_key":{"type":"string","minLength":1,"title":"Api Key","description":"API key for provider"},"config":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Config","description":"Provider-specific settings"},"is_active":{"type":"boolean","title":"Is Active","description":"Whether provider is active","default":true},"is_default":{"type":"boolean","title":"Is Default","description":"Set as default provider for tenant","default":false}},"type":"object","required":["provider_type","name","api_key"],"title":"EmailProviderConfigCreate","description":"Schema for creating email provider configuration","example":{"api_key":"SG.xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx","config":{"from_email":"marketing@example.com","from_name":"Example Company"},"is_active":true,"is_default":true,"name":"Marketing Emails","provider_type":"sendgrid"}},"EmailProviderConfigResponse":{"properties":{"id":{"type":"string","title":"Id"},"tenant_id":{"type":"string","title":"Tenant Id"},"provider_type":{"$ref":"#/components/schemas/ProviderType"},"name":{"type":"string","title":"Name"},"api_key_preview":{"type":"string","title":"Api Key Preview","description":"Masked API key (last 4 chars only)"},"config":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Config"},"verified_domains":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Verified Domains","description":"List of verified domains from provider"},"key_status":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Key Status","description":"BYOK API-key health, mirrored from config.key_status: {valid: bool|null, error: str|null, checked_at: iso}. valid=false -> show a 'reconnect key' banner. null field -> never checked."},"is_active":{"type":"boolean","title":"Is Active"},"is_default":{"type":"boolean","title":"Is Default"},"webhook_configured":{"type":"boolean","title":"Webhook Configured","description":"Whether webhooks are configured","default":false},"created_at":{"type":"string","format":"date-time","title":"Created At"},"updated_at":{"type":"string","format":"date-time","title":"Updated At"}},"type":"object","required":["id","tenant_id","provider_type","name","api_key_preview","is_active","is_default","created_at","updated_at"],"title":"EmailProviderConfigResponse","description":"Schema for email provider configuration response","example":{"api_key_preview":"****xxxx","config":{"from_email":"marketing@example.com","from_name":"Example Company"},"created_at":"2024-01-01T12:00:00Z","id":"prov-123","is_active":true,"is_default":true,"name":"Marketing Emails","provider_type":"sendgrid","tenant_id":"tenant-456","updated_at":"2024-01-01T12:00:00Z"}},"EmailProviderConfigUpdate":{"properties":{"name":{"anyOf":[{"type":"string","maxLength":255,"minLength":1},{"type":"null"}],"title":"Name"},"api_key":{"anyOf":[{"type":"string","minLength":1},{"type":"null"}],"title":"Api Key"},"config":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Config"},"is_active":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Is Active"},"is_default":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Is Default"}},"type":"object","title":"EmailProviderConfigUpdate","description":"Schema for updating email provider configuration"},"EmailReplyRequest":{"properties":{"text_body":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Text Body","description":"Plain text body of reply"},"html_body":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Html Body","description":"HTML body of reply (for rich text)"},"cc_emails":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Cc Emails","description":"CC recipients"},"bcc_emails":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Bcc Emails","description":"BCC recipients"},"preserve_cc":{"type":"boolean","title":"Preserve Cc","description":"Auto-include original CC recipients","default":true},"attachment_ids":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Attachment Ids","description":"IDs of attachments to include"},"to_emails":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"To Emails","description":"Explicit recipient override; when omitted, defaults to the original message's Reply-To or From address. When provided and non-empty, these addresses are used exactly (reply-all expansion is not applied on top)."},"reply_all":{"type":"boolean","title":"Reply All","description":"Reply to all original recipients","default":false},"subject_override":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Subject Override","description":"Override reply subject (default: Re: original)"},"include_thread_history":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Include Thread History","description":"Override whether to append the quoted prior-thread history block beneath this reply. When omitted/null, the owning mailbox's include_thread_history default (TRUE) is used."},"sender_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Sender Id","description":"Override the connection used to send this reply. Must be one of the ids returned by GET /api/v1/conversations/{conversation_id}/sender-options."}},"type":"object","title":"EmailReplyRequest","description":"Request body for sending an email reply"},"EmailReplyResponse":{"properties":{"email_message_id":{"type":"string","title":"Email Message Id","description":"ID of the created email message"},"message_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Message Id","description":"RFC 5322 Message-ID"},"status":{"type":"string","title":"Status","description":"Message status (queued, sent, etc.)"},"sent_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Sent At"},"to_emails":{"items":{"type":"string"},"type":"array","title":"To Emails"},"cc_emails":{"items":{"type":"string"},"type":"array","title":"Cc Emails"},"bcc_emails":{"items":{"type":"string"},"type":"array","title":"Bcc Emails"}},"type":"object","required":["email_message_id","status"],"title":"EmailReplyResponse","description":"Response after sending an email reply"},"EmailSendNextBatchResponse":{"properties":{"batch_number":{"type":"integer","title":"Batch Number","description":"Number of the batch that is now sending (1-based)"},"armed":{"type":"integer","title":"Armed","description":"Recipients released for this batch"},"skipped_ineligible":{"type":"integer","title":"Skipped Ineligible","description":"Recipients skipped because they opted out, unsubscribed or are blocked since the campaign was prepared","default":0},"remaining":{"type":"integer","title":"Remaining","description":"Recipients still waiting for a later batch","default":0},"campaign":{"$ref":"#/components/schemas/EmailCampaignResponse"}},"type":"object","required":["batch_number","armed","campaign"],"title":"EmailSendNextBatchResponse","description":"Response for ``POST /email-campaigns/{id}/send-next-batch``."},"EmailSettingsResetResponse":{"properties":{"message":{"type":"string","title":"Message","description":"Success message"}},"type":"object","required":["message"],"title":"EmailSettingsResetResponse","description":"Response from resetting email settings to defaults","example":{"message":"Email settings reset to defaults"}},"EmailSettingsResponse":{"properties":{"default_from_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Default From Name"},"default_from_email":{"anyOf":[{"type":"string","format":"email"},{"type":"null"}],"title":"Default From Email"},"default_reply_to":{"anyOf":[{"type":"string","format":"email"},{"type":"null"}],"title":"Default Reply To"},"double_opt_in_enabled":{"type":"boolean","title":"Double Opt In Enabled","default":true},"default_double_opt_in_template_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Default Double Opt In Template Id"},"compliance_footer_html":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Compliance Footer Html"},"footer_mode":{"type":"string","title":"Footer Mode","default":"both"},"unsubscribe_link_format":{"type":"string","title":"Unsubscribe Link Format","default":""},"has_email_provider":{"type":"boolean","title":"Has Email Provider","description":"Whether tenant has configured an email provider","default":false},"verified_domains":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Verified Domains","description":"List of verified domains from default email provider"}},"type":"object","title":"EmailSettingsResponse","description":"Schema for email settings response (includes all settings with defaults)"},"EmailSettingsUpdate":{"properties":{"default_from_name":{"anyOf":[{"type":"string","maxLength":100},{"type":"null"}],"title":"Default From Name","description":"Default sender name for emails"},"default_from_email":{"anyOf":[{"type":"string","format":"email"},{"type":"null"}],"title":"Default From Email","description":"Default sender email address"},"default_reply_to":{"anyOf":[{"type":"string","format":"email"},{"type":"null"}],"title":"Default Reply To","description":"Default reply-to email address"},"double_opt_in_enabled":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Double Opt In Enabled","description":"Require double opt-in for new subscriptions"},"default_double_opt_in_template_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Default Double Opt In Template Id","description":"UUID of default confirmation email template"},"compliance_footer_html":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Compliance Footer Html","description":"HTML footer for compliance (GDPR/CAN-SPAM)"},"footer_mode":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Footer Mode","description":"Which compliance links emails carry by default: `both` (unsubscribe + manage preferences), `unsubscribe_only`, or `none` (no visible footer — transactional senders only; RFC 8058 List-Unsubscribe headers are still sent). Individual emails can override this via the email content's `footer_mode`. `unsubscribe_only` and `none` also disable the hosted preference center for this account."}},"additionalProperties":false,"type":"object","title":"EmailSettingsUpdate","description":"Schema for updating email settings (all fields optional)"},"EmailTemplatePreviewRequest":{"properties":{"email_content_id":{"type":"string","title":"Email Content Id","description":"ID of the EmailContent template to preview"},"variable_overrides":{"anyOf":[{"additionalProperties":{"type":"string"},"type":"object"},{"type":"null"}],"title":"Variable Overrides","description":"Override template variable values for preview"}},"type":"object","required":["email_content_id"],"title":"EmailTemplatePreviewRequest","description":"Request body for previewing an EmailContent template with contact data"},"EmailTemplatePreviewResponse":{"properties":{"html_content":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Html Content","description":"Fully compiled HTML with contact data substituted (html type)"},"text_content":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Text Content","description":"Compiled plain text with contact data substituted (plain_text type)"},"subject_line":{"type":"string","title":"Subject Line","description":"Compiled subject line"},"from_email":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"From Email","description":"Resolved sender email address"},"from_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"From Name","description":"Resolved sender display name"},"variables_used":{"items":{"type":"string"},"type":"array","title":"Variables Used","description":"List of variables in the template"},"content_type":{"type":"string","title":"Content Type","description":"Content type of the previewed template","default":"html"}},"type":"object","required":["subject_line"],"title":"EmailTemplatePreviewResponse","description":"Response containing compiled template preview"},"EmailThreadMessage":{"properties":{"id":{"type":"string","title":"Id"},"message_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Message Id","description":"RFC 5322 Message-ID"},"thread_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Thread Id","description":"Thread ID for grouping"},"in_reply_to":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"In Reply To","description":"Parent Message-ID"},"references":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"References","description":"Reference chain"},"direction":{"type":"string","title":"Direction","description":"inbound or outbound"},"from_email":{"type":"string","title":"From Email"},"from_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"From Name"},"to_emails":{"items":{"$ref":"#/components/schemas/EmailAddress"},"type":"array","title":"To Emails"},"cc_emails":{"anyOf":[{"items":{"$ref":"#/components/schemas/EmailAddress"},"type":"array"},{"type":"null"}],"title":"Cc Emails"},"bcc_emails":{"anyOf":[{"items":{"$ref":"#/components/schemas/EmailAddress"},"type":"array"},{"type":"null"}],"title":"Bcc Emails"},"reply_to":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Reply To"},"subject":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Subject"},"text_body":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Text Body","description":"Plain text body for display"},"html_body":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Html Body","description":"HTML body content"},"attachments":{"items":{"$ref":"#/components/schemas/AttachmentInfo"},"type":"array","title":"Attachments"},"status":{"type":"string","title":"Status","description":"Message status (received, sent, delivered, etc.)"},"sent_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Sent At"},"received_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Received At"},"created_at":{"type":"string","title":"Created At"},"email_metadata":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Email Metadata","description":"JSON metadata including cached translations"}},"type":"object","required":["id","direction","from_email","status","created_at"],"title":"EmailThreadMessage","description":"Single message in an email thread"},"EmailThreadResponse":{"properties":{"conversation_id":{"type":"string","title":"Conversation Id"},"thread_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Thread Id"},"subject":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Subject","description":"Thread subject (from first or most recent message)"},"messages":{"items":{"$ref":"#/components/schemas/EmailThreadMessage"},"type":"array","title":"Messages"},"total_messages":{"type":"integer","title":"Total Messages","description":"Total number of messages in thread","default":0},"original_from":{"anyOf":[{"$ref":"#/components/schemas/EmailAddress"},{"type":"null"}]},"original_to":{"items":{"$ref":"#/components/schemas/EmailAddress"},"type":"array","title":"Original To"},"original_cc":{"items":{"$ref":"#/components/schemas/EmailAddress"},"type":"array","title":"Original Cc"}},"type":"object","required":["conversation_id"],"title":"EmailThreadResponse","description":"Response containing full email thread for a conversation"},"EmailTranscriptRequest":{"properties":{"mailbox_id":{"type":"string","maxLength":36,"minLength":1,"title":"Mailbox Id","description":"EmailMailbox id to send from (must be active and sendable)."},"to_email":{"type":"string","maxLength":320,"minLength":3,"title":"To Email","description":"Recipient email address. Format is validated server-side (400 on invalid)."},"note":{"anyOf":[{"type":"string","maxLength":5000},{"type":"null"}],"title":"Note","description":"Optional free-text note from the agent, shown above the transcript (HTML-escaped; line breaks preserved)."},"save_as_contact_method":{"type":"boolean","title":"Save As Contact Method","description":"If true, store to_email as an EMAIL contact method on the conversation's contact (idempotent — no duplicate if it already exists).","default":false},"language":{"anyOf":[{"type":"string","maxLength":10},{"type":"null"}],"title":"Language","description":"Optional ISO 639-1 language override for the email copy (greeting/intro/subject/labels). Unsupported or missing values fall back to the contact's stored language, then English."}},"type":"object","required":["mailbox_id","to_email"],"title":"EmailTranscriptRequest","description":"Request body for POST /conversations/{id}/email-transcript.\n\nSends the conversation transcript to ``to_email`` as a branded HTML email\nfrom the tenant's selected ``EmailMailbox`` — synchronously, in-request\n(single email, HTML only; no PDF, no queue).","example":{"mailbox_id":"6f2b9a44-1c7e-4b7f-9d1a-2e8c5a7b9c01","note":"Thanks for reaching out — here is everything we discussed.","save_as_contact_method":true,"to_email":"customer@example.com"}},"EmailTranscriptResponse":{"properties":{"success":{"type":"boolean","title":"Success","description":"Whether the email was accepted by the provider","default":true},"to_email":{"type":"string","title":"To Email","description":"Recipient the transcript was sent to"}},"type":"object","required":["to_email"],"title":"EmailTranscriptResponse","description":"Response for a successful transcript email send.","example":{"success":true,"to_email":"customer@example.com"}},"EmailVariablesResponse":{"properties":{"categories":{"additionalProperties":{"$ref":"#/components/schemas/VariableCategory"},"type":"object","title":"Categories","description":"Variable categories (contact, company, campaign, system, custom_fields)"}},"type":"object","required":["categories"],"title":"EmailVariablesResponse","description":"Response containing all available email template variables.\n\nVariables are organized by category for easy discovery in the UI.","example":{"categories":{"contact":{"description":"Recipient contact information","label":"Contact","variables":[{"description":"Contact's full name","example":"John Doe","name":"contact.name","preview_filled":false,"required":false}]}}}},"EntryScoreUpdate":{"properties":{"faq_item_id":{"type":"string","title":"Faq Item Id"},"question":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Question"},"status":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Status"},"quality_score":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Quality Score","description":"NULL = not enough rating mass to judge. Never 0 for 'unrated'."},"positive_mass":{"type":"number","title":"Positive Mass","default":0.0},"negative_mass":{"type":"number","title":"Negative Mass","default":0.0},"rating_count":{"type":"integer","title":"Rating Count","default":0},"verified_by_usage":{"type":"boolean","title":"Verified By Usage","default":false},"retrieval_rank_multiplier":{"type":"number","title":"Retrieval Rank Multiplier","default":1.0},"demoted_to_review":{"type":"boolean","title":"Demoted To Review","default":false}},"type":"object","required":["faq_item_id"],"title":"EntryScoreUpdate","description":"Recomputed score for one FAQ entry after a rating."},"EscalationReasonEnum":{"type":"string","enum":["low_confidence","keyword_match","user_request","user_requested","no_answer","required_fields","agent_takeover","agent_disabled","max_interactions","bot_error"],"title":"EscalationReasonEnum","description":"Reason for bot session escalation"},"EscalationRoutingStrategyEnum":{"type":"string","enum":["notify_only","round_robin","least_busy","round_robin_all","least_busy_all"],"title":"EscalationRoutingStrategyEnum","description":"\"Assignment on handoff\" for a bot escalation (field value ``null`` =\nkeep the conversation unassigned).\n\nBy tag (``escalation_tag_id`` REQUIRED, RoutingService.route_to_tag):\n``notify_only`` tags the conversation and alerts the tag holders;\n``round_robin`` / ``least_busy`` also auto-assign an UNASSIGNED\nconversation to an eligible tag holder.\nAll members (NO tag — a stored tag is cleared, RoutingService.route_to_all):\n``round_robin_all`` / ``least_busy_all``.\nNever reassigns. Values are a frozen cross-agent contract."},"EscalationSettings":{"properties":{"enabled":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Enabled"},"offers_escalation":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Offers Escalation"},"handoff_message":{"anyOf":[{"type":"string","maxLength":2000},{"type":"null"}],"title":"Handoff Message"},"escalation_keywords":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Escalation Keywords"},"offline_escalation_behavior":{"anyOf":[{"$ref":"#/components/schemas/OfflineEscalationBehaviorEnum"},{"type":"null"}]},"offline_message":{"anyOf":[{"type":"string","maxLength":2000},{"type":"null"}],"title":"Offline Message"},"outside_hours_message":{"anyOf":[{"type":"string","maxLength":2000},{"type":"null"}],"title":"Outside Hours Message"},"is_escalation_schedule_enabled":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Is Escalation Schedule Enabled"},"escalation_timezone":{"anyOf":[{"type":"string","maxLength":50},{"type":"null"}],"title":"Escalation Timezone"},"escalation_schedule":{"anyOf":[{"items":{"$ref":"#/components/schemas/TimeRangeConfig"},"type":"array"},{"type":"null"}],"title":"Escalation Schedule"},"escalation_schedule_mode":{"anyOf":[{"type":"string","enum":["workspace","custom"]},{"type":"null"}],"title":"Escalation Schedule Mode"}},"type":"object","title":"EscalationSettings","description":"Bot-row escalation fields (same names/semantics as the bot API)."},"EscalationSettingsResponse":{"properties":{"enabled":{"type":"boolean","title":"Enabled","default":true},"offers_escalation":{"type":"boolean","title":"Offers Escalation","default":true},"handoff_message":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Handoff Message"},"escalation_keywords":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Escalation Keywords"},"offline_escalation_behavior":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Offline Escalation Behavior","default":"leave_message"},"offline_message":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Offline Message"},"outside_hours_message":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Outside Hours Message"},"is_escalation_schedule_enabled":{"type":"boolean","title":"Is Escalation Schedule Enabled","default":false},"escalation_timezone":{"type":"string","title":"Escalation Timezone","default":"UTC"},"escalation_schedule":{"anyOf":[{"items":{"additionalProperties":true,"type":"object"},"type":"array"},{"type":"null"}],"title":"Escalation Schedule"},"escalation_schedule_mode":{"type":"string","enum":["workspace","custom"],"title":"Escalation Schedule Mode","default":"workspace"}},"type":"object","title":"EscalationSettingsResponse"},"EstimateRequest":{"properties":{"description":{"anyOf":[{"type":"string","maxLength":4000},{"type":"null"}],"title":"Description"}},"type":"object","title":"EstimateRequest"},"EstimateResponse":{"properties":{"dynamic_allowed":{"type":"boolean","title":"Dynamic Allowed","description":"Plan allows dynamic flows (Scale+). False = the AI builds static flows only"},"credits":{"type":"integer","title":"Credits","description":"AI credits charged for ONE successful generation"},"price_euros":{"type":"string","title":"Price Euros"},"billable":{"type":"boolean","title":"Billable"},"charged_on":{"type":"string","title":"Charged On","description":"Credits are only charged when the result is valid","default":"valid_generation"},"refine_credits":{"type":"integer","title":"Refine Credits"},"max_repair_rounds":{"type":"integer","title":"Max Repair Rounds"}},"type":"object","required":["dynamic_allowed","credits","price_euros","billable","refine_credits","max_repair_rounds"],"title":"EstimateResponse"},"ExistingConversationInfo":{"properties":{"conversation_id":{"type":"string","title":"Conversation Id","description":"Existing conversation ID"},"message_count":{"type":"integer","title":"Message Count","description":"Number of messages in the conversation","default":0},"last_message_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Last Message At","description":"When the last message was sent/received"},"closed_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Closed At","description":"When the conversation was closed"}},"type":"object","required":["conversation_id"],"title":"ExistingConversationInfo","description":"Info about an existing closed conversation that could be reopened"},"ExistingEmailThreadInfo":{"properties":{"conversation_id":{"type":"string","title":"Conversation Id","description":"Existing conversation ID"},"subject":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Subject","description":"Email thread subject"},"message_count":{"type":"integer","title":"Message Count","description":"Number of messages in the thread","default":0},"last_message_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Last Message At","description":"When the last message was sent/received"},"created_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Created At","description":"When the conversation was created"}},"type":"object","required":["conversation_id"],"title":"ExistingEmailThreadInfo","description":"Info about an existing open email thread/conversation"},"ExtractUrlsResponse":{"properties":{"urls":{"items":{"type":"string"},"type":"array","title":"Urls","description":"List of extracted URLs"},"count":{"type":"integer","title":"Count","description":"Number of URLs found"}},"type":"object","required":["urls","count"],"title":"ExtractUrlsResponse","description":"Response from extracting URLs from content"},"FAQBatchCreateResponse":{"properties":{"created_count":{"type":"integer","title":"Created Count","description":"Number of items created"},"items":{"items":{"$ref":"#/components/schemas/FAQItemResponse"},"type":"array","title":"Items","description":"Created FAQ items"}},"type":"object","required":["created_count","items"],"title":"FAQBatchCreateResponse","description":"Response after batch creating FAQ items"},"FAQEntryRevisionListResponse":{"properties":{"items":{"items":{"$ref":"#/components/schemas/FAQEntryRevisionResponse"},"type":"array","title":"Items"},"pagination":{"$ref":"#/components/schemas/PaginationMeta"}},"type":"object","required":["items","pagination"],"title":"FAQEntryRevisionListResponse"},"FAQEntryRevisionResponse":{"properties":{"id":{"type":"string","title":"Id"},"tenant_id":{"type":"string","title":"Tenant Id"},"faq_item_id":{"type":"string","title":"Faq Item Id"},"revision_number":{"type":"integer","title":"Revision Number"},"question_before":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Question Before"},"answer_before":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Answer Before"},"question_after":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Question After"},"answer_after":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Answer After"},"status_before":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Status Before"},"status_after":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Status After"},"change_type":{"type":"string","title":"Change Type","description":"create | update | delete | status_change | merge"},"changed_by_user_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Changed By User Id"},"changed_by_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Changed By Name"},"correction_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Correction Id"},"auto_applied":{"type":"boolean","title":"Auto Applied","default":false},"rationale":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Rationale"},"created_at":{"type":"string","format":"date-time","title":"Created At"}},"type":"object","required":["id","tenant_id","faq_item_id","revision_number","change_type","created_at"],"title":"FAQEntryRevisionResponse"},"FAQItemBatchCreate":{"properties":{"items":{"items":{"$ref":"#/components/schemas/FAQItemCreate"},"type":"array","maxItems":50,"minItems":1,"title":"Items","description":"List of FAQ items to create (1-50)"}},"type":"object","required":["items"],"title":"FAQItemBatchCreate","description":"Schema for batch creating FAQ items"},"FAQItemCreate":{"properties":{"question":{"type":"string","maxLength":2000,"minLength":1,"title":"Question","description":"The question"},"answer":{"type":"string","maxLength":10000,"minLength":1,"title":"Answer","description":"The answer"},"document_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Document Id","description":"Legacy: append to this compiled managed FAQ document. Omit to create a standalone entry (recommended)."},"folder_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Folder Id","description":"KB folder for a standalone entry. Defaults to the tenant's 'Managed FAQ' system folder. Ignored when document_id is given."},"status":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Status","description":"draft | published | needs_review | archived. Defaults to 'published' — an entry written in the editor is authored, not proposed. Only 'published' entries are indexed for retrieval. Ignored when document_id is given."},"language":{"anyOf":[{"type":"string","maxLength":10},{"type":"null"}],"title":"Language","description":"BCP-47-ish language tag for a standalone entry"},"allow_duplicate":{"type":"boolean","title":"Allow Duplicate","description":"When false (default), a standalone create is rejected with 409 if a near-identical entry already exists. Set true to create anyway. Ignored when document_id is given.","default":false}},"type":"object","required":["question","answer"],"title":"FAQItemCreate","description":"Schema for creating a FAQ entry.\n\n``document_id`` is optional and legacy. Omit it (the normal case) and the\nentry is created STANDALONE in ``folder_id`` — no compiled parent document,\none ``faq_<id>.md`` RAG file of its own. Pass it and the entry is appended\nto that managed FAQ document, exactly as before."},"FAQItemListResponse":{"properties":{"items":{"items":{"$ref":"#/components/schemas/FAQItemResponse"},"type":"array","title":"Items"},"pagination":{"$ref":"#/components/schemas/PaginationMeta"}},"type":"object","required":["items","pagination"],"title":"FAQItemListResponse","description":"Folder-scoped / filtered FAQ entry list (unified list shape)."},"FAQItemReorderRequest":{"properties":{"document_id":{"type":"string","title":"Document Id","description":"Parent managed FAQ document ID"},"item_ids":{"items":{"type":"string"},"type":"array","title":"Item Ids","description":"List of FAQ item IDs in the desired order"}},"type":"object","required":["document_id","item_ids"],"title":"FAQItemReorderRequest","description":"Schema for reordering FAQ items"},"FAQItemResponse":{"properties":{"question":{"type":"string","maxLength":2000,"minLength":1,"title":"Question","description":"The question"},"answer":{"type":"string","maxLength":10000,"minLength":1,"title":"Answer","description":"The answer"},"id":{"type":"string","title":"Id"},"tenant_id":{"type":"string","title":"Tenant Id"},"document_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Document Id"},"folder_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Folder Id"},"sort_order":{"type":"integer","title":"Sort Order","default":0},"status":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Status","description":"draft | published | needs_review | archived"},"origin":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Origin","description":"manual | auto_conversation | migrated | correction"},"language":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Language"},"source_conversation_ids":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Source Conversation Ids"},"confidence":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Confidence","description":"Extraction confidence, NULL for manual entries"},"review_notes":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Review Notes"},"rag_file_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Rag File Name","description":"NULL = not indexed for retrieval"},"times_retrieved":{"type":"integer","title":"Times Retrieved","default":0},"times_cited":{"type":"integer","title":"Times Cited","default":0},"times_in_sent_reply":{"type":"integer","title":"Times In Sent Reply","default":0},"duplicate_count":{"type":"integer","title":"Duplicate Count","default":0},"flagged_count":{"type":"integer","title":"Flagged Count","default":0},"quality_score":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Quality Score","description":"Weighted Wilson lower bound. NULL = not enough rating mass to judge."},"positive_mass":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Positive Mass"},"negative_mass":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Negative Mass"},"verified_by_usage":{"type":"boolean","title":"Verified By Usage","description":"quality_score > 0.8 with at least 5.0 rating mass","default":false},"retrieval_rank_multiplier":{"type":"number","title":"Retrieval Rank Multiplier","description":"Rank boost derived from quality_score; 1.0 when unscored","default":1.0},"last_used_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Last Used At"},"last_verified_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Last Verified At"},"content_updated_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Content Updated At"},"created_by_user_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Created By User Id"},"updated_by_user_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Updated By User Id"},"created_at":{"type":"string","format":"date-time","title":"Created At"},"updated_at":{"type":"string","format":"date-time","title":"Updated At"}},"type":"object","required":["question","answer","id","tenant_id","created_at","updated_at"],"title":"FAQItemResponse","description":"Schema for FAQ item response.\n\n``document_id`` is Optional: Managed FAQ entries are standalone — they live\nin a folder and own a single RAG file, with no compiled parent document.\nDeclaring it required made every standalone entry a 500."},"FAQItemUpdate":{"properties":{"question":{"anyOf":[{"type":"string","maxLength":2000,"minLength":1},{"type":"null"}],"title":"Question","description":"The question"},"answer":{"anyOf":[{"type":"string","maxLength":10000,"minLength":1},{"type":"null"}],"title":"Answer","description":"The answer"},"status":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Status","description":"draft | published | needs_review | archived"},"folder_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Folder Id","description":"Move the entry to another KB folder"},"language":{"anyOf":[{"type":"string","maxLength":10},{"type":"null"}],"title":"Language","description":"BCP-47-ish language tag"},"review_notes":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Review Notes","description":"Why the entry needs review / what to check"}},"type":"object","title":"FAQItemUpdate","description":"Schema for updating a FAQ item"},"FAQMigrationClusterPreview":{"properties":{"size":{"type":"integer","title":"Size","description":"Conversations in the cluster"},"cohesion":{"type":"number","title":"Cohesion","description":"Mean intra-cluster similarity (0-1)"},"representative_intent":{"type":"string","title":"Representative Intent","description":"Intent text of the representative conversation (truncated)"},"eligible":{"type":"boolean","title":"Eligible","description":"Whether size >= the effective min_cluster_size"}},"type":"object","required":["size","cohesion","representative_intent","eligible"],"title":"FAQMigrationClusterPreview","description":"One cluster as a dry run saw it."},"FAQMigrationEstimateResponse":{"properties":{"tenant_id":{"type":"string","title":"Tenant Id","description":"Tenant the estimate was computed for"},"summaries_total":{"type":"integer","title":"Summaries Total","description":"Ticket summaries readable by the migration right now"},"summaries_already_migrated":{"type":"integer","title":"Summaries Already Migrated","description":"Of those, how many a previous run already consumed"},"summaries_pending":{"type":"integer","title":"Summaries Pending","description":"Summaries a run would still have to process"},"clusters":{"type":"integer","title":"Clusters","description":"Clusters the pending summaries group into (one entry each)"},"clusters_at_or_above_min_size":{"type":"integer","title":"Clusters At Or Above Min Size","description":"Clusters with at least `min_cluster_size` members — the half that reliably produces FAQ entries"},"singleton_clusters":{"type":"integer","title":"Singleton Clusters","description":"Clusters below `min_cluster_size`. They still cost one LLM call each and the model refuses nearly all of them; `include_singletons=false` skips them"},"min_cluster_size":{"type":"integer","title":"Min Cluster Size","description":"Effective floor (tenant setting `faq_min_cluster_size`)"},"estimated_llm_calls":{"type":"integer","title":"Estimated Llm Calls","description":"One generation call per cluster processed"},"estimated_cost_eur":{"type":"number","title":"Estimated Cost Eur","description":"Cost of processing every cluster, including singletons"},"estimated_cost_eur_without_singletons":{"type":"number","title":"Estimated Cost Eur Without Singletons","description":"Cost with `include_singletons=false`"},"estimated_runs":{"type":"integer","title":"Estimated Runs","description":"Worker messages the job will take (it re-enqueues itself; 40 clusters per message by default)"}},"type":"object","required":["tenant_id","summaries_total","summaries_already_migrated","summaries_pending","clusters","clusters_at_or_above_min_size","singleton_clusters","min_cluster_size","estimated_llm_calls","estimated_cost_eur","estimated_cost_eur_without_singletons","estimated_runs"],"title":"FAQMigrationEstimateResponse","description":"What a full migration would cost, computed by actually clustering.\n\nNo LLM call is made, nothing is written. This is the number the \"migrate\nhistory\" button shows before the tenant commits."},"FAQMigrationParams":{"properties":{"dry_run":{"type":"boolean","title":"Dry Run","default":false},"limit":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Limit"},"max_clusters":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Max Clusters"},"min_cluster_size":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Min Cluster Size"},"include_singletons":{"type":"boolean","title":"Include Singletons","default":true},"retire_folder_when_done":{"type":"boolean","title":"Retire Folder When Done","default":false}},"type":"object","title":"FAQMigrationParams","description":"The parameters a run was started with (echoed back on start/status)."},"FAQMigrationPreview":{"properties":{"summaries_read":{"type":"integer","title":"Summaries Read"},"summaries_pending":{"type":"integer","title":"Summaries Pending"},"clusters":{"type":"integer","title":"Clusters"},"singletons_excluded":{"type":"boolean","title":"Singletons Excluded","description":"True when the run was started with include_singletons=false","default":false},"estimated_cost_eur":{"type":"number","title":"Estimated Cost Eur"},"sample_clusters":{"items":{"$ref":"#/components/schemas/FAQMigrationClusterPreview"},"type":"array","title":"Sample Clusters","description":"The first 50 clusters, biggest first"},"message":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Message","description":"Set when there was nothing to do (e.g. everything already migrated)"}},"type":"object","required":["summaries_read","summaries_pending","clusters","estimated_cost_eur"],"title":"FAQMigrationPreview","description":"Dry-run report. Nothing was written and no LLM call was made."},"FAQMigrationRunInfo":{"properties":{"run_id":{"type":"string","title":"Run Id","description":"Identifier minted when the run was started ('unknown' if re-created by a worker)"},"started_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Started At"},"started_by_user_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Started By User Id"},"source":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Source","description":"'api' when a user started it, 'enqueue' when re-created mid-chain"},"params":{"anyOf":[{"$ref":"#/components/schemas/FAQMigrationParams"},{"type":"null"}]}},"type":"object","required":["run_id"],"title":"FAQMigrationRunInfo","description":"The in-flight marker for a migration.\n\nHeld in Redis with a TTL and refreshed on every enqueue, so it exists for\nas long as the self-re-enqueueing chain does. It can be absent while a\nmigration is genuinely running (Redis unavailable) — which is why\n`running` is not derived from it alone."},"FAQMigrationStartRequest":{"properties":{"dry_run":{"type":"boolean","title":"Dry Run","description":"Preview only. Runs INLINE (no job is enqueued, no LLM call, no row written) and returns the clustering it would have processed","default":false},"limit":{"anyOf":[{"type":"integer","maximum":20000.0,"minimum":1.0},{"type":"null"}],"title":"Limit","description":"Cap on summaries read in one pass (default: all, max 20000)"},"max_clusters":{"anyOf":[{"type":"integer","maximum":100.0,"minimum":1.0},{"type":"null"}],"title":"Max Clusters","description":"Clusters processed per worker message (default 40). Higher values risk the 600s worker message timeout; the job re-enqueues itself either way, so raising this buys nothing but risk"},"min_cluster_size":{"anyOf":[{"type":"integer","maximum":50.0,"minimum":1.0},{"type":"null"}],"title":"Min Cluster Size","description":"Override the tenant's `faq_min_cluster_size` for this run — the floor at which a cluster counts as recurring rather than one-off"},"include_singletons":{"type":"boolean","title":"Include Singletons","description":"Process one-off cases too. Set false for the cheap high-value pass: singletons dominate the cluster count and the cost, and are refused almost every time","default":true},"retire_folder_when_done":{"type":"boolean","title":"Retire Folder When Done","description":"Hide the legacy 'Ticket Summaries' KB folder once nothing is left to migrate. A visible change to the tenant's KB tree, so opt-in. Ignored for `dry_run`","default":false},"force":{"type":"boolean","title":"Force","description":"Start even though a migration appears to be in flight, overriding the 409. Only for a run known to be dead — two live chains double-bill the tenant and race on the same summaries","default":false}},"type":"object","title":"FAQMigrationStartRequest","description":"Parameters for one migration run (or preview)."},"FAQMigrationStartResponse":{"properties":{"success":{"type":"boolean","title":"Success"},"status":{"type":"string","title":"Status","description":"'queued' — a job was published; 'preview' — dry run, nothing enqueued"},"dry_run":{"type":"boolean","title":"Dry Run"},"run_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Run Id","description":"Null for a preview (a preview is not a run)"},"started_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Started At"},"params":{"$ref":"#/components/schemas/FAQMigrationParams"},"forced":{"type":"boolean","title":"Forced","description":"True when the in-flight guard was explicitly overridden","default":false},"lease_held":{"type":"boolean","title":"Lease Held","description":"Whether the in-flight marker was actually stored. False means Redis was unavailable and duplicate-start protection falls back to the database activity check alone","default":false},"message":{"type":"string","title":"Message"},"preview":{"anyOf":[{"$ref":"#/components/schemas/FAQMigrationPreview"},{"type":"null"}],"description":"Populated only for `dry_run`"}},"type":"object","required":["success","status","dry_run","params","message"],"title":"FAQMigrationStartResponse","description":"Result of POST /knowledge-base/faq-migration/start."},"FAQMigrationStatusResponse":{"properties":{"tenant_id":{"type":"string","title":"Tenant Id"},"running":{"type":"boolean","title":"Running","description":"INFERRED, not recorded: true when the in-flight marker exists, or when the migration wrote a row in the last few minutes while work remains. See `running_evidence`"},"running_evidence":{"items":{"type":"string"},"type":"array","title":"Running Evidence","description":"Which signals fired: 'lease' and/or 'recent_activity'"},"run":{"anyOf":[{"$ref":"#/components/schemas/FAQMigrationRunInfo"},{"type":"null"}],"description":"The in-flight marker, when one exists"},"summaries_total":{"type":"integer","title":"Summaries Total","description":"Distinct conversations with a summary the migration can read. Marginally optimistic: a run also drops records with empty intent text and caps itself at 20000"},"summaries_migrated":{"type":"integer","title":"Summaries Migrated","description":"Distinct conversation ids recorded as consumed by migrated rows"},"summaries_pending":{"type":"integer","title":"Summaries Pending","description":"`summaries_total - summaries_migrated`, floored at 0"},"percent_summaries_processed":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Percent Summaries Processed","description":"summaries_migrated / summaries_total * 100, rounded to 1 decimal. This is the ONLY progress ratio the backend can substantiate — it measures summaries consumed, NOT clusters done and NOT time elapsed, and it moves in jumps of a whole cluster. Null when the tenant has no summaries. There is no cluster-level or ETA progress record; ask `?include_estimate=true` (or /faq-migration/estimate) for a fresh cluster count"},"entries_imported":{"type":"integer","title":"Entries Imported","description":"FAQ entries the migration created (drafts, unless later changed)"},"entries_by_status":{"additionalProperties":{"type":"integer"},"type":"object","title":"Entries By Status","description":"Imported entries grouped by current status (draft/published/...)"},"clusters_rejected":{"type":"integer","title":"Clusters Rejected","description":"Clusters consumed without producing an entry — refused as non-reusable, blocked for PII, or merged into an existing entry. Stored as ARCHIVED ledger rows"},"rejected_reasons":{"additionalProperties":{"type":"integer"},"type":"object","title":"Rejected Reasons","description":"Breakdown of `clusters_rejected` by reason"},"migrated_rows_total":{"type":"integer","title":"Migrated Rows Total","description":"`entries_imported + clusters_rejected`"},"last_activity_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Last Activity At","description":"Newest migrated row — the migration's real heartbeat"},"estimate":{"anyOf":[{"$ref":"#/components/schemas/FAQMigrationEstimateResponse"},{"type":"null"}],"description":"Fresh clustering pass. Only present with `?include_estimate=true` — it re-clusters the whole pending corpus, so do not poll with it"},"checked_at":{"type":"string","title":"Checked At"}},"type":"object","required":["tenant_id","running","summaries_total","summaries_migrated","summaries_pending","entries_imported","clusters_rejected","migrated_rows_total","checked_at"],"title":"FAQMigrationStatusResponse","description":"Everything the backend can truthfully say about migration progress.\n\nPoll-safe: no clustering, no LLM call. Every count is read back from rows\nthe migration wrote."},"FAQOperationResponse":{"properties":{"success":{"type":"boolean","title":"Success","default":true},"message":{"type":"string","title":"Message"},"document_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Document Id"},"item_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Item Id"}},"type":"object","required":["message"],"title":"FAQOperationResponse","description":"Generic success response for FAQ operations"},"FAQSyncResponse":{"properties":{"success":{"type":"boolean","title":"Success","default":true},"message":{"type":"string","title":"Message"},"document_id":{"type":"string","title":"Document Id"},"item_count":{"type":"integer","title":"Item Count"},"synced_at":{"type":"string","format":"date-time","title":"Synced At"}},"type":"object","required":["message","document_id","item_count","synced_at"],"title":"FAQSyncResponse","description":"Response after syncing to Gemini"},"FacebookPagePostItem":{"properties":{"id":{"type":"string","title":"Id"},"thumbnail_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Thumbnail Url"},"media_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Media Url"},"caption":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Caption"},"media_type":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Media Type"},"permalink":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Permalink"},"timestamp":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Timestamp"}},"type":"object","required":["id"],"title":"FacebookPagePostItem","description":"A single published Facebook Page post, in the Instagram item shape."},"FacebookPagePostListResponse":{"properties":{"items":{"items":{"$ref":"#/components/schemas/FacebookPagePostItem"},"type":"array","title":"Items"},"pagination":{"$ref":"#/components/schemas/FacebookPagePostPagination"}},"type":"object","required":["items","pagination"],"title":"FacebookPagePostListResponse"},"FacebookPagePostPagination":{"properties":{"next_cursor":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Next Cursor"}},"type":"object","title":"FacebookPagePostPagination","description":"Cursor-based pagination metadata (Meta returns no total count)."},"FeatureAnalyticsResponse":{"properties":{"feature_type":{"type":"string","title":"Feature Type"},"feature_id":{"type":"string","title":"Feature Id"},"total_links":{"type":"integer","title":"Total Links"},"total_clicks":{"type":"integer","title":"Total Clicks"},"top_links":{"items":{"$ref":"#/components/schemas/TopLink"},"type":"array","title":"Top Links"}},"type":"object","required":["feature_type","feature_id","total_links","total_clicks","top_links"],"title":"FeatureAnalyticsResponse","description":"Aggregate analytics for a feature (campaign/conversation/etc.).\n\nSimplified model: unique_clicks no longer available."},"FeatureAvailabilityResponse":{"properties":{"package_type":{"type":"string","title":"Package Type"},"is_api_only":{"type":"boolean","title":"Is Api Only","default":false},"ai_enabled":{"type":"boolean","title":"Ai Enabled"},"ai_available":{"type":"boolean","title":"Ai Available","default":true},"translation_enabled":{"type":"boolean","title":"Translation Enabled","default":true},"ui_features":{"additionalProperties":{"type":"boolean"},"type":"object","title":"Ui Features"},"api_features":{"additionalProperties":{"type":"boolean"},"type":"object","title":"Api Features"},"disabled_features":{"items":{"type":"string"},"type":"array","title":"Disabled Features"},"is_trial":{"type":"boolean","title":"Is Trial"},"subscription_status":{"type":"string","title":"Subscription Status"},"trial_suite_toggle_eligible":{"type":"boolean","title":"Trial Suite Toggle Eligible","default":false},"original_suite_type":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Original Suite Type"},"pro_trial_active":{"type":"boolean","title":"Pro Trial Active","default":false},"pro_trial_ends_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Pro Trial Ends At"},"pro_trial_prompt_due":{"type":"boolean","title":"Pro Trial Prompt Due","default":false},"pro_trial_eligible":{"type":"boolean","title":"Pro Trial Eligible","default":false},"pricing_model":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Pricing Model"},"summarize_on_close_available":{"type":"boolean","title":"Summarize On Close Available","default":false}},"type":"object","required":["package_type","ai_enabled","ui_features","api_features","disabled_features","is_trial","subscription_status"],"title":"FeatureAvailabilityResponse","description":"Feature availability for frontend gating"},"FeatureType":{"type":"string","enum":["email_campaign","conversation","automation","opt_in","reach","other","flow"],"title":"FeatureType","description":"Feature type for tracked links"},"FetchFlowsResponse":{"properties":{"success":{"type":"boolean","title":"Success"},"flows":{"items":{"$ref":"#/components/schemas/app__schemas__whatsapp_flow__FlowResponse"},"type":"array","title":"Flows"},"total":{"type":"integer","title":"Total","description":"Total number of flows","default":0},"error":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Error"}},"type":"object","required":["success"],"title":"FetchFlowsResponse","description":"Response from fetching available flows"},"FieldFilterOperator":{"type":"string","enum":["eq","ne","in","between","is_set","is_not_set","contains"],"title":"FieldFilterOperator","description":"Operators supported by CustomInboxFieldFilter."},"FieldFilterRule":{"properties":{"custom_field_id":{"type":"string","title":"Custom Field Id","description":"ID of the contact custom-field definition this rule applies to"},"operator":{"$ref":"#/components/schemas/FieldFilterOperator","description":"Comparison operator"},"value":{"anyOf":[{},{"type":"null"}],"title":"Value","description":"Operator-dependent value. Scalar for eq/ne, list for in, {'min':..., 'max':...} for between, omitted for is_set/is_not_set."}},"type":"object","required":["custom_field_id","operator"],"title":"FieldFilterRule","description":"A single custom-field rule on an inbox.\n\n``value`` shape depends on ``operator``:\n  - ``eq`` / ``ne``         : scalar (str / int / float / bool)\n  - ``in``                  : list of scalars\n  - ``between``             : ``{\"min\": <scalar>, \"max\": <scalar>}``\n  - ``is_set`` / ``is_not_set`` : value is ignored (should be omitted / None)"},"FieldType":{"type":"string","enum":["text","number","boolean","date","datetime","select","multiselect","email","phone","url","location","collection"],"title":"FieldType","description":"Supported custom field types"},"FilterOperator":{"type":"string","enum":["equals","not_equals","contains","not_contains","in","not_in","greater_than","less_than","greater_than_or_equal","less_than_or_equal","is_set","is_not_set"],"title":"FilterOperator"},"FilterType":{"type":"string","enum":["tag","custom_field","all_contacts","dynamic_list"],"title":"FilterType"},"FlowDetailsResponse":{"properties":{"success":{"type":"boolean","title":"Success"},"flow":{"anyOf":[{"$ref":"#/components/schemas/app__schemas__whatsapp_flow__FlowResponse"},{"type":"null"}],"description":"Basic flow information"},"screens":{"items":{"$ref":"#/components/schemas/FlowScreenInfo"},"type":"array","title":"Screens","description":"Flow screens with fields"},"variables":{"items":{"type":"string"},"type":"array","title":"Variables","description":"Variables that can be passed via data parameter"},"entry_screen":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Entry Screen","description":"Screen the flow opens on"},"is_static":{"type":"boolean","title":"Is Static","description":"True when the flow has no data endpoint (first-screen data must be sent with the message)","default":true},"flow_json_available":{"type":"boolean","title":"Flow Json Available","description":"Whether the flow JSON could be read","default":false},"defaults":{"additionalProperties":{"additionalProperties":true,"type":"object"},"type":"object","title":"Defaults","description":"Saved default data per screen id: {screen_id: data}"},"error":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Error"}},"type":"object","required":["success"],"title":"FlowDetailsResponse","description":"Detailed information about a WhatsApp Flow"},"FlowFieldInfo":{"properties":{"type":{"type":"string","title":"Type","description":"Component type (TextInput, Dropdown, etc.)"},"name":{"type":"string","title":"Name","description":"Field name/ID"},"label":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Label","description":"Display label"},"required":{"type":"boolean","title":"Required","description":"Whether field is required","default":false}},"type":"object","required":["type","name"],"title":"FlowFieldInfo","description":"Information about an input field in a flow screen"},"FlowListResponse":{"properties":{"items":{"items":{"$ref":"#/components/schemas/app__schemas__flow_schema__FlowResponse"},"type":"array","title":"Items"},"pagination":{"$ref":"#/components/schemas/PaginationMeta"}},"type":"object","required":["items","pagination"],"title":"FlowListResponse","description":"Paginated list of Flows — matches the unified pagination shape."},"FlowMessageEvent":{"properties":{"event_type":{"type":"string","const":"flow_message","title":"Event Type","default":"flow_message"},"id":{"type":"string","title":"Id","description":"Stable client-side key. Matches ``message_id``."},"occurred_at":{"type":"string","title":"Occurred At","description":"Chronological anchor (``messages.created_at``). Used as the merge sort key."},"message_id":{"type":"string","title":"Message Id"},"flow_run_id":{"type":"string","title":"Flow Run Id"},"flow_id":{"type":"string","title":"Flow Id"},"flow_name":{"type":"string","title":"Flow Name"},"conversation_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Conversation Id"},"direction":{"$ref":"#/components/schemas/MessageDirection-Output"},"message_type":{"$ref":"#/components/schemas/MessageType"},"text":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Text"},"status":{"$ref":"#/components/schemas/MessageStatus-Output"},"platform":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Platform"},"sent_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Sent At"}},"type":"object","required":["id","occurred_at","message_id","flow_run_id","flow_id","flow_name","direction","message_type","status"],"title":"FlowMessageEvent","description":"Timeline row representing a single flow-sent message.\n\nDistinct from :class:`ConversationEvent` because the contact may have\nreceived a flow message *before* the messages_in_worker stitched it\ninto a conversation (``messages.conversation_id`` is nullable after\nfl06)."},"FlowMode":{"type":"string","enum":["draft","published"],"title":"FlowMode","description":"WhatsApp Flow mode"},"FlowOptAction":{"properties":{"type":{"type":"string","const":"flow","title":"Type","default":"flow"},"flow_id":{"type":"string","minLength":1,"title":"Flow Id","description":"Logical ref to flows.id (same tenant/shard)"}},"type":"object","required":["flow_id"],"title":"FlowOptAction","description":"Opt-in / opt-out action that STARTS A FLOW.\n\n`flow_id` is a LOGICAL reference to a same-shard `flows.id` — there is NO\nDB foreign key (an FK can't live in a JSON column and config must not\ncascade-delete a flow); the service validates existence + same-tenant."},"FlowRunEvent":{"properties":{"event_type":{"type":"string","const":"flow_run","title":"Event Type","default":"flow_run"},"id":{"type":"string","title":"Id","description":"Stable client-side key. Matches ``flow_run_id``."},"occurred_at":{"type":"string","title":"Occurred At","description":"Chronological anchor (``flow_runs.started_at``). Used as the merge sort key."},"flow_run_id":{"type":"string","title":"Flow Run Id"},"flow_id":{"type":"string","title":"Flow Id"},"flow_name":{"type":"string","title":"Flow Name"},"status":{"type":"string","title":"Status","description":"FlowRunStatus value: running / waiting / completed / failed / canceled."},"started_at":{"type":"string","title":"Started At"},"completed_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Completed At"},"current_node_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Current Node Id"},"current_node_label":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Current Node Label","description":"Human-readable label of the current node, resolved from ``flow_runs.context.definition_snapshot.nodes[node_id].label``. Falls back to ``current_node_id`` when no label is present in the snapshot."},"wait":{"anyOf":[{"$ref":"#/components/schemas/FlowRunWaitSummary"},{"type":"null"}],"description":"The soonest-expiring active wait for this run — populated only when status == waiting."},"messages_count":{"type":"integer","title":"Messages Count","description":"Number of messages attributed to this flow_run (``messages.flow_run_id == flow_run_id``).","default":0},"error_message":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Error Message","description":"Run-level failure summary mirrored from ``flow_runs.context.last_error``. ``None`` for successful or in-flight runs."},"failed_node_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Failed Node Id","description":"Node where the engine gave up. Mirrors the run-detail endpoint's field."},"failed_node_label":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Failed Node Label","description":"Human label for ``failed_node_id``, resolved from the same snapshot used by ``current_node_label``."}},"type":"object","required":["id","occurred_at","flow_run_id","flow_id","flow_name","status","started_at"],"title":"FlowRunEvent","description":"Timeline row representing a single flow run for the contact."},"FlowRunListResponse":{"properties":{"items":{"items":{"$ref":"#/components/schemas/FlowRunResponse"},"type":"array","title":"Items"},"pagination":{"$ref":"#/components/schemas/PaginationMeta"}},"type":"object","required":["items","pagination"],"title":"FlowRunListResponse","description":"Paginated list of FlowRuns."},"FlowRunMessageListResponse":{"properties":{"items":{"items":{"$ref":"#/components/schemas/FlowRunMessageResponse"},"type":"array","title":"Items","description":"Messages attributed to the flow run, oldest-first"},"pagination":{"$ref":"#/components/schemas/MessagePaginationMeta","description":"Pagination metadata with cursor support"}},"type":"object","required":["items","pagination"],"title":"FlowRunMessageListResponse","description":"Paginated flow-run transcript — same envelope as ``MessageListResponse``."},"FlowRunMessageResponse":{"properties":{"id":{"type":"string","title":"Id"},"conversation_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Conversation Id","description":"Conversation the message belongs to. Null for flow messages that were sent before any conversation existed (e.g. test runs, or runs the contact never replied to)."},"tenant_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Tenant Id"},"platform":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Platform"},"channel_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Channel Id"},"contact_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Contact Id"},"contact_method_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Contact Method Id"},"contact_method":{"anyOf":[{"$ref":"#/components/schemas/ContactMethodInfo"},{"type":"null"}]},"direction":{"$ref":"#/components/schemas/MessageDirection-Output"},"message_type":{"$ref":"#/components/schemas/MessageType"},"from":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"From"},"to":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"To"},"contact_info":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Contact Info","description":"Provider-echoed sender profile captured from the inbound channel webhook at receipt time (inbound message.received only; not a DB lookup). Fields vary by channel: WhatsApp {name, phone}, Telegram {name, username, profile_picture}, Messenger {name, profile_picture}, Instagram {name?, username, profile_picture}. NOTE: `phone` (WhatsApp-only) is deprecated — use `data.contact_method.value` for the authoritative DB-sourced identifier of the method actually used."},"text":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Text"},"status":{"$ref":"#/components/schemas/MessageStatus-Output"},"attachments":{"items":{"$ref":"#/components/schemas/app__schemas__message__AttachmentResponse"},"type":"array","title":"Attachments"},"related_message_ids":{"items":{"type":"string"},"type":"array","title":"Related Message Ids","description":"IDs of the additional messages created by the same send request, in delivery order. Non-empty only when a send carried several attachments on a channel that delivers one attachment per message — the response body itself is the FIRST message (it holds the caption and `attachments[0]`), and each id here is a further message carrying the next attachment. Each has its own status and its own delivery webhooks, so poll them individually via `GET /api/v1/messages/{id}`. Empty array for every other message."},"meta":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Meta"},"content_format":{"type":"string","title":"Content Format","description":"How this message's body is formatted for the send pipeline: `markdown` (canonical markdown, rendered per-channel) or `plain` (human text, passed through untouched). Derived from `meta.content_format`; defaults to `plain` for every message that did not explicitly opt into markdown.","default":"plain"},"interactive":{"anyOf":[{"$ref":"#/components/schemas/InteractiveContent"},{"type":"null"}],"description":"Interactive message content (WhatsApp-style Meta format) for messages with reply buttons or a list. Hydrated from `meta.interactive`. Null for non-interactive/legacy messages."},"header_media_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Header Media Url","description":"Resolved media URL for an interactive image/video header"},"header_media_attachment_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Header Media Attachment Id","description":"Durable attachment id for an interactive image/video header"},"external_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"External Id"},"reply_to_external_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Reply To External Id","description":"Platform message ID of quoted message"},"quoted_message":{"anyOf":[{"$ref":"#/components/schemas/QuotedMessage"},{"type":"null"}],"description":"Preview of the quoted message"},"created_at":{"type":"string","title":"Created At"},"sent_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Sent At"},"delivered_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Delivered At"},"read_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Read At"},"is_internal":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Is Internal","default":false},"error_message":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Error Message"},"error_code":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Error Code","description":"Machine-readable failure code for failed messages, e.g. 'contact_blocked' (the contact is blocked) or a provider error code."},"campaign":{"anyOf":[{"$ref":"#/components/schemas/CampaignInfo"},{"type":"null"}],"description":"Campaign info when this message was sent via a campaign/newsletter"},"flow_run_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Flow Run Id","description":"FlowRun UUID — set when this message was sent by an automation flow. Frontends use this (together with `flow_name`) to render a 'Sent by flow' badge in the conversation timeline."},"flow_step_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Flow Step Id","description":"FlowRunStep UUID — identifies the specific Send-node step inside the flow that produced this message. Optional companion to `flow_run_id`."},"flow_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Flow Name","description":"Display name of the flow that sent this message. NOT stored on the message row — populated by the API layer via a single batch lookup against `flow_runs JOIN flows` and only set when `flow_run_id` is non-null."},"edited_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Edited At","description":"When this message was last edited by the sender (real column on `messages`, stamped when an edit event is applied). Non-null is the ONLY signal that a message was edited — `text` is silently replaced in place, so without this a client cannot tell an edited message from an original one. Null for every message that was never edited (the overwhelming majority)."},"previous_text":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Previous Text","description":"Body this message had immediately BEFORE its most recent edit. Not stored on the message row — resolved by the API layer from the newest APPLIED `message_revisions` row in one batched lookup per page. Null means 'edited, but the superseded body is unknown': most edits arrive before/without the message they target, so the original body was often never captured. Clients must therefore treat null as valid and fall back to a plain non-interactive 'edited' marker instead of an edit-history UI."},"revision_count":{"type":"integer","title":"Revision Count","description":"Number of edits that have actually been APPLIED to this message. Pending revisions (an edit whose target message we never resolved) are deliberately excluded — they describe no visible change to this row. 0 for un-edited messages; a value >1 tells the client the body was edited repeatedly, so `previous_text` is only the last step of a longer history.","default":0}},"type":"object","required":["id","direction","message_type","status","created_at"],"title":"FlowRunMessageResponse","description":"``MessageResponse`` variant for the flow-run transcript endpoint\n(``GET /flows/{flow_id}/runs/{run_id}/messages``).\n\nFlow Send-node messages are persisted BEFORE any conversation exists\n(``messages.conversation_id`` was relaxed to NULL in ``fl06``; the\nmessages_in_worker backfill stitches them into the contact's first\ninbound reply later). Test runs and runs that never got a reply keep\n``conversation_id = NULL`` forever, which the public ``MessageResponse``\noriginally rejected (``conversation_id: str``) — Sentry BACKEND-8Y. The\nbase ``MessageResponse.conversation_id`` is now itself ``Optional[str]``\n(campaign mirror messages hit the same case); this subclass keeps its\nflow-specific field description and remains the endpoint's response model.","example":{"attachments":[],"channel_id":"channel-123-456-789","contact_id":"contact-123-456-789","conversation_id":"conv-123-456-789","created_at":"2025-01-15T10:30:00Z","delivered_at":"2025-01-15T10:30:05Z","direction":"inbound","from":"+1234567890","id":"msg-123-456-789","message_type":"text","platform":"whatsapp","status":"delivered","tenant_id":"tenant-123-456-789","text":"Hello, I need help!"}},"FlowRunResponse":{"properties":{"id":{"type":"string","title":"Id"},"tenant_id":{"type":"string","title":"Tenant Id"},"flow_id":{"type":"string","title":"Flow Id"},"contact_id":{"type":"string","title":"Contact Id"},"current_node_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Current Node Id"},"status":{"type":"string","title":"Status"},"context":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Context"},"idempotency_key":{"type":"string","title":"Idempotency Key"},"started_at":{"type":"string","title":"Started At"},"completed_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Completed At"},"created_at":{"type":"string","title":"Created At"},"updated_at":{"type":"string","title":"Updated At"},"error_message":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Error Message"},"failed_node_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Failed Node Id"},"failed_node_label":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Failed Node Label"}},"type":"object","required":["id","tenant_id","flow_id","contact_id","status","idempotency_key","started_at","created_at","updated_at"],"title":"FlowRunResponse","description":"Wire format for a FlowRun row.\n\nThe ``error_message`` / ``failed_node_id`` / ``failed_node_label`` fields\nare derived by the API layer (see ``_to_run_response``) — they are not\nstored on the ``flow_runs`` row directly. ``error_message`` mirrors\n``context.last_error``; ``failed_node_id`` is the node where the engine\ngave up (resolved from ``current_node_id`` or the last failed\n``flow_run_steps`` row); ``failed_node_label`` is the human label for\nthat node, looked up against the current ``flows.definition``."},"FlowRunStatus":{"type":"string","enum":["running","waiting","completed","failed","canceled"],"title":"FlowRunStatus","description":"Lifecycle of a flow run.\n\nLowercase values to match the MySQL ENUM (see fl02 migration).\n``values_callable`` is set on the SQLAlchemy column so the lowercase\nvalues are written/read correctly."},"FlowRunStepListResponse":{"properties":{"items":{"items":{"$ref":"#/components/schemas/FlowRunStepResponse"},"type":"array","title":"Items"},"pagination":{"$ref":"#/components/schemas/PaginationMeta"}},"type":"object","required":["items","pagination"],"title":"FlowRunStepListResponse","description":"Paginated list of FlowRunSteps (run audit trail)."},"FlowRunStepResponse":{"properties":{"id":{"type":"string","title":"Id"},"tenant_id":{"type":"string","title":"Tenant Id"},"flow_run_id":{"type":"string","title":"Flow Run Id"},"node_id":{"type":"string","title":"Node Id"},"node_type":{"type":"string","title":"Node Type"},"status":{"type":"string","title":"Status"},"input_snapshot":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Input Snapshot"},"output_snapshot":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Output Snapshot"},"error_text":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Error Text"},"started_at":{"type":"string","title":"Started At"},"finished_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Finished At"},"node_label":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Node Label","description":"Human-readable label for the node, looked up in the current ``flows.definition`` (``display_name`` → snapshot ``label``/``name`` → derived from ``node_type`` + config)."},"node_config_snapshot":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Node Config Snapshot","description":"Subset of the node's CURRENT config from ``flows.definition`` (not a point-in-time snapshot of what the engine executed). Surfaced fields are node-type specific (e.g. ``validation``, ``max_attempts``, ``on_timeout_node_id``, ``prompt`` for Collect Input). ``None`` when the node no longer exists in the flow definition."},"branch_taken":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Branch Taken","description":"Branch label the engine routed to from this node, when applicable. Derived from ``output_snapshot.chosen_branch``."}},"type":"object","required":["id","tenant_id","flow_run_id","node_id","node_type","status","started_at"],"title":"FlowRunStepResponse","description":"Wire format for a FlowRunStep row (audit trail entry).\n\n``node_label`` and ``node_config_snapshot`` are derived by the API\nlayer from the CURRENT ``flows.definition`` (we don't freeze per-step\nconfig snapshots in the audit table). For a published-then-edited\nflow, the config returned here is the latest authored config, not\nthe config the engine executed against — callers that need\npoint-in-time fidelity should use ``input_snapshot``, which the\nengine populates from the per-run ``definition_snapshot``.\n\n``branch_taken`` is best-effort: derived from\n``output_snapshot.chosen_branch`` for branching node types (choice /\ncollect_input). ``None`` for nodes that don't branch."},"FlowRunWaitSummary":{"properties":{"wait_type":{"type":"string","title":"Wait Type","description":"FlowRunWait.wait_type (timer, reply, event, ...)"},"expires_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Expires At","description":"When this wait will fire / time out (UTC). NULL for waits that have no timeout."},"event_key":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Event Key","description":"Wait resolution key — e.g. 'reply:WHATSAPP' or 'event:contact.created'. NULL for timers."}},"type":"object","required":["wait_type"],"title":"FlowRunWaitSummary","description":"Compact summary of the wait a flow_run is parked on.\n\nPopulated only when ``FlowRunEvent.status == \"waiting\"``. The picker\nin :mod:`app.services.contact_history_service` selects the\nsoonest-expiring wait per run (so the FE can render \"Waiting until\n2026-05-20 14:00\" without inspecting every wait row)."},"FlowScreenInfo":{"properties":{"id":{"type":"string","title":"Id","description":"Screen ID"},"title":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Title","description":"Screen title"},"fields":{"items":{"$ref":"#/components/schemas/FlowFieldInfo"},"type":"array","title":"Fields","description":"Input fields on this screen"},"terminal":{"type":"boolean","title":"Terminal","description":"Whether this screen ends the flow","default":false},"data_schema":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Data Schema","description":"Raw `data` declaration of the screen from the flow JSON (types, nested items/properties, __example__). Null when the screen declares no data."}},"type":"object","required":["id"],"title":"FlowScreenInfo","description":"Information about a screen in a flow"},"FlowSecretEntry":{"properties":{"name":{"type":"string","title":"Name"},"preview":{"type":"string","title":"Preview","description":"Masked hint: first 3 + last 2 chars, or *** for short values"},"updated_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Updated At"}},"type":"object","required":["name","preview"],"title":"FlowSecretEntry","description":"One secret as listed — the value is write-only and never returned."},"FlowSecretListResponse":{"properties":{"items":{"items":{"$ref":"#/components/schemas/FlowSecretEntry"},"type":"array","title":"Items","default":[]},"pagination":{"additionalProperties":true,"type":"object","title":"Pagination","default":{}}},"type":"object","title":"FlowSecretListResponse"},"FlowSecretUpsertRequest":{"properties":{"value":{"type":"string","maxLength":4096,"minLength":1,"title":"Value"}},"type":"object","required":["value"],"title":"FlowSecretUpsertRequest","description":"Body for ``PUT /flows/secrets/{NAME}`` — create or rotate."},"FlowStatus":{"type":"string","enum":["draft","published","paused","archived"],"title":"FlowStatus","description":"Flow lifecycle status.\n\nLowercase values to match the MySQL ENUM (see fl01_create_flows\nmigration). values_callable is set on the SQLAlchemy column so the\nlowercase values are written/read correctly."},"FlowValidationError":{"properties":{"path":{"type":"string","title":"Path","default":""},"message":{"type":"string","title":"Message"}},"type":"object","required":["message"],"title":"FlowValidationError","description":"One structured publish-time validation error.\n\n``path`` is a best-effort dotted/bracketed locator (e.g.\n``\"nodes[sales_bot].bot_id\"`` or ``\"trigger.recurrence_rule\"``) so\nthe Flows editor can highlight the offending node / trigger inline\nwithout re-running save-then-fail. Graph-level errors (orphan\nnode, unreachable from entry, cycle) carry ``path=\"\"`` and the\nhuman-readable message stays in ``message``."},"FlowValidationStatus":{"properties":{"can_activate":{"type":"boolean","title":"Can Activate"},"errors":{"items":{"$ref":"#/components/schemas/FlowValidationError"},"type":"array","title":"Errors","default":[]},"warnings":{"items":{"$ref":"#/components/schemas/FlowValidationError"},"type":"array","title":"Warnings","default":[]}},"type":"object","required":["can_activate"],"title":"FlowValidationStatus","description":"Publish-time validation snapshot embedded in FlowResponse.\n\n``can_activate`` mirrors the result of\n``FlowDefinitionValidator.validate_publish`` — when False the FE\nshould gate the \"Publish\" / \"Activate\" button and surface\n``errors`` inline. Save-time leniency (placeholder ``bot_id`` on a\ntemplate-just-imported flow) means a flow may exist with\n``can_activate=false`` and still be a valid draft."},"FlowVariableEntry":{"properties":{"path":{"type":"string","title":"Path"},"label":{"type":"string","title":"Label"},"type":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Type"},"key":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Key","description":"Alias of `path` (always equal); the field the Flow Builder reads."}},"type":"object","required":["path","label"],"title":"FlowVariableEntry","description":"One row in the Flow Builder's Branch-node variable picker.\n\n* ``path``  — the canonical reference the Branch node expression\n  evaluates (e.g. ``\"contact.first_name\"``, ``\"contact.custom.vip\"``,\n  or a flow-scoped ``output_var`` like ``\"flow.vars.user_email\"``).\n* ``label`` — human-readable name shown in the picker dropdown.\n* ``type``  — best-effort type hint (e.g. ``\"string\"``, ``\"number\"``,\n  ``\"string[]\"``). Used by the FE to pick the right operator set.\n* ``key``   — same value as ``path``. The frontend contract\n  (``frontend/src/features/flows/api.ts`` ``FlowVariableEntry``) reads\n  ``key``; ``path`` stays for existing API clients. Always equal."},"FlowVariablesResponse":{"properties":{"contact_fields":{"items":{"$ref":"#/components/schemas/FlowVariableEntry"},"type":"array","title":"Contact Fields","default":[]},"custom_fields":{"items":{"$ref":"#/components/schemas/FlowVariableEntry"},"type":"array","title":"Custom Fields","default":[]},"tags":{"items":{"$ref":"#/components/schemas/FlowVariableEntry"},"type":"array","title":"Tags","default":[]},"lists":{"items":{"$ref":"#/components/schemas/FlowVariableEntry"},"type":"array","title":"Lists","default":[]},"flow_variables":{"items":{"$ref":"#/components/schemas/FlowVariableEntry"},"type":"array","title":"Flow Variables","default":[]}},"type":"object","title":"FlowVariablesResponse","description":"Catalog payload for ``GET /flows/variables``.\n\nUsed by the Branch node's \"match contact field / tag / list / custom\nfield\" UI to render a typed picker instead of a free-text input.\n\nSections:\n\n* ``contact_fields``  — built-in Contact columns (first/last name,\n  email, phone, language, country, tags, lists).\n* ``custom_fields``   — tenant-specific custom field definitions\n  (``contact.custom.<slug>`` paths).\n* ``tags``            — tenant tags (``path=name``, ``label=name``).\n* ``lists``           — tenant subscription lists (``path=list_id``,\n  ``label=list_name``).\n* ``flow_variables``  — when the ``flow_id`` query param is supplied,\n  the names produced by the flow's ``collect_input`` /\n  ``create_tracked_link`` / ``kb_query`` nodes\n  (``flow.vars.<output_var_name>``)."},"FolderDeleteResponse":{"properties":{"folder_id":{"type":"string","title":"Folder Id","description":"ID of the folder queued for deletion"},"queued_documents":{"type":"integer","title":"Queued Documents","description":"Number of contained documents also queued for deletion"},"job_id":{"type":"string","title":"Job Id","description":"Async job ID assigned to the folder deletion"}},"type":"object","required":["folder_id","queued_documents","job_id"],"title":"FolderDeleteResponse","description":"Response body for DELETE /folders/{id}."},"FolderRecrawlRequest":{"properties":{"clear_existing":{"type":"boolean","title":"Clear Existing","description":"If true, delete all existing documents before re-crawling","default":false}},"type":"object","title":"FolderRecrawlRequest","description":"Request to re-crawl a website folder.\n\nOnly applicable to folders under \"Websites\".","example":{"clear_existing":true}},"FunnelItem":{"properties":{"node_id":{"type":"string","title":"Node Id"},"node_name":{"type":"string","title":"Node Name"},"node_type":{"type":"string","title":"Node Type"},"count":{"type":"integer","title":"Count"},"drop_from_previous":{"type":"number","title":"Drop From Previous","description":"Fraction of contacts who reached the previous node but not this one. ``0.0`` for the first row and whenever the previous count was zero.","default":0.0}},"type":"object","required":["node_id","node_name","node_type","count"],"title":"FunnelItem","description":"Single row in the funnel — one node in canvas order."},"FunnelResponse":{"properties":{"items":{"items":{"$ref":"#/components/schemas/FunnelItem"},"type":"array","title":"Items"},"total_entered":{"type":"integer","title":"Total Entered"}},"type":"object","required":["items","total_entered"],"title":"FunnelResponse","description":"Body of ``GET /flows/{id}/observability/funnel``."},"GenerateRequest":{"properties":{"description":{"type":"string","maxLength":4000,"minLength":5,"title":"Description","description":"What the flow should do, in plain language"},"language":{"anyOf":[{"type":"string","maxLength":16},{"type":"null"}],"title":"Language","description":"BCP-47 code; defaults to the workspace language"},"channel_id":{"anyOf":[{"type":"string","maxLength":64},{"type":"null"}],"title":"Channel Id","description":"WhatsApp channel the flow is built for (narrows catalogue context)"},"flow_name":{"anyOf":[{"type":"string","maxLength":200},{"type":"null"}],"title":"Flow Name"},"request_id":{"anyOf":[{"type":"string","maxLength":64,"minLength":8,"pattern":"^[A-Za-z0-9_.:-]+$"},{"type":"null"}],"title":"Request Id","description":"Client idempotency key; a retry with the same key returns the same job"}},"type":"object","required":["description"],"title":"GenerateRequest"},"GenerateResponse":{"properties":{"builder_doc":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Builder Doc"},"issues":{"items":{"$ref":"#/components/schemas/AiIssue"},"type":"array","title":"Issues"},"valid":{"type":"boolean","title":"Valid","description":"False = the doc still has errors (shown in issues); never publishable as-is"},"compiled_preview":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Compiled Preview","description":"Meta Flow JSON, only when valid"},"credits_used":{"type":"integer","title":"Credits Used"},"model_used":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Model Used"},"repair_rounds":{"type":"integer","title":"Repair Rounds"},"generation_id":{"type":"string","title":"Generation Id"},"language":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Language"},"fallback_used":{"type":"boolean","title":"Fallback Used","default":false}},"type":"object","required":["builder_doc","issues","valid","credits_used","model_used","repair_rounds","generation_id"],"title":"GenerateResponse"},"GmailSyncResponse":{"properties":{"integration_id":{"type":"string","title":"Integration Id"},"status":{"type":"string","title":"Status"},"messages_synced":{"type":"integer","title":"Messages Synced","default":0},"new_history_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"New History Id"},"message":{"type":"string","title":"Message"}},"type":"object","required":["integration_id","status","message"],"title":"GmailSyncResponse","description":"Response from Gmail sync operation"},"HTTPValidationError":{"properties":{"detail":{"items":{"$ref":"#/components/schemas/ValidationError"},"type":"array","title":"Detail"}},"type":"object","title":"HTTPValidationError"},"HandoffConfig":{"properties":{"keywords":{"items":{"type":"string"},"type":"array","maxItems":100,"title":"Keywords"},"max_turns":{"anyOf":[{"type":"integer","maximum":200.0,"minimum":1.0},{"type":"null"}],"title":"Max Turns"},"max_consecutive_fallbacks":{"anyOf":[{"type":"integer","maximum":20.0,"minimum":1.0},{"type":"null"}],"title":"Max Consecutive Fallbacks"},"handoff_tag_ids":{"items":{"type":"string"},"type":"array","maxItems":40,"title":"Handoff Tag Ids"},"handoff_message":{"anyOf":[{"type":"string","maxLength":2000},{"type":"null"}],"title":"Handoff Message"}},"additionalProperties":false,"type":"object","title":"HandoffConfig","description":"Deterministic handoff triggers + the tags the handoff tool may set."},"HubAgentRow":{"properties":{"bot_id":{"type":"string","title":"Bot Id"},"agent_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Agent Id"},"name":{"type":"string","title":"Name"},"bot_type":{"type":"string","title":"Bot Type"},"kind":{"type":"string","title":"Kind"},"subtitle":{"$ref":"#/components/schemas/HubSubtitle"},"status":{"type":"string","title":"Status"},"publish_state":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Publish State"},"has_unpublished_changes":{"type":"boolean","title":"Has Unpublished Changes","default":false},"priority":{"type":"integer","title":"Priority","default":0},"avatar_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Avatar Url"},"channels":{"items":{"$ref":"#/components/schemas/HubChannel"},"type":"array","title":"Channels"},"widgets":{"items":{"$ref":"#/components/schemas/HubWidget"},"type":"array","title":"Widgets"},"flow_targeted":{"type":"boolean","title":"Flow Targeted","default":false},"skills_count":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Skills Count"},"conversations":{"type":"integer","title":"Conversations","default":0},"conversations_prev":{"type":"integer","title":"Conversations Prev","default":0},"resolved":{"type":"integer","title":"Resolved","default":0},"resolved_pct":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Resolved Pct"},"handoffs":{"type":"integer","title":"Handoffs","default":0},"handoff_pct":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Handoff Pct"},"insufficient_data":{"type":"boolean","title":"Insufficient Data","default":true},"credits":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Credits"},"sparkline":{"items":{"type":"integer"},"type":"array","title":"Sparkline"},"updated_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Updated At"},"updated_by_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Updated By Name"}},"type":"object","required":["bot_id","name","bot_type","kind","subtitle","status"],"title":"HubAgentRow"},"HubChannel":{"properties":{"channel_id":{"type":"string","title":"Channel Id"},"channel_type":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Channel Type"},"name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Name"}},"type":"object","required":["channel_id"],"title":"HubChannel"},"HubSpotContactCountResponse":{"properties":{"total":{"type":"integer","title":"Total","description":"HubSpot portal contact count for the scope"},"cached":{"type":"boolean","title":"Cached","description":"True when served from the ≤5-min server cache"},"as_of":{"type":"string","title":"As Of","description":"ISO-8601 (Z) timestamp the count reflects"}},"type":"object","required":["total","cached","as_of"],"title":"HubSpotContactCountResponse"},"HubSpotContactSyncFilterList":{"properties":{"hubspot_list_id":{"type":"string","maxLength":64,"minLength":1,"title":"Hubspot List Id"},"list_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"List Name"}},"type":"object","required":["hubspot_list_id"],"title":"HubSpotContactSyncFilterList"},"HubSpotContactSyncFilterModel":{"properties":{"version":{"type":"integer","const":1,"title":"Version","default":1},"mode":{"type":"string","enum":["properties","list","none"],"title":"Mode"},"properties":{"anyOf":[{"$ref":"#/components/schemas/HubSpotContactSyncFilterProperties"},{"type":"null"}]},"list":{"anyOf":[{"$ref":"#/components/schemas/HubSpotContactSyncFilterList"},{"type":"null"}]},"updated_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Updated At"},"updated_by_user_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Updated By User Id"}},"type":"object","required":["mode"],"title":"HubSpotContactSyncFilterModel","description":"The persisted selective-sync config (GET/PUT /contact-sync-filter),\nstored verbatim in ``HubSpotConnection.contact_sync_filter_config``.\n\n``mode`` picks which sub-object is authoritative: ``properties``\n(filter_groups), ``list`` (a single HubSpot list), or ``none`` (import\neverything — the pre-feature default). ``updated_at`` / ``updated_by_user_id``\nare server-populated on PUT and echoed on GET (they live INSIDE the JSON\ncolumn — no dedicated columns)."},"HubSpotContactSyncFilterProperties":{"properties":{"filter_groups":{"items":{"$ref":"#/components/schemas/HubSpotSearchFilterGroupSchema"},"type":"array","title":"Filter Groups"}},"type":"object","title":"HubSpotContactSyncFilterProperties"},"HubSpotDisconnectResponse":{"properties":{"success":{"type":"boolean","title":"Success"},"id":{"type":"string","title":"Id"}},"type":"object","required":["success","id"],"title":"HubSpotDisconnectResponse"},"HubSpotExportRequest":{"properties":{"contact_ids":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Contact Ids","description":"Specific SendSeven contact ids (default: all)"}},"type":"object","title":"HubSpotExportRequest"},"HubSpotFieldMapping":{"properties":{"property_name":{"type":"string","minLength":1,"title":"Property Name","description":"HubSpot contact property internal name"},"opt_in_value":{"type":"string","title":"Opt In Value","description":"Value written to the property on opt-in"},"opt_out_value":{"type":"string","title":"Opt Out Value","description":"Value written to the property on opt-out"}},"type":"object","required":["property_name","opt_in_value","opt_out_value"],"title":"HubSpotFieldMapping","description":"HubSpot contact-property mapping applied on opt-in / opt-out."},"HubSpotImportRequest":{"properties":{"list_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"List Id","description":"HubSpot list id — imports only that list's members"}},"type":"object","title":"HubSpotImportRequest"},"HubSpotListItem":{"properties":{"hubspot_list_id":{"type":"string","title":"Hubspot List Id"},"name":{"type":"string","title":"Name"},"processing_type":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Processing Type","description":"HubSpot processing type: MANUAL | DYNAMIC | SNAPSHOT"},"size":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Size","description":"Member count (best-effort)"},"already_linked":{"type":"boolean","title":"Already Linked","description":"True when a HUBSPOT_SEGMENT list already backs this id","default":false},"linked_list_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Linked List Id","description":"The SendSeven list id backing this HubSpot list, if any"}},"type":"object","required":["hubspot_list_id","name"],"title":"HubSpotListItem","description":"One HubSpot ILS list, annotated with SendSeven linkage state."},"HubSpotListMigrateRequest":{"properties":{"hubspot_list_id":{"type":"string","title":"Hubspot List Id"},"list_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"List Name","description":"Name for the new SendSeven list (default: HubSpot list name)"}},"type":"object","required":["hubspot_list_id"],"title":"HubSpotListMigrateRequest"},"HubSpotListsPagination":{"properties":{"next":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Next","description":"Opaque cursor for the next page (null on last page)"}},"type":"object","title":"HubSpotListsPagination"},"HubSpotListsResponse":{"properties":{"items":{"items":{"$ref":"#/components/schemas/HubSpotListItem"},"type":"array","title":"Items"},"pagination":{"$ref":"#/components/schemas/HubSpotListsPagination"}},"type":"object","required":["items","pagination"],"title":"HubSpotListsResponse"},"HubSpotOptinFieldConfig":{"properties":{"default":{"anyOf":[{"$ref":"#/components/schemas/HubSpotFieldMapping"},{"type":"null"}],"description":"Mapping applied to all channels unless overridden. null = no default HubSpot sync."},"per_channel":{"additionalProperties":{"$ref":"#/components/schemas/HubSpotFieldMapping"},"type":"object","title":"Per Channel","description":"Per-channel HubSpot field mappings keyed by SubscriptionChannelType value."}},"type":"object","title":"HubSpotOptinFieldConfig","description":"`{default, per_channel}` wrapper for `hubspot_optin_field_config`.\n\nCustomers map different HubSpot fields per channel (e.g. `whatsapp_optin`\nfor WhatsApp, `email_optin` for email) or one field for all. `default`\napplies to every channel; `per_channel` overrides it for specific channels.\nA NULL column means no HubSpot sync (no legacy source to synthesize)."},"HubSpotPropertyCreate":{"properties":{"name":{"type":"string","title":"Name","description":"Internal property name (no spaces)"},"label":{"type":"string","title":"Label","description":"Human-readable label"},"type":{"type":"string","title":"Type","description":"HubSpot data type, e.g. string/number/enumeration","default":"string"},"fieldType":{"type":"string","title":"Fieldtype","description":"HubSpot field widget type, e.g. text/select","default":"text"},"groupName":{"type":"string","title":"Groupname","description":"HubSpot property group the new property belongs to","default":"contactinformation"}},"type":"object","required":["name","label"],"title":"HubSpotPropertyCreate","description":"Create a custom HubSpot contact property.\n\nAccepts either snake_case or the HubSpot camelCase field names\n(``fieldType`` / ``groupName``) so the frontend can post HubSpot-native\npayloads directly."},"HubSpotSearchFilterGroupSchema":{"properties":{"filters":{"items":{"$ref":"#/components/schemas/HubSpotSearchFilterSchema"},"type":"array","minItems":1,"title":"Filters"}},"type":"object","required":["filters"],"title":"HubSpotSearchFilterGroupSchema"},"HubSpotSearchFilterSchema":{"properties":{"property_name":{"type":"string","maxLength":255,"minLength":1,"title":"Property Name"},"operator":{"type":"string","enum":["EQ","NEQ","LT","LTE","GT","GTE","BETWEEN","IN","NOT_IN","HAS_PROPERTY","NOT_HAS_PROPERTY","CONTAINS_TOKEN","NOT_CONTAINS_TOKEN"],"title":"Operator"},"value":{"anyOf":[{"type":"string","maxLength":4096},{"type":"null"}],"title":"Value"}},"type":"object","required":["property_name","operator"],"title":"HubSpotSearchFilterSchema","description":"One leaf filter (AND with its group siblings). `value` is absent for the\npresence operators (HAS_PROPERTY / NOT_HAS_PROPERTY); the builder serialises\nBETWEEN as \"low,high\" and IN/NOT_IN as a comma-joined token list."},"HubSpotSegmentCreate":{"properties":{"hubspot_list_id":{"type":"string","title":"Hubspot List Id","description":"HubSpot ILS list id to back the segment"},"name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Name","description":"Name for the SendSeven list (default: HubSpot list name)"},"refresh_mode":{"$ref":"#/components/schemas/HubSpotSegmentRefreshMode","default":"send_time"},"interval_minutes":{"anyOf":[{"type":"integer","maximum":1440.0,"minimum":5.0},{"type":"null"}],"title":"Interval Minutes","description":"Cached mode: refresh cadence in minutes (ignored for send_time)"},"unresolved_policy":{"$ref":"#/components/schemas/HubSpotSegmentUnresolvedPolicy","default":"reachable_only"},"method_scope":{"$ref":"#/components/schemas/HubSpotSegmentMethodScope","default":"all_reachable"}},"type":"object","required":["hubspot_list_id"],"title":"HubSpotSegmentCreate"},"HubSpotSegmentMethodScope":{"type":"string","enum":["all_reachable","single_preferred"],"title":"HubSpotSegmentMethodScope","description":"How many of a resolved contact's channels are targeted.\n\n* ``all_reachable`` (default) — one recipient per reachable, non-opted-out\n  channel the contact has (email + phone + messenger id …). Matches how a\n  native SendSeven dynamic list fans out.\n* ``single_preferred`` — collapse to a single preferred channel per contact."},"HubSpotSegmentPreviewResponse":{"properties":{"total_members":{"type":"integer","title":"Total Members","description":"HubSpot members inspected (capped when sampled)"},"resolved_contacts":{"type":"integer","title":"Resolved Contacts","description":"Members matched to an existing SendSeven contact"},"would_create":{"type":"integer","title":"Would Create","description":"Members that would auto-create a new contact"},"unresolved_members":{"type":"integer","title":"Unresolved Members","description":"Members skipped (no usable identifier / policy=skip)"},"sampled":{"type":"boolean","title":"Sampled","description":"True when only a sample of the list was inspected"}},"type":"object","required":["total_members","resolved_contacts","would_create","unresolved_members","sampled"],"title":"HubSpotSegmentPreviewResponse"},"HubSpotSegmentRefreshMode":{"type":"string","enum":["send_time","cached"],"title":"HubSpotSegmentRefreshMode","description":"How segment membership is resolved.\n\n* ``send_time`` (default) — resolve HubSpot membership live at send time;\n  ``hubspot_segment_cache`` is never read/written.\n* ``cached`` — a scheduled refresh (every ``interval_minutes``) resolves\n  membership into ``hubspot_segment_cache``; sends read from the cache."},"HubSpotSegmentResponse":{"properties":{"id":{"type":"string","title":"Id"},"tenant_id":{"type":"string","title":"Tenant Id"},"name":{"type":"string","title":"Name"},"description":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Description"},"list_type":{"type":"string","title":"List Type"},"hubspot_connection_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Hubspot Connection Id"},"hubspot_list_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Hubspot List Id"},"hubspot_segment_config":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Hubspot Segment Config"},"contact_count":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Contact Count"},"created_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Created At"},"updated_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Updated At"}},"type":"object","required":["id","tenant_id","name","list_type"],"title":"HubSpotSegmentResponse","description":"A HUBSPOT_SEGMENT list resource.\n\nMirrors the shared List resource but with ``list_type`` as a plain string\n(the shared ``ListTypeEnum`` does not enumerate ``hubspot_segment``)."},"HubSpotSegmentUnresolvedPolicy":{"type":"string","enum":["reachable_only","skip","all"],"title":"HubSpotSegmentUnresolvedPolicy","description":"What to do with a HubSpot member that has no existing SendSeven contact.\n\n* ``reachable_only`` (default) — auto-create a contact ONLY when the member\n  has a reachable identifier (valid phone and/or email); otherwise skip.\n* ``skip`` — never create; only send to already-existing contacts.\n* ``all`` — attempt to auto-create for every member (still subject to the\n  platform's no-identifier ghost-contact guard)."},"HubSpotSegmentUpdate":{"properties":{"name":{"anyOf":[{"type":"string","maxLength":255,"minLength":1},{"type":"null"}],"title":"Name"},"refresh_mode":{"anyOf":[{"$ref":"#/components/schemas/HubSpotSegmentRefreshMode"},{"type":"null"}]},"interval_minutes":{"anyOf":[{"type":"integer","maximum":1440.0,"minimum":5.0},{"type":"null"}],"title":"Interval Minutes"},"unresolved_policy":{"anyOf":[{"$ref":"#/components/schemas/HubSpotSegmentUnresolvedPolicy"},{"type":"null"}]},"method_scope":{"anyOf":[{"$ref":"#/components/schemas/HubSpotSegmentMethodScope"},{"type":"null"}]}},"type":"object","title":"HubSpotSegmentUpdate"},"HubSpotSettingsUpdate":{"properties":{"sync_direction_config":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Sync Direction Config","description":"Legacy wholesale config, e.g. {\"contacts\": \"bidirectional\" | \"hubspot_to_ss\" | \"ss_to_hubspot\" | \"off\", \"transcripts\": true}. Prefer the flat sync_direction / sync_transcripts fields."},"field_mapping_config":{"anyOf":[{"additionalProperties":{"type":"string"},"type":"object"},{"type":"null"}],"title":"Field Mapping Config","description":"Legacy alias for field_mapping: {\"hubspot_property\": \"sendseven_field\"} overrides."},"field_mapping":{"anyOf":[{"additionalProperties":{"type":"string"},"type":"object"},{"type":"null"}],"title":"Field Mapping","description":"{\"hubspot_property\": \"sendseven_field\"} overrides. A first-class contact field (first_name/last_name/name/birthday — NOT email/phone, see contact_method_mapping) OR a custom field referenced as \"custom:<field_key>\". A legacy override that still targets email/phone keeps working unchanged. \"birthday\" is ONE-WAY (HubSpot -> SendSeven only) — mapping it never projects contact.birthday back to HubSpot."},"contact_method_mapping":{"anyOf":[{"additionalProperties":{"additionalProperties":{"items":{"type":"string"},"type":"array"},"type":"object"},"type":"object"},{"type":"null"}],"title":"Contact Method Mapping","description":"Category-based mapping for the 'Contact methods' (Email/WhatsApp/Phone primary) and 'Social media profiles' (LinkedIn/Instagram Profile/Facebook Profile/Website/Other) settings sections: {\"contact_methods\": {hubspot_property: [target, ...]}, \"social_profiles\": {hubspot_property: [target, ...]}}. See GET /status's field of the same name for the full shape + targets."},"sync_direction":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Sync Direction","description":"\"bidirectional\" | \"hubspot_to_ss\" | \"ss_to_hubspot\" | \"off\""},"sync_transcripts":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Sync Transcripts","description":"Attach conversation transcripts to HubSpot on close"}},"type":"object","title":"HubSpotSettingsUpdate","description":"Settings PATCH body.\n\nThe settings UI sends the FLAT names that mirror the ``GET /status``\nresponse (``field_mapping`` / ``sync_direction`` / ``sync_transcripts``) so\nwrite and read are symmetric. The legacy ``*_config`` names are still\naccepted for API clients / backwards compatibility. When both a flat name\nand its legacy counterpart are present the flat name wins."},"HubSpotStatusResponse":{"properties":{"connected":{"type":"boolean","title":"Connected"},"is_connected":{"type":"boolean","title":"Is Connected","default":false},"portal_id":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Portal Id"},"sync_direction":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Sync Direction"},"sync_transcripts":{"type":"boolean","title":"Sync Transcripts","default":true},"connected_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Connected At"},"status":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Status","description":"pending | active | suspended | disconnected"},"hub_id":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Hub Id"},"hub_domain":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Hub Domain"},"granted_scopes":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Granted Scopes"},"sync_direction_config":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Sync Direction Config"},"field_mapping":{"anyOf":[{"additionalProperties":{"type":"string"},"type":"object"},{"type":"null"}],"title":"Field Mapping"},"contact_method_mapping":{"anyOf":[{"additionalProperties":{"additionalProperties":{"items":{"type":"string"},"type":"array"},"type":"object"},"type":"object"},{"type":"null"}],"title":"Contact Method Mapping","description":"Category-based mapping: {\"contact_methods\": {hubspot_property: [target, ...]}, \"social_profiles\": {hubspot_property: [target, ...]}}. Targets are contact-method/social-profile enum values (email/whatsapp_id/phone for contact_methods; linkedin/instagram_handle/facebook/homepage/social_other for social_profiles). One HubSpot property may map to MULTIPLE targets (e.g. HubSpot 'phone' -> both 'phone' and 'whatsapp_id')."},"sendseven_link_completed":{"type":"boolean","title":"Sendseven Link Completed","default":false},"linked_contacts":{"type":"integer","title":"Linked Contacts","default":0},"last_full_sync_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Last Full Sync At"},"installed_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Installed At"}},"type":"object","required":["connected"],"title":"HubSpotStatusResponse"},"HubStatsResponse":{"properties":{"range_days":{"type":"integer","title":"Range Days"},"period_start":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Period Start"},"period_end":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Period End"},"series_dates":{"items":{"type":"string"},"type":"array","title":"Series Dates"},"min_sample":{"type":"integer","title":"Min Sample"},"truncated":{"type":"boolean","title":"Truncated","default":false},"totals":{"$ref":"#/components/schemas/HubTotals"},"agents":{"items":{"$ref":"#/components/schemas/HubAgentRow"},"type":"array","title":"Agents"}},"type":"object","required":["range_days","series_dates","min_sample","totals","agents"],"title":"HubStatsResponse"},"HubSubtitle":{"properties":{"type":{"type":"string","title":"Type"},"label":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Label"}},"type":"object","required":["type"],"title":"HubSubtitle"},"HubTemplateItem":{"properties":{"key":{"type":"string","title":"Key"},"has_goal":{"type":"boolean","title":"Has Goal"},"suggested_channels":{"items":{"type":"string"},"type":"array","title":"Suggested Channels"},"skills":{"items":{"type":"string"},"type":"array","title":"Skills"}},"type":"object","required":["key","has_goal","suggested_channels","skills"],"title":"HubTemplateItem"},"HubTemplateList":{"properties":{"items":{"items":{"$ref":"#/components/schemas/HubTemplateItem"},"type":"array","title":"Items"}},"type":"object","required":["items"],"title":"HubTemplateList"},"HubTotals":{"properties":{"conversations":{"type":"integer","title":"Conversations","default":0},"conversations_prev":{"type":"integer","title":"Conversations Prev","default":0},"conversations_delta_pct":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Conversations Delta Pct"},"resolved":{"type":"integer","title":"Resolved","default":0},"resolved_pct":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Resolved Pct"},"handoffs":{"type":"integer","title":"Handoffs","default":0},"handoff_pct":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Handoff Pct"},"insufficient_data":{"type":"boolean","title":"Insufficient Data","default":true},"credits":{"type":"integer","title":"Credits","default":0},"credits_eur":{"type":"number","title":"Credits Eur","default":0.0}},"type":"object","title":"HubTotals"},"HubWidget":{"properties":{"widget_id":{"type":"string","title":"Widget Id"},"name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Name"},"widget_type":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Widget Type"}},"type":"object","required":["widget_id"],"title":"HubWidget"},"ImapConfig":{"properties":{"imap_host":{"type":"string","title":"Imap Host","description":"IMAP server hostname"},"imap_port":{"type":"integer","title":"Imap Port","description":"IMAP port (143, 993)","default":993},"imap_username":{"type":"string","title":"Imap Username","description":"IMAP username"},"imap_password":{"type":"string","title":"Imap Password","description":"IMAP password"},"imap_use_ssl":{"type":"boolean","title":"Imap Use Ssl","description":"Use SSL encryption","default":true}},"type":"object","required":["imap_host","imap_username","imap_password"],"title":"ImapConfig","description":"IMAP configuration for Type 2 integrations"},"ImportJobStartResponse":{"properties":{"job_id":{"type":"string","title":"Job Id","description":"PubSubJob id; use to poll status"},"status":{"type":"string","title":"Status","description":"Initial job status","default":"queued"},"message":{"type":"string","title":"Message","description":"Human-readable status message"},"poll_url":{"type":"string","title":"Poll Url","description":"Relative URL to poll for progress"}},"type":"object","required":["job_id","message","poll_url"],"title":"ImportJobStartResponse","description":"Returned by POST /email-contacts/import (202 Accepted).\n\nThe import now runs in a background worker so large CSVs no longer hit the\nrequest/proxy timeout. Poll ``poll_url`` (or call getImportStatus(job_id))\nuntil ``status`` is ``completed`` or ``failed``."},"ImportJobStatusResponse":{"properties":{"job_id":{"type":"string","title":"Job Id"},"status":{"type":"string","title":"Status","description":"queued | processing | completed | failed | cancelled"},"message":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Message","description":"Human-readable status message"},"progress_percent":{"type":"integer","title":"Progress Percent","description":"0-100","default":0},"total_items":{"type":"integer","title":"Total Items","description":"Total rows detected in the CSV","default":0},"processed_items":{"type":"integer","title":"Processed Items","description":"Rows processed so far","default":0},"failed_items":{"type":"integer","title":"Failed Items","description":"Invalid/failed rows","default":0},"summary":{"anyOf":[{"$ref":"#/components/schemas/EmailContactImportSummary"},{"type":"null"}],"description":"Full import summary; present once status == completed"},"error_message":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Error Message","description":"Present when status == failed"}},"type":"object","required":["job_id","status"],"title":"ImportJobStatusResponse","description":"Progress + final result for an async contact import job."},"ImportRequest":{"properties":{"meta_flow_id":{"type":"string","maxLength":64,"minLength":1,"title":"Meta Flow Id"}},"type":"object","required":["meta_flow_id"],"title":"ImportRequest"},"InboxCreate":{"properties":{"name":{"type":"string","maxLength":255,"minLength":1,"title":"Name"},"description":{"anyOf":[{"type":"string","maxLength":500},{"type":"null"}],"title":"Description"},"icon":{"anyOf":[{"type":"string","maxLength":50},{"type":"null"}],"title":"Icon","description":"Lucide icon name"},"color":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Color","description":"#RRGGBB hex color"},"access_mode":{"$ref":"#/components/schemas/AccessMode","description":"'all' (any team member) or 'specific' (whitelisted users)","default":"all"},"tag_filter_mode":{"$ref":"#/components/schemas/TagFilterMode","description":"How tag rules are combined: any (has at least one), all (has every), none (has none of them). Ignored when tag_ids is empty.","default":"none"},"channel_ids":{"items":{"type":"string"},"type":"array","maxItems":100,"title":"Channel Ids","description":"Non-email channel IDs that belong to this inbox. Email connections are listed separately under ``email_connections``."},"email_connections":{"anyOf":[{"items":{"$ref":"#/components/schemas/InboxEmailConnection"},"type":"array","maxItems":50},{"type":"null"}],"title":"Email Connections","description":"Email connections (EmailIntegration rows with can_receive_support=True) routed to this inbox, each with an optional alias-subset filter."},"tag_ids":{"anyOf":[{"items":{"type":"string"},"type":"array","maxItems":50},{"type":"null"}],"title":"Tag Ids","description":"Tag IDs used by the tag rule (interpreted via tag_filter_mode)"},"field_filters":{"anyOf":[{"items":{"$ref":"#/components/schemas/FieldFilterRule"},"type":"array","maxItems":20},{"type":"null"}],"title":"Field Filters","description":"Custom-field rules (AND-combined)"},"allowed_user_ids":{"anyOf":[{"items":{"type":"string"},"type":"array","maxItems":500},{"type":"null"}],"title":"Allowed User Ids","description":"Whitelisted user IDs (only used when access_mode='specific')"},"all_channels":{"type":"boolean","title":"All Channels","description":"True = the inbox covers every channel and email connection of the workspace (channel_ids / email_connections are ignored). False = only the listed ones; with none listed the inbox shows nothing.","default":false},"access_tag_ids":{"anyOf":[{"items":{"type":"string"},"type":"array","maxItems":50},{"type":"null"}],"title":"Access Tag Ids","description":"Team-member tag IDs that grant access (only used when access_mode='specific'): members holding ANY of these tags see the conversations matching this inbox's full filter. Separate from tag_ids, which filter conversations."},"is_active":{"type":"boolean","title":"Is Active","default":true},"sort_order":{"type":"integer","title":"Sort Order","default":0}},"type":"object","required":["name"],"title":"InboxCreate","description":"Payload for creating a custom inbox."},"InboxEmailConnection":{"properties":{"email_integration_id":{"type":"string","title":"Email Integration Id","description":"Email integration row id (the receiving connection — must have can_receive_support=True)."},"allowed_recipients":{"anyOf":[{"items":{"type":"string"},"type":"array","maxItems":100},{"type":"null"}],"title":"Allowed Recipients","description":"Optional subset of addresses on the connection to route to this inbox. None or empty list = all addresses."}},"type":"object","required":["email_integration_id"],"title":"InboxEmailConnection","description":"Email connection assigned to an inbox, with optional alias subset.\n\n``allowed_recipients`` semantics (catchall subset filter):\n  - ``None`` or ``[]`` -> the inbox accepts every address this\n    connection can receive on (its ``email_address`` plus everything\n    in ``accepted_aliases``).\n  - non-empty list     -> the inbox routes only inbound conversations\n    whose ``Conversation.recipient_address`` is one of the listed\n    values. Each entry MUST be in the connection's\n    ``email_address ∪ accepted_aliases``; the service enforces this."},"InboxListResponse":{"properties":{"items":{"items":{"$ref":"#/components/schemas/InboxOut"},"type":"array","title":"Items"}},"type":"object","required":["items"],"title":"InboxListResponse","description":"Wrapper for list endpoints — unified API convention."},"InboxOut":{"properties":{"id":{"type":"string","title":"Id"},"tenant_id":{"type":"string","title":"Tenant Id"},"name":{"type":"string","title":"Name"},"description":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Description"},"icon":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Icon"},"color":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Color"},"access_mode":{"type":"string","title":"Access Mode"},"tag_filter_mode":{"type":"string","title":"Tag Filter Mode"},"is_active":{"type":"boolean","title":"Is Active"},"sort_order":{"type":"integer","title":"Sort Order"},"channel_ids":{"items":{"type":"string"},"type":"array","title":"Channel Ids"},"email_connections":{"items":{"$ref":"#/components/schemas/InboxEmailConnection"},"type":"array","title":"Email Connections"},"tag_ids":{"items":{"type":"string"},"type":"array","title":"Tag Ids"},"field_filters":{"items":{"$ref":"#/components/schemas/FieldFilterRule"},"type":"array","title":"Field Filters"},"allowed_user_ids":{"items":{"type":"string"},"type":"array","title":"Allowed User Ids"},"all_channels":{"type":"boolean","title":"All Channels","default":false},"access_tag_ids":{"items":{"type":"string"},"type":"array","title":"Access Tag Ids"},"created_at":{"type":"string","format":"date-time","title":"Created At"},"updated_at":{"type":"string","format":"date-time","title":"Updated At"}},"type":"object","required":["id","tenant_id","name","access_mode","tag_filter_mode","is_active","sort_order","created_at","updated_at"],"title":"InboxOut","description":"Custom inbox response body."},"InboxUpdate":{"properties":{"name":{"anyOf":[{"type":"string","maxLength":255,"minLength":1},{"type":"null"}],"title":"Name"},"description":{"anyOf":[{"type":"string","maxLength":500},{"type":"null"}],"title":"Description"},"icon":{"anyOf":[{"type":"string","maxLength":50},{"type":"null"}],"title":"Icon"},"color":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Color"},"access_mode":{"anyOf":[{"$ref":"#/components/schemas/AccessMode"},{"type":"null"}]},"tag_filter_mode":{"anyOf":[{"$ref":"#/components/schemas/TagFilterMode"},{"type":"null"}]},"channel_ids":{"anyOf":[{"items":{"type":"string"},"type":"array","maxItems":100},{"type":"null"}],"title":"Channel Ids"},"email_connections":{"anyOf":[{"items":{"$ref":"#/components/schemas/InboxEmailConnection"},"type":"array","maxItems":50},{"type":"null"}],"title":"Email Connections","description":"Replace-style: full list of email connections routed to this inbox. None = leave unchanged; empty list = clear."},"tag_ids":{"anyOf":[{"items":{"type":"string"},"type":"array","maxItems":50},{"type":"null"}],"title":"Tag Ids"},"field_filters":{"anyOf":[{"items":{"$ref":"#/components/schemas/FieldFilterRule"},"type":"array","maxItems":20},{"type":"null"}],"title":"Field Filters"},"allowed_user_ids":{"anyOf":[{"items":{"type":"string"},"type":"array","maxItems":500},{"type":"null"}],"title":"Allowed User Ids"},"all_channels":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"All Channels"},"access_tag_ids":{"anyOf":[{"items":{"type":"string"},"type":"array","maxItems":50},{"type":"null"}],"title":"Access Tag Ids"},"is_active":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Is Active"},"sort_order":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Sort Order"}},"type":"object","title":"InboxUpdate","description":"Payload for partially updating a custom inbox.\n\nAll fields are optional; ``None`` means \"do not change\". An explicit\nempty list (e.g. ``tag_ids=[]``) means \"clear the list\"."},"InboxUsersUpdate":{"properties":{"user_ids":{"items":{"type":"string"},"type":"array","maxItems":500,"title":"User Ids"}},"type":"object","title":"InboxUsersUpdate","description":"Replace the full list of allowed users for an inbox."},"InitiateConversationRequest":{"properties":{"contact_id":{"type":"string","title":"Contact Id","description":"Contact UUID to start conversation with"},"channel_id":{"type":"string","title":"Channel Id","description":"Channel UUID to use for this conversation"},"message_type":{"$ref":"#/components/schemas/InitiateMessageType","description":"Type of message to send","default":"text"},"content":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Content","description":"Message text content (required for message_type=text)"},"template":{"anyOf":[{"$ref":"#/components/schemas/WhatsAppTemplateData"},{"type":"null"}],"description":"WhatsApp template data (required when template is needed)"},"email":{"anyOf":[{"$ref":"#/components/schemas/InitiateEmailData"},{"type":"null"}],"description":"Email message data (required for message_type=email)"}},"type":"object","required":["contact_id","channel_id"],"title":"InitiateConversationRequest","description":"Request to initiate a new outbound conversation.\n\nThe message_type determines which additional fields are required:\n- text: content field is required\n- template: template field is required (WhatsApp only)\n- email: email field is required","examples":[{"channel_id":"channel-uuid","contact_id":"contact-uuid","content":"Hello! How can I help you today?","message_type":"text"},{"channel_id":"whatsapp-channel-uuid","contact_id":"contact-uuid","message_type":"template","template":{"components":[],"language":"en","name":"hello_world"}},{"channel_id":"email-channel-uuid","contact_id":"contact-uuid","email":{"bcc":[],"body_html":"<p>Hello, thank you for reaching out!</p>","cc":[],"include_signature":true,"subject":"Re: Your inquiry"},"message_type":"email"}]},"InitiateConversationResponse":{"properties":{"conversation_id":{"type":"string","title":"Conversation Id","description":"Created or existing conversation ID"},"message_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Message Id","description":"Sent message ID (if successful)"},"channel_type":{"type":"string","title":"Channel Type","description":"Channel type used"},"status":{"type":"string","title":"Status","description":"Message status: sent, queued, failed, template_required"},"requires_template":{"type":"boolean","title":"Requires Template","description":"Whether a WhatsApp template is required (24h window closed)","default":false},"window_status":{"anyOf":[{"$ref":"#/components/schemas/WhatsAppWindowStatus"},{"type":"null"}],"description":"WhatsApp messaging window status (WhatsApp only)"},"error":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Error","description":"Error message if failed"}},"type":"object","required":["conversation_id","channel_type","status"],"title":"InitiateConversationResponse","description":"Response from initiating a conversation"},"InitiateEmailData":{"properties":{"subject":{"type":"string","maxLength":998,"minLength":1,"title":"Subject","description":"Email subject line"},"to_email":{"anyOf":[{"type":"string","format":"email"},{"type":"null"}],"title":"To Email","description":"Explicit recipient address. When provided, the email is sent to THIS exact address even if it is a non-primary contact method on the matched contact, or an address that does not match the contact's primary email. The conversation still threads onto contact_id. When omitted, falls back to the contact's primary email (backward-compatible default)."},"body_html":{"type":"string","maxLength":500000,"minLength":1,"title":"Body Html","description":"HTML body content"},"include_signature":{"type":"boolean","title":"Include Signature","description":"Append agent's email signature","default":true},"from_email":{"anyOf":[{"type":"string","format":"email"},{"type":"null"}],"title":"From Email","description":"Custom sender email address (must be from a verified domain)"},"from_name":{"anyOf":[{"type":"string","maxLength":255},{"type":"null"}],"title":"From Name","description":"Custom sender display name"},"cc":{"items":{"type":"string","format":"email"},"type":"array","maxItems":50,"title":"Cc","description":"CC recipients (valid email addresses)"},"bcc":{"items":{"type":"string","format":"email"},"type":"array","maxItems":50,"title":"Bcc","description":"BCC recipients (valid email addresses)"}},"type":"object","required":["subject","body_html"],"title":"InitiateEmailData","description":"Email-specific message data"},"InitiateMessageType":{"type":"string","enum":["text","template","email"],"title":"InitiateMessageType","description":"Types of messages that can be sent when initiating a conversation"},"InlineImageUploadResponse":{"properties":{"attachment_id":{"type":"string","title":"Attachment Id"},"content_id":{"type":"string","title":"Content Id","description":"Content-ID for use in HTML (cid:xxx)"},"url":{"type":"string","title":"Url","description":"Signed URL for preview"}},"type":"object","required":["attachment_id","content_id","url"],"title":"InlineImageUploadResponse","description":"Response after uploading inline image"},"InstagramMediaItem":{"properties":{"id":{"type":"string","title":"Id"},"thumbnail_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Thumbnail Url"},"media_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Media Url"},"caption":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Caption"},"media_type":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Media Type"},"permalink":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Permalink"},"timestamp":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Timestamp"}},"type":"object","required":["id"],"title":"InstagramMediaItem","description":"A single Instagram media object (post, reel, or carousel)."},"InstagramMediaListResponse":{"properties":{"items":{"items":{"$ref":"#/components/schemas/InstagramMediaItem"},"type":"array","title":"Items"},"pagination":{"$ref":"#/components/schemas/InstagramMediaPagination"}},"type":"object","required":["items","pagination"],"title":"InstagramMediaListResponse"},"InstagramMediaPagination":{"properties":{"next_cursor":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Next Cursor"}},"type":"object","title":"InstagramMediaPagination","description":"Cursor-based pagination metadata for Instagram media listings.\n\nMeta's Graph API returns opaque ``after`` cursors and does not expose a\ntotal count, so we use a minimal cursor shape rather than the standard\npage/page_size metadata."},"IntegrationCatalogEntry":{"properties":{"key":{"type":"string","title":"Key","description":"Stable machine key, e.g. 'zapier'"},"name":{"type":"string","title":"Name","description":"Display name"},"description":{"type":"string","title":"Description","description":"Short marketing description"},"category":{"type":"string","title":"Category","description":"'automation' | 'resources'"},"logo":{"type":"string","title":"Logo","description":"Asset key or URL for the logo (light-mode/default variant)"},"logo_dark":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Logo Dark","description":"Asset key or URL for the logo shown in dark mode"},"auth_type":{"type":"string","title":"Auth Type","description":"'api_token' | 'external_link' | 'oauth' (oauth reserved)"},"docs_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Docs Url","description":"External documentation link"},"action_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Action Url","description":"External link for external_link cards"},"setup_route":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Setup Route","description":"Internal frontend route for setup, if any"},"external":{"type":"boolean","title":"External","description":"Whether this card links off-platform"},"connected":{"type":"boolean","title":"Connected","description":"Whether this tenant has set up the integration"},"status":{"type":"string","title":"Status","description":"'available' | 'connected' | 'coming_soon' | 'early_access'"}},"type":"object","required":["key","name","description","category","logo","auth_type","external","connected","status"],"title":"IntegrationCatalogEntry","description":"A single integration card in the Integrations Hub."},"IntegrationCatalogResponse":{"properties":{"items":{"items":{"$ref":"#/components/schemas/IntegrationCatalogEntry"},"type":"array","title":"Items","description":"Available integrations"}},"type":"object","required":["items"],"title":"IntegrationCatalogResponse","description":"Unified list response for the integrations catalog."},"InteractiveButton":{"properties":{"id":{"type":"string","maxLength":256,"title":"Id","description":"Unique button identifier (returned in reply)"},"title":{"type":"string","maxLength":20,"title":"Title","description":"Button display text (max 20 chars)"}},"type":"object","required":["id","title"],"title":"InteractiveButton","description":"Button for interactive reply buttons message"},"InteractiveButtonsCapability":{"properties":{"supported":{"type":"boolean","title":"Supported","default":true},"max_buttons":{"type":"integer","title":"Max Buttons"}},"type":"object","required":["max_buttons"],"title":"InteractiveButtonsCapability","description":"Interactive buttons capability details"},"InteractiveContent":{"properties":{"type":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Type","description":"Interactive type: 'button' or 'list'"},"header":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Header","description":"Header object (text or media), Meta format"},"body":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Body","description":"Body object: {text: ...}"},"footer":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Footer","description":"Footer object: {text: ...}"},"action":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Action","description":"Action object: {buttons:[...]} or {button, sections:[...]}"}},"additionalProperties":true,"type":"object","title":"InteractiveContent","description":"Outbound interactive message content (WhatsApp-style Meta format).\n\nMirrors the object an adapter sends and is persisted verbatim under\n``messages.meta.interactive`` by the flow Send-node handler. The\nconversation UI + flow test panel render an interactive bubble from\nthis shape (``MessageBubble.getInteractiveContent``).\n\nAll fields optional / extra allowed so older rows and channel-specific\nvariants pass through untouched.\n\nHeader media (image/video headers) is surfaced as the sibling\n``MessageResponse.header_media_url`` / ``header_media_attachment_id``\nfields, NOT inside this object — the FE streams header media via the\nsigned-GCS-URL path keyed on the attachment id."},"InteractiveHeader":{"properties":{"type":{"type":"string","enum":["text","image","video","document"],"title":"Type","description":"Header type","default":"text"},"text":{"anyOf":[{"type":"string","maxLength":60},{"type":"null"}],"title":"Text","description":"Header text (for type=text)"},"media_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Media Url","description":"Media URL for image/video/document headers"}},"type":"object","title":"InteractiveHeader","description":"Header for interactive message"},"InteractiveListRow":{"properties":{"id":{"type":"string","maxLength":200,"title":"Id","description":"Unique row identifier (returned when selected)"},"title":{"type":"string","maxLength":24,"title":"Title","description":"Row title (max 24 chars)"},"description":{"anyOf":[{"type":"string","maxLength":72},{"type":"null"}],"title":"Description","description":"Row description (max 72 chars)"}},"type":"object","required":["id","title"],"title":"InteractiveListRow","description":"Row item within a list section"},"InteractiveListSection":{"properties":{"title":{"type":"string","maxLength":24,"title":"Title","description":"Section title"},"rows":{"items":{"$ref":"#/components/schemas/InteractiveListRow"},"type":"array","maxItems":10,"minItems":1,"title":"Rows","description":"Rows in this section (1-10)"}},"type":"object","required":["title","rows"],"title":"InteractiveListSection","description":"Section in a list menu"},"InteractiveListsCapability":{"properties":{"supported":{"type":"boolean","title":"Supported","default":true},"max_sections":{"type":"integer","title":"Max Sections"},"max_rows_per_section":{"type":"integer","title":"Max Rows Per Section"}},"type":"object","required":["max_sections","max_rows_per_section"],"title":"InteractiveListsCapability","description":"Interactive lists capability details"},"InteractiveMessageCreate":{"properties":{"channel_id":{"type":"string","title":"Channel Id","description":"Channel UUID to send through"},"contact_id":{"type":"string","title":"Contact Id","description":"Contact UUID to send to"},"type":{"$ref":"#/components/schemas/InteractiveType","description":"Interactive message type: 'buttons', 'list', or 'request_contact_info'"},"body":{"type":"string","maxLength":1024,"title":"Body","description":"Main message body text (max 1024 chars)"},"header":{"anyOf":[{"$ref":"#/components/schemas/InteractiveHeader"},{"type":"null"}],"description":"Optional header (text, image, video, or document)"},"footer":{"anyOf":[{"type":"string","maxLength":60},{"type":"null"}],"title":"Footer","description":"Optional footer text (max 60 chars)"},"buttons":{"anyOf":[{"items":{"$ref":"#/components/schemas/InteractiveButton"},"type":"array","maxItems":3},{"type":"null"}],"title":"Buttons","description":"Buttons for type='buttons' (max 3)"},"sections":{"anyOf":[{"items":{"$ref":"#/components/schemas/ListSection"},"type":"array","maxItems":10},{"type":"null"}],"title":"Sections","description":"Sections for type='list' (max 10)"},"button_text":{"anyOf":[{"type":"string","maxLength":20},{"type":"null"}],"title":"Button Text","description":"Text on the list menu button (for type='list')","default":"View Options"},"meta":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Meta","description":"Custom metadata to attach to the message"}},"type":"object","required":["channel_id","contact_id","type","body"],"title":"InteractiveMessageCreate","description":"Schema for sending an interactive message.\n\nInteractive messages provide rich UI elements like buttons and list menus.\nCurrently supported on WhatsApp only.\n\nFor type=\"buttons\":\n- Maximum 3 buttons\n- Each button has id and title (max 20 chars)\n\nFor type=\"list\":\n- One or more sections with rows\n- Each section has a title and 1-10 rows\n- Each row has id, title, and optional description\n\nFor type=\"request_contact_info\":\n- Only `body` is used — no buttons or sections\n- The single button's label is fixed by Meta and auto-localized","examples":[{"description":"Send a message with up to 3 reply buttons","summary":"Reply buttons","value":{"body":"How would you like to proceed with your order?","buttons":[{"id":"btn_confirm","title":"Confirm Order"},{"id":"btn_modify","title":"Modify Order"},{"id":"btn_cancel","title":"Cancel Order"}],"channel_id":"a1b2c3d4-5678-9abc-def0-1234567890ab","contact_id":"c1d2e3f4-5678-9abc-def0-1234567890ab","footer":"Reply within 24 hours","header":{"text":"Order #12345","type":"text"},"type":"buttons"}},{"description":"Send a list menu with selectable options organized in sections","summary":"List menu","value":{"body":"Please select a department to connect with:","button_text":"View Departments","channel_id":"a1b2c3d4-5678-9abc-def0-1234567890ab","contact_id":"c1d2e3f4-5678-9abc-def0-1234567890ab","sections":[{"rows":[{"description":"Start a new sales conversation","id":"sales_new","title":"New Inquiry"},{"description":"Follow up on a quote","id":"sales_existing","title":"Existing Quote"}],"title":"Sales"},{"rows":[{"description":"Get help with a technical problem","id":"support_technical","title":"Technical Issue"},{"description":"Ask about invoices or payments","id":"support_billing","title":"Billing Question"}],"title":"Support"}],"type":"list"}},{"description":"Ask the user to share their own WhatsApp phone number. Useful for contacts who adopted a WhatsApp username, whose number Meta withholds from every webhook until they share it. The shared number arrives as a contacts message and is linked to the contact automatically.","summary":"Request contact info","value":{"body":"To send you delivery updates by SMS, please share your phone number.","channel_id":"a1b2c3d4-5678-9abc-def0-1234567890ab","contact_id":"c1d2e3f4-5678-9abc-def0-1234567890ab","type":"request_contact_info"}}]},"InteractiveMessageResponse":{"properties":{"id":{"type":"string","title":"Id","description":"Message UUID"},"conversation_id":{"type":"string","title":"Conversation Id","description":"Conversation UUID"},"external_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"External Id","description":"Platform message ID (e.g., WhatsApp wamid)"},"status":{"type":"string","title":"Status","description":"Message status: pending, sent, failed"},"platform":{"type":"string","title":"Platform","description":"Platform: whatsapp, telegram, etc."},"type":{"type":"string","title":"Type","description":"Interactive type: buttons, list"},"error":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Error","description":"Error message if status is 'failed'"},"error_code":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Error Code","description":"WhatsApp API error code (e.g., 131042 for payment issues)"}},"type":"object","required":["id","conversation_id","status","platform","type"],"title":"InteractiveMessageResponse","description":"Response after sending an interactive message","example":{"conversation_id":"b5d8e2a1-4c3f-4a8b-9d2e-1f6a7b8c9d0e","external_id":"wamid.HBgNNDkxNTIxNTU4MDg2NRUCABIYFjNFQjBCNUYxMjdFRkI5","id":"f47ac10b-58cc-4372-a567-0e02b2c3d479","platform":"whatsapp","status":"sent","type":"buttons"}},"InteractiveType":{"type":"string","enum":["buttons","list","request_contact_info"],"title":"InteractiveType","description":"Interactive message type"},"InternalNoteCreate":{"properties":{"conversation_id":{"type":"string","title":"Conversation Id","description":"Conversation UUID to attach the note to"},"text":{"type":"string","maxLength":10000,"minLength":1,"title":"Text","description":"Note text content"},"mentioned_user_ids":{"items":{"type":"string"},"type":"array","maxItems":50,"title":"Mentioned User Ids","description":"User IDs mentioned with @ in this note (triggers notifications, max 50)"},"assign_to_user_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Assign To User Id","description":"If provided, assign the conversation to this user"}},"type":"object","required":["conversation_id","text"],"title":"InternalNoteCreate","description":"Schema for creating an internal note on a conversation.\n\nInternal notes are visible only to agents (never sent to customers).\nSupports @mentions to notify other team members.\n\nIf `assign_to_user_id` is provided, the conversation is also assigned\nto that user (triggered by @mentioning someone in the note).","examples":[{"summary":"Simple internal note","value":{"conversation_id":"b5d8e2a1-4c3f-4a8b-9d2e-1f6a7b8c9d0e","text":"Customer called about order #12345, needs follow-up tomorrow."}},{"summary":"Note with @mentions","value":{"conversation_id":"b5d8e2a1-4c3f-4a8b-9d2e-1f6a7b8c9d0e","mentioned_user_ids":["user-uuid-1234"],"text":"@John can you check the billing issue for this customer?"}},{"summary":"Note with assignment","value":{"assign_to_user_id":"user-uuid-5678","conversation_id":"b5d8e2a1-4c3f-4a8b-9d2e-1f6a7b8c9d0e","mentioned_user_ids":["user-uuid-5678"],"text":"@Bosko can you handle this customer?"}}]},"InvitationAcceptResponse":{"properties":{"message":{"type":"string","title":"Message"},"tenant_id":{"type":"string","title":"Tenant Id"},"tenant_name":{"type":"string","title":"Tenant Name"},"role":{"type":"string","title":"Role"}},"type":"object","required":["message","tenant_id","tenant_name","role"],"title":"InvitationAcceptResponse","description":"Schema for accepting an invitation"},"InvitationCreate":{"properties":{"email":{"type":"string","format":"email","title":"Email","description":"Email address to invite"},"role_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Role Id","description":"RBAC Role UUID to assign (system OR this tenant's custom role). Get valid ids from GET /roles. Preferred over the legacy `role` field."},"role":{"anyOf":[{"$ref":"#/components/schemas/InvitationRoleEnum"},{"type":"null"}],"description":"DEPRECATED legacy role slug (owner, admin, marketing_agent, support_agent, viewer, billing_admin, developer, analyst, content_manager, designer). Ignored when `role_id` is set."}},"type":"object","required":["email"],"title":"InvitationCreate","description":"Schema for creating an invitation.\n\nRole assignment supports BOTH system roles and tenant-created custom roles\nvia ``role_id`` (the RBAC Role UUID from ``GET /roles`` — system roles also\nhave rows/UUIDs). The legacy ``role`` slug enum is retained for backwards\ncompatibility and is ignored when ``role_id`` is provided."},"InvitationDeclineResponse":{"properties":{"message":{"type":"string","title":"Message"}},"type":"object","required":["message"],"title":"InvitationDeclineResponse","description":"Schema for declining an invitation"},"InvitationListResponse":{"properties":{"invitations":{"items":{"$ref":"#/components/schemas/InvitationResponse"},"type":"array","title":"Invitations"},"total":{"type":"integer","title":"Total"}},"type":"object","required":["invitations","total"],"title":"InvitationListResponse","description":"Schema for list of invitations"},"InvitationResendResponse":{"properties":{"message":{"type":"string","title":"Message"},"invitation":{"$ref":"#/components/schemas/InvitationResponse"}},"type":"object","required":["message","invitation"],"title":"InvitationResendResponse","description":"Schema for resending an invitation"},"InvitationResponse":{"properties":{"id":{"type":"string","title":"Id"},"tenant_id":{"type":"string","title":"Tenant Id"},"email":{"type":"string","title":"Email"},"invited_role":{"type":"string","title":"Invited Role"},"invited_by_user_id":{"type":"string","title":"Invited By User Id"},"invitation_token":{"type":"string","title":"Invitation Token"},"status":{"$ref":"#/components/schemas/InvitationStatusEnum"},"expires_at":{"type":"string","format":"date-time","title":"Expires At"},"accepted_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Accepted At"},"declined_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Declined At"},"created_at":{"type":"string","format":"date-time","title":"Created At"},"updated_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Updated At"},"tenant_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Tenant Name"},"invited_by_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Invited By Name"}},"type":"object","required":["id","tenant_id","email","invited_role","invited_by_user_id","invitation_token","status","expires_at","created_at"],"title":"InvitationResponse","description":"Schema for invitation response"},"InvitationRoleEnum":{"type":"string","enum":["owner","admin","viewer","marketing_agent","support_agent","billing_admin","developer","analyst","content_manager","designer"],"title":"InvitationRoleEnum","description":"Allowed invitation roles - maps to RBAC system roles.\n\nThere are exactly 7 system roles (migration 059, Jan 2026, consolidated the\noriginal 10 and renamed them SCREAMING_SNAKE -> display case). The slugs below\nmap onto them in ``app/api/invitations.py::role_mapping``:\n\n- OWNER            -> \"Owner\"          Full access to everything (*:*)\n- ADMIN            -> \"Admin\"          Full access EXCEPT billing (zero billing scopes)\n- SUPPORT_AGENT    -> \"Support Agent\"  Conversations, tickets, contacts, knowledge base\n- MARKETING_AGENT  -> \"Marketing\"      Campaigns, email, lists, contacts\n- VIEWER           -> \"Viewer\"         Read-only access to all resources\n- BILLING_ADMIN    -> \"Billing\"        Billing management only\n- DEVELOPER        -> \"Developer\"      API tokens, webhooks, technical settings\n\nANALYST, CONTENT_MANAGER and DESIGNER are RETIRED roles. They are kept in this\nenum only so old pending invitations still resolve, and they fold onto a\nsurviving role (analyst/designer -> Viewer, content_manager -> Marketing).\nDo NOT offer them in new UI or docs.\n\nNew integrations should pass ``role_id`` (an RBAC Role UUID from ``GET /roles``)\ninstead of a slug -- that path also supports tenant-created custom roles."},"InvitationStatusEnum":{"type":"string","enum":["pending","accepted","declined","expired"],"title":"InvitationStatusEnum","description":"Invitation status"},"InvoiceDetailResponse":{"properties":{"id":{"type":"string","title":"Id","description":"Internal invoice ID"},"stripe_invoice_id":{"type":"string","title":"Stripe Invoice Id","description":"Stripe invoice ID (starts with 'in_')"},"status":{"type":"string","title":"Status","description":"Invoice status: draft, open, paid, void, uncollectible"},"period_start":{"type":"string","format":"date-time","title":"Period Start","description":"Start of billing period"},"period_end":{"type":"string","format":"date-time","title":"Period End","description":"End of billing period"},"subtotal_euros":{"type":"string","pattern":"^(?!^[-+.]*$)[+-]?0*\\d*\\.?\\d*$","title":"Subtotal Euros","description":"Subtotal before taxes"},"total_euros":{"type":"string","pattern":"^(?!^[-+.]*$)[+-]?0*\\d*\\.?\\d*$","title":"Total Euros","description":"Total amount"},"amount_paid_euros":{"type":"string","pattern":"^(?!^[-+.]*$)[+-]?0*\\d*\\.?\\d*$","title":"Amount Paid Euros","description":"Amount already paid"},"amount_due_euros":{"type":"string","pattern":"^(?!^[-+.]*$)[+-]?0*\\d*\\.?\\d*$","title":"Amount Due Euros","description":"Amount still due"},"invoice_pdf_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Invoice Pdf Url","description":"URL to download invoice PDF"},"hosted_invoice_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Hosted Invoice Url","description":"URL to Stripe-hosted invoice page"},"created_at":{"type":"string","format":"date-time","title":"Created At"},"paid_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Paid At"},"line_items":{"items":{"$ref":"#/components/schemas/InvoiceLineItemResponse"},"type":"array","title":"Line Items"},"total_cents":{"type":"integer","title":"Total Cents","description":"Total in cents for backward compatibility","readOnly":true}},"type":"object","required":["id","stripe_invoice_id","status","period_start","period_end","subtotal_euros","total_euros","amount_paid_euros","amount_due_euros","created_at","total_cents"],"title":"InvoiceDetailResponse","description":"Detailed invoice with line items"},"InvoiceLineItemResponse":{"properties":{"description":{"type":"string","title":"Description","description":"Human-readable description (e.g., 'WhatsApp Messages +49171231231')"},"quantity":{"type":"integer","title":"Quantity","description":"Number of units"},"unit_price_euros":{"type":"string","pattern":"^(?!^[-+.]*$)[+-]?0*\\d*\\.?\\d*$","title":"Unit Price Euros","description":"Price per unit in EUR"},"total_euros":{"type":"string","pattern":"^(?!^[-+.]*$)[+-]?0*\\d*\\.?\\d*$","title":"Total Euros","description":"Total amount in EUR (quantity x unit_price)"},"cost_type":{"type":"string","title":"Cost Type","description":"Cost type identifier (e.g., 'message_whatsapp')"},"connection_identifier":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Connection Identifier","description":"Connection identifier (e.g., '@BotName', '+49171231231')"},"unit_price_cents":{"type":"integer","title":"Unit Price Cents","description":"Unit price in cents for backward compatibility","readOnly":true},"total_cents":{"type":"integer","title":"Total Cents","description":"Total in cents for backward compatibility","readOnly":true}},"type":"object","required":["description","quantity","unit_price_euros","total_euros","cost_type","unit_price_cents","total_cents"],"title":"InvoiceLineItemResponse","description":"Single line item on an invoice"},"InvoicePreviewResponse":{"properties":{"billing_account_id":{"type":"string","title":"Billing Account Id"},"period_start":{"type":"string","format":"date-time","title":"Period Start"},"period_end":{"type":"string","format":"date-time","title":"Period End"},"line_items":{"items":{"$ref":"#/components/schemas/InvoiceLineItemResponse"},"type":"array","title":"Line Items"},"subtotal_euros":{"type":"string","pattern":"^(?!^[-+.]*$)[+-]?0*\\d*\\.?\\d*$","title":"Subtotal Euros","description":"Sum of all usage line items"},"minimum_spend_euros":{"type":"string","pattern":"^(?!^[-+.]*$)[+-]?0*\\d*\\.?\\d*$","title":"Minimum Spend Euros","description":"Plan minimum spend in EUR (0 for Pay As You Go)","default":"0"},"minimum_spend_adjustment_euros":{"type":"string","pattern":"^(?!^[-+.]*$)[+-]?0*\\d*\\.?\\d*$","title":"Minimum Spend Adjustment Euros","description":"Amount added to reach minimum spend (0 if usage exceeds minimum)","default":"0"},"package_type":{"type":"string","title":"Package Type","description":"Current plan: PAY_AS_YOU_GO, SCALE, or BUSINESS","default":"PAY_AS_YOU_GO"},"total_euros":{"type":"string","pattern":"^(?!^[-+.]*$)[+-]?0*\\d*\\.?\\d*$","title":"Total Euros","description":"Grand total in EUR: max(subtotal, minimum_spend)"},"currency":{"type":"string","title":"Currency","description":"Currency code","default":"EUR"},"is_preview":{"type":"boolean","title":"Is Preview","description":"True for previews, False for finalized invoices","default":true},"tenant_breakdown":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Tenant Breakdown","description":"Per-tenant usage breakdown"},"subtotal_cents":{"type":"integer","title":"Subtotal Cents","description":"Subtotal in cents for backward compatibility","readOnly":true},"total_cents":{"type":"integer","title":"Total Cents","description":"Total in cents for backward compatibility","readOnly":true}},"type":"object","required":["billing_account_id","period_start","period_end","line_items","subtotal_euros","total_euros","subtotal_cents","total_cents"],"title":"InvoicePreviewResponse","description":"Preview of an upcoming invoice (Feb 2026: usage-based with minimum spend)"},"InvoiceResponse":{"properties":{"id":{"type":"string","title":"Id","description":"Internal invoice ID"},"stripe_invoice_id":{"type":"string","title":"Stripe Invoice Id","description":"Stripe invoice ID (starts with 'in_')"},"status":{"type":"string","title":"Status","description":"Invoice status: draft, open, paid, void, uncollectible"},"period_start":{"type":"string","format":"date-time","title":"Period Start","description":"Start of billing period"},"period_end":{"type":"string","format":"date-time","title":"Period End","description":"End of billing period"},"subtotal_euros":{"type":"string","pattern":"^(?!^[-+.]*$)[+-]?0*\\d*\\.?\\d*$","title":"Subtotal Euros","description":"Subtotal before taxes"},"total_euros":{"type":"string","pattern":"^(?!^[-+.]*$)[+-]?0*\\d*\\.?\\d*$","title":"Total Euros","description":"Total amount"},"amount_paid_euros":{"type":"string","pattern":"^(?!^[-+.]*$)[+-]?0*\\d*\\.?\\d*$","title":"Amount Paid Euros","description":"Amount already paid"},"amount_due_euros":{"type":"string","pattern":"^(?!^[-+.]*$)[+-]?0*\\d*\\.?\\d*$","title":"Amount Due Euros","description":"Amount still due"},"invoice_pdf_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Invoice Pdf Url","description":"URL to download invoice PDF"},"hosted_invoice_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Hosted Invoice Url","description":"URL to Stripe-hosted invoice page"},"created_at":{"type":"string","format":"date-time","title":"Created At"},"paid_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Paid At"},"total_cents":{"type":"integer","title":"Total Cents","description":"Total in cents for backward compatibility","readOnly":true}},"type":"object","required":["id","stripe_invoice_id","status","period_start","period_end","subtotal_euros","total_euros","amount_paid_euros","amount_due_euros","created_at","total_cents"],"title":"InvoiceResponse","description":"Summary of a Stripe invoice"},"JobError":{"properties":{"code":{"type":"string","title":"Code","description":"ai_unavailable | ai_error | invalid_request | interrupted | cancelled | not_found"},"message":{"type":"string","title":"Message"}},"type":"object","required":["code","message"],"title":"JobError"},"JobResponse":{"properties":{"job_id":{"type":"string","title":"Job Id"},"kind":{"type":"string","title":"Kind","description":"generate | refine"},"status":{"type":"string","title":"Status","description":"queued | running | succeeded | failed"},"stage":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Stage","description":"generating | repairing | validating (only while running)"},"repair_round":{"type":"integer","title":"Repair Round","description":"Current/last repair round (1..max_repair_rounds)","default":0},"result":{"anyOf":[{"$ref":"#/components/schemas/GenerateResponse"},{"type":"null"}],"description":"Set when status=succeeded (may still be valid=false)"},"error":{"anyOf":[{"$ref":"#/components/schemas/JobError"},{"type":"null"}],"description":"Set when status=failed; nothing was charged"},"request_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Request Id"},"created_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Created At"},"updated_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Updated At"}},"type":"object","required":["job_id","kind","status"],"title":"JobResponse"},"JobStatusResponse":{"properties":{"job_id":{"type":"string","title":"Job Id","description":"Job ID being queried"},"resource_type":{"type":"string","enum":["document","folder"],"title":"Resource Type","description":"Which resource this job_id attaches to."},"status":{"type":"string","title":"Status","description":"Lifecycle status of the target resource. Values come from KBDocumentStatus (for documents) or FolderStatus (for folders)."},"document_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Document Id","description":"Document ID if resource_type='document'"},"folder_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Folder Id","description":"Folder ID if resource_type='folder'"},"last_error":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Last Error","description":"Last error message if the job failed (documents only)."}},"type":"object","required":["job_id","resource_type","status"],"title":"JobStatusResponse","description":"Response body for GET /jobs/{job_id}.\n\nA job_id may belong to either a document-level operation (upload /\ndelete) or a folder deletion. The ``resource_type`` discriminator\ntells the client which."},"KBCorrectionApprove":{"properties":{"change_indexes":{"anyOf":[{"items":{"type":"integer"},"type":"array"},{"type":"null"}],"title":"Change Indexes","description":"Subset of proposed_changes to apply (0-based). Omit to apply all."}},"type":"object","title":"KBCorrectionApprove"},"KBCorrectionCreate":{"properties":{"what_is_wrong":{"type":"string","maxLength":5000,"minLength":1,"title":"What Is Wrong"},"why":{"anyOf":[{"type":"string","maxLength":2000},{"type":"null"}],"title":"Why"},"correct_answer":{"anyOf":[{"type":"string","maxLength":20000},{"type":"null"}],"title":"Correct Answer"},"ai_answer_event_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Ai Answer Event Id"},"conversation_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Conversation Id"},"faq_item_ids":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Faq Item Ids","description":"Entries the reporter believes are wrong"}},"type":"object","required":["what_is_wrong"],"title":"KBCorrectionCreate","description":"Manual 'this is wrong' report (no AI answer required)."},"KBCorrectionListResponse":{"properties":{"items":{"items":{"$ref":"#/components/schemas/KBCorrectionResponse"},"type":"array","title":"Items"},"pagination":{"$ref":"#/components/schemas/PaginationMeta"}},"type":"object","required":["items","pagination"],"title":"KBCorrectionListResponse"},"KBCorrectionReject":{"properties":{"reason":{"anyOf":[{"type":"string","maxLength":2000},{"type":"null"}],"title":"Reason"}},"type":"object","title":"KBCorrectionReject"},"KBCorrectionResponse":{"properties":{"id":{"type":"string","title":"Id"},"tenant_id":{"type":"string","title":"Tenant Id"},"ai_answer_event_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Ai Answer Event Id"},"ai_answer_rating_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Ai Answer Rating Id"},"conversation_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Conversation Id"},"reported_by_user_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Reported By User Id"},"reported_by_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Reported By Name"},"what_is_wrong":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"What Is Wrong"},"why":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Why"},"correct_answer":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Correct Answer"},"status":{"type":"string","title":"Status","description":"open | analyzing | proposed | applied | rejected | failed"},"proposed_changes":{"items":{"$ref":"#/components/schemas/ProposedChange"},"type":"array","title":"Proposed Changes"},"analysis_confidence":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Analysis Confidence"},"auto_applied":{"type":"boolean","title":"Auto Applied","default":false},"resolved_by_user_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Resolved By User Id"},"resolved_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Resolved At"},"last_error":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Last Error"},"created_at":{"type":"string","format":"date-time","title":"Created At"},"updated_at":{"type":"string","format":"date-time","title":"Updated At"},"answer_text":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Answer Text"},"query_text":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Query Text"},"affected_entries":{"items":{"$ref":"#/components/schemas/EntryScoreUpdate"},"type":"array","title":"Affected Entries"},"correction_kind":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Correction Kind","description":"fact | policy (null = legacy, treated as fact)"},"policy_rule":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Policy Rule"},"skill_suggestion_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Skill Suggestion Id","description":"Set when a policy correction was routed to an agent skill suggestion; decide it there"},"policy_target":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Policy Target","description":"Policy corrections only: {\"type\": \"skill\"|\"bot\"|null, \"id\", \"name\", \"agent_id\"}"},"policy_note":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Policy Note"}},"type":"object","required":["id","tenant_id","status","created_at","updated_at"],"title":"KBCorrectionResponse"},"KBDocumentRawResponse":{"properties":{"content":{"type":"string","title":"Content","description":"Raw text content from GCS"},"mime_type":{"type":"string","title":"Mime Type","description":"Resolved MIME type of the stored blob"},"editable":{"type":"boolean","title":"Editable","description":"Always true on 200 response. Non-editable docs return 400.","default":true},"reason":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Reason","description":"Populated only if editable is false (reserved for future preview flows)."}},"type":"object","required":["content","mime_type"],"title":"KBDocumentRawResponse","description":"Raw text payload for editable KB documents.\n\nReturned by ``GET /documents/{id}/raw``. For non-editable document types\n(pdf/image/crawled/binary) the endpoint returns 400; this schema is only\nused for successful responses where the GCS blob was decoded as UTF-8\ntext."},"KBDocumentRawUpdate":{"properties":{"content":{"type":"string","title":"Content","description":"New UTF-8 text content (max 1 MB)"}},"type":"object","required":["content"],"title":"KBDocumentRawUpdate","description":"Request body for ``PUT /documents/{id}/raw``.\n\nReplaces the GCS blob with new UTF-8 text, updates ``updated_at``, and\nre-queues Vertex AI RAG ingestion via the existing\n``kb-administration`` Pub/Sub topic. Size limit: 1 MB (enforced in the\nhandler, returns 413)."},"KBDocumentStatus":{"type":"string","enum":["pending_upload","pending_ingestion","ready","deleting","failed"],"title":"KBDocumentStatus","description":"Lifecycle status values exposed to the API layer.\n\nMirrors ``app.models.knowledge_base_document.DocumentStatus`` — kept as a\nseparate schema-layer enum so we can version the API shape independently\nfrom the DB model."},"KBFolderCreate":{"properties":{"name":{"type":"string","maxLength":255,"minLength":1,"title":"Name","description":"Folder name"},"parent_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Parent Id","description":"Parent folder ID. NULL for root-level folders."}},"type":"object","required":["name"],"title":"KBFolderCreate","description":"Request model for creating a KB folder.\n\nSystem folders cannot be created via API - they are auto-generated.","example":{"name":"Product Documentation"}},"KBFolderResponse":{"properties":{"id":{"type":"string","title":"Id","description":"Unique identifier (UUID)"},"tenant_id":{"type":"string","title":"Tenant Id","description":"Tenant this resource belongs to"},"name":{"type":"string","title":"Name","description":"Folder name"},"slug":{"type":"string","title":"Slug","description":"URL-safe folder identifier"},"parent_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Parent Id","description":"Parent folder ID"},"is_system":{"type":"boolean","title":"Is System","description":"Whether this is a system folder"},"sort_order":{"type":"integer","title":"Sort Order","description":"Display order within parent"},"document_count":{"type":"integer","title":"Document Count","description":"Number of documents directly in this folder","default":0},"has_corpus":{"type":"boolean","title":"Has Corpus","description":"Whether a Vertex AI corpus exists for this folder","default":false},"is_ai_searchable":{"type":"boolean","title":"Is Ai Searchable","description":"Whether this folder is included in AI agent suggestions (/ai, /suggest)","default":true},"children":{"items":{"$ref":"#/components/schemas/KBFolderResponse"},"type":"array","title":"Children","description":"Child folders (for tree structure)"},"created_at":{"type":"string","title":"Created At","description":"ISO datetime when created"},"updated_at":{"type":"string","title":"Updated At","description":"ISO datetime when last updated"}},"type":"object","required":["id","tenant_id","name","slug","is_system","sort_order","created_at","updated_at"],"title":"KBFolderResponse","description":"Full response model for a KB folder.\n\nIncludes document count and recursive children for tree display.","example":{"children":[],"created_at":"2026-01-15T10:30:00Z","document_count":15,"has_corpus":true,"id":"folder-uuid-123","is_ai_searchable":true,"is_system":false,"name":"Product Documentation","slug":"product-documentation","sort_order":0,"tenant_id":"tenant-uuid-456","updated_at":"2026-01-15T10:30:00Z"}},"KBFolderTreeResponse":{"properties":{"items":{"items":{"$ref":"#/components/schemas/KBFolderResponse"},"type":"array","title":"Items","description":"Root-level folders with nested children"},"total":{"type":"integer","title":"Total","description":"Total number of folders"}},"type":"object","required":["items","total"],"title":"KBFolderTreeResponse","description":"Response for the folder tree endpoint.\n\nReturns all folders in a hierarchical structure.","example":{"items":[{"children":[],"id":"folder-uuid-general","is_system":true,"name":"General","slug":"general"},{"children":[{"children":[],"id":"folder-uuid-docs","is_system":false,"name":"docs.example.com","slug":"docs-example-com"}],"id":"folder-uuid-websites","is_system":true,"name":"Websites","slug":"websites"}],"total":5}},"KBFolderUpdate":{"properties":{"name":{"anyOf":[{"type":"string","maxLength":255,"minLength":1},{"type":"null"}],"title":"Name","description":"New folder name"},"parent_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Parent Id","description":"New parent folder ID. Use 'null' for root level."},"sort_order":{"anyOf":[{"type":"integer","minimum":0.0},{"type":"null"}],"title":"Sort Order","description":"Display order within parent folder"},"is_ai_searchable":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Is Ai Searchable","description":"Whether this folder is included in AI agent suggestions (/ai, /suggest)"}},"type":"object","title":"KBFolderUpdate","description":"Request model for updating a KB folder.\n\nSystem folders cannot be renamed, moved, or deleted.","example":{"is_ai_searchable":true,"name":"Updated Folder Name","sort_order":10}},"KBHealthEntry":{"properties":{"id":{"type":"string","title":"Id"},"question":{"type":"string","title":"Question"},"answer_preview":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Answer Preview"},"folder_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Folder Id"},"status":{"type":"string","title":"Status"},"origin":{"type":"string","title":"Origin"},"language":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Language"},"times_retrieved":{"type":"integer","title":"Times Retrieved","default":0},"times_cited":{"type":"integer","title":"Times Cited","default":0},"times_in_sent_reply":{"type":"integer","title":"Times In Sent Reply","default":0},"flagged_count":{"type":"integer","title":"Flagged Count","default":0},"duplicate_count":{"type":"integer","title":"Duplicate Count","default":0},"quality_score":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Quality Score"},"positive_mass":{"type":"number","title":"Positive Mass","default":0.0},"negative_mass":{"type":"number","title":"Negative Mass","default":0.0},"verified_by_usage":{"type":"boolean","title":"Verified By Usage","default":false},"last_used_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Last Used At"},"last_verified_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Last Verified At"},"content_updated_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Content Updated At"},"updated_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Updated At"},"stale_reason":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Stale Reason"},"duplicate_of":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Duplicate Of"}},"type":"object","required":["id","question","status","origin"],"title":"KBHealthEntry","description":"A FAQ entry as it appears in any KB-health bucket."},"KBHealthResponse":{"properties":{"totals":{"$ref":"#/components/schemas/KBHealthTotals"},"most_used":{"items":{"$ref":"#/components/schemas/KBHealthEntry"},"type":"array","title":"Most Used"},"never_used":{"items":{"$ref":"#/components/schemas/KBHealthEntry"},"type":"array","title":"Never Used","description":"Published >90d ago, never in a sent reply"},"flagged":{"items":{"$ref":"#/components/schemas/KBHealthEntry"},"type":"array","title":"Flagged"},"low_quality":{"items":{"$ref":"#/components/schemas/KBHealthEntry"},"type":"array","title":"Low Quality"},"possible_duplicates":{"items":{"$ref":"#/components/schemas/KBHealthEntry"},"type":"array","title":"Possible Duplicates"},"stale":{"items":{"$ref":"#/components/schemas/KBHealthEntry"},"type":"array","title":"Stale","description":"Contains a price/date/version and not verified since"},"generated_at":{"type":"string","format":"date-time","title":"Generated At"}},"type":"object","required":["totals","generated_at"],"title":"KBHealthResponse"},"KBHealthTotals":{"properties":{"total_entries":{"type":"integer","title":"Total Entries","default":0},"published":{"type":"integer","title":"Published","default":0},"draft":{"type":"integer","title":"Draft","default":0},"needs_review":{"type":"integer","title":"Needs Review","default":0},"archived":{"type":"integer","title":"Archived","default":0},"open_corrections":{"type":"integer","title":"Open Corrections","default":0},"scored_entries":{"type":"integer","title":"Scored Entries","default":0},"unscored_entries":{"type":"integer","title":"Unscored Entries","description":"Entries with quality_score NULL — unknown, not bad","default":0},"average_quality_score":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Average Quality Score"}},"type":"object","title":"KBHealthTotals"},"KbGapDraftItem":{"properties":{"id":{"type":"string","title":"Id"},"question":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Question"},"answer":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Answer"},"status":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Status"},"created_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Created At"},"review_notes":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Review Notes"},"source_conversation_count":{"type":"integer","title":"Source Conversation Count","default":0}},"type":"object","required":["id"],"title":"KbGapDraftItem"},"KbGapRunRequest":{"properties":{"days":{"type":"integer","maximum":90.0,"minimum":1.0,"title":"Days","default":30}},"additionalProperties":false,"type":"object","title":"KbGapRunRequest"},"KbGapRunResponse":{"properties":{"created":{"type":"integer","title":"Created","default":0},"skipped_existing":{"type":"integer","title":"Skipped Existing","default":0},"skipped_volatile":{"type":"integer","title":"Skipped Volatile","default":0},"candidates":{"type":"integer","title":"Candidates","default":0},"draft_ids":{"items":{"type":"string"},"type":"array","title":"Draft Ids"}},"type":"object","title":"KbGapRunResponse"},"KeywordTriggerCreate":{"properties":{"keyword":{"type":"string","maxLength":255,"minLength":1,"title":"Keyword","description":"Keyword to match"},"match_type":{"type":"string","title":"Match Type","description":"Match type: 'exact' or 'contains'","default":"exact"},"channel_ids":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Channel Ids","description":"Limit trigger to specific channel IDs. None = all channels."},"priority":{"type":"integer","minimum":0.0,"title":"Priority","description":"Trigger priority (higher = evaluated first)","default":0},"is_active":{"type":"boolean","title":"Is Active","description":"Whether this trigger is active","default":true},"handoff_bot_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Handoff Bot Id","description":"Bot ID to hand off to after sending responses"},"handover_to_human":{"type":"boolean","title":"Handover To Human","description":"Escalate to human agent after sending responses. Mutually exclusive with handoff_bot_id.","default":false},"flow_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Flow Id","description":"Top-level 'Start Flow' action — kick off a Flow Engine run when this keyword matches, INSTEAD of sending responses or handing off. Mutually exclusive with handoff_bot_id and handover_to_human. The referenced flow must be published."},"responses":{"items":{"$ref":"#/components/schemas/KeywordTriggerResponseCreate"},"type":"array","title":"Responses","description":"Response sequence to send when keyword is matched"}},"additionalProperties":false,"type":"object","required":["keyword"],"title":"KeywordTriggerCreate","description":"Schema for creating a keyword trigger"},"KeywordTriggerResponseCreate":{"properties":{"response_type":{"type":"string","title":"Response Type","description":"Response type: text, image, video, audio, document, template, buttons, list, trigger_flow"},"text":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Text"},"media_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Media Url"},"media_filename":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Media Filename"},"attachment_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Attachment Id","description":"Attachment ID from uploaded file (alternative to media_url for media types)"},"template_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Template Id"},"template_variables":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Template Variables"},"interactive_data":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Interactive Data"},"header_data":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Header Data"},"footer_text":{"anyOf":[{"type":"string","maxLength":60},{"type":"null"}],"title":"Footer Text"},"order":{"type":"integer","minimum":0.0,"title":"Order","description":"Order in the response sequence","default":0},"delay_seconds":{"type":"integer","maximum":30.0,"minimum":0.0,"title":"Delay Seconds","description":"Delay before sending this response (0-30 seconds)","default":0},"target_flow_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Target Flow Id","description":"Flow UUID to start when this response fires. Required when response_type='trigger_flow'. The flow must be PUBLISHED at trigger time; the bot integration logs + skips otherwise."},"pass_message_to_flow":{"type":"boolean","title":"Pass Message To Flow","description":"When response_type='trigger_flow', forward the inbound message id + text into the new run's context.trigger.payload so flow nodes can branch on it. Default: True.","default":true}},"additionalProperties":false,"type":"object","required":["response_type"],"title":"KeywordTriggerResponseCreate","description":"Schema for creating a keyword trigger response"},"KeywordTriggerResponseSchema":{"properties":{"id":{"type":"string","title":"Id"},"trigger_id":{"type":"string","title":"Trigger Id"},"response_type":{"type":"string","title":"Response Type"},"text":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Text"},"media_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Media Url"},"media_filename":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Media Filename"},"attachment_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Attachment Id"},"template_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Template Id"},"template_variables":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Template Variables"},"interactive_data":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Interactive Data"},"header_data":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Header Data"},"footer_text":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Footer Text"},"order":{"type":"integer","title":"Order"},"delay_seconds":{"type":"integer","title":"Delay Seconds"},"created_at":{"type":"string","format":"date-time","title":"Created At"},"target_flow_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Target Flow Id"},"pass_message_to_flow":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Pass Message To Flow"}},"type":"object","required":["id","trigger_id","response_type","order","delay_seconds","created_at"],"title":"KeywordTriggerResponseSchema","description":"Response schema for a keyword trigger response.\n\nFlow Builder Phase 2 — when ``response_type='trigger_flow'`` the\nstorage layer stashes ``target_flow_id`` + ``pass_message_to_flow``\ninside ``interactive_data`` under the ``_target_flow_id`` /\n``_pass_message_to_flow`` keys (see\n``services/keyword_bot_service._merge_flow_trigger_into_interactive_data``).\nThe model_validator below pulls them back out to top-level fields on\nthe wire so FE consumers don't have to know the storage convention."},"KeywordTriggerSchema":{"properties":{"id":{"type":"string","title":"Id"},"bot_id":{"type":"string","title":"Bot Id"},"keyword":{"type":"string","title":"Keyword"},"match_type":{"type":"string","title":"Match Type"},"channel_ids":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Channel Ids"},"priority":{"type":"integer","title":"Priority"},"is_active":{"type":"boolean","title":"Is Active"},"handoff_bot_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Handoff Bot Id"},"handover_to_human":{"type":"boolean","title":"Handover To Human","default":false},"flow_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Flow Id"},"responses":{"items":{"$ref":"#/components/schemas/KeywordTriggerResponseSchema"},"type":"array","title":"Responses"},"created_at":{"type":"string","format":"date-time","title":"Created At"},"updated_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Updated At"}},"type":"object","required":["id","bot_id","keyword","match_type","priority","is_active","created_at"],"title":"KeywordTriggerSchema","description":"Response schema for a keyword trigger with its responses"},"KeywordTriggerUpdate":{"properties":{"keyword":{"anyOf":[{"type":"string","maxLength":255,"minLength":1},{"type":"null"}],"title":"Keyword"},"match_type":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Match Type"},"channel_ids":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Channel Ids"},"priority":{"anyOf":[{"type":"integer","minimum":0.0},{"type":"null"}],"title":"Priority"},"is_active":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Is Active"},"handoff_bot_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Handoff Bot Id"},"handover_to_human":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Handover To Human"},"flow_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Flow Id","description":"Top-level 'Start Flow' action. Mutually exclusive with handoff_bot_id and handover_to_human."},"responses":{"anyOf":[{"items":{"$ref":"#/components/schemas/KeywordTriggerResponseCreate"},"type":"array"},{"type":"null"}],"title":"Responses"}},"additionalProperties":false,"type":"object","title":"KeywordTriggerUpdate","description":"Schema for updating a keyword trigger"},"KeywordTriggersReorder":{"properties":{"trigger_ids":{"items":{"type":"string"},"type":"array","minItems":1,"title":"Trigger Ids","description":"Ordered list of trigger IDs (first = highest priority)"}},"additionalProperties":false,"type":"object","required":["trigger_ids"],"title":"KeywordTriggersReorder","description":"Request schema for reordering keyword triggers"},"KnowledgeBaseDeleteResponse":{"properties":{"success":{"type":"boolean","title":"Success","description":"Whether the operation succeeded","default":true},"message":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Message","description":"Optional success message"}},"type":"object","title":"KnowledgeBaseDeleteResponse","description":"Response for document deletion","example":{"message":"Document deleted","success":true}},"KnowledgeBaseDocumentCreate":{"properties":{"title":{"type":"string","maxLength":255,"minLength":1,"title":"Title","description":"Document title"},"document_type":{"type":"string","title":"Document Type","description":"Document type: 'faq' or 'manual'"},"source_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Source Url","description":"Optional source URL reference"},"content":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Content","description":"Text content for FAQs/manuals"},"folder_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Folder Id","description":"Target folder ID. If not provided, document goes to General folder."}},"type":"object","required":["title","document_type"],"title":"KnowledgeBaseDocumentCreate","description":"Request model for creating a knowledge base document.\n\nUse this for FAQ and manual types only. For file uploads,\nuse POST /documents/upload. For website crawling, use POST /documents/crawl.","example":{"content":"To reset your password, click the 'Forgot Password' link on the login page...","document_type":"faq","folder_id":"folder-uuid-123","title":"How to reset your password"}},"KnowledgeBaseDocumentListResponse":{"properties":{"items":{"items":{"$ref":"#/components/schemas/KnowledgeBaseDocumentResponse"},"type":"array","title":"Items","description":"List of items for this page"},"pagination":{"$ref":"#/components/schemas/PaginationMeta","description":"Pagination metadata"}},"type":"object","required":["items","pagination"],"title":"KnowledgeBaseDocumentListResponse","description":"Paginated list of knowledge base documents.\n\nExample response:\n```json\n{\n    \"items\": [...],\n    \"pagination\": {\n        \"total\": 50,\n        \"page\": 1,\n        \"page_size\": 20,\n        \"total_pages\": 3,\n        \"has_next\": true,\n        \"has_prev\": false\n    }\n}\n```"},"KnowledgeBaseDocumentResponse":{"properties":{"id":{"type":"string","title":"Id","description":"Unique identifier (UUID)"},"tenant_id":{"type":"string","title":"Tenant Id","description":"Tenant this resource belongs to"},"document_type":{"type":"string","title":"Document Type","description":"Document type: faq, manual, uploaded_doc, website_doc"},"title":{"type":"string","title":"Title","description":"Document title"},"source_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Source Url","description":"Source URL if applicable"},"folder_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Folder Id","description":"Folder ID containing this document"},"gemini_file_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Gemini File Name","description":"RAG file reference in corpus"},"gemini_store_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Gemini Store Name","description":"Vertex AI RAG Corpus name"},"uploaded_by_user_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Uploaded By User Id","description":"Auth0 user ID who uploaded"},"status":{"anyOf":[{"$ref":"#/components/schemas/KBDocumentStatus"},{"type":"null"}],"description":"Async lifecycle status. Clients should treat a document as usable only when ``status == 'ready'``."},"job_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Job Id","description":"ID of the currently in-flight async job (ingestion/deletion), if any."},"last_error":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Last Error","description":"Last error message if a previous async job failed (status='failed')."},"created_at":{"type":"string","title":"Created At","description":"ISO datetime when created"},"updated_at":{"type":"string","title":"Updated At","description":"ISO datetime when last updated"}},"type":"object","required":["id","tenant_id","document_type","title","created_at","updated_at"],"title":"KnowledgeBaseDocumentResponse","description":"Full response model for a knowledge base document.\n\nIncludes Gemini integration metadata.","example":{"created_at":"2025-01-15T10:30:00Z","document_type":"faq","folder_id":"folder-uuid-789","gemini_file_name":"files/abc123","gemini_store_name":"ragCorpora/tenant_abc_folder_xyz","id":"doc-uuid-123","tenant_id":"tenant-uuid-456","title":"How to reset your password","updated_at":"2025-01-15T10:30:00Z","uploaded_by_user_id":"auth0|user123"}},"KnowledgeSourceCreate":{"properties":{"source_type":{"type":"string","enum":["kb_folder","kb_all","web_search","faq","live_source"],"title":"Source Type"},"kb_folder_id":{"anyOf":[{"type":"string","maxLength":36},{"type":"null"}],"title":"Kb Folder Id"},"allowed_domains":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Allowed Domains"},"is_enabled":{"type":"boolean","title":"Is Enabled","default":true},"config":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Config"},"sort_order":{"anyOf":[{"type":"integer","maximum":10000.0,"minimum":0.0},{"type":"null"}],"title":"Sort Order"}},"additionalProperties":false,"type":"object","required":["source_type"],"title":"KnowledgeSourceCreate"},"KnowledgeSourceResponse":{"properties":{"id":{"type":"string","title":"Id"},"source_type":{"type":"string","title":"Source Type"},"kb_folder_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Kb Folder Id"},"kb_folder_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Kb Folder Name"},"allowed_domains":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Allowed Domains"},"is_enabled":{"type":"boolean","title":"Is Enabled","default":true},"config":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Config"},"sort_order":{"type":"integer","title":"Sort Order","default":0},"created_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Created At"},"updated_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Updated At"}},"type":"object","required":["id","source_type"],"title":"KnowledgeSourceResponse"},"KnowledgeSourceUpdate":{"properties":{"kb_folder_id":{"anyOf":[{"type":"string","maxLength":36},{"type":"null"}],"title":"Kb Folder Id"},"allowed_domains":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Allowed Domains"},"is_enabled":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Is Enabled"},"config":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Config"},"sort_order":{"anyOf":[{"type":"integer","maximum":10000.0,"minimum":0.0},{"type":"null"}],"title":"Sort Order"}},"additionalProperties":false,"type":"object","title":"KnowledgeSourceUpdate"},"LanguageSettings":{"properties":{"response_language":{"anyOf":[{"type":"string","maxLength":10},{"type":"null"}],"title":"Response Language"},"base_language":{"anyOf":[{"type":"string","maxLength":10},{"type":"null"}],"title":"Base Language"},"auto_translate_messages":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Auto Translate Messages"},"language_detection_strategy":{"anyOf":[{"type":"string","enum":["cascade","fixed"]},{"type":"null"}],"title":"Language Detection Strategy"}},"type":"object","title":"LanguageSettings","description":"Bot-row language fields (same names/semantics as the bot API)."},"LanguageSettingsResponse":{"properties":{"response_language":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Response Language"},"base_language":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Base Language","default":"en"},"auto_translate_messages":{"type":"boolean","title":"Auto Translate Messages","default":false},"language_detection_strategy":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Language Detection Strategy"}},"type":"object","title":"LanguageSettingsResponse"},"LastMessageInList":{"properties":{"message_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Message Id"},"campaign_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Campaign Id"},"campaign_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Campaign Name"},"sent_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Sent At","description":"ISO 8601 (Z); null when attribution is 'none'"},"preview":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Preview"},"attribution":{"type":"string","enum":["exact","ambiguous","none"],"title":"Attribution"}},"type":"object","required":["attribution"],"title":"LastMessageInList","description":"The last outbound campaign message this contact received that belongs to the\nlist (populated only for ``unsubscribed`` events).\n\n``attribution``:\n  - ``exact``     — the campaign targeted ONLY this list.\n  - ``ambiguous`` — the campaign targeted multiple lists, or was an\n    all-contacts send (``ignores_newsletter_subscriptions``).\n  - ``none``      — no matching campaign message was found (all other fields\n    are then null)."},"LastMessagePreview":{"properties":{"text":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Text"},"message_type":{"type":"string","title":"Message Type","description":"Message type: text, image, video, audio, document","default":"text"},"direction":{"type":"string","title":"Direction","description":"Message direction: inbound or outbound","default":"inbound"},"created_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Created At"}},"type":"object","title":"LastMessagePreview","description":"Preview of the last message in a conversation"},"LayoutTemplateInfo":{"properties":{"id":{"type":"string","title":"Id"},"name":{"type":"string","title":"Name"},"category":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Category"}},"type":"object","required":["id","name"],"title":"LayoutTemplateInfo","description":"Minimal layout template info for embedding in responses"},"LibraryButtonInput":{"properties":{"type":{"type":"string","title":"Type","description":"URL | PHONE_NUMBER | OTP | MPM | CATALOG | FLOW | VOICE_CALL | APP"},"url":{"anyOf":[{"$ref":"#/components/schemas/LibraryButtonUrlInput"},{"type":"null"}]},"phone_number":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Phone Number"},"otp_type":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Otp Type","description":"COPY_CODE | ONE_TAP | ZERO_TAP"}},"type":"object","required":["type"],"title":"LibraryButtonInput","description":"Customer-specific button value, mirroring Meta's\n``library_template_button_inputs`` entries.\n\nOne entry per button that needs input, in the same order as the library\nentry's buttons of that type. ``QUICK_REPLY`` buttons take no input — Meta\nfills them from the library definition."},"LibraryButtonUrlInput":{"properties":{"base_url":{"type":"string","title":"Base Url","description":"The URL the button opens. May contain a single ``{{1}}`` variable, in which case ``url_suffix_example`` is required."},"url_suffix_example":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Url Suffix Example","description":"Full example URL with the variable filled in"}},"type":"object","required":["base_url"],"title":"LibraryButtonUrlInput"},"LibraryFacets":{"properties":{"languages":{"items":{"type":"string"},"type":"array","title":"Languages"},"topics":{"items":{"type":"string"},"type":"array","title":"Topics"},"usecases":{"items":{"type":"string"},"type":"array","title":"Usecases"},"industries":{"items":{"type":"string"},"type":"array","title":"Industries"}},"type":"object","title":"LibraryFacets","description":"Filter options derived from the current result set.\n\nMeta has no facets endpoint, so these are computed from what the catalog\ncall returned. They drive the browse UI's dropdowns."},"LibraryTemplate":{"properties":{"id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Id","description":"Meta's library template ID"},"name":{"type":"string","title":"Name","description":"Library template name — the value to pass as ``library_template_name`` when adopting (e.g. ``delivery_update_1``)"},"language":{"type":"string","title":"Language","description":"Meta language code, e.g. ``en_US``"},"category":{"type":"string","title":"Category","description":"UTILITY or AUTHENTICATION","default":"UTILITY"},"topic":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Topic","description":"e.g. ``PAYMENTS``"},"usecase":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Usecase","description":"e.g. ``LOW_BALANCE_WARNING``"},"industry":{"items":{"type":"string"},"type":"array","title":"Industry","description":"e.g. ``[\"FINANCIAL_SERVICES\"]``"},"header":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Header","description":"Text header, if any"},"body":{"type":"string","title":"Body","description":"Body text with ``{{1}}`` placeholders"},"footer":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Footer","description":"Footer text, if any"},"body_params":{"items":{"type":"string"},"type":"array","title":"Body Params","description":"Meta's example values, index-aligned with the placeholders"},"body_param_types":{"items":{"type":"string"},"type":"array","title":"Body Param Types","description":"Semantic type per placeholder, when Meta provides it"},"buttons":{"items":{"$ref":"#/components/schemas/LibraryTemplateButton"},"type":"array","title":"Buttons"},"already_added":{"type":"boolean","title":"Already Added","description":"True when this tenant already has a template with the same name and language on the selected channel. Set by the API, not by Meta.","default":false},"requires_input":{"type":"boolean","title":"Requires Input","description":"True when at least one button needs a customer-specific value (link URL, phone number). Meta's catalog ships placeholder destinations for those, so they cannot be adopted unchanged — the UI must collect the value first. False means the entry can be added with a single click.","default":false},"has_call_permission_request":{"type":"boolean","title":"Has Call Permission Request","description":"True when the entry includes a call permission request (asks the customer for permission to call them). Such a template can only be sent from a number with WhatsApp Calling turned on.","default":false}},"type":"object","required":["name","language","body"],"title":"LibraryTemplate","description":"One entry in Meta's template library."},"LibraryTemplateButton":{"properties":{"type":{"type":"string","title":"Type","description":"QUICK_REPLY | URL | PHONE_NUMBER | OTP | …"},"text":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Text","description":"Button label"},"url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Url","description":"Example URL (URL buttons)"},"phone_number":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Phone Number","description":"Example phone number (PHONE_NUMBER buttons)"},"otp_type":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Otp Type","description":"COPY_CODE | ONE_TAP | ZERO_TAP (OTP buttons)"}},"type":"object","required":["type"],"title":"LibraryTemplateButton","description":"A button as Meta's library describes it.\n\n``url`` / ``phone_number`` carry Meta's *example* values. When the customer\nadopts the template these become the defaults, overridable via\n``AdoptLibraryTemplateRequest.button_inputs``."},"LibraryTemplateListResponse":{"properties":{"items":{"items":{"$ref":"#/components/schemas/LibraryTemplate"},"type":"array","title":"Items"},"total":{"type":"integer","title":"Total","description":"Matches found before the display limit","default":0},"facets":{"$ref":"#/components/schemas/LibraryFacets"},"language":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Language","description":"Normalised language filter applied"},"stale":{"type":"boolean","title":"Stale","description":"True when Meta was unreachable and this is the last known catalog. The UI shows a soft 'may be out of date' hint rather than an error.","default":false},"warning":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Warning","description":"Human-readable note about staleness"}},"type":"object","title":"LibraryTemplateListResponse"},"LinkAnalyticsResponse":{"properties":{"tracked_link_id":{"type":"string","title":"Tracked Link Id"},"short_code":{"type":"string","title":"Short Code"},"short_url":{"type":"string","title":"Short Url"},"original_url":{"type":"string","title":"Original Url"},"feature_type":{"type":"string","title":"Feature Type"},"feature_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Feature Id"},"contact_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Contact Id"},"label":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Label"},"total_clicks":{"type":"integer","title":"Total Clicks"},"first_click_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"First Click At"},"last_click_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Last Click At"},"created_at":{"type":"string","title":"Created At"},"expires_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Expires At"},"is_active":{"type":"boolean","title":"Is Active"}},"type":"object","required":["tracked_link_id","short_code","short_url","original_url","feature_type","feature_id","contact_id","label","total_clicks","first_click_at","last_click_at","created_at","expires_at","is_active"],"title":"LinkAnalyticsResponse","description":"Analytics for a specific tracked link.\n\nSimplified model: only click_count, first_click_at, last_click_at are available.\nUnique clicks, device breakdown, and timeline are no longer tracked."},"LinkOutboundProviderRequest":{"properties":{"outbound_provider_id":{"type":"string","title":"Outbound Provider Id","description":"ID of the email provider config to use for sending replies"}},"type":"object","required":["outbound_provider_id"],"title":"LinkOutboundProviderRequest","description":"Request to link an outbound provider to a proxy integration"},"LinkOutboundProviderResponse":{"properties":{"success":{"type":"boolean","title":"Success","description":"Whether the operation succeeded"},"message":{"type":"string","title":"Message","description":"Status message"},"connection":{"$ref":"#/components/schemas/EmailConnectionResponse","description":"Updated connection details"}},"type":"object","required":["success","message","connection"],"title":"LinkOutboundProviderResponse","description":"Response after linking outbound provider"},"LinkPerformance":{"properties":{"url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Url","description":"Original URL"},"short_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Short Url","description":"Short tracking URL"},"clicks":{"type":"integer","title":"Clicks","description":"Total click count","default":0},"click_through_rate":{"type":"number","title":"Click Through Rate","description":"CTR percentage","default":0.0},"unique_recipients":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Unique Recipients","description":"Count of distinct recipients that clicked. Only populated for per-recipient tracking mode; null for shared mode (can't be attributed)."},"tracking_mode":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Tracking Mode","description":"'shared' or 'per_recipient' for the template link"},"first_click_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"First Click At","description":"First click timestamp (ISO format)"},"last_click_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Last Click At","description":"Last click timestamp (ISO format)"}},"type":"object","title":"LinkPerformance","description":"Individual link performance data","example":{"click_through_rate":12.5,"clicks":120,"first_click_at":"2024-01-01T12:00:00","last_click_at":"2024-01-02T14:30:00","short_url":"https://example.com/l/abc123","url":"https://example.com/page"}},"LinkPerformanceSummaryResponse":{"properties":{"total_links":{"type":"integer","title":"Total Links","description":"Total number of tracked links"},"total_clicks":{"type":"integer","title":"Total Clicks","description":"Total clicks across all links"},"clicks_today":{"type":"integer","title":"Clicks Today","description":"Clicks in the last 24 hours (not available in simplified model)","default":0},"clicks_this_week":{"type":"integer","title":"Clicks This Week","description":"Clicks in the last 7 days (not available in simplified model)","default":0},"active_links_this_week":{"type":"integer","title":"Active Links This Week","description":"Number of links with activity in the last 7 days"},"top_performing_links":{"items":{"$ref":"#/components/schemas/TopPerformingLink"},"type":"array","title":"Top Performing Links","description":"Top 5 most clicked links"},"clicks_by_day":{"additionalProperties":{"type":"integer"},"type":"object","title":"Clicks By Day","description":"Click counts by day (not available in simplified model)"},"device_breakdown":{"additionalProperties":{"type":"integer"},"type":"object","title":"Device Breakdown","description":"Clicks by device type (not available in simplified model)"},"recent_activity":{"items":{"$ref":"#/components/schemas/RecentLinkActivity"},"type":"array","title":"Recent Activity","description":"Links with recent clicks (last 7 days)"},"link_tracking_enabled":{"type":"boolean","title":"Link Tracking Enabled","description":"Whether link tracking is enabled for this tenant"}},"type":"object","required":["total_links","total_clicks","active_links_this_week","link_tracking_enabled"],"title":"LinkPerformanceSummaryResponse","description":"Link tracking performance summary for dashboard widget.\n\nReturned by GET /api/v1/analytics/links/summary","example":{"active_links_this_week":8,"clicks_by_day":{},"clicks_this_week":0,"clicks_today":0,"device_breakdown":{},"link_tracking_enabled":true,"recent_activity":[],"top_performing_links":[{"clicks":150,"first_click_at":"2026-01-10T10:00:00Z","label":"Holiday Promo","last_click_at":"2026-01-18T14:30:00Z","original_url":"https://example.com/promo","short_code":"abc123"}],"total_clicks":1500,"total_links":25}},"LinkTrackingSettingsResponse":{"properties":{"tracking_domain":{"type":"string","title":"Tracking Domain","default":"c.sendseven.com"},"link_expiration_days":{"type":"integer","title":"Link Expiration Days","default":365}},"type":"object","title":"LinkTrackingSettingsResponse","description":"Schema for link tracking settings response"},"LinkTrackingSettingsUpdate":{"properties":{"tracking_domain":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Tracking Domain","description":"Selected tracking domain"},"link_expiration_days":{"anyOf":[{"type":"integer","maximum":3650.0,"minimum":1.0},{"type":"null"}],"title":"Link Expiration Days","description":"Number of days before tracked links expire (1-3650)"}},"additionalProperties":false,"type":"object","title":"LinkTrackingSettingsUpdate","description":"Schema for updating link tracking settings (all fields optional)"},"ListContactInfo":{"properties":{"id":{"type":"string","title":"Id"},"name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Name"},"phone":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Phone"},"email":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Email"}},"type":"object","required":["id"],"title":"ListContactInfo","description":"Contact information when listing contacts in a list"},"ListContactsResponse":{"properties":{"list_id":{"type":"string","title":"List Id"},"list_name":{"type":"string","title":"List Name"},"list_type":{"type":"string","title":"List Type"},"total_contacts":{"type":"integer","title":"Total Contacts"},"contacts":{"items":{"$ref":"#/components/schemas/ListContactInfo"},"type":"array","title":"Contacts"}},"type":"object","required":["list_id","list_name","list_type","total_contacts","contacts"],"title":"ListContactsResponse","description":"Response for getting contacts in a list","example":{"contacts":[{"email":"john@example.com","id":"cnt-456","name":"John Doe","phone":"+1234567890"}],"list_id":"lst-123","list_name":"VIP Customers","list_type":"static","total_contacts":42}},"ListCreate":{"properties":{"name":{"type":"string","maxLength":255,"minLength":1,"title":"Name","description":"List name"},"description":{"anyOf":[{"type":"string","maxLength":1000},{"type":"null"}],"title":"Description","description":"List description"},"list_type":{"$ref":"#/components/schemas/ListType","description":"Type of list: static, dynamic, or newsletter"},"segment_conditions":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Segment Conditions","description":"Segment conditions for dynamic lists. Required for dynamic lists and must start from list subscribers: a top-level `AND` whose children include a positive `has_optin` condition (`operator: equals`, `value: true`, `list_id` of a static or newsletter list of this workspace, optional `channel_type`) or a group of only such conditions (`OR` = any of the lists, `AND` = all of them). Other children exclude contacts (`has_optin` with `value: false`) or narrow the audience down. Otherwise 422 `{code, message}`: `audience_list_membership_required`, `audience_membership_list_invalid`, `audience_conditions_invalid`."},"slug":{"anyOf":[{"type":"string","maxLength":100},{"type":"null"}],"title":"Slug","description":"URL-safe slug for newsletter lists (auto-generated if not provided)"},"newsletter_config":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Newsletter Config","description":"JSON string for newsletter configuration. Keys: `opt_in_keywords`, `opt_out_keywords`, `welcome_message`, `goodbye_message`, `confirmation_message`, `auto_subscribe_via_deeplink`, plus the button lists `welcome_buttons` / `goodbye_buttons` — each up to 3 objects of `{\"type\": \"unsubscribe\"|\"unsubscribe_all\"|\"subscribe\"|\"url\", \"caption\": \"max 20 chars\", \"url\": \"https://… (url type only)\"}`. Buttons render natively on WhatsApp / Telegram / Messenger / Instagram and degrade to a keyword text hint elsewhere. Omitting `goodbye_buttons` entirely means a default \"Re-subscribe\" button is added; send `[]` for none."},"show_in_preference_center":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Show In Preference Center","description":"Whether this newsletter list is offered in the hosted email preference center (the 'Manage preferences' link in emails). Defaults to false — opt in explicitly to expose a list. Lists created before this field existed remain visible.","default":false},"is_default":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Is Default","description":"Whether this is the default list for global 'start'/'stop' keywords. Only one list per tenant can be default.","default":false},"opt_in_action":{"anyOf":[{"$ref":"#/components/schemas/OptActionConfig"},{"type":"null"}],"description":"Per-channel opt-in action (send a MESSAGE or start a FLOW — never both). `{default, per_channel}`; per_channel keys are SubscriptionChannelType values. Omit to keep the legacy welcome message from `newsletter_config`. Flow actions must reference a published, same-tenant flow."},"opt_out_action":{"anyOf":[{"$ref":"#/components/schemas/OptActionConfig"},{"type":"null"}],"description":"Per-channel opt-out action (MESSAGE or FLOW). Same shape as `opt_in_action`. Omit to keep the legacy goodbye message."},"hubspot_optin_field_config":{"anyOf":[{"$ref":"#/components/schemas/HubSpotOptinFieldConfig"},{"type":"null"}],"description":"Per-channel HubSpot contact-property mapping applied on opt-in / opt-out (`{default, per_channel}` of `{property_name, opt_in_value, opt_out_value}`). Persisted here; the HubSpot write is performed off the contact.subscribed/unsubscribed event by the integrations layer."}},"type":"object","required":["name","list_type"],"title":"ListCreate","description":"Schema for creating a new list","examples":[{"description":"High-value customers for priority messaging","list_type":"static","name":"VIP Customers"},{"description":"Newsletter subscribers who use Telegram","list_type":"dynamic","name":"Telegram Users","segment_conditions":{"groups":[{"channel_type":"any","field":"has_optin","list_id":"lst-a1b2c3d4-e5f6-7890-abcd-ef1234567890","operator":"equals","type":"condition","value":true},{"field":"has_telegram","operator":"equals","type":"condition","value":true}],"operator":"AND"}},{"list_type":"newsletter","name":"Weekly Newsletter","newsletter_config":"{\"welcome_message\": \"Welcome aboard!\", \"goodbye_message\": \"You have been unsubscribed.\", \"welcome_buttons\": [{\"type\": \"unsubscribe\", \"caption\": \"Stop Weekly\"}], \"goodbye_buttons\": [{\"type\": \"subscribe\", \"caption\": \"Re-subscribe\"}]}","slug":"weekly-newsletter"}]},"ListResponse":{"properties":{"name":{"type":"string","maxLength":255,"minLength":1,"title":"Name","description":"List name"},"description":{"anyOf":[{"type":"string","maxLength":1000},{"type":"null"}],"title":"Description","description":"List description"},"created_at":{"type":"string","title":"Created At","description":"When the resource was created (UTC)"},"updated_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Updated At","description":"When the resource was last updated (UTC)"},"id":{"type":"string","title":"Id","description":"Unique identifier (UUID)"},"tenant_id":{"type":"string","title":"Tenant Id","description":"Tenant this resource belongs to"},"list_type":{"$ref":"#/components/schemas/ListType","description":"Type of list"},"segment_conditions":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Segment Conditions","description":"Segment conditions for dynamic lists"},"contact_count":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Contact Count","description":"Number of contacts in the list (computed for dynamic lists)"},"slug":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Slug","description":"URL-safe slug for newsletter lists"},"newsletter_config":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Newsletter Config","description":"Newsletter configuration JSON (messages, keywords and the `welcome_buttons` / `goodbye_buttons` lists — see ListCreate)"},"show_in_preference_center":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Show In Preference Center","description":"Whether this newsletter list is offered in the hosted email preference center. null = created before this setting existed and is treated as visible."},"is_default":{"type":"boolean","title":"Is Default","description":"Whether this is the default list for global 'start'/'stop' keywords","default":false},"last_evaluated_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Last Evaluated At","description":"When dynamic list was last evaluated"},"opt_in_action":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Opt In Action","description":"Stored per-channel opt-in action config, or null (legacy welcome message applies)."},"opt_out_action":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Opt Out Action","description":"Stored per-channel opt-out action config, or null (legacy goodbye message applies)."},"hubspot_optin_field_config":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Hubspot Optin Field Config","description":"Stored per-channel HubSpot opt-in/opt-out field mapping, or null."}},"type":"object","required":["name","created_at","id","tenant_id","list_type"],"title":"ListResponse","description":"Full list response with all fields.\n\nIncludes segment conditions (parsed), contact count, and newsletter config.","example":{"contact_count":"42","created_at":"2025-01-15T10:30:00Z","description":"High-value customers for priority messaging","id":"lst-a1b2c3d4-e5f6-7890-abcd-ef1234567890","list_type":"static","name":"VIP Customers","tenant_id":"tnt-a1b2c3d4-e5f6-7890-abcd-ef1234567890","updated_at":"2025-01-15T14:45:00Z"}},"ListRow":{"properties":{"id":{"type":"string","maxLength":200,"title":"Id","description":"Unique row identifier (returned when selected)"},"title":{"type":"string","maxLength":24,"title":"Title","description":"Row title (max 24 chars)"},"description":{"anyOf":[{"type":"string","maxLength":72},{"type":"null"}],"title":"Description","description":"Row description (max 72 chars)"}},"type":"object","required":["id","title"],"title":"ListRow","description":"Row item within a list section"},"ListSection":{"properties":{"title":{"type":"string","maxLength":24,"title":"Title","description":"Section title"},"rows":{"items":{"$ref":"#/components/schemas/ListRow"},"type":"array","maxItems":10,"minItems":1,"title":"Rows","description":"Rows in this section (1-10)"}},"type":"object","required":["title","rows"],"title":"ListSection","description":"Section in a list menu"},"ListSignupOptionsResponse":{"properties":{"list_id":{"type":"string","title":"List Id"},"list_name":{"type":"string","title":"List Name"},"list_slug":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"List Slug"},"signup_options":{"additionalProperties":{"$ref":"#/components/schemas/SignupChannelOption"},"type":"object","title":"Signup Options","description":"Signup options keyed by channel: telegram, messenger, etc."}},"type":"object","required":["list_id","list_name"],"title":"ListSignupOptionsResponse","description":"Response for newsletter list signup options","example":{"list_id":"lst-123","list_name":"Weekly Newsletter","list_slug":"weekly-newsletter","signup_options":{"messenger":{"direct_link":"https://m.me/page-id?ref=subscribe_lst-123","qr_code_base64":"data:image/png;base64,..."},"telegram":{"direct_link":"https://t.me/MyBot?start=subscribe_lst-123","qr_code_base64":"data:image/png;base64,..."}}}},"ListStatusBreakdown":{"properties":{"list_id":{"type":"string","title":"List Id","description":"List ID"},"list_name":{"type":"string","title":"List Name","description":"List name"},"subscribed":{"type":"integer","title":"Subscribed","description":"Active subscriptions","default":0},"pending":{"type":"integer","title":"Pending","description":"Pending confirmations","default":0},"unsubscribed":{"type":"integer","title":"Unsubscribed","description":"Opted-out subscriptions","default":0},"bounced":{"type":"integer","title":"Bounced","description":"Bounced subscriptions","default":0},"complained":{"type":"integer","title":"Complained","description":"Spam-complained subscriptions","default":0},"total":{"type":"integer","title":"Total","description":"Total subscriptions for this list","default":0}},"type":"object","required":["list_id","list_name"],"title":"ListStatusBreakdown","description":"Subscription counts for a single list"},"ListSubscribersResponse":{"properties":{"list_id":{"type":"string","title":"List Id"},"total":{"type":"integer","title":"Total"},"subscriptions":{"items":{"$ref":"#/components/schemas/SubscriptionResponse"},"type":"array","title":"Subscriptions"}},"type":"object","required":["list_id","total","subscriptions"],"title":"ListSubscribersResponse","description":"Schema for list subscribers response","example":{"list_id":"list-123","subscriptions":[],"total":100}},"ListSubscriptionStatsItem":{"properties":{"list_id":{"type":"string","title":"List Id","description":"List UUID"},"opt_ins":{"type":"integer","title":"Opt Ins","description":"Confirmed opt-ins whose opt-in timestamp falls in the window"},"opt_outs":{"type":"integer","title":"Opt Outs","description":"Opt-outs (status=unsubscribed) whose unsubscribed_at falls in the window"},"net_change":{"type":"integer","title":"Net Change","description":"opt_ins - opt_outs"}},"type":"object","required":["list_id","opt_ins","opt_outs","net_change"],"title":"ListSubscriptionStatsItem","description":"Opt-in / opt-out counts for a single list within the window."},"ListSubscriptionStatsResponse":{"properties":{"window_days":{"type":"integer","title":"Window Days","description":"Size of the rolling window in days"},"items":{"items":{"$ref":"#/components/schemas/ListSubscriptionStatsItem"},"type":"array","title":"Items"}},"type":"object","required":["window_days"],"title":"ListSubscriptionStatsResponse","description":"Batch stats for every list the tenant has (zeros for non-subscription lists)."},"ListType":{"type":"string","enum":["static","dynamic","newsletter","hubspot_segment"],"title":"ListType","description":"Type of list.\n\nStored LOWERCASE in the MySQL ENUM (`values_callable`). ENUM members are\nAPPENDED, never reordered — MySQL stores the ordinal, so reordering would\nsilently reinterpret existing rows. Widening the DB ENUM requires an\n``ALTER TABLE lists MODIFY COLUMN list_type ENUM(...)`` migration on the\nshards (precedent: ``newsletter``, then ``hubspot_segment``)."},"ListUpdate":{"properties":{"name":{"anyOf":[{"type":"string","maxLength":255,"minLength":1},{"type":"null"}],"title":"Name"},"description":{"anyOf":[{"type":"string","maxLength":1000},{"type":"null"}],"title":"Description"},"segment_conditions":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Segment Conditions","description":"New segment conditions (dynamic lists). Same membership rule and 422 codes as on create. Omit to leave unchanged."},"slug":{"anyOf":[{"type":"string","maxLength":100},{"type":"null"}],"title":"Slug"},"newsletter_config":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Newsletter Config","description":"Newsletter configuration JSON — same shape as on create, including `welcome_buttons` / `goodbye_buttons`"},"show_in_preference_center":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Show In Preference Center","description":"Whether this newsletter list is offered in the hosted email preference center. Omit to leave unchanged."},"is_default":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Is Default","description":"Whether this is the default list for global 'start'/'stop' keywords. Setting to true unsets any other default list for this tenant."},"opt_in_action":{"anyOf":[{"$ref":"#/components/schemas/OptActionConfig"},{"type":"null"}],"description":"Per-channel opt-in action (MESSAGE or FLOW). Omit to leave unchanged. Flow actions must reference a published, same-tenant flow."},"opt_out_action":{"anyOf":[{"$ref":"#/components/schemas/OptActionConfig"},{"type":"null"}],"description":"Per-channel opt-out action (MESSAGE or FLOW). Omit to leave unchanged."},"hubspot_optin_field_config":{"anyOf":[{"$ref":"#/components/schemas/HubSpotOptinFieldConfig"},{"type":"null"}],"description":"Per-channel HubSpot opt-in/opt-out field mapping. Omit to leave unchanged."}},"type":"object","title":"ListUpdate","description":"Schema for updating a list (all fields optional)","example":{"description":"Updated description","name":"Updated List Name"}},"ListsListResponse":{"properties":{"items":{"items":{"$ref":"#/components/schemas/ListResponse"},"type":"array","title":"Items","description":"List of items for this page"},"pagination":{"$ref":"#/components/schemas/PaginationMeta","description":"Pagination metadata"}},"type":"object","required":["items","pagination"],"title":"ListsListResponse","description":"Paginated list of lists.\n\nExample response:\n```json\n{\n    \"items\": [...],\n    \"pagination\": {\n        \"total\": 25,\n        \"page\": 1,\n        \"page_size\": 20,\n        \"total_pages\": 2,\n        \"has_next\": true,\n        \"has_prev\": false\n    }\n}\n```"},"LiveChatAgentAssignRequest":{"properties":{"agent_id":{"type":"string","title":"Agent Id","description":"User ID of the agent to assign"}},"type":"object","required":["agent_id"],"title":"LiveChatAgentAssignRequest","description":"Schema for assigning an agent to a live chat session"},"LiveChatEndResponse":{"properties":{"success":{"type":"boolean","title":"Success","description":"Whether session was ended successfully"},"session_id":{"type":"string","title":"Session Id","description":"ID of the ended session"},"ended_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Ended At","description":"When the session was ended"}},"type":"object","required":["success","session_id"],"title":"LiveChatEndResponse","description":"Response from ending a live chat session"},"LiveChatMessageSend":{"properties":{"text":{"type":"string","maxLength":5000,"minLength":1,"title":"Text","description":"Message text"},"attachments":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Attachments","description":"List of attachment URLs"}},"type":"object","required":["text"],"title":"LiveChatMessageSend","description":"Schema for sending a message in live chat"},"LiveChatMessageSendResponse":{"properties":{"success":{"type":"boolean","title":"Success","description":"Whether message was sent successfully"},"message_id":{"type":"string","title":"Message Id","description":"ID of the sent message"},"created_at":{"type":"string","format":"date-time","title":"Created At","description":"When the message was created"}},"type":"object","required":["success","message_id","created_at"],"title":"LiveChatMessageSendResponse","description":"Response from sending a live chat message"},"LiveChatSchedule":{"properties":{"enabled":{"type":"boolean","title":"Enabled","description":"Enable the weekly schedule gate","default":false},"timezone":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Timezone","description":"IANA tz to evaluate the schedule in; None/empty falls back to tenant.default_timezone"},"days":{"anyOf":[{"additionalProperties":{"$ref":"#/components/schemas/LiveChatScheduleDay"},"type":"object"},{"type":"null"}],"title":"Days","description":"Per-weekday hours keyed by mon|tue|wed|thu|fri|sat|sun"}},"additionalProperties":true,"type":"object","title":"LiveChatSchedule","description":"Weekly SCHEDULE gate for the live-chat option.\n\nWhen ``enabled`` is True, the availability check restricts live chat to the\nconfigured per-weekday hours, evaluated in ``timezone`` (falls back to the\ntenant's ``default_timezone`` when None/empty). ``days`` is keyed by the\nlowercase 3-letter weekday abbreviation (mon..sun); a missing/disabled day\nmeans closed."},"LiveChatScheduleDay":{"properties":{"enabled":{"type":"boolean","title":"Enabled","description":"Whether live chat is offered on this weekday","default":false},"start":{"type":"string","title":"Start","description":"Opening time, 'HH:MM' 24h","default":"09:00"},"end":{"type":"string","title":"End","description":"Closing time, 'HH:MM' 24h","default":"17:00"}},"additionalProperties":true,"type":"object","title":"LiveChatScheduleDay","description":"One weekday entry of the live-chat weekly schedule.\n\n``start``/``end`` are \"HH:MM\" 24h strings evaluated in the schedule's\ntimezone. A day with ``enabled=False`` (the default) is treated as closed."},"LiveChatSessionResponse":{"properties":{"id":{"type":"string","title":"Id"},"widget_id":{"type":"string","title":"Widget Id"},"contact_id":{"type":"string","title":"Contact Id"},"agent_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Agent Id"},"conversation_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Conversation Id"},"status":{"$ref":"#/components/schemas/LiveChatSessionStatus"},"started_at":{"type":"string","format":"date-time","title":"Started At"},"agent_joined_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Agent Joined At"},"ended_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Ended At"},"session_data":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Session Data"}},"type":"object","required":["id","widget_id","contact_id","status","started_at"],"title":"LiveChatSessionResponse","description":"Schema for live chat session response"},"LiveChatSessionStatus":{"type":"string","enum":["waiting","active","closed"],"title":"LiveChatSessionStatus","description":"Status of live chat sessions"},"LogicalOperator":{"type":"string","enum":["and","or"],"title":"LogicalOperator"},"LogoVariants":{"properties":{"favicon":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Favicon","description":"Favicon path (16x16 or 32x32)"},"64":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"64","description":"Small logo (64x64)"},"128":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"128","description":"Medium logo (128x128)"},"256":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"256","description":"Large logo (256x256)"},"original":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Original","description":"Original uploaded file"}},"type":"object","title":"LogoVariants","description":"Logo file variants at different sizes"},"LowBalanceThresholdUpdateRequest":{"properties":{"threshold_eur":{"anyOf":[{"type":"number","maximum":1000.0,"minimum":0.0},{"type":"string","pattern":"^(?!^[-+.]*$)[+-]?0*\\d*\\.?\\d{0,2}0*$"},{"type":"null"}],"title":"Threshold Eur","description":"Warning threshold in EUR (0 – 1000, max 2 decimals); null reverts to the platform default."}},"type":"object","title":"LowBalanceThresholdUpdateRequest","description":"Body of ``PUT /sms/low-balance-threshold``.\n\n``threshold_eur``: EUR amount (0 – 1000, max 2 decimals) below which the\nworkspace owners/admins get the low-balance warning. ``null`` removes the\nper-workspace override and reverts to the platform default."},"MPMSectionItem":{"properties":{"title":{"type":"string","maxLength":24,"title":"Title","description":"Section title (Meta cap: 24 chars)"},"product_retailer_ids":{"items":{"type":"string"},"type":"array","title":"Product Retailer Ids","description":"SKUs in this section"}},"type":"object","required":["title","product_retailer_ids"],"title":"MPMSectionItem"},"ManagedFAQCreate":{"properties":{"title":{"type":"string","maxLength":500,"minLength":1,"title":"Title","description":"Document title"},"folder_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Folder Id","description":"Parent folder ID (null for root level)"}},"type":"object","required":["title"],"title":"ManagedFAQCreate","description":"Schema for creating a managed FAQ document"},"ManagedFAQDetailResponse":{"properties":{"id":{"type":"string","title":"Id"},"tenant_id":{"type":"string","title":"Tenant Id"},"title":{"type":"string","title":"Title"},"item_count":{"type":"integer","title":"Item Count","description":"Number of Q&A pairs","default":0},"last_synced":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Last Synced","description":"When the document was last synced to Gemini"},"sync_status":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Sync Status","description":"Sync status: pending, syncing, synced, error"},"sync_error":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Sync Error","description":"Error message if sync_status is 'error'"},"created_at":{"type":"string","format":"date-time","title":"Created At"},"updated_at":{"type":"string","format":"date-time","title":"Updated At"},"items":{"items":{"$ref":"#/components/schemas/FAQItemResponse"},"type":"array","title":"Items","description":"All Q&A pairs in order"}},"type":"object","required":["id","tenant_id","title","created_at","updated_at"],"title":"ManagedFAQDetailResponse","description":"Schema for managed FAQ document with all items"},"ManagedFAQListResponse":{"properties":{"items":{"items":{"$ref":"#/components/schemas/ManagedFAQResponse"},"type":"array","title":"Items"},"total":{"type":"integer","title":"Total"}},"type":"object","required":["items","total"],"title":"ManagedFAQListResponse","description":"Schema for list of managed FAQ documents"},"ManagedFAQResponse":{"properties":{"id":{"type":"string","title":"Id"},"tenant_id":{"type":"string","title":"Tenant Id"},"title":{"type":"string","title":"Title"},"item_count":{"type":"integer","title":"Item Count","description":"Number of Q&A pairs","default":0},"last_synced":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Last Synced","description":"When the document was last synced to Gemini"},"sync_status":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Sync Status","description":"Sync status: pending, syncing, synced, error"},"sync_error":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Sync Error","description":"Error message if sync_status is 'error'"},"created_at":{"type":"string","format":"date-time","title":"Created At"},"updated_at":{"type":"string","format":"date-time","title":"Updated At"}},"type":"object","required":["id","tenant_id","title","created_at","updated_at"],"title":"ManagedFAQResponse","description":"Schema for managed FAQ document response"},"ManagedFAQUpdate":{"properties":{"title":{"anyOf":[{"type":"string","maxLength":500,"minLength":1},{"type":"null"}],"title":"Title","description":"Document title"}},"type":"object","title":"ManagedFAQUpdate","description":"Schema for updating a managed FAQ document"},"MediaArchivedFilter":{"type":"string","enum":["active","only","all"],"title":"MediaArchivedFilter","description":"Archive-status filter for the gallery list endpoint.\n\n* ``active`` (DEFAULT) — only non-archived assets (unchanged legacy behavior).\n* ``only`` — ONLY archived assets (the dedicated \"Archived\" tab; archived-only,\n  NOT mixed with active — this is what makes an accidentally-archived asset\n  recoverable from the UI).\n* ``all`` — no archive filter (unified view: active + archived together)."},"MediaGalleryAssetResponse":{"properties":{"id":{"type":"string","title":"Id","description":"Gallery asset id (media_assets.id)"},"attachment_id":{"type":"string","title":"Attachment Id","description":"Durable reference to the stored attachment (attachments.id)"},"url":{"type":"string","title":"Url","description":"Fresh, usable download/preview URL for the attachment. May expire — persist attachment_id, not this."},"thumbnail_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Thumbnail Url","description":"Fresh, ready-to-render thumbnail URL (small image/video poster). Generated asynchronously after upload and on lazy view; null while generation is pending or for documents. May expire — do not persist."},"display_name":{"type":"string","title":"Display Name","description":"Human-friendly asset name"},"description":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Description","description":"Optional longer description"},"media_type":{"type":"string","title":"Media Type","description":"image | video | document"},"content_type":{"type":"string","title":"Content Type","description":"MIME type of the underlying attachment"},"file_size":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"File Size","description":"Size in bytes (may be null)"},"tags":{"items":{"type":"string"},"type":"array","title":"Tags","description":"Normalized lowercase tags"},"is_favorite":{"type":"boolean","title":"Is Favorite","description":"Whether the asset is marked as a favorite by the tenant","default":false},"created_at":{"type":"string","format":"date-time","title":"Created At","description":"When the asset was saved to the gallery"},"last_used_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Last Used At","description":"Last outbound reuse; null if never reused"}},"type":"object","required":["id","attachment_id","url","display_name","media_type","content_type","created_at"],"title":"MediaGalleryAssetResponse","description":"One gallery asset as returned to any client (list item AND single).\n\n``attachment_id`` is the DURABLE reference (persist this for later sends);\n``url`` is a FRESH, ready-to-render download/preview URL for the underlying\nattachment and may change/expire — never persist ``url``."},"MediaGalleryCreateRequest":{"properties":{"attachment_id":{"type":"string","title":"Attachment Id","description":"Id of an attachment the caller's tenant already owns"},"display_name":{"type":"string","maxLength":255,"minLength":1,"title":"Display Name"},"description":{"anyOf":[{"type":"string","maxLength":10000},{"type":"null"}],"title":"Description"},"tags":{"items":{"type":"string"},"type":"array","title":"Tags","description":"Tags; normalized to lowercase, trimmed, deduped on save"}},"type":"object","required":["attachment_id","display_name"],"title":"MediaGalleryCreateRequest","description":"Save an already-stored attachment into the gallery (authenticated app)."},"MediaGalleryDeleteResponse":{"properties":{"success":{"type":"boolean","title":"Success","default":true},"id":{"type":"string","title":"Id"}},"type":"object","required":["id"],"title":"MediaGalleryDeleteResponse","description":"Unified DELETE envelope."},"MediaGalleryUpdateRequest":{"properties":{"display_name":{"anyOf":[{"type":"string","maxLength":255,"minLength":1},{"type":"null"}],"title":"Display Name"},"description":{"anyOf":[{"type":"string","maxLength":10000},{"type":"null"}],"title":"Description"},"tags":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Tags","description":"Full replacement tag set (normalized on save)"},"is_archived":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Is Archived","description":"Soft-archive toggle; archived assets drop out of the default list"},"is_favorite":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Is Favorite","description":"Favorite toggle; surface with the ?favorites_only=true list filter"}},"type":"object","title":"MediaGalleryUpdateRequest","description":"Patch a gallery asset. All fields optional; only provided fields change.\n\n``tags`` (when provided) REPLACES the full tag set (delete + insert)."},"MediaUploadResponse":{"properties":{"success":{"type":"boolean","title":"Success","description":"Whether the upload was successful"},"handle":{"type":"string","title":"Handle","description":"Media handle to use in template creation"},"file_name":{"type":"string","title":"File Name","description":"Original file name"},"media_type":{"type":"string","title":"Media Type","description":"Type of media: image, video, or document"},"media_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Media Url","description":"Permanent public URL to the uploaded media in our own storage. Pass this back as `default_header_media_url` on POST /whatsapp-templates (create) or PATCH /whatsapp-templates/{id} (update) so the send path can use it as a fallback when no `header_media_url` is supplied."},"attachment_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Attachment Id","description":"UUID of the corresponding `attachments` row created for the uploaded media. Lets callers re-resolve a fresh download URL from the Attachments API without re-uploading the file. Stored alongside `media_url` so future migrations to signed/proxied URLs can be a service-internal change without breaking persisted `default_header_media_url` values."}},"type":"object","required":["success","handle","file_name","media_type"],"title":"MediaUploadResponse","description":"Response after uploading media for WhatsApp template header"},"MergeContactsPreviewFlow":{"properties":{"flow_id":{"type":"string","title":"Flow Id"},"flow_name":{"type":"string","title":"Flow Name","default":""},"count":{"type":"integer","title":"Count","description":"Number of running flow runs of this flow that the merge would stop"}},"type":"object","required":["flow_id","count"],"title":"MergeContactsPreviewFlow","description":"One flow whose runs the merge would stop, with how many runs."},"MergeContactsPreviewResponse":{"properties":{"primary_contact_id":{"type":"string","title":"Primary Contact Id","description":"The contact that would survive the merge"},"flow_runs_to_cancel":{"type":"integer","title":"Flow Runs To Cancel","description":"Running flow runs the merge would stop"},"flow_runs_to_keep":{"type":"integer","title":"Flow Runs To Keep","description":"Running flow runs that would continue on the surviving contact"},"cancelled_flows":{"items":{"$ref":"#/components/schemas/MergeContactsPreviewFlow"},"type":"array","title":"Cancelled Flows"}},"type":"object","required":["primary_contact_id","flow_runs_to_cancel","flow_runs_to_keep"],"title":"MergeContactsPreviewResponse","description":"Response for POST /contacts/{id}/merge/preview — read-only, nothing is changed.\n\nA merge keeps a merged contact's running flows going on the surviving\ncontact. A flow run is only stopped when the surviving contact is already\nrunning the same flow, or when several merged contacts run the same flow\n(one of them keeps running)."},"MergeContactsRequest":{"properties":{"secondary_ids":{"items":{"type":"string"},"type":"array","maxItems":50,"minItems":1,"title":"Secondary Ids","description":"UUIDs of contacts to merge into the primary contact (the {id} in the URL)."},"primary_override":{"type":"boolean","title":"Primary Override","description":"When False (default), the oldest contact among {id, *secondary_ids} is used as primary regardless of which UUID is in the URL. Set True to force {id} as primary even if a secondary is older.","default":false}},"type":"object","required":["secondary_ids"],"title":"MergeContactsRequest","description":"Body for POST /contacts/{id}/merge — manual merge of secondary contacts into {id}."},"MergeContactsResponse":{"properties":{"contact":{"$ref":"#/components/schemas/ContactResponse"},"merged_count":{"type":"integer","title":"Merged Count","description":"Number of secondary contacts merged into the primary"}},"type":"object","required":["contact","merged_count"],"title":"MergeContactsResponse","description":"Response for POST /contacts/{id}/merge."},"MergedFromInfo":{"properties":{"old_id":{"type":"string","title":"Old Id","description":"The old UUID the caller requested"},"merged_at":{"type":"string","format":"date-time","title":"Merged At","description":"When the merge that produced the alias happened"},"reason":{"type":"string","title":"Reason","description":"Reason code: auto, manual, import, live_chat, api"}},"type":"object","required":["old_id","merged_at","reason"],"title":"MergedFromInfo","description":"Surfaced on GET /contacts/{old_uuid} when the request hit a contact_alias row."},"MessageActionButton":{"properties":{"type":{"type":"string","title":"Type","description":"unsubscribe | unsubscribe_all | subscribe | url"},"caption":{"anyOf":[{"type":"string","maxLength":40},{"type":"null"}],"title":"Caption","description":"Button label"},"url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Url","description":"Target URL (type='url' only)"}},"additionalProperties":true,"type":"object","required":["type"],"title":"MessageActionButton","description":"A single button on an opt-in / opt-out MESSAGE action.\n\nSame contract as the newsletter welcome/goodbye buttons (see\n`app/core/newsletter_buttons.py`, which normalizes/validates on the send\npath). Kept permissive here so the two do not drift."},"MessageContent":{"properties":{"text":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Text"},"type":{"$ref":"#/components/schemas/MessageType","default":"text"}},"type":"object","title":"MessageContent","description":"Message content (legacy schema)"},"MessageCreate":{"properties":{"to":{"anyOf":[{},{"type":"null"}],"title":"To","description":"Recipient external ID (phone, telegram_id, etc.) - optional when conversation_id or contact_method_id provided"},"channel_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Channel Id","description":"Channel UUID (used when creating new conversation or for ambiguous recipients)"},"conversation_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Conversation Id","description":"Conversation UUID (auto-resolved from contact_id if not provided)"},"contact_method_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Contact Method Id","description":"Contact method UUID - cleanest way to send, resolves to, channel_id, and contact_id automatically"},"contact_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Contact Id","description":"Contact UUID (used to auto-resolve conversation when conversation_id is omitted)"},"message_type":{"$ref":"#/components/schemas/MessageType","description":"Message type","default":"text"},"reply_to_message_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Reply To Message Id","description":"Optional UUID of a prior message in the SAME conversation to reply to (threaded / quote reply). The target message must already have a platform external_id; the outbound message is sent as a native quote-reply on channels that support it (WhatsApp, Telegram, Instagram, Live Chat). Rejected with 4xx if the target is not found in this conversation or has no external_id yet."},"text":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Text","description":"Message text content"},"subject":{"anyOf":[{"type":"string","maxLength":998},{"type":"null"}],"title":"Subject","description":"Email subject line — REQUIRED for email channel sends, ignored for chat-app channels (whatsapp/telegram/sms/etc.). When omitted for an email reply, the conversation's existing subject is used (prefixed with 'Re: ' if not already). The API rejects an email send with no resolvable subject (SendGrid 400s on empty subject)."},"attachments":{"anyOf":[{"type":"string"},{"items":{"type":"string"},"type":"array"}],"title":"Attachments","description":"Attachment IDs (UUIDs) returned from POST /api/v1/attachments/upload or POST /api/v1/attachments/from-url. Do NOT pass raw URLs here — fetch them via /attachments/from-url first to get an ID.\n\nAccepts either an array or a single bare ID string (`\"<uuid>\"` is treated as `[\"<uuid>\"]`). Whitespace is trimmed and empty entries are dropped.\n\nOn channels that can only carry ONE media object per message (WhatsApp, Telegram, Messenger, Instagram, SMS/MMS, browser push, RCS) several attachments are delivered as several messages — one per attachment, with `text` used as the caption of the FIRST one. This response returns the first message; the additional ones are listed in `related_message_ids`. Email (MIME parts) and live chat deliver all attachments inside a single message."},"attachment_filenames":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Attachment Filenames","description":"Optional per-message display-filename overrides for `attachments`, aligned positionally by index (entry N overrides the filename of `attachments[N]`). Controls the filename shown to the recipient — e.g. Meta's `filename` field on a WhatsApp document, so the user sees `Invoice-2026.pdf` instead of the stored object name. When omitted (or an entry is null/empty), the send path falls back to the original filename stored on the Attachment record (`attachments.filename`). If the list is shorter than `attachments`, only the leading attachments are overridden; extra entries beyond the number of attachments are ignored. Every entry now takes effect: on a channel that delivers one attachment per message, entry N is the filename of the message carrying `attachments[N]`."},"meta":{"additionalProperties":true,"type":"object","title":"Meta","description":"Customer metadata"},"is_voice":{"type":"boolean","title":"Is Voice","description":"Mark this message as a voice message (push-to-talk). Only valid when `message_type` is `audio` AND at least one attachment (the recorded audio) is supplied; the server rejects the send (422 `INVALID_VOICE_MESSAGE`) otherwise. The recorded file is the first entry in `attachments` (a normal Attachment ID); it is converted to the channel's native voice format before delivery. The duration is determined automatically from the recorded attachment and stored on the message as `meta.duration_ms` (alongside `meta.is_voice`; sibling meta keys are preserved). If it cannot be read at send time, `meta.duration_ms` is filled in once the recording has been processed. Voice messages are limited to 5 minutes (300000 ms): a longer recording is rejected with 422 `INVALID_VOICE_MESSAGE`, or, if its length can only be determined during processing, the message fails delivery (status `failed`). Default false: ordinary audio file send (unchanged behavior).","default":false},"include_signature":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Include Signature","description":"Append the sender's email signature to outbound email (EmailSignature for the user, falling back to EmailSignatureTemplate for the tenant). Default `true` for email; ignored for chat-app channels."},"sender_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Sender Id","description":"Optional sender override for email channel sends. The id either of an EmailIntegration (proxy / gmail / smtp-imap / ses-managed) or an EmailProviderConfig (SendGrid / Mailgun / SES). Surface candidates via GET /api/v1/conversations/{id}/sender-options. The server validates the choice against the user's EmailMailbox ACL, additionally allowing the conversation's default reply sender (the receiving connection's outbound_provider) even when the user is not on that mailbox's ACL. Ignored for non-email channels."},"message_traffic_type":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Message Traffic Type","description":"RCS only. Declares the traffic class of this send to the mobile carrier (`AgentMessage.messageTrafficType`): one of `AUTHENTICATION`, `TRANSACTION`, `PROMOTION`, `SERVICEREQUEST`, `ACKNOWLEDGEMENT`. This is a REGULATORY declaration, not a billing option — if the message is marketing you must declare `PROMOTION`. Omitted (the default) means `SERVICEREQUEST`, which is correct only for replies to a customer's own enquiry. Unrecognised values are ignored (the default applies) rather than failing the send. Ignored by all non-RCS channels."},"content_format":{"anyOf":[{"type":"string","enum":["markdown","plain"]},{"type":"null"}],"title":"Content Format","description":"How the send pipeline must treat the message body. `plain` (the default when omitted) = human-typed text, passed through to the channel UNTOUCHED. `markdown` = canonical markdown that the channel adapter renders into each channel's own formatting. Omit this for normal human replies — only AI/bot/flow-generated content should opt into `markdown`. Stored on the message as `meta.content_format`; absence is always treated as `plain`."},"content":{"anyOf":[{"$ref":"#/components/schemas/MessageContent"},{"type":"null"}],"description":"DEPRECATED: use text and message_type directly"},"metadata":{"additionalProperties":true,"type":"object","title":"Metadata","description":"DEPRECATED: use meta"}},"type":"object","title":"MessageCreate","description":"Schema for creating/sending a new message.\n\nSupports 3 resolution modes:\n\n**Mode 1: conversation_id only** (reply to existing conversation)\n- Provide just `conversation_id` and `text`. The recipient (`to`) is automatically\n  resolved from the conversation's contact method or last inbound message.\n- Example: ``{\"conversation_id\": \"...\", \"text\": \"Hello!\"}``\n\n**Mode 2: contact_method_id** (cleanest, resolves everything)\n- Provide `contact_method_id` and `text`. The `to`, `channel_id`, and `contact_id`\n  are all resolved from the contact method record.\n- Example: ``{\"contact_method_id\": \"...\", \"text\": \"Hello!\"}``\n\n**Mode 3: contact_id + optional to + channel_id** (explicit, auto-creates contact_method)\n- Provide `contact_id` (and optionally `to` and `channel_id`). The conversation is\n  auto-resolved from the contact. A contact_method is auto-created if one doesn't exist.\n- Example: ``{\"contact_id\": \"...\", \"to\": \"+49...\", \"text\": \"Hello!\"}``\n\nFields:\n- to: Recipient external ID (phone, telegram_id, etc.) - optional when conversation_id\n  or contact_method_id is provided\n- conversation_id: Optional, conversation to send in (auto-resolved if not provided)\n- contact_method_id: Optional, cleanest way to send - resolves to, channel_id, and\n  contact_id automatically\n- contact_id: Optional, used to auto-resolve conversation when conversation_id is omitted\n- channel_id: Optional, used when creating a new conversation or for ambiguous recipients\n- message_type: Optional, defaults to \"text\"\n- text: Message content\n- attachments: Optional list of attachment IDs\n- meta: Optional customer-defined metadata","examples":[{"description":"Send a text message to an existing conversation. The recipient is auto-resolved from the conversation's contact method.","summary":"Mode 1: Reply to conversation (no 'to' needed)","value":{"conversation_id":"b5d8e2a1-4c3f-4a8b-9d2e-1f6a7b8c9d0e","message_type":"text","text":"Hello! Your order has been shipped."}},{"description":"Send a message using a contact method ID. Everything (to, channel_id, contact_id) is resolved automatically.","summary":"Mode 2: Send via contact_method_id (cleanest)","value":{"contact_method_id":"d4e5f6a7-8901-2345-6789-0abcdef12345","message_type":"text","text":"Your appointment is confirmed for tomorrow."}},{"description":"Send a message to a contact. The conversation is auto-resolved and a contact_method is auto-created if needed.","summary":"Mode 3: Send via contact_id (auto-resolve)","value":{"contact_id":"c1d2e3f4-5678-9abc-def0-1234567890ab","message_type":"text","text":"Welcome to our support channel!","to":"+491521558086"}},{"description":"Send a text message with explicit recipient to an existing conversation.","summary":"Mode 1 with explicit 'to' (backward compatible)","value":{"conversation_id":"b5d8e2a1-4c3f-4a8b-9d2e-1f6a7b8c9d0e","message_type":"text","text":"Hello! Your order has been shipped.","to":"+491521558086"}},{"description":"Send an image to a contact with attachments and metadata.","summary":"Image message via contact auto-resolve","value":{"attachments":["550e8400-e29b-41d4-a716-446655440000"],"contact_id":"c1d2e3f4-5678-9abc-def0-1234567890ab","message_type":"image","meta":{"order_id":"ORD-98765"},"text":"Here is your receipt.","to":"+491521558086"}}]},"MessageDirection-Input":{"type":"string","enum":["inbound","outbound"],"title":"MessageDirection","description":"Message direction.\n\nNote: Uses lowercase member names to match MySQL ENUM values exactly.\nDatabase has: enum('inbound','outbound')"},"MessageDirection-Output":{"type":"string","enum":["inbound","outbound"],"title":"MessageDirection","description":"Message direction (lowercase to match MySQL ENUM values)"},"MessageEvent":{"properties":{"event_type":{"type":"string","const":"message","title":"Event Type","default":"message"},"id":{"type":"string","title":"Id","description":"Stable client-side key. Matches ``message_id``."},"occurred_at":{"type":"string","title":"Occurred At","description":"Chronological anchor (``messages.created_at``). Used as the merge sort key."},"message_id":{"type":"string","title":"Message Id"},"conversation_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Conversation Id"},"direction":{"$ref":"#/components/schemas/MessageDirection-Output"},"message_type":{"$ref":"#/components/schemas/MessageType"},"text":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Text"},"status":{"$ref":"#/components/schemas/MessageStatus-Output"},"platform":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Platform"},"sent_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Sent At"},"flow_run_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Flow Run Id"},"flow_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Flow Id"},"flow_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Flow Name"}},"type":"object","required":["id","occurred_at","message_id","direction","message_type","status"],"title":"MessageEvent","description":"Timeline row representing a single message for the contact.\n\nSuperset of :class:`FlowMessageEvent` — includes every message\naddressed to / from the contact regardless of flow attribution\n(manual sends, AI bot replies, inbound, and flow-sent rows). When\nthe message *was* sent by a flow, ``flow_run_id`` / ``flow_id`` /\n``flow_name`` are populated via the same batch lookup used by\n:class:`FlowMessageEvent`; otherwise those fields are ``None``."},"MessageListResponse":{"properties":{"items":{"items":{"$ref":"#/components/schemas/MessageResponse"},"type":"array","title":"Items","description":"List of messages for this page"},"pagination":{"$ref":"#/components/schemas/MessagePaginationMeta","description":"Pagination metadata with cursor support"}},"type":"object","required":["items","pagination"],"title":"MessageListResponse","description":"Paginated list of messages with cursor-based pagination support.\n\nSupports two pagination modes:\n1. Page-based (standard): Use page/page_size parameters\n2. Cursor-based (efficient): Use cursor parameter for O(1) performance\n\nFor tables with billions of rows (like messages), cursor pagination\nis recommended.\n\nExample response:\n```json\n{\n    \"items\": [...],\n    \"pagination\": {\n        \"total\": 1500,\n        \"page\": 1,\n        \"page_size\": 50,\n        \"total_pages\": 30,\n        \"has_next\": true,\n        \"has_prev\": false,\n        \"next_cursor\": \"eyJjcmVhdGVkX2F0IjoiMjAyNS0wMS0xNVQxMDozMDowMFoiLCJpZCI6Im1zZy0xMjMifQ==\",\n        \"has_more\": true\n    }\n}\n```"},"MessageOptAction":{"properties":{"type":{"type":"string","const":"message","title":"Type","default":"message"},"text":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Text","description":"Message body sent on opt-in / opt-out (may be null when the action is buttons-only)."},"buttons":{"anyOf":[{"items":{"$ref":"#/components/schemas/MessageActionButton"},"type":"array"},{"type":"null"}],"title":"Buttons","description":"Up to 3 native buttons; degrade to a keyword hint on channels without button support. See newsletter buttons."}},"type":"object","title":"MessageOptAction","description":"Opt-in / opt-out action that SENDS A MESSAGE."},"MessagePaginationMeta":{"properties":{"total":{"type":"integer","title":"Total","description":"Total number of items matching the query"},"page":{"type":"integer","minimum":1.0,"title":"Page","description":"Current page number (1-indexed)"},"page_size":{"type":"integer","minimum":1.0,"title":"Page Size","description":"Number of items per page"},"total_pages":{"type":"integer","title":"Total Pages","description":"Total number of pages"},"has_next":{"type":"boolean","title":"Has Next","description":"Whether there are more pages"},"has_prev":{"type":"boolean","title":"Has Prev","description":"Whether there are previous pages"},"next_cursor":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Next Cursor","description":"Cursor for next page (use instead of page for large datasets)"},"prev_cursor":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Prev Cursor","description":"Cursor for previous page"},"has_more":{"type":"boolean","title":"Has More","description":"Whether there are more messages after this page","default":false}},"type":"object","required":["total","page","page_size","total_pages","has_next","has_prev"],"title":"MessagePaginationMeta","description":"Extended pagination metadata for messages with cursor support.\n\nMessages tables can have billions of rows, so cursor-based pagination\nis recommended for efficient keyset pagination."},"MessagePricingResponse":{"properties":{"country_iso":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Country Iso"},"country_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Country Name"},"country_code":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Country Code"},"prefix":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Prefix"},"phone_sample":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Phone Sample"},"platform_fee_eur":{"type":"string","pattern":"^(?!^[-+.]*$)[+-]?0*\\d*\\.?\\d*$","title":"Platform Fee Eur"},"sendseven_fee_eur":{"type":"string","pattern":"^(?!^[-+.]*$)[+-]?0*\\d*\\.?\\d*$","title":"Sendseven Fee Eur"},"total_per_segment_eur":{"type":"string","pattern":"^(?!^[-+.]*$)[+-]?0*\\d*\\.?\\d*$","title":"Total Per Segment Eur"},"price_per_message_eur":{"type":"string","pattern":"^(?!^[-+.]*$)[+-]?0*\\d*\\.?\\d*$","title":"Price Per Message Eur"},"segments":{"type":"integer","title":"Segments","default":1},"encoding":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Encoding"},"char_count":{"type":"integer","title":"Char Count","default":0},"total_eur":{"type":"string","pattern":"^(?!^[-+.]*$)[+-]?0*\\d*\\.?\\d*$","title":"Total Eur"},"source":{"type":"string","title":"Source"},"is_supported":{"type":"boolean","title":"Is Supported","default":true},"quote_required":{"type":"boolean","title":"Quote Required","default":false}},"type":"object","required":["platform_fee_eur","sendseven_fee_eur","total_per_segment_eur","price_per_message_eur","total_eur","source"],"title":"MessagePricingResponse","description":"Estimated retail price for an outbound SMS.\n\nUsed by the frontend to show \"this campaign will cost ~X EUR\" before\nthe customer clicks Send. ``platform_fee_eur`` is seven.io's per-segment\ncost (min across networks for the destination country); ``sendseven_fee_eur``\nis our flat margin on top. ``total_per_segment_eur`` = platform + margin\nand is also returned as the legacy ``price_per_message_eur`` field for\nbackward compatibility with older frontend builds.\n\nWhen a ``text`` is supplied, the response also includes the segment\ncount (GSM-7 vs UCS-2) and the projected total cost."},"MessageResponse":{"properties":{"id":{"type":"string","title":"Id"},"conversation_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Conversation Id","description":"Conversation the message belongs to. May be null for campaign and flow messages sent to a contact that has no conversation on that channel (campaigns never create conversations; flow messages are attached once the contact replies)."},"tenant_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Tenant Id"},"platform":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Platform"},"channel_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Channel Id"},"contact_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Contact Id"},"contact_method_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Contact Method Id"},"contact_method":{"anyOf":[{"$ref":"#/components/schemas/ContactMethodInfo"},{"type":"null"}]},"direction":{"$ref":"#/components/schemas/MessageDirection-Output"},"message_type":{"$ref":"#/components/schemas/MessageType"},"from":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"From"},"to":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"To"},"contact_info":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Contact Info","description":"Provider-echoed sender profile captured from the inbound channel webhook at receipt time (inbound message.received only; not a DB lookup). Fields vary by channel: WhatsApp {name, phone}, Telegram {name, username, profile_picture}, Messenger {name, profile_picture}, Instagram {name?, username, profile_picture}. NOTE: `phone` (WhatsApp-only) is deprecated — use `data.contact_method.value` for the authoritative DB-sourced identifier of the method actually used."},"text":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Text"},"status":{"$ref":"#/components/schemas/MessageStatus-Output"},"attachments":{"items":{"$ref":"#/components/schemas/app__schemas__message__AttachmentResponse"},"type":"array","title":"Attachments"},"related_message_ids":{"items":{"type":"string"},"type":"array","title":"Related Message Ids","description":"IDs of the additional messages created by the same send request, in delivery order. Non-empty only when a send carried several attachments on a channel that delivers one attachment per message — the response body itself is the FIRST message (it holds the caption and `attachments[0]`), and each id here is a further message carrying the next attachment. Each has its own status and its own delivery webhooks, so poll them individually via `GET /api/v1/messages/{id}`. Empty array for every other message."},"meta":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Meta"},"content_format":{"type":"string","title":"Content Format","description":"How this message's body is formatted for the send pipeline: `markdown` (canonical markdown, rendered per-channel) or `plain` (human text, passed through untouched). Derived from `meta.content_format`; defaults to `plain` for every message that did not explicitly opt into markdown.","default":"plain"},"interactive":{"anyOf":[{"$ref":"#/components/schemas/InteractiveContent"},{"type":"null"}],"description":"Interactive message content (WhatsApp-style Meta format) for messages with reply buttons or a list. Hydrated from `meta.interactive`. Null for non-interactive/legacy messages."},"header_media_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Header Media Url","description":"Resolved media URL for an interactive image/video header"},"header_media_attachment_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Header Media Attachment Id","description":"Durable attachment id for an interactive image/video header"},"external_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"External Id"},"reply_to_external_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Reply To External Id","description":"Platform message ID of quoted message"},"quoted_message":{"anyOf":[{"$ref":"#/components/schemas/QuotedMessage"},{"type":"null"}],"description":"Preview of the quoted message"},"created_at":{"type":"string","title":"Created At"},"sent_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Sent At"},"delivered_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Delivered At"},"read_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Read At"},"is_internal":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Is Internal","default":false},"error_message":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Error Message"},"error_code":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Error Code","description":"Machine-readable failure code for failed messages, e.g. 'contact_blocked' (the contact is blocked) or a provider error code."},"campaign":{"anyOf":[{"$ref":"#/components/schemas/CampaignInfo"},{"type":"null"}],"description":"Campaign info when this message was sent via a campaign/newsletter"},"flow_run_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Flow Run Id","description":"FlowRun UUID — set when this message was sent by an automation flow. Frontends use this (together with `flow_name`) to render a 'Sent by flow' badge in the conversation timeline."},"flow_step_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Flow Step Id","description":"FlowRunStep UUID — identifies the specific Send-node step inside the flow that produced this message. Optional companion to `flow_run_id`."},"flow_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Flow Name","description":"Display name of the flow that sent this message. NOT stored on the message row — populated by the API layer via a single batch lookup against `flow_runs JOIN flows` and only set when `flow_run_id` is non-null."},"edited_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Edited At","description":"When this message was last edited by the sender (real column on `messages`, stamped when an edit event is applied). Non-null is the ONLY signal that a message was edited — `text` is silently replaced in place, so without this a client cannot tell an edited message from an original one. Null for every message that was never edited (the overwhelming majority)."},"previous_text":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Previous Text","description":"Body this message had immediately BEFORE its most recent edit. Not stored on the message row — resolved by the API layer from the newest APPLIED `message_revisions` row in one batched lookup per page. Null means 'edited, but the superseded body is unknown': most edits arrive before/without the message they target, so the original body was often never captured. Clients must therefore treat null as valid and fall back to a plain non-interactive 'edited' marker instead of an edit-history UI."},"revision_count":{"type":"integer","title":"Revision Count","description":"Number of edits that have actually been APPLIED to this message. Pending revisions (an edit whose target message we never resolved) are deliberately excluded — they describe no visible change to this row. 0 for un-edited messages; a value >1 tells the client the body was edited repeatedly, so `previous_text` is only the last step of a longer history.","default":0}},"type":"object","required":["id","direction","message_type","status","created_at"],"title":"MessageResponse","description":"Schema for message response.\n\nUnified message format for all channels with simplified from/to fields.\n- platform: Channel type (whatsapp, telegram, etc.) at message level for filtering\n- channel_id: Always represents \"our side\" (the business's channel)\n- contact_id: Link to Contact for name lookup\n- from: External ID (only set for inbound messages)\n- to: External ID (only set for outbound messages)\n- contact_info: Messenger-specific data (only for inbound)\n- meta: Customer-defined arbitrary JSON","example":{"attachments":[],"channel_id":"channel-123-456-789","contact_id":"contact-123-456-789","conversation_id":"conv-123-456-789","created_at":"2025-01-15T10:30:00Z","delivered_at":"2025-01-15T10:30:05Z","direction":"inbound","from":"+1234567890","id":"msg-123-456-789","message_type":"text","platform":"whatsapp","status":"delivered","tenant_id":"tenant-123-456-789","text":"Hello, I need help!"}},"MessageStatus-Input":{"type":"string","enum":["pending","sent","delivered","read","failed"],"title":"MessageStatus","description":"Message delivery status.\n\nNote: Uses lowercase member names to match MySQL ENUM values exactly.\nDatabase has: enum('pending','sent','delivered','read','failed')"},"MessageStatus-Output":{"type":"string","enum":["pending","sent","delivered","read","failed"],"title":"MessageStatus","description":"Message status (lowercase to match MySQL ENUM values)"},"MessageTimePoint":{"properties":{"date":{"type":"string","title":"Date","description":"Date in YYYY-MM-DD format"},"message_count":{"type":"integer","title":"Message Count","description":"Number of messages on this date"},"inbound":{"type":"integer","title":"Inbound","description":"Number of inbound messages on this date","default":0},"outbound":{"type":"integer","title":"Outbound","description":"Number of outbound messages on this date","default":0}},"type":"object","required":["date","message_count"],"title":"MessageTimePoint","description":"Message count at a specific point in time"},"MessageType":{"type":"string","enum":["text","image","video","audio","document","location","contact","sticker","system","quick_reply","interactive","carousel","button","file","reaction","referral","whatsapp_template","kb_response","notification","call","order"],"title":"MessageType","description":"Message content type (lowercase to match MySQL ENUM values).\n\nMUST stay in lockstep with ``app.models.message.MessageType`` AND the\nMySQL ENUM column ``messages.message_type``. Validation here is what\ngates the API response — any value the DB column accepts MUST be a\nmember of this enum, or ``MessageResponse.model_validate`` raises\n``ValidationError`` and the whole list endpoint 500s. Bug found\n2026-05-30: prod conversations carrying ``button`` rows from a\ntemplate-quick-reply click crashed ``GET /api/v1/messages``.\n\nInteractive message types:\n- quick_reply: Cross-platform quick reply buttons (Messenger, Instagram, Telegram)\n- interactive: WhatsApp interactive messages (buttons/lists)\n- carousel: Multi-card carousel messages\n- button: WhatsApp legacy template-button reply (callback button payload)\n\nAdapter-level types:\n- file: Generic file attachment fallback (Messenger/Instagram unknown content_type)\n- reaction: Emoji reaction (merged into target message metadata)\n- referral: Messenger m.me deep link / ad referral event\n\nWorker-emitted types:\n- whatsapp_template: Campaign-dispatched WhatsApp template message\n- kb_response: Team-chat AI knowledge-base response\n- notification: System notification message"},"MessengerCarouselButton":{"properties":{"type":{"type":"string","enum":["web_url","postback","phone_number"],"title":"Type","description":"Button type"},"title":{"type":"string","maxLength":20,"title":"Title","description":"Button text (max 20 characters)"},"url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Url","description":"URL for web_url buttons"},"payload":{"anyOf":[{"type":"string","maxLength":1000},{"type":"null"}],"title":"Payload","description":"Payload for postback buttons (max 1000 characters)"}},"type":"object","required":["type","title"],"title":"MessengerCarouselButton","description":"Button for a Messenger carousel element"},"MessengerCarouselDefaultAction":{"properties":{"type":{"type":"string","const":"web_url","title":"Type","description":"Action type (currently only web_url supported)","default":"web_url"},"url":{"type":"string","title":"Url","description":"URL to open when card is tapped"},"webview_height_ratio":{"anyOf":[{"type":"string","enum":["compact","tall","full"]},{"type":"null"}],"title":"Webview Height Ratio","description":"Height of webview (compact, tall, full)"}},"type":"object","required":["url"],"title":"MessengerCarouselDefaultAction","description":"Default action when Messenger carousel card is tapped"},"MessengerCarouselElement":{"properties":{"title":{"type":"string","maxLength":80,"title":"Title","description":"Card title (required, max 80 characters)"},"subtitle":{"anyOf":[{"type":"string","maxLength":80},{"type":"null"}],"title":"Subtitle","description":"Card subtitle (max 80 characters)"},"image_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Image Url","description":"URL to card image"},"default_action":{"anyOf":[{"$ref":"#/components/schemas/MessengerCarouselDefaultAction"},{"type":"null"}],"description":"Action when card is tapped"},"buttons":{"items":{"$ref":"#/components/schemas/MessengerCarouselButton"},"type":"array","maxItems":3,"title":"Buttons","description":"Card buttons (max 3 per element)","default":[]}},"type":"object","required":["title"],"title":"MessengerCarouselElement","description":"A single element (card) in a Messenger generic template carousel.\n\nMessenger carousels can have up to 10 elements, each with:\n- Title (required)\n- Subtitle\n- Image\n- Default action (when card is tapped)\n- Up to 3 buttons"},"MessengerContent":{"properties":{"channel_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Channel Id"},"message_type":{"type":"string","title":"Message Type","default":"text"},"message_text":{"type":"string","title":"Message Text"},"attachment_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Attachment Id"},"attachment_filename":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Attachment Filename"}},"type":"object","required":["message_text"],"title":"MessengerContent","description":"Messenger-specific campaign content."},"MessengerResubscribeResponse":{"properties":{"results":{"items":{"$ref":"#/components/schemas/MessengerResubscribeResult"},"type":"array","title":"Results","description":"One entry per Messenger channel acted on. Empty when the tenant has no matching Messenger channel (the endpoint 404s in that case)."}},"type":"object","title":"MessengerResubscribeResponse","description":"Result of the one-click \"Enable comments\" action for Messenger pages."},"MessengerResubscribeResult":{"properties":{"channel_id":{"type":"string","title":"Channel Id"},"channel_type":{"type":"string","title":"Channel Type"},"channel_name":{"type":"string","title":"Channel Name"},"page_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Page Id"},"ok":{"type":"boolean","title":"Ok","description":"True when Facebook accepted the `subscribed_apps` update."},"reason":{"type":"string","title":"Reason","description":"Stable machine code: `resubscribed` (success), `missing_credentials`, `graph_error`."},"detail":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Detail","description":"Human-readable explanation (English; UI translates by `reason`)."},"subscribed_fields":{"items":{"type":"string"},"type":"array","title":"Subscribed Fields","description":"The full field list POSTed (a full replace, incl. `feed`)."},"checked_at":{"type":"string","format":"date-time","title":"Checked At"}},"type":"object","required":["channel_id","channel_type","channel_name","ok","reason","checked_at"],"title":"MessengerResubscribeResult","description":"Outcome of resubscribing ONE Facebook Messenger Page to ``feed``.\n\nThe write-side counterpart to ``ChannelCommentCapability``: the capability\nprobe says a Messenger page needs a resubscribe (``resubscribe_required``);\nthis reports whether that resubscribe succeeded. Instagram is never here — it\nneeds a customer reconnect, which a resubscribe cannot substitute for."},"MultiFileUploadResponse":{"properties":{"total":{"type":"integer","title":"Total","description":"Total files submitted"},"successful":{"type":"integer","title":"Successful","description":"Number of successful uploads"},"failed":{"type":"integer","title":"Failed","description":"Number of failed uploads"},"results":{"items":{"$ref":"#/components/schemas/MultiFileUploadResult"},"type":"array","title":"Results","description":"Per-file results"}},"type":"object","required":["total","successful","failed","results"],"title":"MultiFileUploadResponse","description":"Response for multi-file upload endpoint."},"MultiFileUploadResult":{"properties":{"filename":{"type":"string","title":"Filename","description":"Original filename"},"success":{"type":"boolean","title":"Success","description":"Whether upload succeeded"},"document":{"anyOf":[{"$ref":"#/components/schemas/KnowledgeBaseDocumentResponse"},{"type":"null"}],"description":"Created document if successful"},"error":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Error","description":"Error message if failed"}},"type":"object","required":["filename","success"],"title":"MultiFileUploadResult","description":"Result for a single file in a multi-file upload."},"MyScopesResponse":{"properties":{"tenant_id":{"type":"string","title":"Tenant Id","description":"Tenant UUID"},"scopes":{"items":{"type":"string"},"type":"array","title":"Scopes","description":"List of effective scopes"},"total":{"type":"integer","title":"Total","description":"Total number of scopes"}},"type":"object","required":["tenant_id","scopes","total"],"title":"MyScopesResponse","description":"Response containing current user's scopes","example":{"scopes":["campaigns:read","campaigns:update","contacts:read"],"tenant_id":"uuid-of-tenant","total":3}},"NoteCreate":{"properties":{"content":{"type":"string","maxLength":10000,"minLength":1,"title":"Content","description":"Note content (plain text or markdown)"}},"type":"object","required":["content"],"title":"NoteCreate","description":"Schema for creating a new note","example":{"content":"Customer prefers email communication. Follow up next week."}},"NoteListResponse":{"properties":{"items":{"items":{"$ref":"#/components/schemas/NoteResponse"},"type":"array","title":"Items","description":"List of items for this page"},"pagination":{"$ref":"#/components/schemas/PaginationMeta","description":"Pagination metadata"}},"type":"object","required":["items","pagination"],"title":"NoteListResponse","description":"Paginated list of conversation notes.\n\nExample response:\n```json\n{\n    \"items\": [...],\n    \"pagination\": {\n        \"total\": 25,\n        \"page\": 1,\n        \"page_size\": 20,\n        \"total_pages\": 2,\n        \"has_next\": true,\n        \"has_prev\": false\n    }\n}\n```"},"NoteResponse":{"properties":{"created_at":{"type":"string","title":"Created At","description":"When the resource was created (UTC)"},"updated_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Updated At","description":"When the resource was last updated (UTC)"},"id":{"type":"string","title":"Id","description":"Unique identifier (UUID)"},"tenant_id":{"type":"string","title":"Tenant Id","description":"Tenant this resource belongs to"},"conversation_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Conversation Id","description":"Associated conversation ID"},"user_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"User Id","description":"ID of user who created the note (None for system-created notes)"},"content":{"type":"string","title":"Content","description":"Note content"},"user_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"User Name","description":"Name of user who created the note"},"user_email":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"User Email","description":"Email of user who created the note"},"message_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Message Id","description":"ID of the internal-note message that shows this note in the conversation timeline (null for older notes created before notes and internal notes were unified)."}},"type":"object","required":["created_at","id","tenant_id","content"],"title":"NoteResponse","description":"Full note response for conversation notes.\n\nIncludes user information for display purposes.","example":{"content":"Customer prefers email communication.","conversation_id":"conv-uuid-789","created_at":"2025-01-15T10:30:00Z","id":"note-uuid-123","message_id":"msg-uuid-345","tenant_id":"tenant-uuid-456","updated_at":"2025-01-15T10:30:00Z","user_email":"john@company.com","user_id":"user-uuid-012","user_name":"John Agent"}},"NoteUpdate":{"properties":{"content":{"type":"string","maxLength":10000,"minLength":1,"title":"Content","description":"Updated note content"}},"type":"object","required":["content"],"title":"NoteUpdate","description":"Schema for updating a note","example":{"content":"Updated: Customer now prefers phone calls."}},"OAuthAppCreate":{"properties":{"name":{"type":"string","maxLength":255,"minLength":1,"title":"Name","description":"App display name"},"redirect_uris":{"items":{"type":"string"},"type":"array","minItems":1,"title":"Redirect Uris","description":"Allowed redirect URIs"},"allowed_scopes":{"items":{"type":"string"},"type":"array","minItems":1,"title":"Allowed Scopes","description":"Scopes the app can request"},"description":{"anyOf":[{"type":"string","maxLength":2000},{"type":"null"}],"title":"Description","description":"App description for consent screen"},"logo_url":{"anyOf":[{"type":"string","maxLength":500},{"type":"null"}],"title":"Logo Url","description":"App logo URL"},"homepage_url":{"anyOf":[{"type":"string","maxLength":500},{"type":"null"}],"title":"Homepage Url","description":"App homepage URL"},"is_public":{"type":"boolean","title":"Is Public","description":"Can authorize users outside creator's organization","default":false}},"type":"object","required":["name","redirect_uris","allowed_scopes"],"title":"OAuthAppCreate","description":"Request to register a new OAuth app."},"OAuthAppCreateResponse":{"properties":{"id":{"type":"string","title":"Id"},"tenant_id":{"type":"string","title":"Tenant Id"},"name":{"type":"string","title":"Name"},"client_id":{"type":"string","title":"Client Id"},"redirect_uris":{"items":{"type":"string"},"type":"array","title":"Redirect Uris"},"allowed_scopes":{"items":{"type":"string"},"type":"array","title":"Allowed Scopes"},"description":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Description"},"logo_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Logo Url"},"homepage_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Homepage Url"},"is_verified":{"type":"boolean","title":"Is Verified"},"is_active":{"type":"boolean","title":"Is Active"},"is_public":{"type":"boolean","title":"Is Public","default":false},"is_first_party":{"type":"boolean","title":"Is First Party","default":false},"created_at":{"type":"string","format":"date-time","title":"Created At"},"updated_at":{"type":"string","format":"date-time","title":"Updated At"},"client_secret":{"type":"string","title":"Client Secret","description":"Client secret - shown only once!"}},"type":"object","required":["id","tenant_id","name","client_id","redirect_uris","allowed_scopes","is_verified","is_active","created_at","updated_at","client_secret"],"title":"OAuthAppCreateResponse","description":"Response when creating an OAuth app (includes client_secret ONCE)."},"OAuthAppResponse":{"properties":{"id":{"type":"string","title":"Id"},"tenant_id":{"type":"string","title":"Tenant Id"},"name":{"type":"string","title":"Name"},"client_id":{"type":"string","title":"Client Id"},"redirect_uris":{"items":{"type":"string"},"type":"array","title":"Redirect Uris"},"allowed_scopes":{"items":{"type":"string"},"type":"array","title":"Allowed Scopes"},"description":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Description"},"logo_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Logo Url"},"homepage_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Homepage Url"},"is_verified":{"type":"boolean","title":"Is Verified"},"is_active":{"type":"boolean","title":"Is Active"},"is_public":{"type":"boolean","title":"Is Public","default":false},"is_first_party":{"type":"boolean","title":"Is First Party","default":false},"created_at":{"type":"string","format":"date-time","title":"Created At"},"updated_at":{"type":"string","format":"date-time","title":"Updated At"}},"type":"object","required":["id","tenant_id","name","client_id","redirect_uris","allowed_scopes","is_verified","is_active","created_at","updated_at"],"title":"OAuthAppResponse","description":"OAuth app information (without client_secret)."},"OAuthAppUpdate":{"properties":{"name":{"anyOf":[{"type":"string","maxLength":255,"minLength":1},{"type":"null"}],"title":"Name"},"redirect_uris":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Redirect Uris"},"allowed_scopes":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Allowed Scopes"},"description":{"anyOf":[{"type":"string","maxLength":2000},{"type":"null"}],"title":"Description"},"logo_url":{"anyOf":[{"type":"string","maxLength":500},{"type":"null"}],"title":"Logo Url"},"homepage_url":{"anyOf":[{"type":"string","maxLength":500},{"type":"null"}],"title":"Homepage Url"},"is_active":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Is Active"},"is_public":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Is Public"}},"type":"object","title":"OAuthAppUpdate","description":"Request to update an OAuth app."},"OAuthAuthorizationResponse":{"properties":{"id":{"type":"string","title":"Id"},"oauth_app":{"$ref":"#/components/schemas/OAuthAppResponse"},"tenant_id":{"type":"string","title":"Tenant Id"},"tenant_name":{"type":"string","title":"Tenant Name"},"granted_scopes":{"items":{"type":"string"},"type":"array","title":"Granted Scopes"},"created_at":{"type":"string","format":"date-time","title":"Created At"},"updated_at":{"type":"string","format":"date-time","title":"Updated At"}},"type":"object","required":["id","oauth_app","tenant_id","tenant_name","granted_scopes","created_at","updated_at"],"title":"OAuthAuthorizationResponse","description":"User's authorized app (for connected apps list)."},"OAuthDeleteResponse":{"properties":{"success":{"type":"boolean","title":"Success","description":"Whether deletion was successful"},"id":{"type":"string","title":"Id","description":"ID of deleted resource"}},"type":"object","required":["success","id"],"title":"OAuthDeleteResponse","description":"Response from OAuth app or authorization deletion."},"OfflineEscalationBehaviorEnum":{"type":"string","enum":["leave_message","escalate_anyway","continue_bot"],"title":"OfflineEscalationBehaviorEnum","description":"What to do when escalation triggers but no agents are online (live chat)"},"OpenOrCreateConversationRequest":{"properties":{"contact_id":{"type":"string","title":"Contact Id","description":"Contact UUID to start conversation with"},"channel_id":{"type":"string","title":"Channel Id","description":"Channel UUID to use for this conversation"},"contact_method_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Contact Method Id","description":"Optional. Bind the conversation to a SPECIFIC contact method (e.g. one of the contact's several email addresses / phone numbers) instead of resolving the recipient via the channel's primary method. Must belong to the same contact + tenant and be compatible with the channel's method type (a PHONE method is also accepted for a WhatsApp channel). For email the synthetic 'email_mailbox_<uuid>' / 'email_integration_<uuid>' channel_id is validated as an EMAIL method type. When a NEW conversation is created both contact_method_id and active_contact_method_id are set to this value; when an existing conversation is reused only active_contact_method_id is updated. When omitted, behavior is unchanged (recipient resolved via the channel's primary method)."},"action":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Action","description":"Action to take: 'create_new', 'reopen_existing', or 'auto' (default). When using two-phase API, specify action explicitly to control webhook behavior."},"reopen_existing":{"type":"boolean","title":"Reopen Existing","description":"DEPRECATED: Use action='reopen_existing' instead. If True and a closed conversation exists, reopen it. If False, always create new.","default":false},"use_conversation_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Use Conversation Id","description":"For email: specify an existing conversation ID to use instead of creating new. This allows continuing an existing email thread."}},"type":"object","required":["contact_id","channel_id"],"title":"OpenOrCreateConversationRequest","description":"Request to open an existing conversation or create a new one.\n\nThis is a simplified endpoint that does NOT send a message.\nUse this when you want to navigate to a conversation and compose\nmessages using the normal chat UI.\n\nFor email channels, use `use_conversation_id` to specify which existing\nthread to use instead of creating a new one.\n\n**Two-phase API usage:**\n1. First call `/conversations/check-existing` to see if open/closed conversations exist\n2. Then call this endpoint with `action` to specify what to do:\n   - `action=\"create_new\"`: Always create a new conversation (fires webhook)\n   - `action=\"reopen_existing\"`: Reopen closed conversation (fires conversation.reopened webhook)\n   - `action=\"auto\"` (default): Backwards compatible behavior","examples":[{"channel_id":"channel-uuid","contact_id":"contact-uuid"},{"action":"create_new","channel_id":"channel-uuid","contact_id":"contact-uuid"},{"action":"reopen_existing","channel_id":"channel-uuid","contact_id":"contact-uuid"}]},"OpenOrCreateConversationResponse":{"properties":{"conversation_id":{"type":"string","title":"Conversation Id","description":"Conversation ID (existing or newly created)"},"is_new":{"type":"boolean","title":"Is New","description":"True if a new conversation was created"},"is_reopened":{"type":"boolean","title":"Is Reopened","description":"True if an existing closed conversation was reopened","default":false},"channel_type":{"type":"string","title":"Channel Type","description":"Channel type"},"window_status":{"anyOf":[{"$ref":"#/components/schemas/WhatsAppChannelWindowStatus"},{"type":"null"}],"description":"WhatsApp window status (WhatsApp channels only)"},"existing_closed_conversation":{"anyOf":[{"$ref":"#/components/schemas/ExistingConversationInfo"},{"type":"null"}],"description":"If a closed conversation exists (and wasn't reopened), info is provided here so UI can offer to reopen it"},"existing_open_email_threads":{"anyOf":[{"items":{"$ref":"#/components/schemas/ExistingEmailThreadInfo"},"type":"array"},{"type":"null"}],"title":"Existing Open Email Threads","description":"For email channels: list of existing open threads with this contact. UI can let user choose to continue one or start fresh."}},"type":"object","required":["conversation_id","is_new","channel_type"],"title":"OpenOrCreateConversationResponse","description":"Response from opening or creating a conversation"},"OptActionConfig":{"properties":{"default":{"anyOf":[{"oneOf":[{"$ref":"#/components/schemas/MessageOptAction"},{"$ref":"#/components/schemas/FlowOptAction"}],"discriminator":{"propertyName":"type","mapping":{"flow":"#/components/schemas/FlowOptAction","message":"#/components/schemas/MessageOptAction"}}},{"type":"null"}],"title":"Default","description":"Action applied to all channels unless overridden."},"per_channel":{"additionalProperties":{"oneOf":[{"$ref":"#/components/schemas/MessageOptAction"},{"$ref":"#/components/schemas/FlowOptAction"}],"discriminator":{"propertyName":"type","mapping":{"flow":"#/components/schemas/FlowOptAction","message":"#/components/schemas/MessageOptAction"}}},"type":"object","title":"Per Channel","description":"Per-channel overrides keyed by SubscriptionChannelType value (browser_push, email, instagram, messenger, rcs, sms, telegram, whatsapp)."}},"type":"object","title":"OptActionConfig","description":"`{default, per_channel}` wrapper for `opt_in_action` / `opt_out_action`.\n\n`default` applies to every channel; `per_channel` holds OPTIONAL overrides\nonly (a channel absent from the map uses `default`). A NULL column at the\nmodel layer is synthesized from the legacy newsletter message — that\nfallback is a model concern, this schema only validates a concrete blob."},"OptInMethod":{"type":"string","enum":["widget","import","api","manual","single_opt_in","double_opt_in","keyword","conversation","whatsapp_flow"],"title":"OptInMethod","description":"How the subscription was created"},"OptInMethodCount":{"properties":{"method":{"type":"string","title":"Method","description":"Opt-in method (widget, import, api, manual, etc.)"},"count":{"type":"integer","title":"Count","description":"Number of subscriptions via this method"}},"type":"object","required":["method","count"],"title":"OptInMethodCount","description":"Count by opt-in method"},"OptInMethodEnum":{"type":"string","enum":["widget","import","api","manual"],"title":"OptInMethodEnum","description":"Method used to subscribe contact"},"PaginatedInvoiceResponse":{"properties":{"items":{"items":{"$ref":"#/components/schemas/InvoiceResponse"},"type":"array","title":"Items"},"total":{"type":"integer","title":"Total"},"page":{"type":"integer","title":"Page"},"page_size":{"type":"integer","title":"Page Size"},"total_pages":{"type":"integer","title":"Total Pages"}},"type":"object","required":["items","total","page","page_size","total_pages"],"title":"PaginatedInvoiceResponse","description":"Paginated list of invoices"},"PaginatedResponse_AgentResponse_":{"properties":{"items":{"items":{"$ref":"#/components/schemas/AgentResponse"},"type":"array","title":"Items","description":"List of items for this page"},"pagination":{"$ref":"#/components/schemas/PaginationMeta","description":"Pagination metadata"}},"type":"object","required":["items","pagination"],"title":"PaginatedResponse[AgentResponse]"},"PaginatedResponse_AgentTurnResponse_":{"properties":{"items":{"items":{"$ref":"#/components/schemas/AgentTurnResponse"},"type":"array","title":"Items","description":"List of items for this page"},"pagination":{"$ref":"#/components/schemas/PaginationMeta","description":"Pagination metadata"}},"type":"object","required":["items","pagination"],"title":"PaginatedResponse[AgentTurnResponse]"},"PaginatedResponse_CustomFieldDefinitionResponse_":{"properties":{"items":{"items":{"$ref":"#/components/schemas/CustomFieldDefinitionResponse"},"type":"array","title":"Items","description":"List of items for this page"},"pagination":{"$ref":"#/components/schemas/PaginationMeta","description":"Pagination metadata"}},"type":"object","required":["items","pagination"],"title":"PaginatedResponse[CustomFieldDefinitionResponse]"},"PaginatedResponse_EmailContentListItem_":{"properties":{"items":{"items":{"$ref":"#/components/schemas/EmailContentListItem"},"type":"array","title":"Items","description":"List of items for this page"},"pagination":{"$ref":"#/components/schemas/PaginationMeta","description":"Pagination metadata"}},"type":"object","required":["items","pagination"],"title":"PaginatedResponse[EmailContentListItem]"},"PaginatedResponse_KbGapDraftItem_":{"properties":{"items":{"items":{"$ref":"#/components/schemas/KbGapDraftItem"},"type":"array","title":"Items","description":"List of items for this page"},"pagination":{"$ref":"#/components/schemas/PaginationMeta","description":"Pagination metadata"}},"type":"object","required":["items","pagination"],"title":"PaginatedResponse[KbGapDraftItem]"},"PaginatedResponse_KnowledgeSourceResponse_":{"properties":{"items":{"items":{"$ref":"#/components/schemas/KnowledgeSourceResponse"},"type":"array","title":"Items","description":"List of items for this page"},"pagination":{"$ref":"#/components/schemas/PaginationMeta","description":"Pagination metadata"}},"type":"object","required":["items","pagination"],"title":"PaginatedResponse[KnowledgeSourceResponse]"},"PaginatedResponse_SkillListItem_":{"properties":{"items":{"items":{"$ref":"#/components/schemas/SkillListItem"},"type":"array","title":"Items","description":"List of items for this page"},"pagination":{"$ref":"#/components/schemas/PaginationMeta","description":"Pagination metadata"}},"type":"object","required":["items","pagination"],"title":"PaginatedResponse[SkillListItem]"},"PaginatedResponse_SkillVersionListItem_":{"properties":{"items":{"items":{"$ref":"#/components/schemas/SkillVersionListItem"},"type":"array","title":"Items","description":"List of items for this page"},"pagination":{"$ref":"#/components/schemas/PaginationMeta","description":"Pagination metadata"}},"type":"object","required":["items","pagination"],"title":"PaginatedResponse[SkillVersionListItem]"},"PaginatedResponse_SmsSenderResponse_":{"properties":{"items":{"items":{"$ref":"#/components/schemas/SmsSenderResponse"},"type":"array","title":"Items","description":"List of items for this page"},"pagination":{"$ref":"#/components/schemas/PaginationMeta","description":"Pagination metadata"}},"type":"object","required":["items","pagination"],"title":"PaginatedResponse[SmsSenderResponse]"},"PaginationMeta":{"properties":{"total":{"type":"integer","title":"Total","description":"Total number of items matching the query"},"page":{"type":"integer","minimum":1.0,"title":"Page","description":"Current page number (1-indexed)"},"page_size":{"type":"integer","minimum":1.0,"title":"Page Size","description":"Number of items per page"},"total_pages":{"type":"integer","title":"Total Pages","description":"Total number of pages"},"has_next":{"type":"boolean","title":"Has Next","description":"Whether there are more pages"},"has_prev":{"type":"boolean","title":"Has Prev","description":"Whether there are previous pages"},"next_cursor":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Next Cursor","description":"Cursor for next page (if using cursor pagination)"},"prev_cursor":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Prev Cursor","description":"Cursor for previous page"}},"type":"object","required":["total","page","page_size","total_pages","has_next","has_prev"],"title":"PaginationMeta","description":"Pagination metadata for list responses.\n\nSupports both offset-based and page-based pagination.\nClients should use page/page_size for new implementations."},"PendingConfirmation":{"properties":{"token_id":{"type":"string","title":"Token Id","description":"Token ID"},"email":{"type":"string","title":"Email","description":"Email address awaiting confirmation"},"sent_at":{"type":"string","format":"date-time","title":"Sent At","description":"When confirmation email was sent"},"expires_at":{"type":"string","format":"date-time","title":"Expires At","description":"When token expires"},"list_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"List Name","description":"List name if subscribing to specific list"},"category":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Category","description":"Subscription category"},"is_expired":{"type":"boolean","title":"Is Expired","description":"Whether token has expired"}},"type":"object","required":["token_id","email","sent_at","expires_at","is_expired"],"title":"PendingConfirmation","description":"Details of a pending email confirmation"},"PendingConfirmationListResponse":{"properties":{"pending_confirmations":{"items":{"$ref":"#/components/schemas/PendingConfirmation"},"type":"array","title":"Pending Confirmations","description":"List of pending confirmations"},"total":{"type":"integer","title":"Total","description":"Total number of pending confirmations"},"limit":{"type":"integer","title":"Limit","description":"Items per page"},"offset":{"type":"integer","title":"Offset","description":"Current offset"}},"type":"object","required":["pending_confirmations","total","limit","offset"],"title":"PendingConfirmationListResponse","description":"List of pending confirmations with pagination"},"PermissionCheckResponse":{"properties":{"has_all":{"type":"boolean","title":"Has All","description":"True if user has all required scopes"},"results":{"additionalProperties":true,"type":"object","title":"Results","description":"Map of scope to whether user has it"}},"type":"object","required":["has_all","results"],"title":"PermissionCheckResponse","description":"Response from checking if user has required scopes","example":{"has_all":true,"results":{"campaigns:update":true,"email:read":true,"lists:*":true}}},"PostCapabilitiesResponse":{"properties":{"beta_enabled":{"type":"boolean","title":"Beta Enabled","description":"Whether the tenant is enrolled in the `social_comments_beta` flag."},"formats":{"items":{"type":"string"},"type":"array","title":"Formats","description":"The post formats the product exposes, in UI order."},"items":{"items":{"$ref":"#/components/schemas/PostChannelCapability"},"type":"array","title":"Items"}},"type":"object","required":["beta_enabled","formats","items"],"title":"PostCapabilitiesResponse","description":"Publish-capability probe across the tenant's post-capable channels."},"PostChannelCapability":{"properties":{"channel_id":{"type":"string","title":"Channel Id"},"channel_type":{"type":"string","title":"Channel Type"},"channel_name":{"type":"string","title":"Channel Name"},"identifier":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Identifier"},"can_publish":{"type":"boolean","title":"Can Publish","description":"True when this channel type supports at least one post format."},"formats":{"additionalProperties":true,"type":"object","title":"Formats","description":"Per-format support, e.g. `{\"text\": false, \"image\": true, ...}`."},"max_media_items":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Max Media Items","description":"Maximum media items (carousel slides) this channel accepts in one post, when the capability matrix reports it (e.g. Meta's carousel ceiling). `null` when the matrix does not advertise a limit — the adapter still enforces the hard Meta rule. Never hardcoded here; surfaced verbatim from `post_publish.post_capabilities_for_channel`."}},"type":"object","required":["channel_id","channel_type","channel_name","can_publish","formats"],"title":"PostChannelCapability","description":"Whether ONE channel can publish authored posts, and which formats."},"PostInsightsResponse":{"properties":{"post_id":{"type":"string","title":"Post Id"},"retrieved_at":{"type":"string","title":"Retrieved At","description":"When THIS response was assembled (server time)."},"last_refreshed_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Last Refreshed At","description":"The OLDEST per-target `last_refreshed_at` — i.e. the staleness of the stalest number in this response. Null when no target is cached yet."},"targets":{"items":{"$ref":"#/components/schemas/SocialInsightsTarget"},"type":"array","title":"Targets"}},"type":"object","required":["post_id","retrieved_at","targets"],"title":"PostInsightsResponse","description":"Insights for every published target of one authored post.\n\nNumbers are served from a per-target cache and refreshed on demand via\n``POST /{post_id}/insights/refresh``. A target that has never been cached is\npopulated live on its first read."},"PreviewDataRequest":{"properties":{"contact_id":{"anyOf":[{"type":"string","maxLength":36},{"type":"null"}],"title":"Contact Id"},"channel_id":{"anyOf":[{"type":"string","maxLength":36},{"type":"null"}],"title":"Channel Id"},"initial_data":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Initial Data","description":"Per-send overrides (any data key, e.g. __prefill_<name>); win over everything."}},"type":"object","title":"PreviewDataRequest"},"PreviewTemplateRequest":{"properties":{"contact_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Contact Id","description":"Contact ID to use for preview (optional - uses sample data if not provided)"},"variable_values":{"anyOf":[{"additionalProperties":{"type":"string"},"type":"object"},{"type":"null"}],"title":"Variable Values","description":"Override values for preview"}},"type":"object","title":"PreviewTemplateRequest","description":"Request to preview a template with variable substitution"},"PreviewTemplateResponse":{"properties":{"header":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Header"},"body":{"type":"string","title":"Body"},"footer":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Footer"},"buttons":{"anyOf":[{"items":{"$ref":"#/components/schemas/TemplateButton"},"type":"array"},{"type":"null"}],"title":"Buttons"},"variables_used":{"additionalProperties":{"type":"string"},"type":"object","title":"Variables Used","description":"Variables and their resolved values"}},"type":"object","required":["body"],"title":"PreviewTemplateResponse","description":"Response with preview of template after variable substitution"},"PreviousConversationItem":{"properties":{"id":{"type":"string","title":"Id"},"subject":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Subject"},"channel_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Channel Id"},"channel_type":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Channel Type"},"status":{"type":"string","title":"Status","description":"Conversation status value"},"created_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Created At"},"last_message_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Last Message At"},"closed_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Closed At"},"ai_summary":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Ai Summary"},"user_intent":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"User Intent"},"resolution_summary":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Resolution Summary"},"message_count":{"type":"integer","title":"Message Count","default":0},"last_message_preview":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Last Message Preview","description":"Safe one-line preview; never leaks attachment paths"}},"type":"object","required":["id","status"],"title":"PreviousConversationItem","description":"Lightweight summary of a contact's prior conversation.\n\nReturned by GET /conversations/{id}/previous to render collapsed\nhistory cards. Intentionally omits message bodies and any storage\npaths — full messages are lazy-loaded via GET /messages on expand."},"PreviousConversationsResponse":{"properties":{"items":{"items":{"$ref":"#/components/schemas/PreviousConversationItem"},"type":"array","title":"Items","default":[]},"total_previous":{"type":"integer","title":"Total Previous","description":"Count of the contact's prior conversations (excludes current)","default":0},"distinct_channel_count":{"type":"integer","title":"Distinct Channel Count","description":"Count of distinct channels across those prior conversations","default":0},"latest_previous_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Latest Previous At","description":"MAX(last_message_at) across those prior conversations"},"mergeable":{"type":"boolean","title":"Mergeable","description":"True when a valid same-channel merge target exists","default":false},"merge_target_conversation_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Merge Target Conversation Id","description":"The ONLY conversation this one may be merged into: the contact's OVERALL most-recent prior conversation, and ONLY when that immediately-previous conversation is on the SAME channel as the current one. Null when the immediately-previous conversation overall is on a different channel, or when none exists."}},"type":"object","title":"PreviousConversationsResponse","description":"Aggregate summary of a contact's previous conversations.\n\nRedesigned (June 2026) to power a single-line history summary plus the\nreopen-&-merge affordance, instead of a list of heavy per-conversation\ncards. The expensive per-item enrichment (bulk message counts,\nlast-message previews, channel-type hydration) is NO LONGER computed —\n`items` is retained for backwards-compatibility but is returned EMPTY.\nConsumers that want the actual messages should use\n`GET /api/v1/contacts/{contact_id}/messages`.\n\nAll aggregate fields respect the SAME contact-history visibility scoping\nas the merge gating, so a non-admin agent never sees counts/targets for\nchannels they cannot access."},"PrivateReplyButton":{"properties":{"type":{"type":"string","enum":["url","postback"],"title":"Type","description":"`url` opens a link; `postback` sends a value back to you. Defaults to `postback`.","default":"postback"},"title":{"type":"string","maxLength":20,"title":"Title","description":"Button label shown to the recipient (Meta limit: 20 characters)."},"url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Url","description":"Destination for a `url` button. Must be an absolute **https** URL. Required when `type` is `url`, and rejected otherwise."},"action":{"anyOf":[{"type":"string","const":"start_flow"},{"type":"null"}],"title":"Action","description":"What a `postback` button does. `start_flow` starts the flow named by `flow_id` when the recipient taps the button. Omit for a plain opaque `payload`. Implied when `flow_id` is set."},"flow_id":{"anyOf":[{"type":"string","maxLength":36},{"type":"null"}],"title":"Flow Id","description":"The flow to start, when `action` is `start_flow`. Must be a published flow in this workspace. A flow whose trigger is \"Comment received\" cannot be used here — that trigger is reserved for the first message and its private reply has already been spent on the message carrying this button."},"payload":{"anyOf":[{"type":"string","maxLength":1000},{"type":"null"}],"title":"Payload","description":"Opaque value echoed back to you when a `postback` button is tapped. It arrives on the resulting inbound message as `meta.button_id`. Defaults to `title` when omitted. Cannot be combined with `action`/`flow_id`, and cannot start with the reserved `flow_start:` prefix."},"attachment_id":{"anyOf":[{"type":"string","maxLength":64},{"type":"null"}],"title":"Attachment Id","description":"Editor-side hint on a `url` button whose URL is an attachment's public link (\"file button\"). Stored so an editor can re-open the button as a file; never sent to the recipient. The link is not reference-counted — deleting the attachment later breaks it."},"source":{"anyOf":[{"type":"string","maxLength":64},{"type":"null"}],"title":"Source","description":"Editor-side hint naming where a `url` button's link came from (e.g. `attachment`). Stored, never sent to the recipient."}},"type":"object","required":["title"],"title":"PrivateReplyButton","description":"One button in a private reply. Supported on Facebook Messenger and\nInstagram alike (verified on live accounts).\n\nTwo kinds, distinguished by `type`:\n\n* `url` — opens a web page. Requires `url`. Nothing comes back to you.\n* `postback` (the default) — sends a value back to you on tap. Either set\n  `action: \"start_flow\"` + `flow_id` to start one of your flows, or set an\n  opaque `payload` you match yourself."},"PrivateReplyMessageRef":{"properties":{"id":{"type":"string","title":"Id","description":"`messages.id` of the private reply."},"conversation_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Conversation Id","description":"Conversation the reply was filed into — open this to continue the DM."},"text":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Text","description":"What we replied."},"status":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Status","description":"Delivery status of the reply message."},"sent_by_user_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Sent By User Id","description":"The agent who sent it. Null when an automation did (see `source`)."},"source":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Source","description":"How the reply was sent: `api` (this API / the Comments UI), or the value stamped by the rules engine or a flow's Send node."},"created_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Created At","description":"When the reply message row was created."},"sent_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Sent At","description":"When the reply was accepted by Meta."}},"type":"object","required":["id"],"title":"PrivateReplyMessageRef","description":"The outbound private reply that answered a comment.\n\nThis is the **comment → message** half of the reply linkage. It carries\nenough to render a preview next to the comment and to deep-link into the\nconversation, without a second round trip."},"PrivateReplyQuickReply":{"properties":{"content_type":{"type":"string","const":"text","title":"Content Type","description":"Meta's chip kind. Only `text` is supported. Optional — Meta's own documented payload includes it, so it is accepted verbatim.","default":"text"},"title":{"type":"string","maxLength":20,"title":"Title","description":"Chip label (Meta limit: 20 characters)."},"action":{"anyOf":[{"type":"string","const":"start_flow"},{"type":"null"}],"title":"Action","description":"Set to `start_flow` (with `flow_id`) to start a flow when the chip is tapped. Implied when `flow_id` is set. Omit for a plain opaque `payload`."},"flow_id":{"anyOf":[{"type":"string","maxLength":36},{"type":"null"}],"title":"Flow Id","description":"The flow to start, when `action` is `start_flow`. Must be a published flow in this workspace, and must NOT use the \"Comment received\" trigger — that trigger answers the comment itself, and this comment's one allowed private reply is the message carrying this chip."},"payload":{"anyOf":[{"type":"string","maxLength":1000},{"type":"null"}],"title":"Payload","description":"Opaque value echoed back on tap, as `meta.button_id`. Defaults to `title` when omitted. Cannot be combined with `action`/`flow_id`, and cannot start with the reserved `flow_start:` prefix."}},"type":"object","required":["title"],"title":"PrivateReplyQuickReply","description":"One quick reply chip in a private reply. Supported on Facebook\nMessenger and Instagram alike.\n\nMeta allows up to 13 chips and they ride ALONGSIDE the message, so quick\nreplies may be combined with `text`, with `image_url`, or with `buttons` —\na button template is just the attachment the chips accompany.\n\nLike a button, a chip can start one of your flows: set\n`action: \"start_flow\"` + `flow_id`. A tapped chip reaches the same handler\na tapped button does, because Meta reports it as `message.quick_reply` and\nwe normalise that to the same `meta.button_id` field."},"PrivateReplyRequest":{"properties":{"text":{"type":"string","maxLength":1000,"minLength":1,"title":"Text","description":"The message body — or the button template's body when `buttons` is set."},"channel_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Channel Id","description":"The Instagram or Facebook Messenger channel whose post was commented on. Optional when the comment is already stored in your workspace (the stored channel wins); **required** otherwise — for example when you received the comment id from a `comment.received` webhook before it was persisted."},"buttons":{"anyOf":[{"items":{"$ref":"#/components/schemas/PrivateReplyButton"},"type":"array","maxItems":3},{"type":"null"}],"title":"Buttons","description":"Up to 3 buttons, sent as a button template. Each is either a `url` button (opens a link) or a `postback` button (sends a value back to you, and can start a flow). Supported on both Facebook Messenger and Instagram. Cannot be combined with `image_url`."},"quick_replies":{"anyOf":[{"items":{"$ref":"#/components/schemas/PrivateReplyQuickReply"},"type":"array","maxItems":13},{"type":"null"}],"title":"Quick Replies","description":"**DEPRECATED / IGNORED.** Comment private replies are buttons-only since 2026-08-13. This field is still accepted so existing clients do not get a 422, but any chips supplied are silently ignored and never sent to Meta — use `buttons` instead."},"image_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Image Url","description":"**Facebook Messenger only.** Publicly reachable image URL sent as an attachment. Rejected with 400 on Instagram. Cannot be combined with `buttons`."}},"type":"object","required":["text"],"title":"PrivateReplyRequest","description":"Body of `POST /comments/{comment_id}/private-reply`."},"PrivateReplyResponse":{"properties":{"comment_id":{"type":"string","title":"Comment Id","description":"Meta's comment id this reply answered."},"channel_id":{"type":"string","title":"Channel Id","description":"Channel the reply was sent from."},"platform":{"type":"string","title":"Platform","description":"`instagram` or `messenger`."},"status":{"type":"string","title":"Status","description":"Always `sent` — this endpoint sends synchronously.","default":"sent"},"message_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Message Id","description":"Id of the `messages` row created for this reply. Null in the rare case where Meta accepted the reply but the commenter could not be resolved to a contact; the reply was still delivered."},"conversation_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Conversation Id","description":"Conversation the reply was filed into."},"contact_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Contact Id","description":"The commenter, as a contact in your workspace."},"external_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"External Id","description":"Meta's message id for the delivered reply."},"recipient_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Recipient Id","description":"The commenter's platform-scoped id (IGSID / PSID), as returned by Meta. For a comment you had not stored, this is the first time it becomes known."},"social_comment_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Social Comment Id","description":"Id of the stored comment record, when the comment was persisted in your workspace."},"state":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"State","description":"The stored comment's new reply state (`replied`), when persisted."},"sent_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Sent At","description":"When the reply was sent."}},"type":"object","required":["comment_id","channel_id","platform"],"title":"PrivateReplyResponse","description":"The sent private reply."},"ProfilePhotoUploadResponse":{"properties":{"success":{"type":"boolean","title":"Success"},"message":{"type":"string","title":"Message"},"profile_picture_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Profile Picture Url"}},"type":"object","required":["success","message"],"title":"ProfilePhotoUploadResponse","description":"Response for profile photo upload"},"ProposedChange":{"properties":{"action":{"type":"string","title":"Action","description":"update_entry | delete_entry | create_entry | flag_document | append_bot_rule"},"target_type":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Target Type","description":"faq_item | document | bot"},"target_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Target Id"},"current_text":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Current Text"},"proposed_text":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Proposed Text"},"rationale":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Rationale"}},"type":"object","required":["action"],"title":"ProposedChange"},"ProviderType":{"type":"string","enum":["sendgrid","sendgrid_managed","mailgun","ses"],"title":"ProviderType","description":"Email provider types"},"PublicReplyRequest":{"properties":{"text":{"type":"string","maxLength":2200,"minLength":1,"title":"Text","description":"The public reply body, posted under the comment on the post (text only in v1 — no buttons, chips or media). Instagram/Facebook comment limit is 2200 characters."},"channel_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Channel Id","description":"The Instagram or Facebook Messenger channel whose post was commented on. Optional when the comment is already stored in your workspace (the stored channel wins); **required** otherwise — for example when you received the comment id from a `comment.received` webhook before it was persisted."}},"type":"object","required":["text"],"title":"PublicReplyRequest","description":"Body of ``POST /comments/{comment_id}/public-reply``."},"PublicReplyResponse":{"properties":{"comment_id":{"type":"string","title":"Comment Id","description":"Meta's comment id this reply answered."},"channel_id":{"type":"string","title":"Channel Id","description":"Channel the reply was posted from."},"platform":{"type":"string","title":"Platform","description":"`instagram` or `messenger`."},"status":{"type":"string","title":"Status","description":"Always `sent` — this endpoint posts synchronously.","default":"sent"},"public_reply_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Public Reply Id","description":"Meta's id for the reply comment we created, when Meta returned it."},"social_comment_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Social Comment Id","description":"Id of the stored comment record, when the comment was persisted in your workspace."},"public_reply_text":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Public Reply Text","description":"The text that was posted."},"public_replied_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Public Replied At","description":"When the public reply was posted."}},"type":"object","required":["comment_id","channel_id","platform"],"title":"PublicReplyResponse","description":"The posted public reply. Deliberately smaller than\n:class:`PrivateReplyResponse` — a public reply has no message, conversation\nor contact (comment-layer only)."},"PushRequest":{"properties":{"channel_ids":{"items":{"type":"string"},"type":"array","maxItems":50,"minItems":1,"title":"Channel Ids"},"version_id":{"anyOf":[{"type":"string","maxLength":36},{"type":"null"}],"title":"Version Id"}},"type":"object","required":["channel_ids"],"title":"PushRequest"},"QuickRepliesCapability":{"properties":{"supported":{"type":"boolean","title":"Supported","default":true},"max_quick_replies":{"type":"integer","title":"Max Quick Replies"}},"type":"object","required":["max_quick_replies"],"title":"QuickRepliesCapability","description":"Quick replies capability details"},"QuickReplyButton":{"properties":{"index":{"type":"integer","title":"Index","description":"0-based position among QUICK_REPLY buttons (0..2)"},"caption":{"type":"string","title":"Caption","description":"Button caption (the template's button text)"}},"type":"object","required":["index","caption"],"title":"QuickReplyButton","description":"Derived, read-only QUICK_REPLY button for the Flow Builder\n``send_template`` node.\n\nComputed server-side from the stored ``buttons`` JSON via\n``WhatsAppTemplateService.derive_quick_reply_buttons`` — filtered to\n``QUICK_REPLY`` type only, capped at 3, with ``index`` = 0-based position\nAMONG QUICK_REPLY buttons. The ``send_template`` node auto-branches on\nthese; its outgoing edges are labelled ``tplbtn_{index}``."},"QuotedMessage":{"properties":{"id":{"type":"string","title":"Id","description":"Message UUID of the quoted message"},"text":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Text","description":"Text preview (truncated to 150 chars)"},"message_type":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Message Type","description":"Type of the quoted message (text, image, etc.)"},"direction":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Direction","description":"Direction of the quoted message"},"sender_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Sender Name","description":"Name of the person who sent the quoted message"},"created_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Created At","description":"When the quoted message was sent"}},"type":"object","required":["id"],"title":"QuotedMessage","description":"Summary of a quoted (replied-to) message shown as reply context.\n\nDisplayed as a preview bubble above the message content, similar to\nhow WhatsApp/Telegram show quoted messages."},"RcsCapabilityResponse":{"properties":{"msisdn":{"type":"string","title":"Msisdn","description":"The probed number, normalised to E.164"},"reachable":{"type":"boolean","title":"Reachable"},"features":{"items":{"type":"string"},"type":"array","title":"Features","description":"RBM feature strings, when Google returned any"},"carrier":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Carrier"},"cached":{"type":"boolean","title":"Cached","description":"True when served from the Redis capability cache","default":false},"reason":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Reason","description":"Coarse cause when unreachable: 'not_rcs_reachable' (Google's 404 — cause unknowable) or 'probe_failed' (we could not complete the call)."},"checked_at":{"type":"string","title":"Checked At","description":"When this answer was produced"},"tester_status":{"anyOf":[{"type":"string","enum":["pending","accepted","rejected","unknown"]},{"type":"null"}],"title":"Tester Status","description":"Allowlist status of this MSISDN after the probe, when it is one of our tester devices. A reachable probe promotes 'pending' to 'accepted' — the only acceptance signal Google gives us."}},"type":"object","required":["msisdn","reachable","checked_at"],"title":"RcsCapabilityResponse","description":"Per-recipient reachability for THIS agent.\n\n``reachable: false`` is an ANSWER, not an error. RBM returns 404 for every\nkind of unreachable — no RCS on the device, RCS switched off, a carrier the\nagent is not launched on, or a tester who has not accepted the invite yet —\nand it cannot tell them apart. So the endpoint returns 200 with\n``reachable: false`` and a coarse ``reason``; inventing a specific cause\nwould be a guess presented as a fact."},"RcsContent":{"properties":{"channel_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Channel Id"},"message_type":{"type":"string","title":"Message Type","default":"text"},"message_text":{"type":"string","title":"Message Text"},"attachment_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Attachment Id"},"suggestions":{"anyOf":[{"items":{"additionalProperties":true,"type":"object"},"type":"array"},{"type":"null"}],"title":"Suggestions"},"rich_card":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Rich Card"},"carousel":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Carousel"}},"type":"object","required":["message_text"],"title":"RcsContent","description":"RCS for Business (Google RBM) campaign content.\n\nNote what is NOT here: RBM has no template approval and no 24-hour\nre-engagement window, so none of the WhatsApp template fields transfer —\nthere is no template id/name/language/category and no variable_mapping.\nContent is composed freely.\n\n``message_traffic_type`` is also deliberately absent. It is a German carrier\n/regulatory declaration about the nature of the message (UWG §7), and the\ncampaigns worker always declares ``PROMOTION`` for Reach traffic; a\nper-campaign override would let marketing be declared as transactional.\n\nBounds are enforced by ``RcsAdapter`` rather than repeated here, so this\nschema cannot drift from the platform: text is auto-split at 3072 chars, the\nwhole AgentMessage is capped at 250 KB, suggestions at 11 per message but 4\nper card with 25-char labels and 2048-char postbackData, and carousels must\nhold 2–10 cards."},"ReactionCreate":{"properties":{"emoji":{"type":"string","maxLength":10,"minLength":1,"title":"Emoji","description":"Emoji to react with (e.g., thumbs up, heart)"}},"type":"object","required":["emoji"],"title":"ReactionCreate","description":"Schema for adding a reaction to a message","example":{"emoji":"thumbsup"}},"ReactionResponse":{"properties":{"success":{"type":"boolean","title":"Success","description":"Whether the operation succeeded"},"message_id":{"type":"string","title":"Message Id","description":"Message UUID that was reacted to"},"external_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"External Id","description":"Platform reaction message ID"},"emoji":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Emoji","description":"Emoji that was added (null for removal)"},"error":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Error","description":"Error message if operation failed"}},"type":"object","required":["success","message_id"],"title":"ReactionResponse","description":"Response after adding/removing a reaction","example":{"emoji":"thumbsup","external_id":"wamid.HBgNNDkxNTIxNTU4MDg2NRUCABIYFjNFQjBCNUYxMjdFRkI5","message_id":"f47ac10b-58cc-4372-a567-0e02b2c3d479","success":true}},"RecentCampaign":{"properties":{"name":{"type":"string","title":"Name","description":"Campaign name"},"sent_count":{"type":"integer","title":"Sent Count","description":"Total messages sent","default":0},"delivered_count":{"type":"integer","title":"Delivered Count","description":"Total messages delivered","default":0},"completed_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Completed At","description":"Completion timestamp (ISO format)"}},"type":"object","required":["name"],"title":"RecentCampaign","description":"Summary of a recently completed campaign"},"RecentLinkActivity":{"properties":{"short_code":{"type":"string","title":"Short Code","description":"Short code for the link"},"original_url":{"type":"string","title":"Original Url","description":"Original destination URL"},"clicks":{"type":"integer","title":"Clicks","description":"Total click count"},"label":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Label","description":"Link label/name"},"last_click_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Last Click At","description":"Timestamp of last click (ISO format)"}},"type":"object","required":["short_code","original_url","clicks"],"title":"RecentLinkActivity","description":"Recent link activity entry"},"RecipientCountRequest":{"properties":{"all_contacts":{"type":"boolean","title":"All Contacts","description":"Target all contacts with email addresses","default":false},"list_ids":{"anyOf":[{"items":{"type":"string"},"type":"array","maxItems":100},{"type":"null"}],"title":"List Ids","description":"Target specific lists (max 100)","default":[]},"audience_conditions":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Audience Conditions","description":"Audience Builder condition tree (same format as a dynamic list's `segment_conditions`). Must be a top-level `AND` whose children include at least one positive list membership: a `has_optin` condition (`operator: equals`, `value: true`, `list_id` of a static or newsletter list of this workspace) or a group of only such conditions (`OR` = any of the lists, `AND` = all of them). Other children narrow the audience down or exclude contacts (e.g. `has_optin` with `value: false`). When set, the campaign's lists are DERIVED from the membership lists and any `list_ids` sent are ignored. Errors: 422 `audience_list_membership_required`, `audience_membership_list_invalid`, or a validation error for a malformed tree. A tree with no conditions counts as null. Takes precedence over all_contacts / list_ids / subscription_categories."},"subscription_categories":{"anyOf":[{"items":{"type":"string"},"type":"array","maxItems":100},{"type":"null"}],"title":"Subscription Categories","description":"Filter by subscription categories (max 100)","default":[]},"email_provider_config_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Email Provider Config Id","description":"Email provider the draft will send with (enables the cost quote)"},"email_integration_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Email Integration Id","description":"SES email integration the draft will send with (enables the cost quote)"}},"type":"object","title":"RecipientCountRequest","description":"Audience + (optional) sending method of an UNSAVED campaign draft.\n\nSend ``email_provider_config_id`` OR ``email_integration_id`` (same\nfields, same either/or rule as ``POST /email-campaigns``) to also get the\nreal price in ``cost``; send neither for counts only.","example":{"all_contacts":false,"email_provider_config_id":"epc-123","list_ids":["list-789","list-456"],"subscription_categories":[]}},"RecipientCountResponse":{"properties":{"total":{"type":"integer","title":"Total","description":"Contacts matching the audience before exclusions"},"subscribed":{"type":"integer","title":"Subscribed","description":"Valid recipients (what the send delivers to)"},"unsubscribed":{"type":"integer","title":"Unsubscribed","description":"Excluded: opted out / complained"},"bounced":{"type":"integer","title":"Bounced","description":"Excluded: hard-bounced, invalid or suppressed address"},"recipient_count":{"type":"integer","title":"Recipient Count","description":"Same as subscribed","default":0},"duplicate_email_skipped_count":{"type":"integer","title":"Duplicate Email Skipped Count","description":"Excluded: another (older) contact has the same address","default":0},"no_email_count":{"type":"integer","title":"No Email Count","description":"Excluded: no usable email address","default":0},"list_breakdown":{"additionalProperties":{"type":"integer"},"type":"object","title":"List Breakdown","description":"Subscribed members per target list id (before exclusions)"},"subscription_breakdown":{"additionalProperties":{"type":"integer"},"type":"object","title":"Subscription Breakdown","description":"Subscribed members per subscription category (before exclusions)"},"cost":{"anyOf":[{"$ref":"#/components/schemas/EmailCampaignCostQuote"},{"type":"null"}],"description":"Price quote; present when a sending method was given"}},"type":"object","required":["total","subscribed","unsubscribed","bounced"],"title":"RecipientCountResponse","description":"Audience preview (and optional price) for an unsaved campaign.\n\n``subscribed`` == ``recipient_count`` == the number of emails the send\nwill actually go to. ``total = subscribed + unsubscribed + bounced +\nduplicate_email_skipped_count + no_email_count``.","example":{"bounced":18,"cost":{"cost_category":"email_campaign_managed","cost_per_email":0.8,"plan_included_messages":1000,"recipient_count":950,"sending_type":"managed","total_cost":760,"total_cost_eur":"7.60","total_cost_eur_precise":"7.600000","unit_price_eur":"0.008000"},"duplicate_email_skipped_count":2,"list_breakdown":{"list-456":420,"list-789":600},"no_email_count":0,"recipient_count":950,"subscribed":950,"subscription_breakdown":{},"total":1000,"unsubscribed":30}},"RecipientUnsubscribeResponse":{"properties":{"success":{"type":"boolean","title":"Success","default":true},"unsubscribed_lists":{"items":{"$ref":"#/components/schemas/UnsubscribedListItem"},"type":"array","title":"Unsubscribed Lists","description":"Lists the contact was opted out of by this action (deduped by list_id)"},"already_unsubscribed":{"type":"boolean","title":"Already Unsubscribed","description":"True when the contact was already opted out of all of the campaign's lists","default":false}},"type":"object","title":"RecipientUnsubscribeResponse","description":"Response for POST /{campaign_id}/recipients/{recipient_id}/unsubscribe.\n\n- ``unsubscribed_lists`` is the set of the campaign's lists the contact was\n  actively subscribed to and has now been opted out of (deduped by list;\n  empty when the campaign targets no lists, or the contact was subscribed to\n  none of them).\n- ``already_unsubscribed`` is True only when the contact HAD subscription\n  rows for the campaign's lists but every one was already opted out (nothing\n  to flip). It stays False when there were simply no originating lists."},"RecrawlRequest":{"properties":{"history_id":{"type":"string","title":"History Id","description":"ID of the crawl history record to use for re-crawling"},"mode":{"type":"string","enum":["existing_pages","full_rescan"],"title":"Mode","description":"Recrawl mode. Defaults to 'full_rescan', which re-discovers pages from scratch via sitemap/BFS and picks up newly added pages. 'existing_pages' re-crawls only the URLs stored from the previous crawl.","default":"full_rescan"},"title_prefix":{"anyOf":[{"type":"string","maxLength":100},{"type":"null"}],"title":"Title Prefix","description":"Optional prefix for document titles"},"page_limit":{"anyOf":[{"type":"integer","maximum":20000.0,"minimum":1.0},{"type":"null"}],"title":"Page Limit","description":"Maximum pages to crawl. Must not exceed your workspace crawl limit (2000 pages unless raised for your workspace; see GET /knowledge-base/crawl/limits); larger values are rejected with 422 `crawl_page_limit_exceeded`. If omitted, the default page limit applies (capped at your workspace limit)."},"clear_existing":{"type":"boolean","title":"Clear Existing","description":"Hard reset. When True, the existing documents for this domain (and their RAG corpus files) are deleted BEFORE the re-crawl runs, then pages are re-discovered and re-imported from scratch. When False (default), the re-crawl is non-destructive: existing documents are updated in-place and orphaned documents are cleaned up only after a successful crawl (crawl-then-reconcile).","default":false}},"type":"object","required":["history_id"],"title":"RecrawlRequest","description":"Request model for re-crawling a previously crawled domain.\n\nSupports two modes:\n- \"existing_pages\": Re-crawl the exact URLs from previous crawl history (faster, predictable)\n- \"full_rescan\": Re-discover pages via sitemap/BFS from scratch (finds new pages, more thorough)\n\nBoth modes use non-destructive crawl-then-reconcile: existing documents are updated in-place,\nnew documents are created, and orphaned documents (not touched by this crawl) are removed\nonly after the crawl completes successfully.","example":{"clear_existing":false,"history_id":"hist-uuid-123","mode":"full_rescan","page_limit":200,"title_prefix":"Docs"}},"RefineRequest":{"properties":{"builder_doc":{"additionalProperties":true,"type":"object","title":"Builder Doc"},"instruction":{"type":"string","maxLength":2000,"minLength":3,"title":"Instruction"},"target":{"$ref":"#/components/schemas/RefineTarget"},"language":{"anyOf":[{"type":"string","maxLength":16},{"type":"null"}],"title":"Language"},"channel_id":{"anyOf":[{"type":"string","maxLength":64},{"type":"null"}],"title":"Channel Id"},"request_id":{"anyOf":[{"type":"string","maxLength":64,"minLength":8,"pattern":"^[A-Za-z0-9_.:-]+$"},{"type":"null"}],"title":"Request Id","description":"Client idempotency key; a retry with the same key returns the same job"}},"type":"object","required":["builder_doc","instruction","target"],"title":"RefineRequest"},"RefineTarget":{"properties":{"screen_id":{"type":"string","maxLength":100,"title":"Screen Id"},"component_index":{"anyOf":[{"type":"integer","maximum":200.0,"minimum":0.0},{"type":"null"}],"title":"Component Index","description":"Omit to regenerate the whole screen"}},"type":"object","required":["screen_id"],"title":"RefineTarget"},"RefreshDocumentResponse":{"properties":{"success":{"type":"boolean","title":"Success","description":"Whether refresh succeeded"},"message":{"type":"string","title":"Message","description":"Status message"},"document_id":{"type":"string","title":"Document Id","description":"ID of refreshed document"},"source_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Source Url","description":"Source URL that was refreshed"}},"type":"object","required":["success","message","document_id"],"title":"RefreshDocumentResponse","description":"Response for single document refresh.","example":{"document_id":"doc-uuid-123","message":"Document refreshed successfully","source_url":"https://docs.example.com/getting-started","success":true}},"RefreshDomainsResponse":{"properties":{"success":{"type":"boolean","title":"Success","description":"Whether domain refresh succeeded"},"verified_domains":{"items":{"type":"string"},"type":"array","title":"Verified Domains","description":"List of verified domains"},"domain_count":{"type":"integer","title":"Domain Count","description":"Number of verified domains"},"updated_at":{"type":"string","title":"Updated At","description":"ISO8601 timestamp of update"}},"type":"object","required":["success","domain_count","updated_at"],"title":"RefreshDomainsResponse","description":"Response from refreshing verified domains","example":{"domain_count":2,"success":true,"updated_at":"2024-01-15T10:30:00Z","verified_domains":["example.com","mail.example.com"]}},"RemoveFailedRecipientsResponse":{"properties":{"success":{"type":"boolean","title":"Success","default":true},"removed_count":{"type":"integer","title":"Removed Count","description":"Number of failed-group recipients removed"}},"type":"object","required":["removed_count"],"title":"RemoveFailedRecipientsResponse","description":"Response for POST /{campaign_id}/recipients/remove-failed."},"RemoveSuppressionResponse":{"properties":{"success":{"type":"boolean","title":"Success"},"message":{"type":"string","title":"Message"}},"type":"object","required":["success","message"],"title":"RemoveSuppressionResponse","description":"Response for removing email from suppression"},"RemoveTagsAction":{"properties":{"type":{"type":"string","const":"remove_tags","title":"Type"},"tags":{"items":{"type":"string"},"type":"array","minItems":1,"title":"Tags","description":"Tag IDs to remove (tenant-owned)"}},"type":"object","required":["type","tags"],"title":"RemoveTagsAction"},"ReorderFlowEntry":{"properties":{"flow_id":{"type":"string","minLength":1,"title":"Flow Id","description":"Flow id to reposition."},"priority":{"type":"integer","minimum":0.0,"title":"Priority","description":"New priority (LOWER = higher priority, evaluated first)."}},"type":"object","required":["flow_id","priority"],"title":"ReorderFlowEntry","description":"A single ``flow_id -> priority`` assignment in a reorder request."},"ReorderFlowsRequest":{"properties":{"flow_ids":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Flow Ids","description":"Ordered flow ids; server assigns priority = position (0-based)."},"items":{"anyOf":[{"items":{"$ref":"#/components/schemas/ReorderFlowEntry"},"type":"array"},{"type":"null"}],"title":"Items","description":"Explicit per-flow priority assignments."}},"type":"object","title":"ReorderFlowsRequest","description":"``PATCH /flows/reorder`` request body.\n\nTwo accepted shapes (pick one):\n\n* ``items``: explicit ``[{flow_id, priority}, ...]`` assignments. Use this\n  when the UI wants full control over the numeric priorities.\n* ``flow_ids``: an ordered list of flow ids. The server assigns\n  ``priority = index`` (0-based) so the first id becomes the highest\n  priority. This is the common drag-and-drop case.\n\nExactly one of the two must be supplied and non-empty. ``flow_id`` values\nmust be unique within the request."},"ReorderFlowsResponse":{"properties":{"items":{"items":{"$ref":"#/components/schemas/ReorderedFlowEntry"},"type":"array","title":"Items"}},"type":"object","required":["items"],"title":"ReorderFlowsResponse","description":"``PATCH /flows/reorder`` — the updated ordering (priority ASC)."},"ReorderedFlowEntry":{"properties":{"flow_id":{"type":"string","title":"Flow Id"},"name":{"type":"string","title":"Name"},"priority":{"type":"integer","title":"Priority"}},"type":"object","required":["flow_id","name","priority"],"title":"ReorderedFlowEntry","description":"One row in the reorder response — the persisted ordering."},"ReplayContact":{"properties":{"id":{"type":"string","title":"Id"},"name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Name"},"avatar_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Avatar Url"}},"type":"object","required":["id"],"title":"ReplayContact"},"ReplayEvent":{"properties":{"type":{"type":"string","title":"Type"},"ts":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Ts"},"node_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Node Id"},"node_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Node Name","description":"Human-readable label of the executed node, resolved from the run's snapshotted definition (``context.definition_snapshot``) so a post-run rename doesn't change historical replays. Falls back to ``node_id`` when the snapshot lacks the node (e.g. it was added after this run). Emitted alongside ``node_type`` so the observability tab identifies WHICH node ran, not just its type."},"node_type":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Node Type"},"summary":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Summary"},"status":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Status"},"channel":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Channel"},"body":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Body"},"message_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Message Id"}},"type":"object","required":["type"],"title":"ReplayEvent","description":"One event in the replay timeline.\n\nThe schema is loose on purpose — `node_executed` carries\n``node_id`` / ``node_name`` / ``node_type`` / ``summary``, while\n``message_sent`` / ``message_received`` carry ``channel`` / ``body`` /\n``message_id``."},"ReplayResponse":{"properties":{"contact":{"$ref":"#/components/schemas/ReplayContact"},"run":{"$ref":"#/components/schemas/ReplayRun"},"events":{"items":{"$ref":"#/components/schemas/ReplayEvent"},"type":"array","title":"Events"}},"type":"object","required":["contact","run","events"],"title":"ReplayResponse","description":"Body of ``GET /flows/{id}/runs/{run_id}/replay``."},"ReplayRun":{"properties":{"id":{"type":"string","title":"Id"},"flow_id":{"type":"string","title":"Flow Id"},"started_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Started At"},"completed_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Completed At"},"status":{"type":"string","title":"Status"},"current_node_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Current Node Id"}},"type":"object","required":["id","flow_id","status"],"title":"ReplayRun"},"RequiredFieldConfig":{"properties":{"name":{"type":"string","title":"Name","description":"Field identifier (e.g., 'email', 'customer_id')"},"prompt":{"type":"string","title":"Prompt","description":"Question to ask the user"},"required":{"type":"boolean","title":"Required","description":"Whether field must be collected","default":true},"type":{"type":"string","title":"Type","description":"Field type: text, email, phone, number","default":"text"},"custom_field_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Custom Field Id","description":"Link to custom field definition"}},"type":"object","required":["name","prompt"],"title":"RequiredFieldConfig","description":"Configuration for a field the bot should collect"},"ResolutionTimeMetricsResponse":{"properties":{"total_closed_conversations":{"type":"integer","title":"Total Closed Conversations","description":"Total number of closed conversations in the period"},"average_resolution_hours":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Average Resolution Hours","description":"Average time to resolution in hours"},"by_channel":{"items":{"$ref":"#/components/schemas/ChannelResolutionTime"},"type":"array","title":"By Channel","description":"Breakdown by channel type"},"period_start":{"type":"string","title":"Period Start","description":"Start of the reporting period (ISO format)"},"period_end":{"type":"string","title":"Period End","description":"End of the reporting period (ISO format)"}},"type":"object","required":["total_closed_conversations","period_start","period_end"],"title":"ResolutionTimeMetricsResponse","description":"Resolution time metrics (time from conversation start to close).\n\nReturned by GET /api/v1/analytics/resolution-time","example":{"average_resolution_hours":2.5,"by_channel":[{"avg_hours":2.1,"channel_type":"whatsapp","count":60},{"avg_hours":3.2,"channel_type":"email","count":45}],"period_end":"2026-01-18T00:00:00Z","period_start":"2026-01-01T00:00:00Z","total_closed_conversations":105}},"ResponseTimeMetricsResponse":{"properties":{"conversations_with_response":{"type":"integer","title":"Conversations With Response","description":"Number of conversations that received a response"},"conversations_without_response":{"type":"integer","title":"Conversations Without Response","description":"Number of conversations that did not receive a response","default":0},"total_conversations":{"type":"integer","title":"Total Conversations","description":"Total conversations in the period"},"response_rate_percent":{"type":"number","title":"Response Rate Percent","description":"Percentage of conversations with at least one response"},"average_response_time_minutes":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Average Response Time Minutes","description":"Average time to first response in minutes"},"by_channel":{"items":{"$ref":"#/components/schemas/ChannelResponseTime"},"type":"array","title":"By Channel","description":"Breakdown by channel type"},"period_start":{"type":"string","title":"Period Start","description":"Start of the reporting period (ISO format)"},"period_end":{"type":"string","title":"Period End","description":"End of the reporting period (ISO format)"}},"type":"object","required":["conversations_with_response","total_conversations","response_rate_percent","period_start","period_end"],"title":"ResponseTimeMetricsResponse","description":"Response time metrics for the tenant.\n\nReturned by GET /api/v1/analytics/response-time","example":{"average_response_time_minutes":5.5,"by_channel":[{"avg_response_minutes":4.2,"channel_type":"whatsapp","conversation_count":80},{"avg_response_minutes":7.1,"channel_type":"telegram","conversation_count":40}],"conversations_with_response":120,"conversations_without_response":30,"period_end":"2026-01-18T00:00:00Z","period_start":"2026-01-01T00:00:00Z","response_rate_percent":80.0,"total_conversations":150}},"ResumeCampaignRequest":{"properties":{"acknowledge_warmup_warning":{"type":"boolean","title":"Acknowledge Warmup Warning","default":false}},"type":"object","title":"ResumeCampaignRequest","description":"Optional body for ``POST /campaigns/{id}/resume`` (no body = false).\n\nRequired (true) to resume a campaign the scheduler paused with\n``throttle_reason = \"warmup_acknowledgement_required\"`` (or one resumed\nbefore its recipients were prepared) while the WhatsApp number is still\nwarming up -- otherwise 409 ``warmup_acknowledgement_required``."},"RetentionPolicyResponse":{"properties":{"tenant_id":{"type":"string","title":"Tenant Id"},"message_retention_days":{"type":"integer","title":"Message Retention Days"},"floor":{"type":"integer","title":"Floor"},"ceiling":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Ceiling"},"is_api_only":{"type":"boolean","title":"Is Api Only"},"is_configurable":{"type":"boolean","title":"Is Configurable"},"allowed_options":{"items":{"type":"integer"},"type":"array","title":"Allowed Options"},"effective_days":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Effective Days"},"contact_retention_days":{"type":"integer","title":"Contact Retention Days","default":0},"contact_effective_days":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Contact Effective Days"},"contact_floor":{"type":"integer","title":"Contact Floor","default":90},"contact_max":{"type":"integer","title":"Contact Max","default":3650},"contact_is_configurable":{"type":"boolean","title":"Contact Is Configurable","default":true},"contact_retention_pending_days":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Contact Retention Pending Days"},"contact_retention_pending_effective_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Contact Retention Pending Effective At"},"contact_retention_grace_days":{"type":"integer","title":"Contact Retention Grace Days","default":7},"can_tighten_contact_retention":{"type":"boolean","title":"Can Tighten Contact Retention","default":false},"message_retention_pending_days":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Message Retention Pending Days"},"message_retention_pending_effective_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Message Retention Pending Effective At"},"message_retention_grace_days":{"type":"integer","title":"Message Retention Grace Days","default":7},"can_tighten_message_retention":{"type":"boolean","title":"Can Tighten Message Retention","default":false}},"type":"object","required":["tenant_id","message_retention_days","floor","ceiling","is_api_only","is_configurable","allowed_options","effective_days"],"title":"RetentionPolicyResponse","description":"Current retention policy + values for the caller's tenant.\n\nDrives the settings UI without the frontend re-deriving product rules.\n``allowed_options`` is the ordered list of message presets valid on the\nplan (``0`` = Never): API Only ``[30, 60, 90]`` (an uncapped API Only plan:\n``[30, 60]`` + the unlimited list); unlimited plans\n``[90, 180, 365, 730, 1095, 1825, 3650, 0]``; a finite cap > 90 the ladder\nvalues below the cap plus the cap. ``floor`` is the plan's message floor\n(30 on API Only, else 90). The frontend maps each integer to a label.\n\nThe ``message_*``/``floor``/``ceiling``/``allowed_options``/``effective_days``\nfields describe message retention (plan-capped). The ``contact_*`` fields\ndescribe inactive-contact deletion (not plan-capped, 0 = never)."},"RetentionUpdate":{"properties":{"message_retention_days":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Message Retention Days"},"contact_retention_days":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Contact Retention Days"}},"type":"object","title":"RetentionUpdate","description":"Set the tenant's retention periods (days). Both optional, at least one.\n\n``message_retention_days``: 0 = unlimited/plan default; otherwise must satisfy\nthe plan policy. ``contact_retention_days``: 0 = never delete inactive\ncontacts; otherwise 90..3650."},"RetryFailedRequest":{"properties":{"error_codes":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Error Codes","description":"Only retry recipients that failed with one of these codes: insufficient_sms_balance, sms_daily_limit_reached, sms_duplicate_send, sms_not_delivered, sms_not_provisioned, sms_rate_limited, sms_rejected_by_carrier. Omit or leave empty to retry every failed recipient.","examples":[["insufficient_sms_balance"],["sms_rate_limited"],["sms_rejected_by_carrier","sms_not_delivered"]]}},"type":"object","title":"RetryFailedRequest","description":"Body for ``POST /campaigns/{id}/retry-failed`` (optional).\n\n``error_codes`` filters the FAILED recipients by their recorded\n``last_error_code`` (e.g. ``[\"insufficient_sms_balance\"]`` after an SMS\ntop-up). Omit / null / empty → retry every failed recipient."},"RetryFailedResponse":{"properties":{"success":{"type":"boolean","title":"Success","description":"Whether the operation succeeded","default":true},"message":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Message","description":"Optional success message"},"retried_count":{"type":"integer","title":"Retried Count","description":"Number of failed documents re-queued for ingestion."}},"type":"object","required":["retried_count"],"title":"RetryFailedResponse","description":"Response for bulk retry of failed documents in a folder.","example":{"retried_count":7,"success":true}},"RetryStrategy":{"type":"string","enum":["exponential","linear","none"],"title":"RetryStrategy","description":"Retry strategies for webhook delivery"},"RoleAssignmentInfo":{"properties":{"role_id":{"type":"string","title":"Role Id","description":"RBAC role ID"},"role_name":{"type":"string","title":"Role Name","description":"RBAC role name"},"assigned_by_user_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Assigned By User Id","description":"User who assigned this role"},"assigned_at":{"type":"string","format":"date-time","title":"Assigned At","description":"When the role was assigned"},"expires_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Expires At","description":"When the role expires (if temporary)"}},"type":"object","required":["role_id","role_name","assigned_at"],"title":"RoleAssignmentInfo","description":"Information about a user's role assignment in a tenant"},"RoleAssignmentResponse":{"properties":{"message":{"type":"string","title":"Message","description":"Success message"},"assignment_id":{"type":"string","title":"Assignment Id","description":"ID of the created role assignment"}},"type":"object","required":["message","assignment_id"],"title":"RoleAssignmentResponse","description":"Response from assigning a role to a user","example":{"assignment_id":"uuid-of-assignment","message":"Role 'Admin' assigned to user"}},"RoleCreate":{"properties":{"name":{"type":"string","maxLength":100,"minLength":3,"title":"Name","description":"Role name"},"description":{"anyOf":[{"type":"string","maxLength":500},{"type":"null"}],"title":"Description","description":"Role description"},"scopes":{"items":{"type":"string"},"type":"array","title":"Scopes","description":"List of scopes to grant to this role"},"icon":{"anyOf":[{"type":"string","maxLength":50},{"type":"null"}],"title":"Icon","description":"Lucide icon name (e.g., 'crown', 'shield-check')","default":"shield"}},"type":"object","required":["name"],"title":"RoleCreate","description":"Schema for creating a new custom role.\n\nSystem roles (OWNER, ADMIN, etc.) cannot be created via API.","example":{"description":"Can manage all marketing campaigns and email templates","name":"Marketing Manager","scopes":["campaigns:*","email:*","lists:read","contacts:read","analytics:read"]}},"RoleListResponse":{"properties":{"items":{"items":{"$ref":"#/components/schemas/RoleResponse"},"type":"array","title":"Items","description":"List of items for this page"},"pagination":{"$ref":"#/components/schemas/PaginationMeta","description":"Pagination metadata"}},"type":"object","required":["items","pagination"],"title":"RoleListResponse","description":"Paginated list of roles.\n\nExample response:\n```json\n{\n    \"items\": [...],\n    \"pagination\": {\n        \"total\": 15,\n        \"page\": 1,\n        \"page_size\": 20,\n        \"total_pages\": 1,\n        \"has_next\": false,\n        \"has_prev\": false\n    }\n}\n```"},"RoleResponse":{"properties":{"created_at":{"type":"string","title":"Created At","description":"When the resource was created (UTC)"},"updated_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Updated At","description":"When the resource was last updated (UTC)"},"id":{"type":"string","title":"Id","description":"Unique identifier (UUID)"},"tenant_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Tenant Id","description":"Tenant ID (None for system roles)"},"name":{"type":"string","title":"Name","description":"Role name"},"description":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Description","description":"Role description"},"is_system":{"type":"boolean","title":"Is System","description":"Whether this is a system-defined role"},"scopes":{"items":{"type":"string"},"type":"array","title":"Scopes","description":"Scopes granted to this role"},"icon":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Icon","description":"Lucide icon name (e.g., 'crown', 'shield-check')"}},"type":"object","required":["created_at","id","name","is_system"],"title":"RoleResponse","description":"Full role response with all fields.\n\nNote: tenant_id is None for system roles (OWNER, ADMIN, etc.)","example":{"created_at":"2025-01-15T10:30:00Z","description":"Can manage all marketing campaigns and email templates","icon":"megaphone","id":"r1a2b3c4-d5e6-f7g8-h9i0-j1k2l3m4n5o6","is_system":false,"name":"Marketing Manager","scopes":["campaigns:*","email:*","lists:read"],"tenant_id":"tenant-123","updated_at":"2025-01-15T10:30:00Z"}},"RoleUpdate":{"properties":{"name":{"anyOf":[{"type":"string","maxLength":100,"minLength":3},{"type":"null"}],"title":"Name","description":"Role name"},"description":{"anyOf":[{"type":"string","maxLength":500},{"type":"null"}],"title":"Description","description":"Role description"},"icon":{"anyOf":[{"type":"string","maxLength":50},{"type":"null"}],"title":"Icon","description":"Lucide icon name"}},"type":"object","title":"RoleUpdate","description":"Schema for updating role metadata (name, description, icon).\n\nUse /roles/{id}/scopes endpoints to modify permissions.\nSystem roles cannot be updated.","example":{"description":"Updated description for marketing manager role","name":"Senior Marketing Manager"}},"RoutingButtonSchema":{"properties":{"label":{"type":"string","maxLength":50,"minLength":1,"title":"Label","description":"Button label text (max 50 chars for WhatsApp compatibility)"},"target_type":{"type":"string","enum":["bot","agent"],"title":"Target Type","description":"Route to another bot or agent queue"},"target_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Target Id","description":"Target bot ID (required if target_type='bot'), null for agent queue"},"agents_online_only":{"type":"boolean","title":"Agents Online Only","description":"If true, this button is only shown when agents are online. When agents are offline, the button is either hidden or replaced with offline_label.","default":false},"offline_label":{"anyOf":[{"type":"string","maxLength":50},{"type":"null"}],"title":"Offline Label","description":"Alternative button label shown when agents are offline (e.g. 'Leave a Message'). If not set and agents_online_only=True, button is hidden when no agents are online."}},"type":"object","required":["label","target_type"],"title":"RoutingButtonSchema","description":"A routing button that routes to a bot or agent queue.\n\nWhen user clicks the button, the conversation will be routed\nto the specified target."},"RoutingConfigSchema":{"properties":{"messages":{"items":{"$ref":"#/components/schemas/RoutingMessageSchema"},"type":"array","maxItems":5,"title":"Messages","description":"Routing messages to send (max 5). First message is sent on session start."},"intent_keywords":{"additionalProperties":{"items":{"type":"string"},"type":"array"},"type":"object","title":"Intent Keywords","description":"Keyword mapping for text-based intent matching. Keys are target_id or 'agent' for agent queue. Values are lists of keywords."},"use_llm_fallback":{"type":"boolean","title":"Use Llm Fallback","description":"Use LLM for intent classification if keyword matching fails","default":true},"max_retry_count":{"type":"integer","maximum":5.0,"minimum":0.0,"title":"Max Retry Count","description":"Max times to re-send routing message before escalating to agent","default":2}},"type":"object","title":"RoutingConfigSchema","description":"Full routing configuration for a router bot.\n\nRouter bots use this configuration to:\n1. Send routing messages with buttons\n2. Match user intent from text responses\n3. Route to appropriate target (bot or agent)"},"RoutingMessageSchema":{"properties":{"text":{"type":"string","maxLength":1024,"minLength":1,"title":"Text","description":"Message text to send (max 1024 chars)"},"buttons":{"items":{"$ref":"#/components/schemas/RoutingButtonSchema"},"type":"array","maxItems":10,"title":"Buttons","description":"Routing buttons (max 10). WhatsApp supports max 3 reply buttons."},"fallback_text":{"anyOf":[{"type":"string","maxLength":1024},{"type":"null"}],"title":"Fallback Text","description":"Text for channels without button support (SMS/Email). If not set, text is used."}},"type":"object","required":["text"],"title":"RoutingMessageSchema","description":"A routing message with optional buttons.\n\nRouter bots send these messages to guide users through a routing flow.\nChannels that don't support buttons will receive the fallback_text instead."},"SavedInteractiveMessageCreate":{"properties":{"title":{"type":"string","maxLength":255,"minLength":1,"title":"Title","description":"Display name for the template"},"shortcut":{"anyOf":[{"type":"string","maxLength":50},{"type":"null"}],"title":"Shortcut","description":"Optional shortcut for slash commands (e.g., 'pricing'). Stored lowercase without leading slash."},"category":{"anyOf":[{"type":"string","maxLength":100},{"type":"null"}],"title":"Category","description":"Category for grouping (e.g., 'Sales', 'Support')"},"interactive_type":{"type":"string","enum":["buttons","list"],"title":"Interactive Type","description":"Interactive message type: 'buttons' or 'list'"},"body":{"type":"string","maxLength":1024,"minLength":1,"title":"Body","description":"Main message body text (max 1024 chars)"},"header_text":{"anyOf":[{"type":"string","maxLength":60},{"type":"null"}],"title":"Header Text","description":"Optional header text (max 60 chars). Used when header_type is 'text' or omitted."},"header_type":{"anyOf":[{"type":"string","enum":["text","image","video","document"]},{"type":"null"}],"title":"Header Type","description":"Header type: 'text', 'image', 'video', 'document', or null (no header). For backward compatibility, if header_text is set but header_type is null, it is treated as 'text'."},"header_media_url":{"anyOf":[{"type":"string","maxLength":500},{"type":"null"}],"title":"Header Media Url","description":"URL for image/video/document header media. Required when header_type is 'image', 'video' or 'document' (unless header_attachment_id is provided)."},"header_attachment_id":{"anyOf":[{"type":"string","maxLength":36},{"type":"null"}],"title":"Header Attachment Id","description":"Attachment ID for uploaded media header. Alternative to header_media_url for image/video/document headers."},"footer_text":{"anyOf":[{"type":"string","maxLength":60},{"type":"null"}],"title":"Footer Text","description":"Optional footer text (max 60 chars)"},"buttons":{"anyOf":[{"items":{"$ref":"#/components/schemas/InteractiveButton"},"type":"array","maxItems":3},{"type":"null"}],"title":"Buttons","description":"Buttons for type='buttons' (max 3)"},"sections":{"anyOf":[{"items":{"$ref":"#/components/schemas/InteractiveListSection"},"type":"array","maxItems":10},{"type":"null"}],"title":"Sections","description":"Sections for type='list' (max 10)"},"button_text":{"anyOf":[{"type":"string","maxLength":20},{"type":"null"}],"title":"Button Text","description":"Menu button text for list type (e.g., 'View Options')"},"is_shared":{"type":"boolean","title":"Is Shared","description":"If true, visible to all team members","default":true}},"type":"object","required":["title","interactive_type","body"],"title":"SavedInteractiveMessageCreate","description":"Schema for creating a new saved interactive message"},"SavedInteractiveMessageListResponse":{"properties":{"items":{"items":{"$ref":"#/components/schemas/SavedInteractiveMessageResponse"},"type":"array","title":"Items"},"pagination":{"$ref":"#/components/schemas/PaginationMeta"}},"type":"object","required":["items","pagination"],"title":"SavedInteractiveMessageListResponse","description":"Paginated list response for saved interactive messages"},"SavedInteractiveMessageResponse":{"properties":{"created_at":{"type":"string","title":"Created At","description":"When the resource was created (UTC)"},"updated_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Updated At","description":"When the resource was last updated (UTC)"},"id":{"type":"string","title":"Id","description":"Unique identifier (UUID)"},"tenant_id":{"type":"string","title":"Tenant Id","description":"Tenant this resource belongs to"},"title":{"type":"string","title":"Title"},"shortcut":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Shortcut"},"category":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Category"},"interactive_type":{"type":"string","title":"Interactive Type"},"body":{"type":"string","title":"Body"},"header_text":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Header Text"},"header_type":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Header Type"},"header_media_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Header Media Url"},"header_attachment_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Header Attachment Id"},"footer_text":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Footer Text"},"buttons":{"anyOf":[{"items":{"$ref":"#/components/schemas/InteractiveButton"},"type":"array"},{"type":"null"}],"title":"Buttons"},"sections":{"anyOf":[{"items":{"$ref":"#/components/schemas/InteractiveListSection"},"type":"array"},{"type":"null"}],"title":"Sections"},"button_text":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Button Text"},"is_shared":{"type":"boolean","title":"Is Shared"},"created_by_user_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Created By User Id"}},"type":"object","required":["created_at","id","tenant_id","title","interactive_type","body","is_shared"],"title":"SavedInteractiveMessageResponse","description":"Response schema for a saved interactive message"},"SavedInteractiveMessageUpdate":{"properties":{"title":{"anyOf":[{"type":"string","maxLength":255,"minLength":1},{"type":"null"}],"title":"Title","description":"Display name for the template"},"shortcut":{"anyOf":[{"type":"string","maxLength":50},{"type":"null"}],"title":"Shortcut","description":"Optional shortcut for slash commands"},"category":{"anyOf":[{"type":"string","maxLength":100},{"type":"null"}],"title":"Category","description":"Category for grouping"},"interactive_type":{"anyOf":[{"type":"string","enum":["buttons","list"]},{"type":"null"}],"title":"Interactive Type","description":"Interactive message type"},"body":{"anyOf":[{"type":"string","maxLength":1024,"minLength":1},{"type":"null"}],"title":"Body","description":"Main message body text"},"header_text":{"anyOf":[{"type":"string","maxLength":60},{"type":"null"}],"title":"Header Text","description":"Optional header text"},"header_type":{"anyOf":[{"type":"string","enum":["text","image","video","document"]},{"type":"null"}],"title":"Header Type","description":"Header type: 'text', 'image', 'video', 'document', or null"},"header_media_url":{"anyOf":[{"type":"string","maxLength":500},{"type":"null"}],"title":"Header Media Url","description":"URL for image/video/document header media"},"header_attachment_id":{"anyOf":[{"type":"string","maxLength":36},{"type":"null"}],"title":"Header Attachment Id","description":"Attachment ID for uploaded media header"},"footer_text":{"anyOf":[{"type":"string","maxLength":60},{"type":"null"}],"title":"Footer Text","description":"Optional footer text"},"buttons":{"anyOf":[{"items":{"$ref":"#/components/schemas/InteractiveButton"},"type":"array","maxItems":3},{"type":"null"}],"title":"Buttons","description":"Buttons for type='buttons'"},"sections":{"anyOf":[{"items":{"$ref":"#/components/schemas/InteractiveListSection"},"type":"array","maxItems":10},{"type":"null"}],"title":"Sections","description":"Sections for type='list'"},"button_text":{"anyOf":[{"type":"string","maxLength":20},{"type":"null"}],"title":"Button Text","description":"Menu button text for list type"},"is_shared":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Is Shared","description":"If true, visible to all team members"}},"type":"object","title":"SavedInteractiveMessageUpdate","description":"Schema for updating a saved interactive message. All fields are optional."},"ScheduleCampaignRequest":{"properties":{"scheduled_at":{"type":"string","format":"date-time","title":"Scheduled At"},"timezone":{"type":"string","title":"Timezone"},"acknowledge_warmup_warning":{"type":"boolean","title":"Acknowledge Warmup Warning","default":false}},"type":"object","required":["scheduled_at","timezone"],"title":"ScheduleCampaignRequest","description":"Request body for scheduling a campaign."},"ScheduleEmailCampaignRequest":{"properties":{"scheduled_at":{"type":"string","format":"date-time","title":"Scheduled At","description":"Local datetime in the given timezone (naive or with offset)"},"timezone":{"type":"string","title":"Timezone","description":"IANA timezone (e.g. 'Europe/Berlin', 'America/New_York')"}},"type":"object","required":["scheduled_at","timezone"],"title":"ScheduleEmailCampaignRequest","description":"Request body for scheduling an email campaign with timezone-aware scheduling.","example":{"scheduled_at":"2026-03-01T14:00:00","timezone":"Europe/Berlin"}},"ScheduledPostCreateRequest":{"properties":{"channel_ids":{"items":{"type":"string"},"type":"array","minItems":1,"title":"Channel Ids","description":"The Instagram / Facebook Messenger (Page) channels to publish to. Each becomes one target that succeeds or fails independently. Every id must be an active, non-archived channel of a publish-capable type in this workspace."},"format":{"type":"string","enum":["text","image","video","reel"],"title":"Format","description":"`text`, `image`, `video` or `reel`. Per-platform: `reel` is Instagram-only, `text` is Facebook-only, and Instagram always requires media. Unsupported combinations are rejected against the capability matrix before any Meta call."},"caption":{"anyOf":[{"type":"string","maxLength":2200},{"type":"null"}],"title":"Caption","description":"Post caption (Instagram/Facebook limit 2200 characters)."},"first_comment":{"anyOf":[{"type":"string","maxLength":2200},{"type":"null"}],"title":"First Comment","description":"Optional self-authored first comment. Posted as a top-level comment on the post from the SAME account immediately after it publishes (IG media / FB post). Best-effort: a comment failure never fails the post. NULL/empty means no first comment."},"media_attachment_ids":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Media Attachment Ids","description":"Ordered list of attachment UUIDs (upload via `POST /api/v1/attachments/*` first). Required for every non-`text` format; NULL/empty for `text`. At most 10. Slide order is preserved. Every id must belong to this workspace."},"scheduled_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Scheduled At","description":"When to publish (UTC, must be in the future). Omit and set `publish_now` to publish immediately; omit both to save a draft."},"publish_now":{"type":"boolean","title":"Publish Now","description":"Publish immediately and return the publish result. Mutually exclusive with `scheduled_at`.","default":false},"is_ai_generated":{"type":"boolean","title":"Is Ai Generated","description":"Mark the post as AI-generated (EU AI Act transparency). When true, the AI-content flag is passed through to Meta on publish — set on the Instagram container and, best-effort, on the Facebook feed / photo / video payload.","default":false}},"type":"object","required":["channel_ids","format"],"title":"ScheduledPostCreateRequest","description":"Body of ``POST /social-posts`` — author a post and draft / schedule /\npublish it to one or more Instagram / Facebook Page channels."},"ScheduledPostListResponse":{"properties":{"items":{"items":{"$ref":"#/components/schemas/ScheduledPostResponse"},"type":"array","title":"Items","description":"List of items for this page"},"pagination":{"$ref":"#/components/schemas/PaginationMeta","description":"Pagination metadata"}},"type":"object","required":["items","pagination"],"title":"ScheduledPostListResponse","description":"`{items, pagination}` page of authored posts."},"ScheduledPostResponse":{"properties":{"created_at":{"type":"string","title":"Created At","description":"When the resource was created (UTC)"},"updated_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Updated At","description":"When the resource was last updated (UTC)"},"id":{"type":"string","title":"Id","description":"Unique identifier (UUID)"},"tenant_id":{"type":"string","title":"Tenant Id","description":"Tenant this resource belongs to"},"status":{"type":"string","title":"Status","description":"`draft`, `scheduled`, `publishing`, `published`, `partially_failed`, `failed` or `cancelled`."},"format":{"type":"string","title":"Format","description":"`text`, `image`, `video` or `reel`."},"caption":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Caption","description":"Post caption."},"first_comment":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"First Comment","description":"Self-authored first comment posted after the post publishes."},"media_attachment_ids":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Media Attachment Ids","description":"Ordered attachment UUIDs used as media."},"is_ai_generated":{"type":"boolean","title":"Is Ai Generated","description":"Whether the post is flagged as AI-generated (EU AI Act transparency), passed through to Meta on publish.","default":false},"scheduled_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Scheduled At","description":"When it is queued to publish (UTC). NULL for draft/publish-now."},"published_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Published At","description":"When it finished publishing (roll-up)."},"created_by_user_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Created By User Id","description":"The author (user UUID), when resolvable."},"error_message":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Error Message","description":"Pre-fanout failure or a roll-up of target errors."},"targets":{"items":{"$ref":"#/components/schemas/ScheduledPostTargetResponse"},"type":"array","title":"Targets","description":"One entry per channel target."},"published_count":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Published Count","description":"Targets published (publish-now response only)."},"failed_count":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Failed Count","description":"Targets failed (publish-now response only)."},"skipped_count":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Skipped Count","description":"Targets skipped as already-published (publish-now response only)."}},"type":"object","required":["created_at","id","tenant_id","status","format"],"title":"ScheduledPostResponse","description":"One authored post with its per-channel targets."},"ScheduledPostTargetResponse":{"properties":{"id":{"type":"string","title":"Id","description":"Target id."},"channel_id":{"type":"string","title":"Channel Id","description":"The channel this target publishes to."},"status":{"type":"string","title":"Status","description":"`pending`, `publishing`, `published`, `failed` or `cancelled`."},"meta_post_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Meta Post Id","description":"Meta's id for the published post (IG media id / FB `{page}_{post}`)."},"permalink":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Permalink","description":"Public link to the published post."},"error_code":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Error Code","description":"Meta error code/subcode when this target failed."},"error_message":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Error Message","description":"Human-readable failure reason for this target."},"published_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Published At","description":"When this target published."}},"type":"object","required":["id","channel_id","status"],"title":"ScheduledPostTargetResponse","description":"One publish target (IG/FB page channel) of an authored post."},"ScheduledPostUpdateRequest":{"properties":{"caption":{"anyOf":[{"type":"string","maxLength":2200},{"type":"null"}],"title":"Caption","description":"Replacement caption (draft/scheduled only)."},"first_comment":{"anyOf":[{"type":"string","maxLength":2200},{"type":"null"}],"title":"First Comment","description":"Replacement self-authored first comment (draft/scheduled only). Send `\"\"` to clear it."},"media_attachment_ids":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Media Attachment Ids","description":"Replacement ordered media list (draft/scheduled only). Re-validated for ownership and re-bumps retention. Send `[]` to clear media."},"scheduled_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Scheduled At","description":"Reschedule (UTC, future). Draft/scheduled only. Setting this on a draft moves it to `scheduled`."},"channel_ids":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Channel Ids","description":"Replacement target channel list (draft/scheduled only). Targets are reconciled: removed channels are dropped, added channels get a new pending target."},"is_ai_generated":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Is Ai Generated","description":"Toggle the AI-generated flag (draft/scheduled only). When set, it is passed through to Meta on publish (EU AI Act transparency)."},"cancel":{"type":"boolean","title":"Cancel","description":"Cancel the post (status → `cancelled`). Cannot be combined with edit fields. Rejected once the post is publishing or published.","default":false}},"type":"object","title":"ScheduledPostUpdateRequest","description":"Body of ``PATCH /social-posts/{id}`` — edit a draft/scheduled post or\ncancel it.\n\nEvery field is optional; only the ones supplied are changed. Editing\n(caption / media / scheduled_at / channels) is allowed only while the post\nis ``draft`` or ``scheduled``. ``cancel: true`` moves it to ``cancelled``\nand is allowed from any not-yet-terminal, not-currently-publishing state."},"ScheduledRuleKind":{"type":"string","enum":["birthday","recurring_cron","segment_blast"],"title":"ScheduledRuleKind","description":"Rule kind — drives how the tick loop materializes the rule.\n\nNote: Lowercase values to match the MySQL ENUM (see\nsr01_create_scheduled_rules migration). values_callable is set on the\nSQLAlchemy column so the lowercase values are written/read correctly."},"ScheduledRuleListResponse":{"properties":{"items":{"items":{"$ref":"#/components/schemas/ScheduledRuleResponse"},"type":"array","title":"Items"},"pagination":{"$ref":"#/components/schemas/PaginationMeta"}},"type":"object","required":["items","pagination"],"title":"ScheduledRuleListResponse","description":"Paginated list of ScheduledRules — matches the unified pagination shape."},"ScheduledRuleResponse":{"properties":{"id":{"type":"string","title":"Id"},"tenant_id":{"type":"string","title":"Tenant Id"},"name":{"type":"string","title":"Name"},"description":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Description"},"kind":{"$ref":"#/components/schemas/ScheduledRuleKind"},"channel_type":{"$ref":"#/components/schemas/ChannelType"},"channel_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Channel Id"},"email_mailbox_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Email Mailbox Id"},"payload":{"additionalProperties":true,"type":"object","title":"Payload"},"time_of_day_local":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Time Of Day Local"},"timezone":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Timezone"},"recurrence_rule":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Recurrence Rule"},"segment_filter":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Segment Filter"},"next_evaluation_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Next Evaluation At"},"last_evaluated_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Last Evaluated At"},"status":{"$ref":"#/components/schemas/ScheduledRuleStatus"},"created_by_user_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Created By User Id"},"created_at":{"type":"string","title":"Created At"},"updated_at":{"type":"string","title":"Updated At"}},"type":"object","required":["id","tenant_id","name","description","kind","channel_type","channel_id","payload","time_of_day_local","timezone","recurrence_rule","segment_filter","next_evaluation_at","last_evaluated_at","status","created_by_user_id","created_at","updated_at"],"title":"ScheduledRuleResponse","description":"Wire format for a ScheduledRule row."},"ScheduledRuleStatus":{"type":"string","enum":["active","paused","archived"],"title":"ScheduledRuleStatus","description":"Scheduled rule lifecycle status.\n\nNote: Lowercase values to match the MySQL ENUM (see\nsr01_create_scheduled_rules migration). values_callable is set on the\nSQLAlchemy column so the lowercase values are written/read correctly."},"ScheduledSendListResponse":{"properties":{"items":{"items":{"$ref":"#/components/schemas/ScheduledSendResponse"},"type":"array","title":"Items"},"pagination":{"$ref":"#/components/schemas/PaginationMeta"}},"type":"object","required":["items","pagination"],"title":"ScheduledSendListResponse","description":"Paginated list of ScheduledSends — matches the unified pagination shape."},"ScheduledSendResponse":{"properties":{"id":{"type":"string","title":"Id"},"tenant_id":{"type":"string","title":"Tenant Id"},"contact_id":{"type":"string","title":"Contact Id"},"channel_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Channel Id"},"email_mailbox_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Email Mailbox Id"},"channel_type":{"$ref":"#/components/schemas/ChannelType"},"payload":{"additionalProperties":true,"type":"object","title":"Payload"},"send_at":{"type":"string","title":"Send At"},"next_run_at":{"type":"string","title":"Next Run At"},"recurrence_rule":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Recurrence Rule"},"status":{"$ref":"#/components/schemas/ScheduledSendStatus"},"flow_run_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Flow Run Id"},"rule_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Rule Id"},"created_by_user_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Created By User Id"},"created_at":{"type":"string","title":"Created At"},"updated_at":{"type":"string","title":"Updated At"}},"type":"object","required":["id","tenant_id","contact_id","channel_id","channel_type","payload","send_at","next_run_at","recurrence_rule","status","flow_run_id","created_by_user_id","created_at","updated_at"],"title":"ScheduledSendResponse","description":"Wire format for a ScheduledSend row."},"ScheduledSendStatus":{"type":"string","enum":["scheduled","running","completed","failed","canceled"],"title":"ScheduledSendStatus","description":"Scheduled send lifecycle status.\n\nNote: Uses lowercase values to match the MySQL ENUM (see\nss01_create_scheduled_sends migration). values_callable is set on the\nSQLAlchemy column so the lowercase values are written/read correctly."},"SchedulerEvent":{"properties":{"event_type":{"type":"string","const":"scheduler","title":"Event Type","default":"scheduler"},"id":{"type":"string","title":"Id","description":"Stable client-side key. Matches ``scheduled_send_id``."},"occurred_at":{"type":"string","title":"Occurred At","description":"Chronological anchor — ``COALESCE(next_run_at, send_at)``. Used as the merge sort key."},"scheduled_send_id":{"type":"string","title":"Scheduled Send Id"},"contact_id":{"type":"string","title":"Contact Id"},"channel_type":{"type":"string","title":"Channel Type","description":"ChannelType value, e.g. 'WHATSAPP', 'EMAIL', 'TELEGRAM'."},"status":{"type":"string","title":"Status","description":"ScheduledSendStatus value: scheduled / running / completed / failed / canceled."},"send_at":{"type":"string","title":"Send At","description":"Original customer-supplied send timestamp."},"next_run_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Next Run At","description":"Next execution timestamp (advances on each fire for recurring sends)."},"recurrence_rule":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Recurrence Rule","description":"iCalendar RRULE / cron expression for recurring sends."},"payload_preview":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Payload Preview","description":"Short (<=140 chars) plain-text preview of the message that will be sent — extracted from payload.text / payload.body / payload.template_name / payload.subject. NULL when nothing usable is present."},"rule_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Rule Id","description":"ScheduledRule.id that materialized this send. NULL for manual sends."},"rule_kind":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Rule Kind","description":"ScheduledRuleKind value when ``rule_id`` is populated: birthday / recurring_cron / segment_blast."}},"type":"object","required":["id","occurred_at","scheduled_send_id","contact_id","channel_type","status","send_at"],"title":"SchedulerEvent","description":"Timeline row representing a scheduled (or already-fired) send.\n\nSourced from ``scheduled_sends``. Surfaces both pending reminders\n(status ``scheduled`` / ``running``) and historical sends (``completed``\n/ ``failed`` / ``canceled``) interleaved with the contact's other\ntimeline streams.\n\nThe chronological anchor (``occurred_at``) is ``next_run_at`` when\npopulated and falls back to ``send_at`` — ``next_run_at`` is NOT NULL\nin the schema today but we keep the fallback for forward-compat with\na future \"paused with no next run\" state."},"ScopeAdd":{"properties":{"scopes":{"items":{"type":"string"},"type":"array","minItems":1,"title":"Scopes","description":"List of scopes to add to the role"}},"type":"object","required":["scopes"],"title":"ScopeAdd","description":"Schema for adding scopes to a role","example":{"scopes":["campaigns:write","email:read"]}},"ScopeGrantResponse":{"properties":{"message":{"type":"string","title":"Message","description":"Success message"},"grant_id":{"type":"string","title":"Grant Id","description":"ID of the created scope grant"}},"type":"object","required":["message","grant_id"],"title":"ScopeGrantResponse","description":"Response from granting a scope directly to a user","example":{"grant_id":"uuid-of-grant","message":"Scope 'campaigns:update' granted to user"}},"ScopeInfo":{"properties":{"scope":{"type":"string","title":"Scope","description":"Full scope identifier (e.g., 'campaigns:read')"},"action":{"type":"string","title":"Action","description":"Action part of the scope (e.g., 'read')"},"description":{"type":"string","title":"Description","description":"Human-readable description of the scope"},"oauth_grantable":{"type":"boolean","title":"Oauth Grantable","description":"False for scopes an OAuth app can never be granted (NEVER_OAUTH_GRANTABLE)","default":true}},"type":"object","required":["scope","action","description"],"title":"ScopeInfo","description":"Information about a single scope"},"ScopeRemoveResponse":{"properties":{"success":{"type":"boolean","title":"Success","description":"Whether the operation succeeded","default":true},"message":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Message","description":"Optional success message"}},"type":"object","title":"ScopeRemoveResponse","description":"Response for scope removal operation","example":{"message":"Scope 'campaigns:write' removed from role","success":true}},"SearchKnowledgeRequest":{"properties":{"query":{"type":"string","maxLength":1000,"minLength":1,"title":"Query","description":"Search query text"},"document_types":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Document Types","description":"Filter by document types (not currently used with RAG retrieval)"},"folder_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Folder Id","description":"Optional folder ID to scope search to a specific folder. When provided, only documents in that folder are searched."},"limit":{"type":"integer","maximum":20.0,"minimum":1.0,"title":"Limit","description":"Maximum number of sources to return","default":5}},"type":"object","required":["query"],"title":"SearchKnowledgeRequest","description":"Request model for searching the knowledge base.\n\nUses Vertex AI RAG for AI-powered answers.\n\nOptionally scope the search to a specific folder by providing folder_id.\nWhen folder_id is provided, the search uses the folder's Vertex AI corpus\nfor efficient pre-filtering. If the folder has no corpus yet, an empty\nresult is returned.","example":{"limit":5,"query":"How do I reset my password?"}},"SearchKnowledgeResponse":{"properties":{"query":{"type":"string","title":"Query","description":"Original search query"},"answer":{"type":"string","title":"Answer","description":"AI-generated answer"},"citations":{"items":{"additionalProperties":true,"type":"object"},"type":"array","title":"Citations","description":"Citations from source documents"},"sources":{"items":{"additionalProperties":true,"type":"object"},"type":"array","title":"Sources","description":"Source documents used for answer"},"source_count":{"type":"integer","title":"Source Count","description":"Number of sources used"}},"type":"object","required":["query","answer","source_count"],"title":"SearchKnowledgeResponse","description":"Response from knowledge base search.\n\nContains AI-generated answer with citations and sources.","example":{"answer":"To reset your password, navigate to the login page and click 'Forgot Password'. Enter your email address and follow the instructions sent to your inbox.","citations":[{"source":"Password Reset Guide","text":"Click 'Forgot Password'"}],"query":"How do I reset my password?","source_count":1,"sources":[{"document_id":"doc-123","title":"Password Reset Guide"}]}},"SegmentCondition":{"properties":{"field":{"type":"string","title":"Field","description":"Field to filter on: contact_method, name, first_name, last_name, language, languages, source, birthday, created_at, last_contacted_at, custom_field, has_tag, not_has_tag, has_optin, not_has_optin, email, phone, has_telegram, has_whatsapp, has_messenger, has_instagram (legacy aliases)"},"operator":{"type":"string","title":"Operator","description":"Comparison operator. Text: equals, not_equals, contains, not_contains, starts_with, is_empty, is_not_empty. Enum/select: in, not_in. Number: gt, gte, lt, lte, between. Date: before, after, between, in_month, is_today, within_last_days, within_next_days. contact_method: has, not_has, equals, not_equals, contains, not_contains. Negative operators also match contacts with no value.","default":"equals"},"value":{"title":"Value","description":"Value to compare against. null for is_empty/is_not_empty/is_today/has/not_has; array of strings for in/not_in; {\"from\": X, \"to\": Y} (inclusive, either end may be null) for between; 1-12 for in_month; N days for within_last_days/within_next_days; dates as YYYY-MM-DD."},"key":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Key","description":"For custom_field conditions - the field key"},"type":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Type","description":"Type discriminator for nested groups","default":"condition"},"method_type":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Method Type","description":"For contact_method conditions - the method type: phone, email, whatsapp_id, whatsapp_bsuid, telegram_id, messenger_id, instagram_id, linkedin, homepage, facebook, instagram_handle, social_other"},"channel_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Channel Id","description":"For contact_method conditions - channel UUID; only meaningful for messenger_id / instagram_id (page-scoped). Ignored for global types."},"phone_type":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Phone Type","description":"For contact_method conditions with method_type=phone - phone classification (e.g. mobile, fixed_line)"},"list_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"List Id","description":"For has_optin/not_has_optin - filter by specific list UUID"},"channel_type":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Channel Type","description":"For has_optin/not_has_optin - filter by channel type (email, sms, whatsapp, telegram, messenger, instagram, browser_push, rcs), or \"any\" to match an active subscription to the list on any channel. Omitted/null also means any channel. Only completed opt-ins count (pending double opt-in never matches); negation (not_has_optin or value=false) with \"any\" means no completed subscription to the list on any channel."},"require_confirmed":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Require Confirmed","description":"DEPRECATED and ignored. Accepted for backwards compatibility only. has_optin/not_has_optin always count only completed opt-ins (subscriptions pending double opt-in never match).","deprecated":true}},"type":"object","required":["field","value"],"title":"SegmentCondition","description":"Schema for a single segment condition.\n\nUsed in dynamic lists to filter contacts based on field values.\n\n**General rules** (all operators):\n\n- ``value`` is always sent; ``null`` for value-less operators (``is_empty``,\n  ``is_not_empty``, ``is_today``, ``has``, ``not_has``).\n- ``between``: ``value = {\"from\": X, \"to\": Y}``, both ends inclusive, either\n  end may be null (open range).\n- ``in`` / ``not_in``: ``value`` is an array of strings.\n- ``in_month``: ``value`` 1-12. ``within_last_days`` / ``within_next_days``:\n  ``value`` = N days (0-3660). Dates are ``YYYY-MM-DD``; \"today\" is the UTC date.\n- Negative operators (``not_equals``, ``not_contains``, ``not_in``,\n  ``not_has``) also match contacts with NO value at all.\n- ``is_empty`` = NULL / empty string / no value; ``equals \"\"`` is the same as\n  ``is_empty``.\n- Malformed values make the condition match nothing (never everyone).\n\nOperator sets: TEXT = equals, not_equals, contains, not_contains, starts_with,\nis_empty, is_not_empty. ENUM = in, not_in, equals, not_equals, is_empty,\nis_not_empty. DATE = equals, before, after, between, in_month, is_today,\nwithin_last_days, within_next_days, is_empty, is_not_empty. NUMBER = equals,\nnot_equals, gt, gte, lt, lte, between, is_empty, is_not_empty.\n\n**Available filter fields:**\n\n- Standard contact fields: ``name``, ``first_name``, ``last_name`` (TEXT);\n  ``language`` (preferred ISO 639-1 code) and ``source`` (ENUM);\n  ``languages`` (observed languages; ``in`` = has any, ``not_in`` = has none,\n  ``is_empty``, ``is_not_empty``); ``birthday``, ``created_at``,\n  ``last_contacted_at`` (DATE). For ``birthday``, ``is_today`` /\n  ``within_*_days`` compare month and day (upcoming birthdays); for the\n  other dates they compare the absolute date.\n- ``contact_method``: Unified contact-method filter evaluated against the\n  ``contact_methods`` table. Parameters:\n\n  - ``method_type`` (required): one of ``phone``, ``email``, ``whatsapp_id``,\n    ``whatsapp_bsuid``, ``telegram_id``, ``messenger_id``, ``instagram_id``,\n    ``linkedin``, ``homepage``, ``facebook``, ``instagram_handle``, ``social_other``.\n  - ``operator``: ``has`` / ``not_has`` (presence checks), or ``equals`` /\n    ``not_equals`` / ``contains`` / ``not_contains`` (value matches,\n    requires ``value``). Negative operators are NOT EXISTS, so contacts with\n    no method of that type match them.\n  - ``value``: string to match (only for the value operators).\n  - ``channel_id`` (optional UUID): only meaningful for the page-scoped\n    types ``messenger_id`` / ``instagram_id``; ignored for global types.\n  - ``phone_type`` (optional, e.g. ``mobile``): only meaningful for\n    ``phone``.\n\n  Value matching normalizes the input the same way the write path does\n  (digits-only for phone-shaped types, lowercase for email), so saved\n  filters reliably match stored rows.\n- ``email``, ``phone``: legacy aliases for ``contact_method`` with that\n  ``method_type`` (same operators).\n- ``custom_field``: Custom field filter (requires ``key``). Operators by the\n  field's type: text/email/phone/url TEXT; number NUMBER; date/datetime DATE\n  (datetime compares its date part); boolean ``equals`` / ``not_equals``\n  (true/false) + ``is_empty`` / ``is_not_empty``; select ENUM; multiselect\n  ``in`` (has any of), ``not_in`` (has none of), ``contains`` (has this\n  option), ``is_empty``, ``is_not_empty``; location/collection\n  ``is_empty`` / ``is_not_empty``. Legacy ``equals``, ``contains``, ``gt``,\n  ``gte``, ``lt``, ``lte`` keep working on every type. A contact with no\n  value for the field matches ``is_empty`` and every negative operator.\n  Boolean: ``equals false`` matches only an explicit false;\n  ``not_equals true`` matches false or unset.\n- ``has_tag`` / ``not_has_tag``: Tag membership (value = tag UUID).\n- ``has_telegram``, ``has_whatsapp``, ``has_messenger``, ``has_instagram``:\n  Retained legacy aliases for boolean channel-presence checks\n  (operator: equals, value: true/false). Prefer ``contact_method`` with\n  ``operator=has``/``not_has`` for new filters.\n- ``has_optin``: Subscription/opt-in check via ``contact_subscriptions``.\n  Extra parameters: ``list_id`` (optional UUID), ``channel_type`` (optional; any\n  ``SubscriptionChannelType`` value — see the field description for the live list —\n  or ``\"any\"``; ``\"any\"`` and null/omitted both mean \"subscribed on any channel\"),\n  ``require_confirmed`` (DEPRECATED, accepted but ignored).\n  Only COMPLETED opt-ins count (``status = subscribed``); subscriptions still\n  pending double opt-in (``status = pending``) never match. This is status-based,\n  not ``opt_in_confirmed_at``-based, so channels where double opt-in does not\n  apply (WhatsApp, Telegram, SMS, ...) still count.\n  When ``value`` is true, matches contacts WITH a matching subscription; when false, matches\n  contacts WITHOUT one.\n- ``not_has_optin``: Alias that negates ``has_optin`` (value=true means no subscription).","examples":[{"field":"contact_method","method_type":"whatsapp_id","operator":"has"},{"field":"contact_method","method_type":"email","operator":"contains","value":"@example.com"},{"channel_id":"chn-a1b2c3d4-e5f6-7890-abcd-ef1234567890","field":"contact_method","method_type":"messenger_id","operator":"has"},{"field":"has_telegram","operator":"equals","value":true},{"channel_type":"any","field":"has_optin","list_id":"lst-a1b2c3d4-e5f6-7890-abcd-ef1234567890","operator":"equals","value":true}]},"SegmentConditionsRequest":{"properties":{"operator":{"type":"string","title":"Operator","description":"Logical operator: AND or OR","default":"AND"},"conditions":{"anyOf":[{"items":{"$ref":"#/components/schemas/SegmentCondition"},"type":"array"},{"type":"null"}],"title":"Conditions","description":"Flat list of conditions (legacy format)"},"groups":{"anyOf":[{"items":{"additionalProperties":true,"type":"object"},"type":"array"},{"type":"null"}],"title":"Groups","description":"Nested condition groups for complex queries"}},"type":"object","title":"SegmentConditionsRequest","description":"Schema for segment conditions with logical grouping.\n\nSupports both flat and nested condition groups for complex queries.","examples":[{"conditions":[{"field":"has_telegram","operator":"equals","value":true},{"field":"has_whatsapp","operator":"equals","value":true}],"operator":"AND"},{"groups":[{"field":"has_telegram","operator":"equals","type":"condition","value":true},{"groups":[{"field":"custom_field","key":"shoe_size","operator":"gt","type":"condition","value":"36"},{"field":"custom_field","key":"shoe_size","operator":"lt","type":"condition","value":"40"}],"operator":"AND","type":"group"}],"operator":"OR"}]},"SegmentCreate":{"properties":{"filter_type":{"$ref":"#/components/schemas/FilterType"},"filter_operator":{"anyOf":[{"$ref":"#/components/schemas/FilterOperator"},{"type":"null"}]},"filter_value":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Filter Value"},"logical_operator":{"$ref":"#/components/schemas/LogicalOperator","default":"and"},"filter_order":{"type":"integer","title":"Filter Order","default":0}},"type":"object","required":["filter_type"],"title":"SegmentCreate"},"SegmentPreviewRequest":{"properties":{"segments":{"items":{"$ref":"#/components/schemas/SegmentCreate"},"type":"array","title":"Segments"},"limit":{"type":"integer","maximum":100.0,"minimum":1.0,"title":"Limit","default":10}},"type":"object","required":["segments"],"title":"SegmentPreviewRequest"},"SegmentResponse":{"properties":{"id":{"type":"string","title":"Id"},"campaign_id":{"type":"string","title":"Campaign Id"},"filter_type":{"$ref":"#/components/schemas/FilterType"},"filter_operator":{"anyOf":[{"$ref":"#/components/schemas/FilterOperator"},{"type":"null"}]},"filter_value":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Filter Value"},"logical_operator":{"$ref":"#/components/schemas/LogicalOperator"},"filter_order":{"type":"integer","title":"Filter Order"},"created_at":{"type":"string","format":"date-time","title":"Created At"}},"type":"object","required":["id","campaign_id","filter_type","filter_operator","filter_value","logical_operator","filter_order","created_at"],"title":"SegmentResponse"},"SendCampaignResponse":{"properties":{"success":{"type":"boolean","title":"Success","description":"Whether the operation succeeded","default":true},"message":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Message","description":"Optional success message"},"campaign_id":{"type":"string","title":"Campaign Id","description":"Base campaign ID"},"estimated_recipients":{"type":"integer","title":"Estimated Recipients","description":"Number of recipients"},"status":{"type":"string","title":"Status","description":"Current campaign status"},"warnings":{"items":{"type":"string"},"type":"array","title":"Warnings","description":"Non-blocking warnings surfaced during validation/send (e.g. small-campaign moderation relaxation). Empty when none."},"pacing":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Pacing","description":"Only set when the campaign is sent at a send speed or in batches: {paced, batch_number, armed, remaining} -- recipients in the first batch and recipients waiting for later batches."}},"type":"object","required":["campaign_id","estimated_recipients","status"],"title":"SendCampaignResponse","description":"Response for send campaign operation","example":{"campaign_id":"campaign-789","estimated_recipients":1000,"message":"Campaign queued for sending","status":"sending","success":true,"warnings":[]}},"SendCarouselRequest":{"properties":{"channel_id":{"type":"string","title":"Channel Id","description":"Channel UUID to send through"},"contact_id":{"type":"string","title":"Contact Id","description":"Contact UUID (the recipient)"},"template_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Template Name","description":"WhatsApp template name (required for WhatsApp)"},"language":{"type":"string","title":"Language","description":"Template language code (default: 'en')","default":"en"},"cards":{"items":{"$ref":"#/components/schemas/WhatsAppCarouselCard"},"type":"array","maxItems":10,"title":"Cards","description":"WhatsApp carousel cards (max 10, required for WhatsApp)","default":[]},"elements":{"items":{"$ref":"#/components/schemas/MessengerCarouselElement"},"type":"array","maxItems":10,"title":"Elements","description":"Messenger carousel elements (max 10, required for Messenger)","default":[]},"meta":{"additionalProperties":true,"type":"object","title":"Meta","description":"Custom metadata to attach to the message","default":{}}},"type":"object","required":["channel_id","contact_id"],"title":"SendCarouselRequest","description":"Request schema for sending a carousel message.\n\nThis endpoint supports both WhatsApp and Messenger carousels:\n\n**WhatsApp (template-based):**\n- Requires a pre-approved carousel template\n- Use `template_name`, `language`, and `cards` fields\n- Each card can have header, body parameters, and buttons\n\n**Messenger (generic template):**\n- No template required\n- Use `elements` field\n- Each element has title, subtitle, image, and buttons","examples":[{"description":"Send a carousel using a pre-approved WhatsApp template","summary":"WhatsApp template carousel","value":{"cards":[{"body_parameters":[{"text":"Wireless Headphones","type":"text"},{"text":"$99.99","type":"text"}],"buttons":[{"index":0,"parameters":[],"type":"quick_reply"}],"header":{"type":"image","url":"https://example.com/product1.jpg"}},{"body_parameters":[{"text":"Smart Watch","type":"text"},{"text":"$249.99","type":"text"}],"buttons":[{"index":0,"parameters":[],"type":"quick_reply"}],"header":{"type":"image","url":"https://example.com/product2.jpg"}}],"channel_id":"a1b2c3d4-5678-9abc-def0-1234567890ab","contact_id":"c1d2e3f4-5678-9abc-def0-1234567890ab","language":"en","template_name":"product_showcase"}},{"description":"Send a carousel using Messenger generic template (no template required)","summary":"Messenger generic carousel","value":{"channel_id":"a1b2c3d4-5678-9abc-def0-1234567890ab","contact_id":"c1d2e3f4-5678-9abc-def0-1234567890ab","elements":[{"buttons":[{"title":"View Details","type":"web_url","url":"https://example.com/products/headphones"},{"payload":"ADD_CART_HEADPHONES","title":"Add to Cart","type":"postback"}],"image_url":"https://example.com/product1.jpg","subtitle":"Premium sound, 30hr battery life - $99.99","title":"Wireless Headphones"},{"buttons":[{"title":"View Details","type":"web_url","url":"https://example.com/products/watch"},{"payload":"ADD_CART_WATCH","title":"Add to Cart","type":"postback"}],"image_url":"https://example.com/product2.jpg","subtitle":"Fitness tracking, notifications - $249.99","title":"Smart Watch"}]}}]},"SendCarouselResponse":{"properties":{"id":{"type":"string","title":"Id","description":"Message UUID"},"conversation_id":{"type":"string","title":"Conversation Id","description":"Conversation UUID"},"external_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"External Id","description":"Platform message ID"},"status":{"type":"string","title":"Status","description":"Message status: pending, sent, failed"},"platform":{"type":"string","title":"Platform","description":"Platform: whatsapp, messenger"},"cards_count":{"type":"integer","title":"Cards Count","description":"Number of cards/elements sent"},"error":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Error","description":"Error message if status is 'failed'"}},"type":"object","required":["id","conversation_id","status","platform","cards_count"],"title":"SendCarouselResponse","description":"Response after sending a carousel message"},"SendContactsRequest":{"properties":{"channel_id":{"type":"string","title":"Channel Id","description":"Channel UUID (must be a WhatsApp channel)"},"contact_id":{"type":"string","title":"Contact Id","description":"Contact UUID (the recipient)"},"contacts":{"items":{"$ref":"#/components/schemas/ContactCard"},"type":"array","maxItems":10,"minItems":1,"title":"Contacts","description":"Contact cards to send (1-10)"},"meta":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Meta","description":"Custom metadata to attach to the message"}},"type":"object","required":["channel_id","contact_id","contacts"],"title":"SendContactsRequest","description":"Request schema for sending contact card(s) via WhatsApp.\n\nThis allows sharing one or more contact cards with a recipient.\nEach contact card can contain name, phone numbers, emails,\naddresses, and organization information.","example":{"channel_id":"a1b2c3d4-5678-9abc-def0-1234567890ab","contact_id":"c1d2e3f4-5678-9abc-def0-1234567890ab","contacts":[{"emails":[{"email":"jane@example.com","type":"WORK"}],"name":{"first_name":"Jane","formatted_name":"Jane Smith","last_name":"Smith"},"org":{"company":"Acme Inc","title":"Sales Manager"},"phones":[{"phone":"+14155551234","type":"WORK"}]}]}},"SendContactsResponse":{"properties":{"id":{"type":"string","title":"Id","description":"Message UUID"},"conversation_id":{"type":"string","title":"Conversation Id","description":"Conversation UUID"},"external_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"External Id","description":"WhatsApp message ID (wamid)"},"status":{"type":"string","title":"Status","description":"Message status: pending, sent, failed"},"platform":{"type":"string","title":"Platform","description":"Platform (always whatsapp)","default":"whatsapp"},"contacts_count":{"type":"integer","title":"Contacts Count","description":"Number of contacts sent"},"error":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Error","description":"Error message if status is 'failed'"}},"type":"object","required":["id","conversation_id","status","contacts_count"],"title":"SendContactsResponse","description":"Response after sending contact card(s)"},"SendFlowRequest":{"properties":{"channel_id":{"type":"string","title":"Channel Id","description":"Channel ID to send from"},"contact_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Contact Id","description":"Contact ID to send to"},"recipient":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Recipient","description":"Recipient phone number (alternative to contact_id)"},"flow_id":{"type":"string","title":"Flow Id","description":"WhatsApp Flow ID"},"flow_cta":{"type":"string","maxLength":30,"minLength":1,"title":"Flow Cta","description":"Button text to trigger flow (max 30 chars)"},"body":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Body","description":"Message body text"},"header":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Header","description":"Optional header text"},"footer":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Footer","description":"Optional footer text"},"screen":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Screen","description":"Initial screen to navigate to"},"data":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Data","description":"Data for the first screen. When omitted, the saved default data for (flow_id, screen) is used. String values may contain contact variables such as {{contact.first_name}} or {{contact.custom.<key>}}; they are rendered per recipient."},"mode":{"$ref":"#/components/schemas/FlowMode","description":"Flow mode: draft or published","default":"published"},"conversation_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Conversation Id","description":"Conversation to send into. Resolves the recipient and channel context; takes precedence over contact_id / recipient."},"flow_token":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Flow Token","description":"Optional tracking token (auto-generated if not provided)"}},"type":"object","required":["channel_id","flow_id","flow_cta"],"title":"SendFlowRequest","description":"Request to send a WhatsApp Flow message"},"SendFlowResponse":{"properties":{"success":{"type":"boolean","title":"Success"},"message_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Message Id","description":"Internal message ID"},"external_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"External Id","description":"WhatsApp message ID"},"conversation_id":{"type":"string","title":"Conversation Id","description":"Conversation ID (new or existing)"},"flow_token":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Flow Token","description":"Flow token for tracking"},"screen":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Screen","description":"Screen the flow was opened on"},"error":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Error"},"error_code":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Error Code","description":"Platform error code when the send failed"}},"type":"object","required":["success","conversation_id"],"title":"SendFlowResponse","description":"Response from sending a Flow message"},"SendMultiProductRequest":{"properties":{"conversation_id":{"type":"string","title":"Conversation Id","description":"Conversation to send the products into"},"header_text":{"type":"string","maxLength":60,"title":"Header Text","description":"Required header text for an MPM (Meta cap: 60 chars)"},"body":{"type":"string","maxLength":1024,"title":"Body","description":"Required body text for an MPM (Meta cap: 1024 chars)"},"footer":{"anyOf":[{"type":"string","maxLength":60},{"type":"null"}],"title":"Footer","description":"Optional footer (Meta cap: 60 chars)"},"sections":{"items":{"$ref":"#/components/schemas/MPMSectionItem"},"type":"array","title":"Sections","description":"1-10 sections, <=30 products total"}},"type":"object","required":["conversation_id","header_text","body","sections"],"title":"SendMultiProductRequest"},"SendNextBatchResponse":{"properties":{"batch_number":{"type":"integer","title":"Batch Number","description":"Number of the batch that is now sending (1-based)"},"armed":{"type":"integer","title":"Armed","description":"Recipients released for this batch"},"skipped_ineligible":{"type":"integer","title":"Skipped Ineligible","description":"Recipients skipped because they opted out, unsubscribed or are blocked since the campaign was prepared","default":0},"remaining":{"type":"integer","title":"Remaining","description":"Recipients still waiting for a later batch","default":0},"campaign":{"$ref":"#/components/schemas/CampaignResponse"}},"type":"object","required":["batch_number","armed","campaign"],"title":"SendNextBatchResponse","description":"Response for ``POST /campaigns/{id}/send-next-batch``."},"SendPlanChannel":{"properties":{"channel_type":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Channel Type"},"channel_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Channel Id"},"recipients":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Recipients"},"remaining":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Remaining","description":"Recipients on this channel not sent yet; null until the campaign is prepared"},"cap_per_second":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Cap Per Second","description":"Maximum messages per second this channel can send"},"cap_source":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Cap Source","description":"Where the channel limit comes from"}},"additionalProperties":true,"type":"object","title":"SendPlanChannel","description":"One channel's share of a send plan."},"SendPlanResponse":{"properties":{"pacing_available":{"type":"boolean","title":"Pacing Available","description":"False when send speed and batches are not available for this account yet"},"pacing_unavailable_reason":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Pacing Unavailable Reason","description":"Why pacing_available is false: \"disabled\" (not available for this account yet) or \"pacer_offline\" (briefly unavailable, try again shortly)"},"total_recipients":{"type":"integer","title":"Total Recipients","default":0},"remaining":{"type":"integer","title":"Remaining","default":0},"channels":{"items":{"$ref":"#/components/schemas/SendPlanChannel"},"type":"array","title":"Channels"},"requested_rate_per_minute":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Requested Rate Per Minute"},"effective_rate_per_minute":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Effective Rate Per Minute"},"batch_size":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Batch Size"},"batches":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Batches"},"eta_seconds":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Eta Seconds"},"eta_seconds_per_batch":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Eta Seconds Per Batch"},"rate_limited_by":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Rate Limited By","description":"\"campaign\" or \"channel\""}},"additionalProperties":true,"type":"object","required":["pacing_available"],"title":"SendPlanResponse","description":"``GET /campaigns/{id}/send-plan``: speed, batches and estimated duration.\n\n``effective_rate_per_minute`` is the requested speed capped by what the\ncampaign's channels can send; ``rate_limited_by`` says which of the two\ndecides. ``eta_seconds`` is the estimated time to send every remaining\nrecipient (excluding the time spent paused between batches)."},"SendRequest":{"properties":{"channel_id":{"anyOf":[{"type":"string","maxLength":36},{"type":"null"}],"title":"Channel Id"},"contact_id":{"anyOf":[{"type":"string","maxLength":36},{"type":"null"}],"title":"Contact Id"},"conversation_id":{"anyOf":[{"type":"string","maxLength":36},{"type":"null"}],"title":"Conversation Id"},"mode":{"type":"string","title":"Mode","description":"'interactive' (inside 24h window) or 'template'","default":"interactive"},"screen":{"anyOf":[{"type":"string","maxLength":128},{"type":"null"}],"title":"Screen"},"initial_data":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Initial Data","description":"Entry-screen data. String values may contain contact variables such as {{contact.first_name}} or {{contact.custom.<key>}}, rendered per recipient."},"header_text":{"anyOf":[{"type":"string","maxLength":60},{"type":"null"}],"title":"Header Text"},"body_text":{"anyOf":[{"type":"string","maxLength":1024},{"type":"null"}],"title":"Body Text"},"footer_text":{"anyOf":[{"type":"string","maxLength":60},{"type":"null"}],"title":"Footer Text"},"cta":{"anyOf":[{"type":"string","maxLength":30},{"type":"null"}],"title":"Cta"},"template_id":{"anyOf":[{"type":"string","maxLength":36},{"type":"null"}],"title":"Template Id"},"template_variables":{"anyOf":[{},{"type":"null"}],"title":"Template Variables"},"draft":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Draft"},"flow_action":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Flow Action","description":"'navigate' | 'data_exchange' | omitted = auto"}},"type":"object","title":"SendRequest"},"SendSavedInteractiveMessageRequest":{"properties":{"channel_id":{"type":"string","title":"Channel Id","description":"Channel UUID (must be a WhatsApp channel)"},"contact_id":{"type":"string","title":"Contact Id","description":"Contact UUID (the recipient)"},"meta":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Meta","description":"Custom metadata to attach to the message"}},"type":"object","required":["channel_id","contact_id"],"title":"SendSavedInteractiveMessageRequest","description":"Request schema for sending a saved interactive message to a contact"},"SendSavedInteractiveMessageResponse":{"properties":{"id":{"type":"string","title":"Id","description":"Message UUID"},"conversation_id":{"type":"string","title":"Conversation Id","description":"Conversation UUID"},"external_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"External Id","description":"WhatsApp message ID (wamid)"},"status":{"type":"string","title":"Status","description":"Message status: pending, sent, failed"},"platform":{"type":"string","title":"Platform","description":"Platform (always whatsapp)","default":"whatsapp"},"type":{"type":"string","title":"Type","description":"Interactive type: buttons, list"},"saved_message_id":{"type":"string","title":"Saved Message Id","description":"ID of the saved message that was sent"},"error":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Error","description":"Error message if status is 'failed'"}},"type":"object","required":["id","conversation_id","status","type","saved_message_id"],"title":"SendSavedInteractiveMessageResponse","description":"Response after sending a saved interactive message"},"SendSingleProductRequest":{"properties":{"conversation_id":{"type":"string","title":"Conversation Id","description":"Conversation to send the product into"},"product_retailer_id":{"type":"string","title":"Product Retailer Id","description":"The product SKU / retailer id"},"body":{"anyOf":[{"type":"string","maxLength":1024},{"type":"null"}],"title":"Body","description":"Optional message body (Meta cap: 1024 chars)"},"footer":{"anyOf":[{"type":"string","maxLength":60},{"type":"null"}],"title":"Footer","description":"Optional footer (Meta cap: 60 chars)"}},"type":"object","required":["conversation_id","product_retailer_id"],"title":"SendSingleProductRequest"},"SendStickerRequest":{"properties":{"channel_id":{"type":"string","title":"Channel Id","description":"Channel UUID (must be a WhatsApp channel)"},"contact_id":{"type":"string","title":"Contact Id","description":"Contact UUID (the recipient)"},"sticker_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Sticker Id","description":"WhatsApp media ID of an uploaded sticker (preferred)"},"sticker_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Sticker Url","description":"Direct URL to a .webp sticker file"},"meta":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Meta","description":"Custom metadata to attach to the message"}},"type":"object","required":["channel_id","contact_id"],"title":"SendStickerRequest","description":"Request schema for sending a sticker via WhatsApp.\n\nStickers must be in .webp format with specific requirements:\n- Static stickers: 512x512 pixels, max 100KB\n- Animated stickers: 512x512 pixels, max 500KB\n\nProvide either sticker_id (for uploaded media) or sticker_url (direct link).","example":{"channel_id":"a1b2c3d4-5678-9abc-def0-1234567890ab","contact_id":"c1d2e3f4-5678-9abc-def0-1234567890ab","sticker_url":"https://example.com/stickers/greeting.webp"}},"SendStickerResponse":{"properties":{"id":{"type":"string","title":"Id","description":"Message UUID"},"conversation_id":{"type":"string","title":"Conversation Id","description":"Conversation UUID"},"external_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"External Id","description":"WhatsApp message ID (wamid)"},"status":{"type":"string","title":"Status","description":"Message status: pending, sent, failed"},"platform":{"type":"string","title":"Platform","description":"Platform (always whatsapp)","default":"whatsapp"},"error":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Error","description":"Error message if status is 'failed'"}},"type":"object","required":["id","conversation_id","status"],"title":"SendStickerResponse","description":"Response after sending a sticker"},"SendTemplateCardParams":{"properties":{"variable_values":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"additionalProperties":{"type":"string"},"type":"object"},{"type":"null"}],"title":"Variable Values","description":"Values for this card's body placeholders. Same shape rules as the top-level `variable_values`: a list (positional only) or a dict (numeric keys for positional, identifier keys for named). Must match the template's `parameter_format`. Reserved dict keys (`url_suffix`, `otp_code`, and their indexed variants) apply per card."},"header_media_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Header Media Url","description":"Header media reference for THIS card's IMAGE/VIDEO header. Accepts the same forms as the top-level `header_media_url` (bare attachment id, proxy URL, permanent public URL, legacy GCS URL, or an external https URL). Each carousel card has its own header media, so supply one per card."}},"type":"object","title":"SendTemplateCardParams","description":"Per-card send parameters for a CAROUSEL template.\n\nSupplied positionally in ``SendTemplateRequest.cards`` — index 0 fills the\nfirst card, index 1 the second, and so on. Each card's ``variable_values``\nfollows the SAME positional/named rules (and reserved-key handling) as the\ntop-level body ``variable_values``, resolved against that card's own body\nplaceholders. This is the template-send INPUT shape; the service maps it\nonto the unified carousel content representation before dispatch."},"SendTemplateRequest":{"properties":{"contact_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Contact Id","description":"Contact to send the message to. OPTIONAL — supply ANY ONE of `contact_id`, `contact_method_id`, `conversation_id`, or `whatsapp_id` to identify the recipient. If `contact_method_id` is also provided it WINS and this `contact_id` is ignored. When combined with `whatsapp_id`, this contact is used (and a matching WhatsApp contact method is auto-created on it if missing)."},"contact_method_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Contact Method Id","description":"OPTIONAL. A `contact_methods.id` (UUID) of type `whatsapp_id` or `whatsapp_bsuid` owned by the tenant. When provided it determines BOTH the recipient WhatsApp id (the method's value) AND the contact (the method's owning contact). Highest-priority recipient selector — if both `contact_id` and `contact_method_id` are given, `contact_method_id` WINS and `contact_id` is ignored. A method of any other type returns 422."},"whatsapp_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Whatsapp Id","description":"OPTIONAL. A WhatsApp recipient id sent directly — a phone number WITHOUT a leading `+` (a leading `+` is stripped; spaces/dashes/parens are tolerated for phone-shaped values), an alphanumeric WhatsApp id (Meta now permits alphanumeric ids — digits are NOT required), OR a Business-scoped user ID such as `US.13491208655302741918` (kept verbatim, periods included, and sent in Meta's `recipient` field instead of `to`). The value is normalized with the same canonical contact-method normalization used elsewhere so dedup is consistent. If `contact_id` is also supplied, that contact gains a `whatsapp_id` (or `whatsapp_bsuid`) method with this value (created if missing); otherwise an existing contact is matched by this identifier, or a new contact + method is created. Note that AUTHENTICATION templates cannot be sent to a Business-scoped user ID (Meta error 131062). When sent without a `conversation_id`, the new conversation's channel is the template's channel (optionally selected via `channel_id`)."},"channel_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Channel Id","description":"OPTIONAL. A WhatsApp channel id (UUID) owned by the tenant. Always allowed alongside any recipient selector. Used to (a) disambiguate a template referenced by NAME that exists on multiple channels, and (b) select the sending channel when sending by `whatsapp_id` with no contact/conversation. If it conflicts with the resolved template's own channel, the request returns 400."},"variable_values":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"additionalProperties":{"type":"string"},"type":"object"},{"type":"null"}],"title":"Variable Values","description":"Override auto-mapped values. Pass a list (positional only) or a dict (positional with numeric keys, or named with identifier keys). Shape must match the template's parameter_format.\n\nReserved dict keys (coexist with body variable keys):\n- `url_suffix` — replaces `{{1}}` inside a dynamic URL button. Value is ONLY the suffix portion that fills `{{1}}`, not the full URL (e.g. for template URL `https://example.com/orders/{{1}}`, pass `\"url_suffix\": \"track/INV-2026-001\"`).\n- `otp_code` — the code copied by the user for authentication / OTP COPY_CODE buttons."},"language":{"anyOf":[{"type":"string","maxLength":10},{"type":"null"}],"title":"Language","description":"OPTIONAL. Template language to send when the template is referenced BY NAME and that name exists in multiple languages. Use the exact code stored in `whatsapp_templates.language` — the Meta language/locale code (e.g. `en`, `de`, `en_US`, `pt_BR`).\n\nResolution when a name maps to multiple language rows (after the channel filter): if only one language remains it is used; if several remain, an explicit `language` selects that variant (400 if no such variant), else the recipient contact's stored `language` is matched (exact, then base-language family e.g. `de_DE`↔`de`), else 400 asking for `language`. When the template is referenced by DB ID the language is fixed by that row; passing a conflicting `language` returns 400."},"conversation_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Conversation Id","description":"Link to existing conversation (creates new if not provided)"},"header_media_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Header Media Url","description":"Header media reference for IMAGE/VIDEO/DOCUMENT templates. Accepts ANY of:\n- a bare `attachment_id` (UUID) returned by `POST /attachments` or `POST /whatsapp-templates/upload-media`\n- the proxy URL `/api/v1/attachments/{id}/download` (relative or absolute)\n- the permanent public URL `/api/v1/attachments/public/{tenant_id}/{id}`\n- a legacy `https://storage.googleapis.com/...` GCS URL\n- any absolute external `https://` URL (customer CDN, etc.)\n\nThe server resolves the input to a Meta-deliverable URL via `WhatsAppHeaderMediaService`. Field name is kept for backwards compatibility — it has not been URL-only since May 2026."},"header_document_filename":{"anyOf":[{"type":"string","maxLength":255},{"type":"null"}],"title":"Header Document Filename","description":"Optional filename override for DOCUMENT-type template headers. Must include the correct file extension matching the document (e.g. 'invoice.pdf' for a PDF). Ignored for non-DOCUMENT headers."},"cards":{"anyOf":[{"items":{"$ref":"#/components/schemas/SendTemplateCardParams"},"type":"array","maxItems":10},{"type":"null"}],"title":"Cards","description":"Per-card parameters for a CAROUSEL template, supplied POSITIONALLY — entry 0 fills the first card, entry 1 the second, etc. Provide one entry per card you want to personalize. When ABSENT the non-carousel send path is unchanged. Each entry carries its own `variable_values` (same positional/named rules as the top-level field) and `header_media_url`. Only meaningful for CAROUSEL templates; ignored otherwise."}},"type":"object","title":"SendTemplateRequest","description":"Request to send a template message.\n\n``variable_values`` accepts EITHER a list (positional templates) OR a dict\n(positional with numeric keys, or named with identifier keys). The accepted\nshape is determined by the template's ``parameter_format``:\n\n* ``positional`` template → ``[\"John\", \"ORD-98765\"]`` or\n  ``{\"1\": \"John\", \"2\": \"ORD-98765\"}``.\n* ``named`` template → ``{\"first_name\": \"John\", \"order_id\": \"ORD-98765\"}``.\n\nSending the wrong shape returns 400 with an actionable error. Order in the\nNAMED dict is irrelevant — Meta resolves by ``parameter_name``.\n\nReserved keys in ``variable_values`` (only valid when passed as a dict;\nthey coexist with the regular body variable keys ``\"1\"``, ``\"2\"``, ... or\nnamed identifiers):\n\n* ``url_suffix`` — replaces the ``{{1}}`` placeholder inside a dynamic URL\n  button (e.g. template URL ``https://example.com/orders/{{1}}``). Pass\n  ONLY the suffix portion that fills ``{{1}}``, not the full URL. This\n  key targets button index 0; use the indexed form below to address\n  additional dynamic URL buttons on the same template.\n* ``url_suffix_<button_index>`` — same as ``url_suffix`` but addresses a\n  specific button by its 0-based index in the template's ``BUTTONS``\n  component (e.g. ``url_suffix_1`` for the second button). When both\n  ``url_suffix`` and ``url_suffix_0`` are present, the indexed form wins.\n* ``otp_code`` — the code value for an authentication / OTP COPY_CODE\n  button. This is the string the user copies when tapping the button.\n* ``otp_code_<button_index>`` — indexed variant for templates with more\n  than one COPY_CODE button. Same precedence rules as ``url_suffix_*``.","example":{"contact_id":"c1d2e3f4-5678-9abc-def0-1234567890ab","variable_values":{"1":"John","2":"ORD-98765"}}},"SendTemplateResponse":{"properties":{"success":{"type":"boolean","title":"Success"},"message_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Message Id","description":"Internal message ID"},"external_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"External Id","description":"WhatsApp message ID"},"conversation_id":{"type":"string","title":"Conversation Id","description":"Conversation ID (new or existing)"},"error":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Error"},"error_code":{"anyOf":[{"type":"integer"},{"type":"string"},{"type":"null"}],"title":"Error Code","description":"WhatsApp API error code (e.g., 131008 for missing parameters, 131042 for payment issues), or \"contact_blocked\" when the recipient is blocked and nothing was sent"},"error_details":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Error Details","description":"Additional error details from the Meta API (e.g., which parameter is missing)"},"is_coexistence":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Is Coexistence","description":"Set on a 138000 (Calling not enabled) failure: whether the sending number is connected through the WhatsApp Business app (Coexistence). Null otherwise."}},"type":"object","required":["success","conversation_id"],"title":"SendTemplateResponse","description":"Response from sending a template message","example":{"conversation_id":"b5d8e2a1-4c3f-4a8b-9d2e-1f6a7b8c9d0e","external_id":"wamid.HBgNNDkxNTIxNTU4MDg2NRUCABIYFjNFQjBCNUYxMjdFRkI5","message_id":"f47ac10b-58cc-4372-a567-0e02b2c3d479","success":true}},"SendableDomainResponse":{"properties":{"domain":{"type":"string","title":"Domain","description":"Domain name (e.g., example.com)"},"type":{"type":"string","title":"Type","description":"Domain type: 'full_domain' or 'fixed_address'"},"address":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Address","description":"Full email address (only for fixed_address type)"},"source":{"type":"string","title":"Source","description":"Integration source (sendgrid_byok, sendgrid_managed, mailgun_byok, ses_managed, gmail, smtp_imap)"},"source_id":{"type":"string","title":"Source Id","description":"ID of the source integration or provider config"},"campaign_capable":{"type":"boolean","title":"Campaign Capable","description":"Whether this source can be used for email campaigns"}},"type":"object","required":["domain","type","source","source_id","campaign_capable"],"title":"SendableDomainResponse","description":"Individual sendable domain entry"},"SendableDomainsResponse":{"properties":{"domains":{"items":{"$ref":"#/components/schemas/SendableDomainResponse"},"type":"array","title":"Domains","description":"List of sendable domains"}},"type":"object","title":"SendableDomainsResponse","description":"List of all sendable domains for a tenant"},"SenderActionErrorDetail":{"properties":{"code":{"type":"string","title":"Code","description":"`sender_action_not_supported`, `message_not_found`, `conversation_not_found`, `message_not_inbound`, `no_inbound_message`, `channel_unavailable`, `sender_action_rate_limited` or `sender_action_failed`."},"message":{"type":"string","title":"Message"},"channel_type":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Channel Type"},"action":{"type":"string","enum":["read","typing"],"title":"Action"}},"type":"object","required":["code","message","action"],"title":"SenderActionErrorDetail"},"SenderActionErrorResponse":{"properties":{"detail":{"$ref":"#/components/schemas/SenderActionErrorDetail"}},"type":"object","required":["detail"],"title":"SenderActionErrorResponse","example":{"detail":{"action":"read","channel_type":"telegram","code":"sender_action_not_supported","message":"Read receipts are not supported on this channel."}}},"SenderActionResponse":{"properties":{"success":{"type":"boolean","title":"Success","description":"Always true on a 200 response.","default":true},"action":{"type":"string","enum":["read","typing"],"title":"Action","description":"The action performed."},"state":{"anyOf":[{"type":"string","enum":["on","off"]},{"type":"null"}],"title":"State","description":"Typing state (`on`/`off`); `null` for read receipts."},"message_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Message Id","description":"The message the action referred to, if any."},"conversation_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Conversation Id","description":"Conversation ID (null only for a message that belongs to no conversation)."},"channel_type":{"type":"string","title":"Channel Type","description":"Channel type of the conversation."},"expires_in_seconds":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Expires In Seconds","description":"For typing `on`: seconds until the platform hides the indicator on its own. `null` when it stays until cleared, or for read receipts."},"implied_read":{"type":"boolean","title":"Implied Read","description":"True when the action also marked the message as read on the platform (WhatsApp typing indicators do this).","default":false},"deduplicated":{"type":"boolean","title":"Deduplicated","description":"True when an identical recent request already covered this one (read receipt already sent for this message, or typing indicator still showing), so nothing was sent to the channel again.","default":false}},"type":"object","required":["action","conversation_id","channel_type"],"title":"SenderActionResponse","description":"Result of a read receipt or typing indicator.","example":{"action":"typing","channel_type":"whatsapp","conversation_id":"7c9e6679-7425-40de-944b-e07fc1f90ae7","deduplicated":false,"expires_in_seconds":25,"implied_read":true,"message_id":"f47ac10b-58cc-4372-a567-0e02b2c3d479","state":"on","success":true}},"SenderActionSupportInfo":{"properties":{"read":{"type":"boolean","title":"Read","description":"The channel can show the contact that their message was read."},"typing":{"type":"boolean","title":"Typing","description":"The channel can show the contact a typing indicator."},"typing_off":{"type":"boolean","title":"Typing Off","description":"The typing indicator can be cleared explicitly. When false, `state: \"off\"` is accepted as a no-op and the indicator ends on its own (timeout or your next reply)."},"typing_ttl_seconds":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Typing Ttl Seconds","description":"How long the platform shows the typing indicator after one call, in seconds. `null` means it stays until cleared or until you reply."},"typing_requires_message":{"type":"boolean","title":"Typing Requires Message","description":"The platform ties the typing indicator to a received message. If you do not pass `message_id`, the latest inbound message of the conversation is used."}},"type":"object","required":["read","typing","typing_off","typing_requires_message"],"title":"SenderActionSupportInfo","description":"Which sender actions a channel type supports."},"SenderActionSupportListResponse":{"properties":{"items":{"items":{"$ref":"#/components/schemas/ChannelSenderActionSupport"},"type":"array","title":"Items"},"pagination":{"$ref":"#/components/schemas/PaginationMeta"}},"type":"object","required":["items","pagination"],"title":"SenderActionSupportListResponse","description":"Sender-action support for every channel type (single page)."},"SenderConfirmRequest":{"properties":{"verification_id":{"type":"string","title":"Verification Id"},"code":{"type":"string","title":"Code"}},"type":"object","required":["verification_id","code"],"title":"SenderConfirmRequest"},"SenderConfirmResponse":{"properties":{"verification_id":{"type":"string","title":"Verification Id"},"channel_id":{"type":"string","title":"Channel Id"},"msisdn":{"type":"string","title":"Msisdn"},"status":{"type":"string","title":"Status"}},"type":"object","required":["verification_id","channel_id","msisdn","status"],"title":"SenderConfirmResponse"},"SenderOptionOut":{"properties":{"id":{"type":"string","title":"Id","description":"EmailMailbox id (canonical) OR — for legacy ACL-override entries — an EmailIntegration / EmailProviderConfig id. The client treats this as opaque and POSTs it back as `MessageCreate.sender_id`; the backend dual-accepts mailbox / integration / provider ids."},"source_type":{"type":"string","title":"Source Type","description":"`mailbox`, `integration` or `provider` — for client-side icon hints only"},"name":{"type":"string","title":"Name","description":"Human-readable mailbox / connection name"},"email_address":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Email Address","description":"The visible 'From:' address (when known)"},"is_default":{"type":"boolean","title":"Is Default","description":"Pre-select this option in the dropdown","default":false},"is_default_reply_sender":{"type":"boolean","title":"Is Default Reply Sender","description":"True when this option is the conversation's / inbox's default reply sender (the receiving connection's outbound_provider).","default":false},"is_acl_override":{"type":"boolean","title":"Is Acl Override","description":"True when this option is included ONLY because of the default-reply-sender ACL override — i.e. the caller is not directly on the underlying EmailMailbox ACL. UI may render a 'via inbox' hint.","default":false}},"type":"object","required":["id","source_type","name"],"title":"SenderOptionOut","description":"One selectable email sender in the reply/compose dropdown."},"SenderOptionsResponse":{"properties":{"options":{"items":{"$ref":"#/components/schemas/SenderOptionOut"},"type":"array","title":"Options"},"default_option_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Default Option Id","description":"The id the client should pre-select"},"default_reply_sender_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Default Reply Sender Id","description":"The id the outbound send path will accept regardless of the user's EmailMailbox ACL (when set)."}},"type":"object","title":"SenderOptionsResponse","description":"List wrapper with pre-resolved default + default-reply-sender ids."},"SenderVerifyRequest":{"properties":{"msisdn":{"type":"string","title":"Msisdn"},"callback_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Callback Url"}},"type":"object","required":["msisdn"],"title":"SenderVerifyRequest"},"SenderVerifyResponse":{"properties":{"verification_id":{"type":"string","title":"Verification Id"},"msisdn":{"type":"string","title":"Msisdn"},"status":{"type":"string","title":"Status"},"code":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Code"}},"type":"object","required":["verification_id","msisdn","status"],"title":"SenderVerifyResponse"},"SendingStatusResponse":{"properties":{"integration_id":{"type":"string","title":"Integration Id"},"ses_tenant_name":{"type":"string","title":"Ses Tenant Name"},"sending_status":{"type":"string","title":"Sending Status"},"message":{"type":"string","title":"Message"}},"type":"object","required":["integration_id","ses_tenant_name","sending_status","message"],"title":"SendingStatusResponse","description":"Response from pause/resume sending operations"},"SesSetupDnsRecord":{"properties":{"record_type":{"type":"string","title":"Record Type"},"record_name":{"type":"string","title":"Record Name"},"record_value":{"type":"string","title":"Record Value"},"purpose":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Purpose"},"type":{"type":"string","title":"Type","description":"Alias of `record_type` (CNAME, TXT, MX).","readOnly":true},"name":{"type":"string","title":"Name","description":"Alias of `record_name` (the DNS host to create).","readOnly":true},"value":{"type":"string","title":"Value","description":"Alias of `record_value` (the DNS record content).","readOnly":true}},"type":"object","required":["record_type","record_name","record_value","type","name","value"],"title":"SesSetupDnsRecord","description":"DNS record from SES setup (without database ID).\n\nSerialised with both `record_type`/`record_name`/`record_value` and the\n`type`/`name`/`value` aliases used by GET /domain-status."},"SessionStatusEnum":{"type":"string","enum":["active","escalated","completed","abandoned"],"title":"SessionStatusEnum","description":"Bot conversation session status"},"SetBlockedAction":{"properties":{"type":{"type":"string","const":"set_blocked","title":"Type"},"blocked":{"type":"boolean","title":"Blocked","description":"true = block, false = unblock"}},"type":"object","required":["type","blocked"],"title":"SetBlockedAction","description":"Block or unblock every selected contact (all channels, all of the\ncontact's identifiers). Contacts already in the requested state are not\ncounted as changed."},"SetCampaignProviderResponse":{"properties":{"success":{"type":"boolean","title":"Success","description":"Whether the operation succeeded"},"message":{"type":"string","title":"Message","description":"Status message"},"connection":{"$ref":"#/components/schemas/EmailConnectionResponse","description":"Updated connection details"}},"type":"object","required":["success","message","connection"],"title":"SetCampaignProviderResponse","description":"Response after setting campaign provider"},"SetCustomFieldAction":{"properties":{"type":{"type":"string","const":"set_custom_field","title":"Type"},"key":{"type":"string","title":"Key","description":"Custom field key (custom_field_definitions.key)"},"value":{"title":"Value","description":"New value, validated against the field's type (select/multiselect options, number, boolean, date/datetime ISO). null clears the value."}},"type":"object","required":["type","key"],"title":"SetCustomFieldAction","description":"Create-or-update a custom field value for every selected contact."},"SetFieldAction":{"properties":{"type":{"type":"string","const":"set_field","title":"Type"},"field":{"type":"string","title":"Field","description":"Editable standard contact column. Allowed: name (alias: display_name), first_name, last_name, language, languages, birthday. Anything else is rejected with 400."},"value":{"title":"Value","description":"New value (null clears the field)"}},"type":"object","required":["type","field"],"title":"SetFieldAction","description":"Set a standard Contact column (allowlisted editable fields only)."},"SetFieldValueRequest":{"properties":{"value":{"title":"Value","description":"Field value (type depends on field_type)"}},"type":"object","required":["value"],"title":"SetFieldValueRequest","description":"Request schema for setting a custom field value on a contact.\n\nThe value is stored as-is and validated based on the field type.","example":{"value":"51-200"}},"SignaturePreviewRequest":{"properties":{"content_html":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Content Html","description":"Custom HTML to preview (if not provided, uses tenant template)"},"agent_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Agent Name","description":"Override agent name"},"agent_email":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Agent Email","description":"Override agent email"},"agent_phone":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Agent Phone","description":"Override agent phone"},"agent_title":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Agent Title","description":"Override agent title"},"company_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Company Name","description":"Override company name"},"company_logo":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Company Logo","description":"Override company logo URL"},"company_website":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Company Website","description":"Override company website"}},"type":"object","title":"SignaturePreviewRequest","description":"Request schema for previewing a signature with sample data","example":{"agent_name":"John Smith","agent_title":"Support Agent","company_name":"Acme Corp"}},"SignaturePreviewResponse":{"properties":{"resolved_html":{"type":"string","title":"Resolved Html","description":"HTML with placeholders resolved"},"resolved_text":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Resolved Text","description":"Plain text with placeholders resolved"},"placeholders_used":{"items":{"type":"string"},"type":"array","title":"Placeholders Used","description":"List of placeholders found in the template"}},"type":"object","required":["resolved_html","placeholders_used"],"title":"SignaturePreviewResponse","description":"Response schema for signature preview","example":{"placeholders_used":["{agent_name}","{agent_title}","{company_name}"],"resolved_html":"<div><p><strong>John Smith</strong></p><p>Support Agent</p></div>","resolved_text":"John Smith\nSupport Agent"}},"SignatureTemplateResponse":{"properties":{"id":{"type":"string","title":"Id","description":"Template ID"},"tenant_id":{"type":"string","title":"Tenant Id","description":"Tenant ID"},"content_html":{"type":"string","title":"Content Html","description":"HTML content with placeholders"},"content_text":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Content Text","description":"Plain text version"},"created_by_user_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Created By User Id","description":"User who created the template"},"created_at":{"type":"string","title":"Created At","description":"When the template was created"},"updated_at":{"type":"string","title":"Updated At","description":"When the template was last updated"}},"type":"object","required":["id","tenant_id","content_html","created_at","updated_at"],"title":"SignatureTemplateResponse","description":"Response schema for tenant signature template","example":{"content_html":"<div><p><strong>{agent_name}</strong></p></div>","content_text":"{agent_name}","created_at":"2026-01-16T10:00:00Z","created_by_user_id":"user-789","id":"sig-template-123","tenant_id":"tenant-456","updated_at":"2026-01-16T10:00:00Z"}},"SignatureTemplateUpdate":{"properties":{"content_html":{"type":"string","maxLength":500000,"minLength":1,"title":"Content Html","description":"HTML content of the signature template with placeholders"},"content_text":{"anyOf":[{"type":"string","maxLength":100000},{"type":"null"}],"title":"Content Text","description":"Plain text version for non-HTML emails"}},"type":"object","required":["content_html"],"title":"SignatureTemplateUpdate","description":"Schema for creating or updating a tenant signature template","example":{"content_html":"<div style=\"font-family: Arial, sans-serif;\">\n<p><strong>{agent_name}</strong></p>\n<p>{agent_title}</p>\n<p>{company_name}</p>\n<p>Phone: {agent_phone}</p>\n<p>Website: <a href=\"{company_website}\">{company_website}</a></p>\n</div>","content_text":"{agent_name}\n{agent_title}\n{company_name}\nPhone: {agent_phone}\nWebsite: {company_website}"}},"SignupChannelOption":{"properties":{"direct_link":{"type":"string","title":"Direct Link","description":"Deep link URL for this channel"},"qr_code_base64":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Qr Code Base64","description":"QR code image as base64 string"}},"type":"object","required":["direct_link"],"title":"SignupChannelOption","description":"Signup option for a specific channel"},"SingleEmailStats":{"properties":{"sent":{"type":"integer","title":"Sent","description":"Billable single/transactional emails sent (status sent|delivered)","default":0},"delivered":{"type":"integer","title":"Delivered","description":"Single/transactional emails confirmed delivered","default":0},"delivery_rate":{"type":"number","title":"Delivery Rate","description":"Delivery rate percentage (delivered/sent)","default":0.0}},"type":"object","title":"SingleEmailStats","description":"Single / transactional (1:1) outbound email stats.\n\nSourced from the `email_messages` table (support replies, 1:1 sends) --\nthe same source billing invoices as MESSAGE_EMAIL. `sent` counts rows in\na billable status (sent or delivered); email has no billable 'read' state."},"SkillAttachmentInstruction":{"properties":{"preset":{"type":"string","enum":["read_text","describe","context","custom"],"title":"Preset"},"custom_text":{"anyOf":[{"type":"string","maxLength":1200},{"type":"null"}],"title":"Custom Text"}},"additionalProperties":false,"type":"object","required":["preset"],"title":"SkillAttachmentInstruction","description":"What a skill tells the agent to do with a customer's image / PDF (Beta).\n\nStored on the skill VERSION. NULL on the version = preset ``context``.\n``custom_text`` is required (1-1000 chars) iff ``preset == \"custom\"``."},"SkillCreate":{"properties":{"slug":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Slug"},"title":{"type":"string","maxLength":255,"minLength":1,"title":"Title"},"description":{"type":"string","maxLength":1000,"minLength":1,"title":"Description"},"default_mode":{"type":"string","enum":["always_on","model_selected","flow_only"],"title":"Default Mode","default":"model_selected"},"body":{"type":"string","title":"Body"},"tool_allowlist":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Tool Allowlist"},"goal":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Goal"},"attachment_instruction":{"anyOf":[{"$ref":"#/components/schemas/SkillAttachmentInstruction"},{"type":"null"}]}},"additionalProperties":false,"type":"object","required":["title","description","body"],"title":"SkillCreate"},"SkillDeleteResponse":{"properties":{"success":{"type":"boolean","title":"Success","default":true},"id":{"type":"string","title":"Id"},"action":{"type":"string","enum":["archived","deleted"],"title":"Action"},"message":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Message"}},"type":"object","required":["id","action"],"title":"SkillDeleteResponse"},"SkillGoalStatsResponse":{"properties":{"days":{"type":"integer","title":"Days"},"started":{"type":"integer","title":"Started","default":0},"completed":{"type":"integer","title":"Completed","default":0},"handed_off":{"type":"integer","title":"Handed Off","default":0},"completion_rate":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Completion Rate"},"by_version":{"items":{"$ref":"#/components/schemas/SkillGoalStatsVersion"},"type":"array","title":"By Version"},"truncated":{"type":"boolean","title":"Truncated","default":false}},"type":"object","required":["days"],"title":"SkillGoalStatsResponse"},"SkillGoalStatsVersion":{"properties":{"version":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Version"},"started":{"type":"integer","title":"Started","default":0},"completed":{"type":"integer","title":"Completed","default":0},"handed_off":{"type":"integer","title":"Handed Off","default":0},"completion_rate":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Completion Rate"}},"type":"object","title":"SkillGoalStatsVersion"},"SkillImportError":{"properties":{"index":{"type":"integer","title":"Index"},"slug":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Slug"},"error":{"type":"string","title":"Error"}},"type":"object","required":["index","error"],"title":"SkillImportError"},"SkillImportResponse":{"properties":{"created":{"items":{"$ref":"#/components/schemas/SkillImportResultItem"},"type":"array","title":"Created"},"updated":{"items":{"$ref":"#/components/schemas/SkillImportResultItem"},"type":"array","title":"Updated"},"unchanged":{"items":{"$ref":"#/components/schemas/SkillImportResultItem"},"type":"array","title":"Unchanged"},"errors":{"items":{"$ref":"#/components/schemas/SkillImportError"},"type":"array","title":"Errors"}},"type":"object","title":"SkillImportResponse"},"SkillImportResultItem":{"properties":{"index":{"type":"integer","title":"Index"},"id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Id"},"slug":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Slug"},"version":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Version"}},"type":"object","required":["index"],"title":"SkillImportResultItem"},"SkillListItem":{"properties":{"id":{"type":"string","title":"Id"},"slug":{"type":"string","title":"Slug"},"title":{"type":"string","title":"Title"},"description":{"type":"string","title":"Description"},"default_mode":{"type":"string","title":"Default Mode"},"current_version":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Current Version"},"current_version_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Current Version Id"},"is_archived":{"type":"boolean","title":"Is Archived","default":false},"used_by_count":{"type":"integer","title":"Used By Count","default":0},"used_by":{"items":{"$ref":"#/components/schemas/SkillUsedBy"},"type":"array","title":"Used By"},"has_goal":{"type":"boolean","title":"Has Goal","default":false},"goal_label":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Goal Label"},"created_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Created At"},"updated_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Updated At"}},"type":"object","required":["id","slug","title","description","default_mode"],"title":"SkillListItem"},"SkillResponse":{"properties":{"id":{"type":"string","title":"Id"},"slug":{"type":"string","title":"Slug"},"title":{"type":"string","title":"Title"},"description":{"type":"string","title":"Description"},"default_mode":{"type":"string","title":"Default Mode"},"current_version":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Current Version"},"current_version_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Current Version Id"},"is_archived":{"type":"boolean","title":"Is Archived","default":false},"used_by_count":{"type":"integer","title":"Used By Count","default":0},"used_by":{"items":{"$ref":"#/components/schemas/SkillUsedBy"},"type":"array","title":"Used By"},"has_goal":{"type":"boolean","title":"Has Goal","default":false},"goal_label":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Goal Label"},"created_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Created At"},"updated_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Updated At"},"body":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Body"},"tool_allowlist":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Tool Allowlist"},"goal":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Goal"},"attachment_instruction":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Attachment Instruction"}},"type":"object","required":["id","slug","title","description","default_mode"],"title":"SkillResponse"},"SkillSourcesOverride":{"properties":{"web_search_mode":{"anyOf":[{"type":"string","enum":["off","model","kb_fallback","always"]},{"type":"null"}],"title":"Web Search Mode"},"live_source_ids":{"items":{"type":"string"},"type":"array","maxItems":20,"title":"Live Source Ids"},"live_mode":{"anyOf":[{"type":"string","enum":["model","kb_fallback","always"]},{"type":"null"}],"title":"Live Mode"}},"additionalProperties":false,"type":"object","title":"SkillSourcesOverride","description":"Per agent<->skill source override (``ai_agents.settings.skill_sources[skill_id]``).\n\n``web_search_mode`` null = inherit the agent default. ``live_source_ids``\nmust be ``live_source`` rows of THIS agent (checked in the service).\n``live_mode`` null = \"model\" when ``live_source_ids`` is non-empty."},"SkillSuggestionApproveRequest":{"properties":{"instructions":{"anyOf":[{"type":"string","maxLength":20000},{"type":"null"}],"title":"Instructions"}},"type":"object","title":"SkillSuggestionApproveRequest"},"SkillSuggestionDismissRequest":{"properties":{"note":{"anyOf":[{"type":"string","maxLength":2000},{"type":"null"}],"title":"Note"}},"type":"object","title":"SkillSuggestionDismissRequest"},"SkillSuggestionGenerateRequest":{"properties":{"skill_id":{"type":"string","maxLength":36,"minLength":1,"title":"Skill Id"},"days":{"type":"integer","maximum":90.0,"minimum":1.0,"title":"Days","default":30}},"type":"object","required":["skill_id"],"title":"SkillSuggestionGenerateRequest"},"SkillUpdate":{"properties":{"title":{"anyOf":[{"type":"string","maxLength":255,"minLength":1},{"type":"null"}],"title":"Title"},"description":{"anyOf":[{"type":"string","maxLength":1000,"minLength":1},{"type":"null"}],"title":"Description"},"default_mode":{"anyOf":[{"type":"string","enum":["always_on","model_selected","flow_only"]},{"type":"null"}],"title":"Default Mode"},"is_archived":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Is Archived"},"body":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Body"},"tool_allowlist":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Tool Allowlist"},"goal":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Goal"},"attachment_instruction":{"anyOf":[{"$ref":"#/components/schemas/SkillAttachmentInstruction"},{"type":"null"}]}},"additionalProperties":false,"type":"object","title":"SkillUpdate"},"SkillUsageAgent":{"properties":{"id":{"type":"string","title":"Id"},"name":{"type":"string","title":"Name"},"mode":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Mode"}},"type":"object","required":["id","name"],"title":"SkillUsageAgent"},"SkillUsageFlow":{"properties":{"id":{"type":"string","title":"Id"},"name":{"type":"string","title":"Name"},"status":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Status"},"node_ids":{"items":{"type":"string"},"type":"array","title":"Node Ids"}},"type":"object","required":["id","name"],"title":"SkillUsageFlow"},"SkillUsageResponse":{"properties":{"agents":{"items":{"$ref":"#/components/schemas/SkillUsageAgent"},"type":"array","title":"Agents"},"flows":{"items":{"$ref":"#/components/schemas/SkillUsageFlow"},"type":"array","title":"Flows"}},"type":"object","title":"SkillUsageResponse"},"SkillUsedBy":{"properties":{"agent_id":{"type":"string","title":"Agent Id"},"bot_id":{"type":"string","title":"Bot Id"},"name":{"type":"string","title":"Name"},"avatar_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Avatar Url"}},"type":"object","required":["agent_id","bot_id","name"],"title":"SkillUsedBy"},"SkillVersionListItem":{"properties":{"id":{"type":"string","title":"Id"},"skill_id":{"type":"string","title":"Skill Id"},"version":{"type":"integer","title":"Version"},"title":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Title"},"description":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Description"},"body_chars":{"type":"integer","title":"Body Chars","default":0},"tool_allowlist":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Tool Allowlist"},"goal":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Goal"},"attachment_instruction":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Attachment Instruction"},"is_current":{"type":"boolean","title":"Is Current","default":false},"created_by_user_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Created By User Id"},"created_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Created At"}},"type":"object","required":["id","skill_id","version"],"title":"SkillVersionListItem"},"SkillVersionResponse":{"properties":{"id":{"type":"string","title":"Id"},"skill_id":{"type":"string","title":"Skill Id"},"version":{"type":"integer","title":"Version"},"title":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Title"},"description":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Description"},"body_chars":{"type":"integer","title":"Body Chars","default":0},"tool_allowlist":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Tool Allowlist"},"goal":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Goal"},"attachment_instruction":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Attachment Instruction"},"is_current":{"type":"boolean","title":"Is Current","default":false},"created_by_user_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Created By User Id"},"created_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Created At"},"body":{"type":"string","title":"Body"}},"type":"object","required":["id","skill_id","version","body"],"title":"SkillVersionResponse"},"SmsContent":{"properties":{"channel_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Channel Id"},"message_type":{"type":"string","title":"Message Type","default":"text"},"message_text":{"type":"string","title":"Message Text"}},"type":"object","required":["message_text"],"title":"SmsContent","description":"SMS-specific campaign content."},"SmsPricelistResponse":{"properties":{"countries":{"items":{"$ref":"#/components/schemas/SmsPricelistRow"},"type":"array","title":"Countries"},"currency":{"type":"string","title":"Currency","default":"EUR"},"sendseven_margin_eur":{"type":"string","pattern":"^(?!^[-+.]*$)[+-]?0*\\d*\\.?\\d*$","title":"Sendseven Margin Eur"},"source":{"type":"string","title":"Source"},"last_updated":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Last Updated"},"supported_countries":{"items":{"type":"string"},"type":"array","title":"Supported Countries"}},"type":"object","required":["countries","sendseven_margin_eur","source"],"title":"SmsPricelistResponse","description":"Full SMS pricelist — every country with per-segment fees.\n\n``supported_countries`` is the canonical list of ISO-2 codes where\nSendSeven currently offers SMS sending; client code can rely on this\ninstead of filtering rows by ``is_supported``."},"SmsPricelistRow":{"properties":{"iso":{"type":"string","title":"Iso"},"name":{"type":"string","title":"Name"},"prefix":{"type":"string","title":"Prefix"},"platform_fee_eur":{"type":"string","pattern":"^(?!^[-+.]*$)[+-]?0*\\d*\\.?\\d*$","title":"Platform Fee Eur"},"sendseven_fee_eur":{"type":"string","pattern":"^(?!^[-+.]*$)[+-]?0*\\d*\\.?\\d*$","title":"Sendseven Fee Eur"},"total_per_segment_eur":{"type":"string","pattern":"^(?!^[-+.]*$)[+-]?0*\\d*\\.?\\d*$","title":"Total Per Segment Eur"},"is_supported":{"type":"boolean","title":"Is Supported","default":false}},"type":"object","required":["iso","name","prefix","platform_fee_eur","sendseven_fee_eur","total_per_segment_eur"],"title":"SmsPricelistRow"},"SmsSenderResponse":{"properties":{"channel_id":{"type":"string","title":"Channel Id","description":"Channel UUID"},"name":{"type":"string","title":"Name","description":"Human-readable sender name (Channel.name)"},"sender_type":{"type":"string","enum":["dedicated","verified_own","alphanumeric","shared_pool"],"title":"Sender Type","description":"dedicated | verified_own | alphanumeric | shared_pool"},"mode":{"type":"string","enum":["send_only","send_and_receive"],"title":"Mode","description":"Operating mode. Legacy rows without config.mode default to send_and_receive; alphanumeric senders are always send_only."},"msisdn_or_alpha":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Msisdn Or Alpha","description":"The MSISDN or alphanumeric sender id. null for shared_pool — seven.io picks a pool number per send."},"can_receive":{"type":"boolean","title":"Can Receive","description":"True iff replies route back into a conversation. False for alphanumeric and for shared_pool in send_only mode."},"inbound_sms_rate_eur":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Inbound Sms Rate Eur","description":"The tenant's per-SMS rate in EUR from their pricing model (cost_message_sms, falling back to cost_message). Populated only when can_receive is true — a routed inbound reply is billed as an ordinary SMS at this rate, NOT as a separate surcharge. null when the sender cannot receive or the rate cannot be resolved."},"is_active":{"type":"boolean","title":"Is Active","description":"False for archived / disabled senders"},"is_verified":{"type":"boolean","title":"Is Verified"},"is_default_for_send":{"type":"boolean","title":"Is Default For Send","description":"True for the ONE sender an auto-selected send will use (see SMS_SENDER_PRIORITY in app/services/sms/sender_identity.py). Never true for an inactive sender."},"created_at":{"type":"string","format":"date-time","title":"Created At"}},"type":"object","required":["channel_id","name","sender_type","mode","can_receive","is_active","is_verified","is_default_for_send","created_at"],"title":"SmsSenderResponse","description":"One configured SMS sender.\n\n**SECURITY — hand-whitelisted projection.** ``ChannelResponse`` in\n``app/schemas/channel.py`` deliberately excludes ``Channel.config``\nbecause it holds API tokens, secrets and credentials that must NEVER be\nreturned via the API. This schema exists so the frontend can see the few\nnon-secret SMS facts it needs (``sender_type``, ``mode``,\n``msisdn_or_alpha``) WITHOUT that ban being relaxed. Every field below is\nnamed explicitly — the endpoint never dumps ``config``, and in particular\nnever exposes ``seven_io_subaccount_id``. Do not add a passthrough field."},"SmtpConfig":{"properties":{"smtp_host":{"type":"string","title":"Smtp Host","description":"SMTP server hostname"},"smtp_port":{"type":"integer","title":"Smtp Port","description":"SMTP port (25, 465, 587)","default":587},"smtp_username":{"type":"string","title":"Smtp Username","description":"SMTP username"},"smtp_password":{"type":"string","title":"Smtp Password","description":"SMTP password"},"smtp_use_tls":{"type":"boolean","title":"Smtp Use Tls","description":"Use TLS encryption","default":true}},"type":"object","required":["smtp_host","smtp_username","smtp_password"],"title":"SmtpConfig","description":"SMTP configuration for Type 2 integrations"},"SnoozeConversationRequest":{"properties":{"snoozed_until":{"type":"string","format":"date-time","title":"Snoozed Until","description":"Timezone-aware ISO datetime when the snooze should expire. Must be in the future. After this moment the conversation behaves as a normal open conversation again."},"reopen_on_message":{"type":"boolean","title":"Reopen On Message","description":"When true, an inbound customer message arriving while the conversation is snoozed clears the snooze and returns it to the Open tab (handled by the inbound-message worker).","default":false}},"type":"object","required":["snoozed_until"],"title":"SnoozeConversationRequest","description":"Request body for snoozing (postponing) a conversation.\n\nSnooze is a derived state: the conversation status remains ``open`` while\nsnoozed; it is hidden from the Open tab and surfaced on the Snoozed tab\nuntil ``snoozed_until`` passes (query-time expiry, no background job).","example":{"reopen_on_message":true,"snoozed_until":"2026-06-23T09:00:00Z"}},"SocialCommentListResponse":{"properties":{"items":{"items":{"$ref":"#/components/schemas/SocialCommentResponse"},"type":"array","title":"Items","description":"List of items for this page"},"pagination":{"$ref":"#/components/schemas/PaginationMeta","description":"Pagination metadata"}},"type":"object","required":["items","pagination"],"title":"SocialCommentListResponse","description":"`{ \"items\": [...], \"pagination\": {...} }` for both comment listings."},"SocialCommentResponse":{"properties":{"created_at":{"type":"string","title":"Created At","description":"When the resource was created (UTC)"},"updated_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Updated At","description":"When the resource was last updated (UTC)"},"id":{"type":"string","title":"Id","description":"Unique identifier (UUID)"},"tenant_id":{"type":"string","title":"Tenant Id","description":"Tenant this resource belongs to"},"social_post_id":{"type":"string","title":"Social Post Id","description":"Our id for the post it was left on."},"channel_id":{"type":"string","title":"Channel Id","description":"Channel the post belongs to."},"external_comment_id":{"type":"string","title":"External Comment Id","description":"Meta's comment id. **This is the value to pass to `POST /comments/{comment_id}/private-reply`**, and the value that appears on the reply message as `meta.comment_id`."},"parent_external_comment_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Parent External Comment Id","description":"Meta's id of the comment this one replies to. Null for a top-level comment."},"is_reply":{"type":"boolean","title":"Is Reply","description":"True when this comment is a threaded reply to another comment (`parent_external_comment_id` is set).","default":false},"author_external_id":{"type":"string","title":"Author External Id","description":"The commenter's platform-scoped id (Instagram IGSID / Facebook PSID)."},"author_username":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Author Username","description":"The commenter's username/handle, when Meta supplied one."},"is_from_owner":{"type":"boolean","title":"Is From Owner","description":"True when the comment is from the connected account itself — most often our own public reply, which Meta echoes back as an ordinary comment. Such comments never count as unanswered and are hidden from listings unless `include_owner=true`.","default":false},"contact_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Contact Id","description":"The commenter as a contact in your workspace. Null until they are resolved — which happens the first time we message them."},"text":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Text","description":"The comment text. Null for a media-only comment."},"attachment_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Attachment Url","description":"URL of the comment's media, for a photo/sticker/video comment (these carry no text). A Facebook feature — Instagram comments cannot contain media. May be a signed CDN URL that expires."},"attachment_type":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Attachment Type","description":"Kind of `attachment_url`: `image` (photos and stickers) or `video`. Null when the comment has no media."},"state":{"type":"string","title":"State","description":"`pending` (unanswered) · `auto_replied` (a rule or flow answered it) · `replied` (an agent answered it) · `handled` (cleared without a private reply) · `ignored` (our own comment, or dismissed)."},"commented_at":{"type":"string","title":"Commented At","description":"Meta's timestamp for the comment — the business time."},"replied_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Replied At","description":"When the private reply was sent."},"replied_by_user_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Replied By User Id","description":"The agent who replied, or who marked the comment handled. Null for an automated reply and for API-token callers."},"handled_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Handled At","description":"When the comment was marked handled or ignored."},"private_reply_window_expires_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Private Reply Window Expires At","description":"End of Meta's 7-day private-reply window, measured from `commented_at`. Null when Meta gave no reliable comment timestamp."},"private_reply_available":{"type":"boolean","title":"Private Reply Available","description":"Whether a private reply can still be sent right now. **False is the normal end state of most comments**, not an error — see `private_reply_unavailable_reason` for which one applies.","default":false},"private_reply_unavailable_reason":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Private Reply Unavailable Reason","description":"Why `private_reply_available` is false, so the UI can say it rather than just grey out a button:\n\n* `already_replied` — a private reply was already sent. Meta permits exactly one per comment, **forever**. Permanent.\n* `window_expired` — the 7-day window closed. Permanent.\n* `window_unknown` — Meta gave no reliable comment timestamp, so we cannot prove the window is open; we fail closed rather than offer a reply that Meta would reject.\n* `marked_handled` — an agent cleared it without replying. Reversible: reopen it with `PATCH /comments/{comment_id}`.\n* `ignored` — our own comment, or dismissed by a rule. Also reversible.\n\nNull when a private reply IS available."},"private_reply_message_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Private Reply Message Id","description":"`messages.id` of the private reply that answered this comment — the link from a comment to its reply. Set for a reply we sent ourselves; null for a flow hand-off, where the flow's Send node owns the outbound message."},"private_reply":{"anyOf":[{"$ref":"#/components/schemas/PrivateReplyMessageRef"},{"type":"null"}],"description":"The reply message itself, hydrated for rendering. Omitted when `include_reply=false`, and null when the message row is gone — message retention can outrun comment retention, and the comment must keep saying it WAS answered."},"public_reply_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Public Reply Id","description":"Meta's comment id of our public reply, when one was posted. Reserved: public replies are not part of the current release."},"post":{"anyOf":[{"$ref":"#/components/schemas/SocialPostRef"},{"type":"null"}],"description":"The post this comment was left on. Included where the caller has no post context (the cross-post inbox, and a single comment); omitted from the per-post thread, where it would repeat."}},"type":"object","required":["created_at","id","tenant_id","social_post_id","channel_id","external_comment_id","author_external_id","state","commented_at"],"title":"SocialCommentResponse","description":"One comment plus everything the UI needs to render its row.\n\nDeliberately answers three questions a list row has to answer on its own:\n*what was said*, *what state is it in*, and *can I still privately reply —\nand if not, why not*."},"SocialCommentRuleAction":{"type":"string","enum":["send_private_reply","start_flow"],"title":"SocialCommentRuleAction","description":"What a matching rule does. Exactly two options, on purpose.\n\n``send_private_reply`` — the 90% case: one static (variable-substituted)\nDM. Text-only, because Meta's private-reply API is text-only.\n\n``start_flow`` — the escape hatch for \"one entry message, then wait for the\nuser's answer\". Hands off to the existing flow engine rather than growing a\nsecond conversation runtime inside the rules engine."},"SocialCommentRuleButton":{"properties":{"type":{"type":"string","enum":["url","postback"],"title":"Type","description":"`url` opens a link; `postback` sends a value back to you.","default":"postback"},"title":{"type":"string","maxLength":20,"minLength":1,"title":"Title","description":"Button label (Meta limit: 20 characters)."},"url":{"anyOf":[{"type":"string","maxLength":2000},{"type":"null"}],"title":"Url","description":"Destination of a `url` button. Absolute **https** URL, required for that type and rejected on a `postback` button."},"action":{"anyOf":[{"type":"string","const":"start_flow"},{"type":"null"}],"title":"Action","description":"What a `postback` button does. `start_flow` starts `flow_id` for the person who tapped. Implied when `flow_id` is set."},"flow_id":{"anyOf":[{"type":"string","maxLength":36},{"type":"null"}],"title":"Flow Id","description":"Flow started by a `start_flow` button. Must be published and in this workspace, and must NOT be a \"Comment received\" flow — that trigger is reserved for the first message, whose one allowed private reply is the message carrying this button."},"payload":{"anyOf":[{"type":"string","maxLength":1000},{"type":"null"}],"title":"Payload","description":"Opaque value returned to you on tap (as `meta.button_id`). Defaults to the label. Mutually exclusive with `action`/`flow_id`."},"attachment_id":{"anyOf":[{"type":"string","maxLength":64},{"type":"null"}],"title":"Attachment Id","description":"Editor-side hint on a `url` button whose URL is an attachment's public link (\"file button\"). Stored so the editor can re-open the button as a file; never sent to the recipient. The link is not reference-counted — deleting the attachment later breaks it."},"source":{"anyOf":[{"type":"string","maxLength":64},{"type":"null"}],"title":"Source","description":"Editor-side hint naming where a `url` button's link came from (e.g. `attachment`). Stored, never sent to the recipient."}},"additionalProperties":false,"type":"object","required":["title"],"title":"SocialCommentRuleButton","description":"One button on a rule's private reply.\n\nShape-only validation lives here (enum membership, lengths). Everything\nneeding the database or the channel — \"is this an Instagram channel?\",\n\"does this flow exist in this tenant and can it be started from a button?\"\n— is enforced by ``private_reply_buttons.validate_and_normalize_buttons``\nin the service layer, which is the SAME validator the manual\n``POST /comments/{id}/private-reply`` endpoint runs. There is exactly one\nset of button rules in the product."},"SocialCommentRuleCreate":{"properties":{"name":{"type":"string","maxLength":255,"minLength":1,"title":"Name","description":"Operator-facing label, e.g. \"Summer sale — send the code\"."},"enabled":{"type":"boolean","title":"Enabled","description":"Disabled rules are skipped entirely during evaluation.","default":true},"priority":{"type":"integer","maximum":1000.0,"minimum":0.0,"title":"Priority","description":"Evaluation order — LOWER runs FIRST (same convention as flows). The first matching rule wins; no further rule is evaluated. Ties are broken by specificity (a post-scoped, keyworded rule beats a catch-all), then by age.","default":0},"post_scope":{"$ref":"#/components/schemas/SocialCommentRulePostScope","description":"Watch every post on the channel, or only selected posts.","default":"all_posts"},"post_ids":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Post Ids","description":"Post identifiers to watch when post_scope is specific_posts. These are the platform's own post ids as returned by the channel's posts/media listing endpoint."},"match_mode":{"$ref":"#/components/schemas/SocialCommentRuleMatchMode","description":"any_comment reacts to every comment (no keywords needed, and it also matches comments with no text at all). contains / exact / starts_with compare against the keyword list, case-insensitively.","default":"any_comment"},"keywords":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Keywords","description":"Keywords to match. Ignored when match_mode is any_comment."},"action":{"$ref":"#/components/schemas/SocialCommentRuleAction","description":"Send a private reply, or hand the comment off to a flow.","default":"send_private_reply"},"reply_text":{"anyOf":[{"type":"string","maxLength":1000},{"type":"null"}],"title":"Reply Text","description":"The private reply to send. Supports the same {{placeholders}} as the flow Send node — for example {{contact.first_name}} — plus {{comment.text}}, {{comment.author}} and {{post.caption}}."},"flow_id":{"anyOf":[{"type":"string","maxLength":36},{"type":"null"}],"title":"Flow Id","description":"Flow to start when action is start_flow. The flow's trigger must be \"Comment received\"."},"post_message_overrides":{"anyOf":[{"additionalProperties":{"type":"string"},"type":"object"},{"type":"null"}],"title":"Post Message Overrides","description":"Optional per-post wording: {post_id: message}. A post listed here gets that message instead of reply_text; every other post falls back to reply_text."},"reply_buttons":{"anyOf":[{"items":{"$ref":"#/components/schemas/SocialCommentRuleButton"},"type":"array","maxItems":3},{"type":"null"}],"title":"Reply Buttons","description":"Up to 3 buttons attached to the private reply. Supported on both Facebook Messenger and Instagram. A control the rule's channel cannot carry is rejected when the rule is saved, not silently stripped when it fires. Only meaningful when action is send_private_reply."},"reply_quick_replies":{"anyOf":[{"items":{"$ref":"#/components/schemas/SocialCommentRuleQuickReply"},"type":"array","maxItems":13},{"type":"null"}],"title":"Reply Quick Replies","description":"DEPRECATED / IGNORED. Comment private replies are buttons-only since 2026-08-13. This field is still accepted for backwards compatibility so older clients do not get a 422, but it is never validated, stored, or sent — use reply_buttons instead."},"channel_id":{"type":"string","maxLength":36,"title":"Channel Id","description":"The Instagram or Facebook channel this rule watches."}},"type":"object","required":["name","channel_id"],"title":"SocialCommentRuleCreate","description":"POST body. ``channel_id`` is immutable, so it only exists here."},"SocialCommentRuleMatchMode":{"type":"string","enum":["any_comment","contains","exact","starts_with"],"title":"SocialCommentRuleMatchMode","description":"How ``keywords`` are compared against the comment text.\n\n``contains`` / ``exact`` / ``starts_with`` are the EXACT modes\n``match_comment_trigger`` already implements (case-insensitive, trimmed).\n``any_comment`` is the \"reply to everything\" option: it needs no keywords\nand matches even a text-less (image / sticker) comment."},"SocialCommentRulePostScope":{"type":"string","enum":["all_posts","specific_posts"],"title":"SocialCommentRulePostScope","description":"Which posts a rule watches. Stored LOWERCASE (values_callable).\n\nValues are IDENTICAL to the ``scope`` values of the ``comment_received``\nflow trigger config so a rule can be handed to ``match_comment_trigger``\nverbatim."},"SocialCommentRuleQuickReply":{"properties":{"content_type":{"type":"string","const":"text","title":"Content Type","description":"Meta's chip kind. Only `text` is supported; the field exists so Meta's own documented payload validates unchanged.","default":"text"},"title":{"type":"string","maxLength":20,"minLength":1,"title":"Title","description":"Chip label (Meta limit: 20 characters, truncated beyond that)."},"action":{"anyOf":[{"type":"string","const":"start_flow"},{"type":"null"}],"title":"Action","description":"What the chip does. `start_flow` starts `flow_id` for the person who tapped. Implied when `flow_id` is set."},"flow_id":{"anyOf":[{"type":"string","maxLength":36},{"type":"null"}],"title":"Flow Id","description":"Flow started by a `start_flow` chip. Must be published and in this workspace, and must NOT be a \"Comment received\" flow — that trigger is reserved for the first message, whose one allowed private reply is the message carrying this chip."},"payload":{"anyOf":[{"type":"string","maxLength":1000},{"type":"null"}],"title":"Payload","description":"Opaque value returned to you on tap (as `meta.button_id`). Defaults to the label. Mutually exclusive with `action`/`flow_id`."}},"additionalProperties":false,"type":"object","required":["title"],"title":"SocialCommentRuleQuickReply","description":"One quick-reply chip on a rule's private reply.\n\nThe sibling of :class:`SocialCommentRuleButton`, and the same division of\nlabour: shape-only validation here, everything needing the database or the\nchannel in ``private_reply_buttons.validate_and_normalize_quick_replies``\n— the SAME validator the manual endpoint runs.\n\nDiffers from a button exactly where Meta differs: 13 permitted instead of\n3, a `content_type` instead of a `type`, and **no URL kind** (Meta's quick\nreply object has no URL variant). A chip that starts a flow behaves\nidentically to a button that starts a flow, including the ban on\n\"Comment received\" flows."},"SocialCommentRuleUpdate":{"properties":{"name":{"anyOf":[{"type":"string","maxLength":255,"minLength":1},{"type":"null"}],"title":"Name"},"enabled":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Enabled"},"priority":{"anyOf":[{"type":"integer","maximum":1000.0,"minimum":0.0},{"type":"null"}],"title":"Priority"},"post_scope":{"anyOf":[{"$ref":"#/components/schemas/SocialCommentRulePostScope"},{"type":"null"}]},"post_ids":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Post Ids"},"match_mode":{"anyOf":[{"$ref":"#/components/schemas/SocialCommentRuleMatchMode"},{"type":"null"}]},"keywords":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Keywords"},"action":{"anyOf":[{"$ref":"#/components/schemas/SocialCommentRuleAction"},{"type":"null"}]},"reply_text":{"anyOf":[{"type":"string","maxLength":1000},{"type":"null"}],"title":"Reply Text"},"flow_id":{"anyOf":[{"type":"string","maxLength":36},{"type":"null"}],"title":"Flow Id"},"post_message_overrides":{"anyOf":[{"additionalProperties":{"type":"string"},"type":"object"},{"type":"null"}],"title":"Post Message Overrides"},"reply_buttons":{"anyOf":[{"items":{"$ref":"#/components/schemas/SocialCommentRuleButton"},"type":"array","maxItems":3},{"type":"null"}],"title":"Reply Buttons"},"reply_quick_replies":{"anyOf":[{"items":{"$ref":"#/components/schemas/SocialCommentRuleQuickReply"},"type":"array","maxItems":13},{"type":"null"}],"title":"Reply Quick Replies"}},"additionalProperties":false,"type":"object","title":"SocialCommentRuleUpdate","description":"PATCH body — every field optional.\n\nDeliberately NOT a subclass of the base: inheriting would drag the base's\ndefaults in, and a default is indistinguishable from an omitted field in a\nPATCH. Here, \"absent\" genuinely means \"leave it alone\" (the endpoint uses\n``exclude_unset``), so a caller can toggle ``enabled`` without restating the\nrule. ``channel_id`` is absent on purpose: moving a rule to another channel\nsilently changes which posts it can match, so that is a delete + create."},"SocialCommentTriageRequest":{"properties":{"state":{"type":"string","title":"State","description":"The new state:\n\n* `handled` — you answered publicly or off-platform. Clears it from the unanswered queue.\n* `ignored` — not actionable; skip it.\n* `pending` — reopen a comment that was marked handled or ignored by mistake.\n\n`replied` and `auto_replied` are **not** settable here: they mean a private reply message exists, and are set by actually sending one.","example":"handled"}},"type":"object","required":["state"],"title":"SocialCommentTriageRequest","description":"Body of `PATCH /comments/{comment_id}` — clear a comment by hand."},"SocialInsightsMetrics":{"properties":{"reach":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Reach"},"impressions":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Impressions"},"engagement":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Engagement"},"likes":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Likes"},"comments":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Comments"},"shares":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Shares"},"saves":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Saves"},"video_views":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Video Views"},"clicks":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Clicks"}},"type":"object","title":"SocialInsightsMetrics","description":"Platform-agnostic metric snapshot. A `null` field means the platform (or\nthis object type) does NOT expose that metric — it is never fabricated.\n\nInstagram media exposes reach + engagement (`total_interactions`) + likes /\ncomments / shares / saves, but NOT `impressions` on the current API. A\nFacebook post/page exposes reach + impressions + engagement via `/insights`;\nper-post likes/comments/shares come from a different edge and stay `null` in\nPhase 1."},"SocialInsightsTarget":{"properties":{"channel_id":{"type":"string","title":"Channel Id","description":"The channel this target published to."},"platform":{"type":"string","title":"Platform","description":"`instagram` or `messenger`."},"object_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Object Id","description":"Meta's id read (IG media id / FB `{page}_{post}`)."},"authorized":{"type":"boolean","title":"Authorized","description":"False when the connection lacks the insights permission — the frontend should prompt a reconnect. See `error_code`.","default":true},"error_code":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Error Code","description":"`insights_not_authorized` (reconnect to grant insights), `insights_unavailable` (Meta has no insights for this object), `insights_unsupported_platform`, or `insights_error`. Null on success."},"message":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Message","description":"Human-readable detail when `error_code` is set."},"metrics":{"$ref":"#/components/schemas/SocialInsightsMetrics"},"last_refreshed_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Last Refreshed At","description":"When this target's cached numbers were last refreshed from Meta. Null if this target has never been cached."}},"type":"object","required":["channel_id","platform"],"title":"SocialInsightsTarget","description":"Insights for one published target (one channel) of an authored post."},"SocialPostInsights":{"properties":{"reach":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Reach"},"impressions":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Impressions"},"engagement":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Engagement"},"likes":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Likes"},"comments":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Comments"},"shares":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Shares"},"saves":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Saves"},"video_views":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Video Views"},"clicks":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Clicks"},"authorized":{"type":"boolean","title":"Authorized","description":"False when the connection lacks the insights permission — the frontend should prompt a reconnect. See `error_code`.","default":true},"error_code":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Error Code","description":"`insights_not_authorized` (reconnect to grant insights), `insights_unavailable` (Meta has no insights for this object), `insights_unsupported_platform` (channel is not IG/FB), or `insights_error`. Null on success / before any refresh."},"last_refreshed_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Last Refreshed At","description":"When these numbers were last refreshed from Meta. Null if this post's insights have never been refreshed (metrics are then null except the public-counter fallback for likes/comments)."}},"type":"object","title":"SocialPostInsights","description":"Cached Meta insights for one comment-synced post (the Posts-LIST view).\n\nFLAT by design: the Posts list renders each metric as its own cell, so the\nnine metrics sit at the top level rather than under a nested ``metrics``\nobject (this differs from the authored-post ``SocialInsightsTarget`` in\n``schemas/social_post.py``, which groups them under ``metrics``). Every\nmetric is ``int | null`` — ``null`` means Meta (or this object type) does\nnot expose that metric, or it has not been fetched yet; a metric is never\nfabricated from a different one.\n\nServed from the ``social_posts.insights_*`` cache columns — the LIST endpoint\nmakes NO live Graph call on load. ``likes`` / ``comments`` fall back to the\npost's public counters (``like_count`` / ``comments_count``) when the richer\ninsights value has not been cached yet, so the list shows a number before the\nfirst insights refresh; a refreshed insights value always wins over the\npublic counter."},"SocialPostInsightsBulkRefreshResponse":{"properties":{"refreshed":{"type":"integer","title":"Refreshed","description":"How many posts were refreshed."},"items":{"items":{"$ref":"#/components/schemas/SocialPostInsightsRefreshResponse"},"type":"array","title":"Items","description":"One entry per refreshed post, most recent first."}},"type":"object","required":["refreshed"],"title":"SocialPostInsightsBulkRefreshResponse","description":"`POST /comments/posts/insights/refresh` — bulk refresh outcome.\n\nRefreshes the tenant's most-recent posts (bounded by ``limit``) to avoid a\nGraph storm, and returns each refreshed post with its updated insights."},"SocialPostInsightsRefreshResponse":{"properties":{"post_id":{"type":"string","title":"Post Id","description":"Our post id."},"insights":{"$ref":"#/components/schemas/SocialPostInsights","description":"The freshly-refreshed (and re-cached) insights for the post."}},"type":"object","required":["post_id","insights"],"title":"SocialPostInsightsRefreshResponse","description":"`POST /comments/posts/{post_id}/insights/refresh` — one post's fresh insights."},"SocialPostInsightsSummaryResponse":{"properties":{"last_30_days":{"$ref":"#/components/schemas/SocialPostInsightsWindow"},"all_time":{"$ref":"#/components/schemas/SocialPostInsightsWindow"},"last_refreshed_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Last Refreshed At","description":"The most recent ``insights_refreshed_at`` across the tenant's posts — how fresh the freshest number in this summary is. Null when no post has ever been refreshed."}},"type":"object","required":["last_30_days","all_time"],"title":"SocialPostInsightsSummaryResponse","description":"`GET /comments/posts/insights/summary` — aggregate insights totals.\n\nTotals across the tenant's comment-synced posts, for two windows. The\n30-day window sums posts published (``posted_at``) in the last 30 days; the\nall-time window sums every non-deleted post. Because per-post insights are\nMeta LIFETIME totals, the window selects WHICH posts are summed, not a daily\nseries."},"SocialPostInsightsWindow":{"properties":{"reach":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Reach"},"impressions":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Impressions"},"engagement":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Engagement"},"likes":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Likes"},"comments":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Comments"},"shares":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Shares"},"saves":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Saves"},"video_views":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Video Views"},"clicks":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Clicks"},"post_count":{"type":"integer","title":"Post Count","description":"Posts in this window that have cached insights (``insights_refreshed_at`` set) and therefore contributed to the totals above.","default":0}},"type":"object","title":"SocialPostInsightsWindow","description":"Summed insights across a set of comment-synced posts (one time window).\n\nPer-post insights are Meta LIFETIME totals, so a window filters WHICH posts\nare summed, NOT a per-day series — the totals are lifetime numbers for the\nposts in scope. Each metric sums only the non-null cached values (so a metric\na platform never exposes never distorts the total); a metric is null only\nwhen NO post in scope has a value for it. ``post_count`` is how many posts\nhave cached insights and thus contributed."},"SocialPostListResponse":{"properties":{"items":{"items":{"$ref":"#/components/schemas/SocialPostResponse"},"type":"array","title":"Items","description":"List of items for this page"},"pagination":{"$ref":"#/components/schemas/PaginationMeta","description":"Pagination metadata"}},"type":"object","required":["items","pagination"],"title":"SocialPostListResponse","description":"`GET /comments/posts` — `{ \"items\": [...], \"pagination\": {...} }`."},"SocialPostRef":{"properties":{"id":{"type":"string","title":"Id","description":"Our post id."},"external_post_id":{"type":"string","title":"External Post Id","description":"Meta's post id."},"page_name":{"type":"string","title":"Page Name","description":"Display name of the connected Page / Instagram account this post belongs to — the post owner (your connected account, not the commenter). Falls back to a channel-type label when the channel has no set name.","default":""},"page_avatar_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Page Avatar Url","description":"Profile-picture URL of the connected Page / Instagram account. Nullable — a Meta CDN URL that can expire, and null for channels connected before the avatar was captured."},"permalink":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Permalink","description":"Public URL of the post."},"caption":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Caption","description":"The post's caption, if any."},"media_type":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Media Type","description":"Meta's media type."},"thumbnail_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Thumbnail Url","description":"Card image for the post — a permanent URL on our own domain when the image was mirrored, Meta's expiring CDN URL otherwise."},"posted_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Posted At","description":"When the post was published on Meta."},"like_count":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Like Count","description":"Meta's total reaction count on the post. Nullable — null until the post has been read from Meta."},"comments_count":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Comments Count","description":"Meta's total comment count on the post. Nullable. DISTINCT from the count of comment rows WE stored (see `SocialPostResponse`)."}},"type":"object","required":["id","external_post_id"],"title":"SocialPostRef","description":"Compact post reference embedded in a comment row.\n\nPresent on the cross-post inbox and on a single comment, where the reader\nhas no post context; omitted from the per-post thread, where it would repeat\non every row."},"SocialPostResponse":{"properties":{"created_at":{"type":"string","title":"Created At","description":"When the resource was created (UTC)"},"updated_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Updated At","description":"When the resource was last updated (UTC)"},"id":{"type":"string","title":"Id","description":"Unique identifier (UUID)"},"tenant_id":{"type":"string","title":"Tenant Id","description":"Tenant this resource belongs to"},"channel_id":{"type":"string","title":"Channel Id","description":"The Instagram or Facebook Messenger channel this post belongs to."},"page_name":{"type":"string","title":"Page Name","description":"Display name of the connected Page / Instagram account this post belongs to — the **post owner**, i.e. YOUR connected account, not the commenter. Taken from the channel's name, falling back to a channel-type label (\"Instagram\" / \"Facebook Page\") when the channel has no set name.","default":""},"page_avatar_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Page Avatar Url","description":"Profile-picture URL of the connected Page / Instagram account (the post owner). **Nullable** — a Meta CDN URL that can expire, and null for channels connected before the avatar was captured or when Meta returned none. Fall back to initials/`page_name` when absent."},"external_post_id":{"type":"string","title":"External Post Id","description":"Meta's post id — an Instagram media id, or a Facebook `{page_id}_{post_id}`."},"permalink":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Permalink","description":"Public URL of the post on Instagram / Facebook."},"caption":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Caption","description":"The post's caption, if any."},"media_type":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Media Type","description":"Meta's media type — `image`, `video`, `carousel_album`, `reel`, `story`, `link`, … Not an enum: Meta adds new ones."},"media_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Media Url","description":"Meta CDN URL of the media. **These URLs expire** — treat them as a cache, not as a durable reference, and fall back to `permalink`."},"thumbnail_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Thumbnail Url","description":"URL of the post's card image. Normally a **permanent** URL on our own domain: we mirror the image once, because Meta's own CDN URLs expire. Falls back to Meta's expiring CDN URL for a post whose image could not be mirrored (it had already expired, or the post has no image at all)."},"posted_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Posted At","description":"When the post was published on Meta — not when we learned about it. Null for a post Meta gave no timestamp for; such posts sort last."},"like_count":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Like Count","description":"Total reactions Meta reports on the post (all reaction types on Facebook, likes on Instagram). **Nullable** — null until the post has been read from Meta, and it can lag reality between syncs. This is Meta's public number, not derived from anything we store."},"comments_count":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Comments Count","description":"Total comments Meta reports on the post. **Nullable** — null until read from Meta. DISTINCT from `comment_count`: `comment_count` is how many comments WE have stored (only those Meta delivered us), while `comments_count` is Meta's own total (includes nested replies and comments we were never sent), so the two legitimately differ."},"comment_count":{"type":"integer","title":"Comment Count","description":"Total comments we have stored for this post.","default":0},"unanswered_count":{"type":"integer","title":"Unanswered Count","description":"Comments still in `pending` — the badge the Comments UI shows. Advisory UI decoration maintained incrementally on ingestion and recomputed whenever a comment is triaged or purged; never used for billing.","default":0},"counters_synced_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Counters Synced At","description":"Last time the two counters were recomputed from the comment rows. Null means they have only ever been maintained incrementally."},"insights":{"anyOf":[{"$ref":"#/components/schemas/SocialPostInsights"},{"type":"null"}],"description":"Cached Meta insights for this post (reach, impressions, engagement, likes, comments, shares, saves, video_views, clicks) plus the authorization / freshness envelope. Served from the cache columns — NEVER a live Graph call on list load. Use `POST /comments/posts/{post_id}/insights/refresh` to force a fresh read. Null only for a post whose channel type carries no insights."}},"type":"object","required":["created_at","id","tenant_id","channel_id","external_post_id"],"title":"SocialPostResponse","description":"One Instagram media / Facebook Page post we have comments for."},"SourceType":{"type":"string","enum":["provider","integration"],"title":"SourceType","description":"Source type indicating which table the connection came from"},"StartRunRequest":{"properties":{"contact_id":{"type":"string","title":"Contact Id","description":"Contact UUID on the tenant's shard"},"idempotency_key":{"anyOf":[{"type":"string","maxLength":128},{"type":"null"}],"title":"Idempotency Key","description":"Optional caller-supplied idempotency key for at-most-once start semantics. If a run with the same (tenant_id, idempotency_key) already exists, the existing run is returned with HTTP 200 instead of creating a new one. If omitted, the engine derives a key from the request payload (e.g. ``manual:<flow_id>:<contact_id>:<request_id>``)."},"context":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Context","description":"Extra variables to seed ``flow_runs.context.vars``. Hard-capped at 16 KB JSON-encoded."},"contact_method_overrides":{"anyOf":[{"additionalProperties":{"type":"string"},"type":"object"},{"type":"null"}],"title":"Contact Method Overrides","description":"Optional map of ``ChannelType`` (string value, e.g. ``email``, ``sms``, ``whatsapp``, ``telegram``, ``messenger``, ``instagram``, ``browser_push``) → ``contact_method.id``. When the flow sends on the matching channel, this specific contact_method's value is used as the destination instead of the contact's primary method. Per-channel keys are independent; omitted channels fall back to the default (primary) resolution. Each referenced contact_method must belong to the same tenant, the same contact, and have a method_type that matches the channel key."},"channel_id":{"anyOf":[{"type":"string","maxLength":64},{"type":"null"}],"title":"Channel Id","description":"Optional channel the run is started on (an active channel of this workspace). It becomes the run's originating/current channel: Send steps prefer it (when the flow prefers the originating channel) and Tag & Update entries scoped to the \"current channel\" use it."},"contact_method_id":{"anyOf":[{"type":"string","maxLength":64},{"type":"null"}],"title":"Contact Method Id","description":"Optional contact method (of this contact) the run is started with. With ``channel_id`` it must be able to address that channel, and it is also applied as the contact_method_overrides pin for that channel type unless one was given explicitly."}},"type":"object","required":["contact_id"],"title":"StartRunRequest","description":"Request body for ``POST /flows/{flow_id}/runs``."},"StatusCount":{"properties":{"status":{"type":"string","title":"Status","description":"Subscription status (subscribed, pending, unsubscribed, bounced, complained)"},"count":{"type":"integer","title":"Count","description":"Number of subscriptions with this status"}},"type":"object","required":["status","count"],"title":"StatusCount","description":"Count for a single subscription status"},"SubscriberOverviewResponse":{"properties":{"total_subscriptions":{"type":"integer","title":"Total Subscriptions","description":"Total subscription records","default":0},"by_status":{"items":{"$ref":"#/components/schemas/StatusCount"},"type":"array","title":"By Status","description":"Counts by subscription status"},"by_channel":{"items":{"$ref":"#/components/schemas/ChannelStatusBreakdown"},"type":"array","title":"By Channel","description":"Breakdown by channel type and status"},"by_list":{"items":{"$ref":"#/components/schemas/ListStatusBreakdown"},"type":"array","title":"By List","description":"Breakdown by list and status"},"period_start":{"type":"string","title":"Period Start","description":"Start of the reporting period (ISO format)"},"period_end":{"type":"string","title":"Period End","description":"End of the reporting period (ISO format)"}},"type":"object","required":["period_start","period_end"],"title":"SubscriberOverviewResponse","description":"Subscriber overview with counts by status, channel, and list.\n\nReturned by GET /api/v1/analytics/subscribers/overview","example":{"by_channel":[{"bounced":100,"channel_type":"email","complained":40,"pending":200,"subscribed":2500,"total":3140,"unsubscribed":300}],"by_list":[{"bounced":50,"complained":10,"list_id":"abc-123","list_name":"Weekly Newsletter","pending":100,"subscribed":1500,"total":1860,"unsubscribed":200}],"by_status":[{"count":4000,"status":"subscribed"},{"count":500,"status":"unsubscribed"},{"count":300,"status":"pending"},{"count":150,"status":"bounced"},{"count":50,"status":"complained"}],"period_end":"2026-03-08T00:00:00Z","period_start":"2026-02-06T00:00:00Z","total_subscriptions":5000}},"SubscriberTrendsResponse":{"properties":{"daily_opt_ins":{"items":{"$ref":"#/components/schemas/DailyOptInPoint"},"type":"array","title":"Daily Opt Ins","description":"Daily opt-in counts by channel"},"daily_opt_outs":{"items":{"$ref":"#/components/schemas/DailyOptOutPoint"},"type":"array","title":"Daily Opt Outs","description":"Daily opt-out counts by channel"},"total_opt_ins":{"type":"integer","title":"Total Opt Ins","description":"Total opt-ins in the period","default":0},"total_opt_outs":{"type":"integer","title":"Total Opt Outs","description":"Total opt-outs in the period","default":0},"net_growth":{"type":"integer","title":"Net Growth","description":"Net subscriber growth (opt-ins minus opt-outs)","default":0},"avg_daily_opt_ins":{"type":"number","title":"Avg Daily Opt Ins","description":"Average daily opt-ins","default":0.0},"avg_daily_opt_outs":{"type":"number","title":"Avg Daily Opt Outs","description":"Average daily opt-outs","default":0.0},"by_opt_in_method":{"items":{"$ref":"#/components/schemas/OptInMethodCount"},"type":"array","title":"By Opt In Method","description":"Breakdown by opt-in method"},"period_start":{"type":"string","title":"Period Start","description":"Start of the reporting period (ISO format)"},"period_end":{"type":"string","title":"Period End","description":"End of the reporting period (ISO format)"}},"type":"object","required":["period_start","period_end"],"title":"SubscriberTrendsResponse","description":"Subscriber growth trends over time.\n\nReturned by GET /api/v1/analytics/subscribers/trends","example":{"avg_daily_opt_ins":15.0,"avg_daily_opt_outs":1.67,"by_opt_in_method":[{"count":200,"method":"widget"},{"count":150,"method":"api"},{"count":100,"method":"import"}],"daily_opt_ins":[{"channel_type":"email","count":15,"date":"2026-03-01"},{"channel_type":"whatsapp","count":8,"date":"2026-03-01"}],"daily_opt_outs":[{"channel_type":"email","count":3,"date":"2026-03-01"}],"net_growth":400,"period_end":"2026-03-08T00:00:00Z","period_start":"2026-02-06T00:00:00Z","total_opt_ins":450,"total_opt_outs":50}},"SubscriptionActivityItem":{"properties":{"contact_id":{"type":"string","title":"Contact Id"},"contact_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Contact Name"},"channel_type":{"type":"string","title":"Channel Type"},"event":{"type":"string","enum":["subscribed","unsubscribed"],"title":"Event"},"occurred_at":{"type":"string","title":"Occurred At","description":"ISO 8601 (Z)"},"unsubscribe_method":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Unsubscribe Method","description":"link/keyword/manual/bounce/complaint/api; null for subscribed events"},"last_message_in_list":{"anyOf":[{"$ref":"#/components/schemas/LastMessageInList"},{"type":"null"}],"description":"Populated only for unsubscribed events; null for subscribed events"}},"type":"object","required":["contact_id","channel_type","event","occurred_at"],"title":"SubscriptionActivityItem","description":"A single opt-in or opt-out event derived from a subscription's current state."},"SubscriptionActivityPagination":{"properties":{"page":{"type":"integer","title":"Page"},"page_size":{"type":"integer","title":"Page Size"},"total":{"type":"integer","title":"Total"}},"type":"object","required":["page","page_size","total"],"title":"SubscriptionActivityPagination"},"SubscriptionActivityResponse":{"properties":{"items":{"items":{"$ref":"#/components/schemas/SubscriptionActivityItem"},"type":"array","title":"Items"},"pagination":{"$ref":"#/components/schemas/SubscriptionActivityPagination"}},"type":"object","required":["pagination"],"title":"SubscriptionActivityResponse"},"SubscriptionChannelType":{"type":"string","enum":["email","sms","whatsapp","telegram","messenger","instagram","browser_push","rcs"],"title":"SubscriptionChannelType","description":"Channels a contact can hold a list subscription on."},"SubscriptionCreate":{"properties":{"list_id":{"type":"string","minLength":1,"title":"List Id","description":"List ID to subscribe to"},"channel_type":{"$ref":"#/components/schemas/SubscriptionChannelType","description":"Channel type for subscription"},"channel_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Channel Id","description":"Channel ID (required for telegram, messenger, instagram)"},"opt_in_method":{"$ref":"#/components/schemas/OptInMethod","description":"Method of opt-in","default":"manual"},"opt_in_ip":{"anyOf":[{"type":"string","maxLength":45},{"type":"null"}],"title":"Opt In Ip","description":"IP address of subscriber"},"opt_in_user_agent":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Opt In User Agent","description":"User agent string"},"opt_in_source_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Opt In Source Url","description":"URL where subscription originated"},"opt_in_widget_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Opt In Widget Id","description":"Widget ID if subscribed via widget"},"send_double_opt_in":{"type":"boolean","title":"Send Double Opt In","description":"Send double opt-in confirmation","default":false},"channel_metadata":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Channel Metadata","description":"Channel-specific metadata"}},"type":"object","required":["list_id","channel_type"],"title":"SubscriptionCreate","description":"Schema for creating a subscription","example":{"channel_metadata":{},"channel_type":"email","list_id":"list-123","opt_in_ip":"192.168.1.1","opt_in_method":"widget","send_double_opt_in":true}},"SubscriptionResponse":{"properties":{"id":{"type":"string","title":"Id"},"tenant_id":{"type":"string","title":"Tenant Id"},"contact_id":{"type":"string","title":"Contact Id"},"list_id":{"type":"string","title":"List Id"},"list_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"List Name"},"channel_type":{"$ref":"#/components/schemas/SubscriptionChannelType"},"channel_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Channel Id"},"status":{"$ref":"#/components/schemas/SubscriptionStatus"},"opt_in_method":{"$ref":"#/components/schemas/OptInMethod"},"opt_in_ip":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Opt In Ip"},"opt_in_user_agent":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Opt In User Agent"},"opt_in_source_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Opt In Source Url"},"opt_in_widget_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Opt In Widget Id"},"opt_in_confirmed_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Opt In Confirmed At"},"opt_in_token_expires_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Opt In Token Expires At"},"unsubscribed_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Unsubscribed At"},"unsubscribe_reason":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Unsubscribe Reason"},"unsubscribe_method":{"anyOf":[{"$ref":"#/components/schemas/UnsubscribeMethod"},{"type":"null"}]},"channel_metadata":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Channel Metadata"},"last_message_sent_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Last Message Sent At"},"last_engagement_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Last Engagement At"},"bounce_count":{"type":"integer","title":"Bounce Count","default":0},"failure_count":{"type":"integer","title":"Failure Count","default":0},"last_bounce_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Last Bounce At"},"last_failure_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Last Failure At"},"created_at":{"type":"string","format":"date-time","title":"Created At"},"updated_at":{"type":"string","format":"date-time","title":"Updated At"}},"type":"object","required":["id","tenant_id","contact_id","list_id","channel_type","status","opt_in_method","created_at","updated_at"],"title":"SubscriptionResponse","description":"Schema for subscription response","example":{"bounce_count":0,"channel_type":"email","contact_id":"contact-789","created_at":"2024-01-01T12:00:00Z","failure_count":0,"id":"sub-123","list_id":"list-abc","opt_in_confirmed_at":"2024-01-01T12:00:00Z","opt_in_ip":"192.168.1.1","opt_in_method":"widget","status":"subscribed","tenant_id":"tenant-456","updated_at":"2024-01-01T12:00:00Z"}},"SubscriptionStatsResponse":{"properties":{"list_id":{"type":"string","title":"List Id"},"list_name":{"type":"string","title":"List Name"},"total_subscriptions":{"type":"integer","title":"Total Subscriptions"},"channels":{"items":{"$ref":"#/components/schemas/ChannelStats"},"type":"array","title":"Channels"}},"type":"object","required":["list_id","list_name","total_subscriptions","channels"],"title":"SubscriptionStatsResponse","description":"Schema for subscription statistics by list","example":{"channels":[{"bounced":8,"channel_type":"email","complained":2,"pending":50,"subscribed":1400,"total":1500,"unsubscribed":40},{"bounced":0,"channel_type":"telegram","complained":0,"pending":10,"subscribed":980,"total":1000,"unsubscribed":10}],"list_id":"list-123","list_name":"Weekly Newsletter","total_subscriptions":2500}},"SubscriptionStatus":{"type":"string","enum":["pending","subscribed","unsubscribed","bounced","complained"],"title":"SubscriptionStatus","description":"Status of a subscription"},"SubscriptionStatusEnum":{"type":"string","enum":["pending","subscribed","unsubscribed"],"title":"SubscriptionStatusEnum","description":"Email subscription status"},"SubscriptionUpdate":{"properties":{"status":{"anyOf":[{"$ref":"#/components/schemas/SubscriptionStatus"},{"type":"null"}],"description":"New subscription status"},"channel_metadata":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Channel Metadata","description":"Channel-specific metadata"}},"type":"object","title":"SubscriptionUpdate","description":"Schema for updating a subscription","example":{"channel_metadata":{"preferred_time":"morning"},"status":"subscribed"}},"SuccessResponse":{"properties":{"success":{"type":"boolean","title":"Success","description":"Whether the operation succeeded","default":true},"message":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Message","description":"Optional success message"}},"type":"object","title":"SuccessResponse","description":"Standard success response for operations without data return.\n\nUse for: DELETE operations, bulk updates, toggles, etc.","example":{"message":"Resource deleted successfully","success":true}},"SuggestRequest":{"properties":{"kb_only":{"type":"boolean","title":"Kb Only","default":true},"detailed_response":{"type":"boolean","title":"Detailed Response","default":false},"use_full_conversation":{"type":"boolean","title":"Use Full Conversation","default":true}},"type":"object","title":"SuggestRequest"},"SuggestionAcceptResponse":{"properties":{"message":{"type":"string","title":"Message","description":"Success message"},"suggestion_id":{"type":"string","title":"Suggestion Id","description":"ID of accepted suggestion"},"ai_answer_event_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Ai Answer Event Id","description":"AI answer event marked as accepted, if one was resolved"}},"type":"object","required":["message","suggestion_id"],"title":"SuggestionAcceptResponse","description":"Response when marking a suggestion as accepted."},"SuggestionSource":{"properties":{"id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Id","description":"Source document ID"},"type":{"type":"string","title":"Type","description":"Source type: faq, article, ticket, manual"},"title":{"type":"string","title":"Title","description":"Source title"},"content":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Content","description":"Relevant content snippet"},"relevance_score":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Relevance Score","description":"Relevance score 0-1"},"ticket_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Ticket Id","description":"Ticket ID if type is ticket"},"subject":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Subject","description":"Ticket subject"},"summary":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Summary","description":"AI-generated summary"},"user_intent":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"User Intent","description":"Detected user intent"},"resolution":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Resolution","description":"Resolution summary"},"sentiment":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Sentiment","description":"Sentiment analysis"},"tags":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Tags","description":"Associated tags"},"closed_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Closed At","description":"When ticket was closed"}},"type":"object","required":["type","title"],"title":"SuggestionSource","description":"A source document used in generating the suggestion."},"SupportBreakdown":{"properties":{"bot_messages":{"type":"integer","title":"Bot Messages","description":"Bot-sent messages in the period (rollup-sourced)","default":0},"human_messages":{"type":"integer","title":"Human Messages","description":"Human/manual agent-sent messages in the period (rollup-sourced)","default":0},"emails":{"type":"integer","title":"Emails","description":"Transactional / single emails sent (= email.single.sent)","default":0}},"type":"object","title":"SupportBreakdown","description":"Customer-support message volume in the period.\n\nNOTE: `bot_messages` / `human_messages` are ROLLUP-SOURCED from the\ninteraction-log hourly rollup (TenantHourlyStats), NOT the billed-messages\ntable -- an explicit product decision. They are not invoice-exact.\n`human_messages` = max(0, messages_out - bot_messages). `emails` equals the\ninvoice-aligned single/transactional count (email.single.sent)."},"SwitchableChannelInfo":{"properties":{"channel_id":{"type":"string","title":"Channel Id"},"channel_name":{"type":"string","title":"Channel Name"},"channel_type":{"type":"string","title":"Channel Type"},"identifier":{"type":"string","title":"Identifier"}},"type":"object","required":["channel_id","channel_name","channel_type","identifier"],"title":"SwitchableChannelInfo","description":"Info about a channel available for switching."},"SwitchableChannelsResponse":{"properties":{"channels":{"items":{"$ref":"#/components/schemas/SwitchableChannelInfo"},"type":"array","title":"Channels"}},"type":"object","required":["channels"],"title":"SwitchableChannelsResponse","description":"List of channels available for switching."},"SyncPostsRequest":{"properties":{"channel_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Channel Id","description":"Only sync this Instagram / Facebook Messenger channel. Omit to sync every connected social channel in the workspace."},"limit":{"type":"integer","maximum":100.0,"minimum":1.0,"title":"Limit","description":"How many of the newest posts to pull per channel. The list is paged as needed to reach this many.","default":25}},"type":"object","title":"SyncPostsRequest","description":"Body of ``POST /comments/posts/sync``."},"SyncPostsResponse":{"properties":{"synced":{"type":"integer","title":"Synced","description":"Posts read from Meta and upserted (created OR refreshed), summed across every channel touched.","default":0},"created":{"type":"integer","title":"Created","description":"Of the synced posts, how many did not exist before this call.","default":0}},"type":"object","title":"SyncPostsResponse","description":"Result of a posts sync — how many posts were seen and how many were new."},"SyncResponse":{"properties":{"integration_id":{"type":"string","title":"Integration Id"},"status":{"type":"string","title":"Status"},"message":{"type":"string","title":"Message"},"emails_found":{"type":"integer","title":"Emails Found","default":0}},"type":"object","required":["integration_id","status","message"],"title":"SyncResponse","description":"Response from sync operation"},"TagAssignmentResponse":{"properties":{"success":{"type":"boolean","title":"Success","description":"Whether the operation succeeded","default":true},"message":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Message","description":"Optional success message"}},"type":"object","title":"TagAssignmentResponse","description":"Response for tag assignment operations (add/remove to contact/conversation)","example":{"message":"Tag added to contact successfully","success":true}},"TagCreate":{"properties":{"name":{"type":"string","maxLength":50,"minLength":1,"title":"Name","description":"Tag name"},"color":{"anyOf":[{"type":"string","pattern":"^#[0-9A-Fa-f]{6}$"},{"type":"null"}],"title":"Color","description":"Hex color code (e.g., #FF0000)"},"description":{"anyOf":[{"type":"string","maxLength":255},{"type":"null"}],"title":"Description","description":"Tag description"}},"type":"object","required":["name"],"title":"TagCreate","description":"Schema for creating a new tag","example":{"color":"#EF4444","description":"High-value customers","name":"VIP"}},"TagFilterMode":{"type":"string","enum":["any","all","none"],"title":"TagFilterMode","description":"How CustomInboxTagFilter rows are combined when filtering conversations."},"TagInfo":{"properties":{"id":{"type":"string","title":"Id"},"name":{"type":"string","title":"Name"},"color":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Color"}},"type":"object","required":["id","name"],"title":"TagInfo","description":"Simplified tag info for embedding in responses"},"TagListResponse":{"properties":{"items":{"items":{"$ref":"#/components/schemas/TagResponse"},"type":"array","title":"Items","description":"List of items for this page"},"pagination":{"$ref":"#/components/schemas/PaginationMeta","description":"Pagination metadata"}},"type":"object","required":["items","pagination"],"title":"TagListResponse","description":"Paginated list of tags.\n\nExample response:\n```json\n{\n    \"items\": [...],\n    \"pagination\": {\n        \"total\": 25,\n        \"page\": 1,\n        \"page_size\": 20,\n        \"total_pages\": 2,\n        \"has_next\": true,\n        \"has_prev\": false\n    }\n}\n```"},"TagResponse":{"properties":{"id":{"type":"string","title":"Id","description":"Unique identifier (UUID)"},"tenant_id":{"type":"string","title":"Tenant Id","description":"Tenant this resource belongs to"},"name":{"type":"string","title":"Name","description":"Tag name"},"color":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Color","description":"Hex color code (e.g., #FF0000)"},"description":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Description","description":"Tag description"},"created_at":{"type":"string","format":"date-time","title":"Created At","description":"When the tag was created"}},"type":"object","required":["id","tenant_id","name","created_at"],"title":"TagResponse","description":"Full tag response with all fields.\n\nNote: Tags only have created_at, no updated_at per current model.","example":{"color":"#EF4444","created_at":"2025-01-15T10:30:00Z","description":"High-value customers","id":"t1a2b3c4-d5e6-f7g8-h9i0-j1k2l3m4n5o6","name":"VIP","tenant_id":"tenant-123"}},"TagUpdate":{"properties":{"name":{"anyOf":[{"type":"string","maxLength":50,"minLength":1},{"type":"null"}],"title":"Name","description":"Tag name"},"color":{"anyOf":[{"type":"string","pattern":"^#[0-9A-Fa-f]{6}$"},{"type":"null"}],"title":"Color","description":"Hex color code (e.g., #FF0000)"},"description":{"anyOf":[{"type":"string","maxLength":255},{"type":"null"}],"title":"Description","description":"Tag description"}},"type":"object","title":"TagUpdate","description":"Schema for updating a tag (all fields optional)","example":{"color":"#10B981","name":"Premium VIP"}},"TelegramBotCommand":{"properties":{"command":{"type":"string","maxLength":32,"title":"Command","description":"Command text without slash"},"description":{"type":"string","maxLength":256,"title":"Description","description":"Description of the command"}},"type":"object","required":["command","description"],"title":"TelegramBotCommand","description":"Schema for a Telegram bot command"},"TelegramContent":{"properties":{"channel_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Channel Id"},"message_type":{"type":"string","title":"Message Type","default":"text"},"message_text":{"type":"string","title":"Message Text"},"attachment_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Attachment Id"},"attachment_filename":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Attachment Filename"}},"type":"object","required":["message_text"],"title":"TelegramContent","description":"Telegram-specific campaign content."},"TelegramProfileResponse":{"properties":{"name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Name"},"username":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Username"},"display_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Display Name"},"description":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Description"},"short_description":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Short Description"},"commands":{"anyOf":[{"items":{"$ref":"#/components/schemas/TelegramBotCommand"},"type":"array"},{"type":"null"}],"title":"Commands"},"can_join_groups":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Can Join Groups"},"can_read_all_group_messages":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Can Read All Group Messages"},"supports_inline_queries":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Supports Inline Queries"}},"type":"object","title":"TelegramProfileResponse","description":"Response schema for Telegram bot profile"},"TemplateButton":{"properties":{"type":{"type":"string","title":"Type","description":"Button type: QUICK_REPLY, URL, PHONE_NUMBER, COPY_CODE, REQUEST_CONTACT_INFO, FLOW"},"text":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Text","description":"Button text. Required for every type EXCEPT `REQUEST_CONTACT_INFO`, whose label is fixed by Meta and filled in automatically."},"url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Url","description":"URL for URL buttons"},"phone_number":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Phone Number","description":"Phone number for PHONE_NUMBER buttons"},"example":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Example","description":"Example value for variable URL"},"flow_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Flow Id","description":"WhatsApp Flow ID (FLOW buttons only)"},"flow_action":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Flow Action","description":"Flow action as synced from Meta, e.g. `NAVIGATE` or `DATA_EXCHANGE` (FLOW buttons only)"},"navigate_screen":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Navigate Screen","description":"First Flow screen opened by the button (FLOW buttons only)"}},"type":"object","required":["type"],"title":"TemplateButton","description":"WhatsApp template button"},"TemplateCategory":{"type":"string","enum":["MARKETING","UTILITY","AUTHENTICATION"],"title":"TemplateCategory","description":"WhatsApp template categories"},"TemplateComponentRequest":{"properties":{"type":{"type":"string","title":"Type","description":"Component type: HEADER, BODY, FOOTER, BUTTONS, CAROUSEL"},"format":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Format","description":"Format for HEADER: TEXT, IMAGE, VIDEO, DOCUMENT"},"text":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Text","description":"Text content for TEXT headers, BODY, FOOTER"},"example":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Example","description":"Example values for components"},"buttons":{"anyOf":[{"items":{"$ref":"#/components/schemas/TemplateButton"},"type":"array"},{"type":"null"}],"title":"Buttons","description":"Buttons for BUTTONS component"},"add_security_recommendation":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Add Security Recommendation","description":"BODY component, AUTHENTICATION templates only. When true, WhatsApp appends \"For your security, do not share this code.\" to the generated body. Ignored for other categories."},"code_expiration_minutes":{"anyOf":[{"type":"integer","maximum":90.0,"minimum":1.0},{"type":"null"}],"title":"Code Expiration Minutes","description":"FOOTER component, AUTHENTICATION templates only. When set (1-90), WhatsApp renders the footer \"This code expires in N minutes.\" Ignored for other categories."},"cards":{"anyOf":[{"items":{"$ref":"#/components/schemas/CarouselCardRequest"},"type":"array","maxItems":10},{"type":"null"}],"title":"Cards","description":"Cards for a CAROUSEL component (up to 10). Each card is a set of components mirroring a mini-template: exactly one HEADER of format IMAGE or VIDEO, exactly one BODY, and an optional BUTTONS component (<=2 buttons). Meta requires every card to share the SAME header format and an IDENTICAL button layout (same count, types, and order). Ignored for non-CAROUSEL components."}},"type":"object","required":["type"],"title":"TemplateComponentRequest","description":"Component for template creation.\n\nA ``CAROUSEL`` component carries its cards in ``cards`` (up to 10). Every\nother component type (``HEADER``, ``BODY``, ``FOOTER``, ``BUTTONS``) ignores\n``cards``. Each card is itself a small set of components — see\n``CarouselCardRequest``."},"TemplateCreateResponse":{"properties":{"agent_id":{"type":"string","title":"Agent Id"},"bot_id":{"type":"string","title":"Bot Id"}},"type":"object","required":["agent_id","bot_id"],"title":"TemplateCreateResponse"},"TemplateHeaderType":{"type":"string","enum":["NONE","TEXT","IMAGE","VIDEO","DOCUMENT"],"title":"TemplateHeaderType","description":"WhatsApp template header types"},"TemplateParameterFormat":{"type":"string","enum":["positional","named"],"title":"TemplateParameterFormat","description":"WhatsApp template parameter format.\n\n* ``positional`` — numbered placeholders ``{{1}}``, ``{{2}}`` (legacy/default).\n* ``named`` — named placeholders ``{{first_name}}``, ``{{order_id}}``."},"TemplateStatus":{"type":"string","enum":["PENDING","APPROVED","REJECTED","PAUSED","DISABLED"],"title":"TemplateStatus","description":"WhatsApp template approval status"},"TenantCreate":{"properties":{"name":{"type":"string","title":"Name"},"slug":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Slug"},"company_email":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Company Email"},"billing_account_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Billing Account Id"},"pricing_model_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Pricing Model Id"},"ai_features_enabled":{"type":"boolean","title":"Ai Features Enabled","default":true},"multi_agent_mode":{"type":"boolean","title":"Multi Agent Mode","default":true},"auto_summarize_on_close":{"type":"string","title":"Auto Summarize On Close","default":"ask"},"auto_transcript_export_format":{"type":"string","title":"Auto Transcript Export Format","default":"none"},"auto_summarize_live_chat":{"type":"boolean","title":"Auto Summarize Live Chat","default":true},"allow_messaging_other_agents_conversations":{"type":"boolean","title":"Allow Messaging Other Agents Conversations","default":true},"suppress_customer_alert":{"type":"boolean","title":"Suppress Customer Alert","default":false},"default_channels":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Default Channels"},"support_email":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Support Email"},"default_timezone":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Default Timezone"},"default_country":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Default Country"}},"type":"object","required":["name"],"title":"TenantCreate","description":"Schema for creating a tenant"},"TenantEmailSettingsResponse":{"properties":{"id":{"type":"string","title":"Id","description":"Settings record ID"},"tenant_id":{"type":"string","title":"Tenant Id","description":"Tenant ID"},"campaign_from_email":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Campaign From Email"},"campaign_from_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Campaign From Name"},"campaign_reply_to":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Campaign Reply To"},"support_from_email":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Support From Email"},"support_from_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Support From Name"},"support_integration_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Support Integration Id"},"campaign_default_mailbox_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Campaign Default Mailbox Id","description":"Default mailbox ID for campaigns"},"support_default_mailbox_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Support Default Mailbox Id","description":"Default mailbox ID for support"},"doi_mailbox_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Doi Mailbox Id","description":"Mailbox ID for double opt-in confirmation emails"},"allow_custom_sender":{"type":"boolean","title":"Allow Custom Sender","default":false},"double_opt_in_enabled":{"type":"boolean","title":"Double Opt In Enabled","default":false},"double_opt_in_template_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Double Opt In Template Id"},"compliance_footer_html":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Compliance Footer Html"},"track_opens_default":{"type":"boolean","title":"Track Opens Default","default":true},"track_clicks_default":{"type":"boolean","title":"Track Clicks Default","default":true},"created_at":{"type":"string","title":"Created At","description":"When settings were created"},"updated_at":{"type":"string","title":"Updated At","description":"When settings were last updated"}},"type":"object","required":["id","tenant_id","created_at","updated_at"],"title":"TenantEmailSettingsResponse","description":"Full response model for tenant email settings"},"TenantEmailSettingsUpdate":{"properties":{"campaign_from_email":{"anyOf":[{"type":"string","maxLength":255},{"type":"null"}],"title":"Campaign From Email","description":"Default from email for campaigns"},"campaign_from_name":{"anyOf":[{"type":"string","maxLength":255},{"type":"null"}],"title":"Campaign From Name","description":"Default from name for campaigns"},"campaign_reply_to":{"anyOf":[{"type":"string","maxLength":255},{"type":"null"}],"title":"Campaign Reply To","description":"Default reply-to for campaigns"},"support_from_email":{"anyOf":[{"type":"string","maxLength":255},{"type":"null"}],"title":"Support From Email","description":"Default from email for support"},"support_from_name":{"anyOf":[{"type":"string","maxLength":255},{"type":"null"}],"title":"Support From Name","description":"Default from name for support"},"support_integration_id":{"anyOf":[{"type":"string","maxLength":36},{"type":"null"}],"title":"Support Integration Id","description":"Preferred email integration ID for support"},"campaign_default_mailbox_id":{"anyOf":[{"type":"string","maxLength":36},{"type":"null"}],"title":"Campaign Default Mailbox Id","description":"Default mailbox ID for campaigns"},"support_default_mailbox_id":{"anyOf":[{"type":"string","maxLength":36},{"type":"null"}],"title":"Support Default Mailbox Id","description":"Default mailbox ID for support"},"doi_mailbox_id":{"anyOf":[{"type":"string","maxLength":36},{"type":"null"}],"title":"Doi Mailbox Id","description":"Mailbox ID for double opt-in confirmation emails"},"allow_custom_sender":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Allow Custom Sender","description":"Allow agents to use custom sender addresses"},"double_opt_in_enabled":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Double Opt In Enabled","description":"Enable double opt-in for subscriptions"},"double_opt_in_template_id":{"anyOf":[{"type":"string","maxLength":36},{"type":"null"}],"title":"Double Opt In Template Id","description":"Template ID for double opt-in confirmation"},"compliance_footer_html":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Compliance Footer Html","description":"HTML compliance footer appended to emails"},"track_opens_default":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Track Opens Default","description":"Default OPEN tracking for new campaigns"},"track_clicks_default":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Track Clicks Default","description":"Default CLICK tracking for new campaigns"}},"additionalProperties":false,"type":"object","title":"TenantEmailSettingsUpdate","description":"PUT request model for updating tenant email settings (all fields optional)"},"TenantResponse":{"properties":{"id":{"type":"string","title":"Id"},"name":{"type":"string","title":"Name"},"slug":{"type":"string","title":"Slug"},"billing_account_id":{"type":"string","title":"Billing Account Id"},"pricing_model_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Pricing Model Id"},"is_trial":{"type":"boolean","title":"Is Trial"},"trial_ends_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Trial Ends At"},"is_active":{"type":"boolean","title":"Is Active"},"subscription_tier":{"type":"string","title":"Subscription Tier"},"multi_agent_mode":{"type":"boolean","title":"Multi Agent Mode"},"auto_summarize_on_close":{"type":"string","title":"Auto Summarize On Close"},"auto_transcript_export_format":{"type":"string","title":"Auto Transcript Export Format","default":"none"},"auto_summarize_live_chat":{"type":"boolean","title":"Auto Summarize Live Chat"},"allow_messaging_other_agents_conversations":{"type":"boolean","title":"Allow Messaging Other Agents Conversations"},"live_chat_contact_visibility":{"type":"string","title":"Live Chat Contact Visibility","default":"never"},"duplicate_contact_mode":{"type":"string","title":"Duplicate Contact Mode","default":"auto_merge"},"faq_generation_mode":{"type":"string","title":"Faq Generation Mode","default":"draft"},"kb_correction_mode":{"type":"string","title":"Kb Correction Mode","default":"moderated"},"kb_auto_apply_threshold":{"type":"number","title":"Kb Auto Apply Threshold","default":0.8},"faq_min_cluster_size":{"type":"integer","title":"Faq Min Cluster Size","default":2},"bot_notification_policy":{"type":"string","title":"Bot Notification Policy","default":"always"},"brand_colors":{"anyOf":[{"additionalProperties":{"type":"string"},"type":"object"},{"type":"null"}],"title":"Brand Colors"},"company_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Company Name"},"company_domain":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Company Domain"},"logo_variants":{"anyOf":[{"additionalProperties":{"type":"string"},"type":"object"},{"type":"null"}],"title":"Logo Variants"},"logo_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Logo Url"},"dashboard_wait_threshold_normal":{"type":"integer","title":"Dashboard Wait Threshold Normal","default":30},"dashboard_wait_threshold_urgent":{"type":"integer","title":"Dashboard Wait Threshold Urgent","default":120},"sms_enabled":{"type":"boolean","title":"Sms Enabled","default":false},"rcs_enabled":{"type":"boolean","title":"Rcs Enabled","default":false},"flows_enabled":{"type":"boolean","title":"Flows Enabled","default":false},"default_timezone":{"type":"string","title":"Default Timezone","default":"UTC"},"default_country":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Default Country"},"default_language":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Default Language"},"created_at":{"type":"string","format":"date-time","title":"Created At"}},"type":"object","required":["id","name","slug","billing_account_id","is_trial","trial_ends_at","is_active","subscription_tier","multi_agent_mode","auto_summarize_on_close","auto_summarize_live_chat","allow_messaging_other_agents_conversations","created_at"],"title":"TenantResponse","description":"Schema for tenant response"},"TenantUpdate":{"properties":{"name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Name"},"company_email":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Company Email"},"multi_agent_mode":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Multi Agent Mode"},"auto_summarize_on_close":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Auto Summarize On Close"},"auto_transcript_export_format":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Auto Transcript Export Format"},"auto_summarize_live_chat":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Auto Summarize Live Chat"},"allow_messaging_other_agents_conversations":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Allow Messaging Other Agents Conversations"},"live_chat_contact_visibility":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Live Chat Contact Visibility"},"duplicate_contact_mode":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Duplicate Contact Mode"},"faq_generation_mode":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Faq Generation Mode"},"kb_correction_mode":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Kb Correction Mode"},"kb_auto_apply_threshold":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Kb Auto Apply Threshold"},"faq_min_cluster_size":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Faq Min Cluster Size"},"bot_notification_policy":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Bot Notification Policy"},"brand_colors":{"anyOf":[{"additionalProperties":{"type":"string"},"type":"object"},{"type":"null"}],"title":"Brand Colors"},"company_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Company Name"},"company_domain":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Company Domain"},"dashboard_wait_threshold_normal":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Dashboard Wait Threshold Normal"},"dashboard_wait_threshold_urgent":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Dashboard Wait Threshold Urgent"},"default_timezone":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Default Timezone"},"default_country":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Default Country"},"default_language":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Default Language"}},"type":"object","title":"TenantUpdate","description":"Schema for updating a tenant"},"TenantUserListResponse":{"properties":{"items":{"items":{"$ref":"#/components/schemas/TenantUserResponse"},"type":"array","title":"Items","description":"List of items for this page"},"pagination":{"$ref":"#/components/schemas/PaginationMeta","description":"Pagination metadata"}},"type":"object","required":["items","pagination"],"title":"TenantUserListResponse","description":"Paginated list of tenant users (team members).\n\nExample response:\n```json\n{\n    \"items\": [...],\n    \"pagination\": {\n        \"total\": 25,\n        \"page\": 1,\n        \"page_size\": 20,\n        \"total_pages\": 2,\n        \"has_next\": true,\n        \"has_prev\": false\n    }\n}\n```"},"TenantUserResponse":{"properties":{"id":{"type":"string","title":"Id","description":"Unique identifier (UUID)"},"email":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Email","description":"User email address"},"name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Name","description":"User display name"},"chat_nickname":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Chat Nickname","description":"Chat display nickname"},"role":{"$ref":"#/components/schemas/UserRole","description":"Legacy role for backwards compatibility"},"avatar_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Avatar Url","description":"URL to user's avatar image"},"live_chat_available":{"type":"boolean","title":"Live Chat Available","description":"Available for live chat assignment","default":true},"joined_at":{"type":"string","format":"date-time","title":"Joined At","description":"When user joined the tenant"},"rbac_roles":{"items":{"$ref":"#/components/schemas/RoleAssignmentInfo"},"type":"array","title":"Rbac Roles","description":"RBAC role assignments"},"is_billing_account_owner":{"type":"boolean","title":"Is Billing Account Owner","description":"True if this member is the OWNER of the billing account owning the current workspace (owner always has account-wide usage access; frontend renders the access toggle as checked+disabled)","default":false},"account_billing_read_granted":{"type":"boolean","title":"Account Billing Read Granted","description":"True if this member holds a DIRECT `account:billing_read` grant (the account-wide-usage checkbox state for non-owners)","default":false}},"type":"object","required":["id","role","joined_at"],"title":"TenantUserResponse","description":"Response for a user within a tenant context.\n\nUsed in GET /users (list team members) endpoint.\nIncludes both legacy role field and RBAC roles.","example":{"avatar_url":"https://example.com/avatar.jpg","chat_nickname":"JohnD","email":"john@example.com","id":"u1a2b3c4-d5e6-f7g8-h9i0-j1k2l3m4n5o6","joined_at":"2025-01-15T10:30:00Z","live_chat_available":true,"name":"John Doe","rbac_roles":[{"assigned_at":"2025-01-15T10:30:00Z","assigned_by_user_id":"u456","role_id":"r123","role_name":"Marketing Agent"}],"role":"ADMIN"}},"TestAlertRequest":{"properties":{"message":{"anyOf":[{"type":"string","maxLength":500},{"type":"null"}],"title":"Message","description":"Optional text shown in the test alert."}},"type":"object","title":"TestAlertRequest"},"TestAlertResponse":{"properties":{"success":{"type":"boolean","title":"Success"},"alert_event_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Alert Event Id"},"note":{"type":"string","title":"Note","default":"Test alerts are delivered to your configured channels. Paid channels (SMS €0.10, WhatsApp €0.08 per message) are billed like real alerts."}},"type":"object","required":["success"],"title":"TestAlertResponse"},"TestAliasRequest":{"properties":{"alias_email":{"type":"string","format":"email","title":"Alias Email","description":"Email alias to test (must be configured in Gmail Send-As settings)"}},"type":"object","required":["alias_email"],"title":"TestAliasRequest","description":"Request to test a Gmail send-as alias"},"TestAliasResponse":{"properties":{"success":{"type":"boolean","title":"Success"},"alias_email":{"type":"string","title":"Alias Email"},"message":{"type":"string","title":"Message"},"mailbox_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Mailbox Id"}},"type":"object","required":["success","alias_email","message"],"title":"TestAliasResponse","description":"Response from Gmail alias test"},"TestEmailRequest":{"properties":{"to_email":{"type":"string","title":"To Email","description":"Email address to send test email to"},"subject":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Subject","description":"Email subject","default":"Test Email"},"body":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Body","description":"Email body","default":"This is a test email to verify your email provider configuration."}},"type":"object","required":["to_email"],"title":"TestEmailRequest","description":"Schema for testing email provider configuration","example":{"body":"This is a test email.","subject":"Test Email","to_email":"test@example.com"}},"TestEmailResponse":{"properties":{"success":{"type":"boolean","title":"Success"},"message":{"type":"string","title":"Message"},"provider_message_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Provider Message Id"},"error":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Error"}},"type":"object","required":["success","message"],"title":"TestEmailResponse","description":"Schema for test email response","example":{"message":"Test email sent successfully","provider_message_id":"msg-abc123","success":true}},"TicketSummaryEvent":{"properties":{"event_type":{"type":"string","const":"ticket_summary","title":"Event Type","default":"ticket_summary"},"id":{"type":"string","title":"Id","description":"Stable client-side key. Matches ``document_id`` (the KB document id)."},"occurred_at":{"type":"string","title":"Occurred At","description":"Chronological anchor (``knowledge_base_documents.created_at``). Merge sort key."},"document_id":{"type":"string","title":"Document Id","description":"KnowledgeBaseDocument id. Use for the KB editor deep-link and the raw GET/PUT endpoints (``/knowledge-base/documents/{document_id}/raw``)."},"title":{"type":"string","title":"Title","description":"KB document title — '{subject} — {conversation_id}'."},"summary_preview":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Summary Preview","description":"Plain-text snippet of the summary (truncated to 140 chars), taken from ``upload_metadata.summary`` and falling back to the persisted markdown ``upload_metadata.content``. NULL when neither is present."},"conversation_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Conversation Id","description":"Conversation this summary was generated from (KB doc ``conversation_id`` column)."},"created_at":{"type":"string","title":"Created At","description":"When the summary document was created (mirrors ``occurred_at``)."}},"type":"object","required":["id","occurred_at","document_id","title","created_at"],"title":"TicketSummaryEvent","description":"Timeline row representing an AI ticket/conversation summary.\n\nSourced from ``knowledge_base_documents`` rows with\n``document_type='ticket_summary'`` queried via the indexed\n``contact_id`` column (no JSON parsing). The summary is an editable\nKB document: the FE deep-links to the KB editor and the raw GET/PUT\nendpoints using ``document_id``:\n\n* KB document view: ``/knowledge-base`` (open doc ``document_id``)\n* Raw read / edit: ``GET``/``PUT /knowledge-base/documents/{document_id}/raw``"},"TicketSummaryListResponse":{"properties":{"items":{"items":{"$ref":"#/components/schemas/TicketSummaryResponse"},"type":"array","title":"Items","description":"List of items for this page"},"pagination":{"$ref":"#/components/schemas/PaginationMeta","description":"Pagination metadata"}},"type":"object","required":["items","pagination"],"title":"TicketSummaryListResponse","description":"Paginated list of ticket summaries stored in knowledge base.\n\nExample response:\n```json\n{\n    \"items\": [...],\n    \"pagination\": {\n        \"total\": 150,\n        \"page\": 1,\n        \"page_size\": 20,\n        \"total_pages\": 8,\n        \"has_next\": true,\n        \"has_prev\": false\n    }\n}\n```"},"TicketSummaryResponse":{"properties":{"id":{"type":"string","title":"Id","description":"Unique identifier (UUID)"},"tenant_id":{"type":"string","title":"Tenant Id","description":"Tenant this resource belongs to"},"conversation_id":{"type":"string","title":"Conversation Id","description":"Related conversation ID"},"contact_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Contact Id","description":"Related contact ID"},"subject":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Subject","description":"Ticket subject"},"ai_summary":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Ai Summary","description":"AI-generated summary"},"user_intent":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"User Intent","description":"What the user wanted"},"resolution_summary":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Resolution Summary","description":"How it was resolved"},"sentiment_analysis":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Sentiment Analysis","description":"Customer sentiment"},"tags":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Tags","description":"AI-generated tags"},"gemini_document_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Gemini Document Name","description":"Vertex AI RAG Corpus reference"},"created_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Created At","description":"ISO datetime when ticket was created"},"closed_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Closed At","description":"ISO datetime when ticket was closed"},"summarized_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Summarized At","description":"ISO datetime when AI summary was generated"}},"type":"object","required":["id","tenant_id","conversation_id"],"title":"TicketSummaryResponse","description":"Response model for a ticket summary stored in knowledge base.\n\nThis represents tickets that have been summarized and uploaded to\nthe Vertex AI RAG Corpus for similarity search.","example":{"ai_summary":"Customer was unable to reset their password. Agent guided them through the process.","closed_at":"2025-01-15T10:30:00Z","contact_id":"contact-uuid-012","conversation_id":"conv-uuid-789","created_at":"2025-01-15T10:00:00Z","gemini_document_name":"ticket_ticket-uuid-123","id":"ticket-uuid-123","resolution_summary":"Sent password reset link, customer successfully reset password","sentiment_analysis":"positive","subject":"Password reset issue","summarized_at":"2025-01-15T10:31:00Z","tags":["password","account_access","resolved"],"tenant_id":"tenant-uuid-456","user_intent":"Reset forgotten password"}},"TimeMetricPoint":{"properties":{"date":{"type":"string","title":"Date","description":"Date in YYYY-MM-DD format"},"response_minutes":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Response Minutes","description":"Average response time in minutes"},"resolution_hours":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Resolution Hours","description":"Average resolution time in hours"}},"type":"object","required":["date"],"title":"TimeMetricPoint","description":"Time metrics at a specific point in time"},"TimeMetricsOverTimeResponse":{"properties":{"timeline":{"items":{"$ref":"#/components/schemas/TimeMetricPoint"},"type":"array","title":"Timeline","description":"Daily averages for charting"},"by_channel":{"additionalProperties":{"items":{"$ref":"#/components/schemas/TimeMetricPoint"},"type":"array"},"type":"object","title":"By Channel","description":"Per-channel daily averages"},"period_start":{"type":"string","title":"Period Start","description":"Start of the reporting period (ISO format)"},"period_end":{"type":"string","title":"Period End","description":"End of the reporting period (ISO format)"}},"type":"object","required":["period_start","period_end"],"title":"TimeMetricsOverTimeResponse","description":"Response time and resolution time metrics over time for charting.\n\nReturned by GET /api/v1/analytics/time-metrics","example":{"by_channel":{"whatsapp":[{"date":"2026-01-15","resolution_hours":1.9,"response_minutes":3.8}]},"period_end":"2026-01-18T00:00:00Z","period_start":"2026-01-01T00:00:00Z","timeline":[{"date":"2026-01-15","resolution_hours":2.1,"response_minutes":4.5},{"date":"2026-01-16","resolution_hours":2.8,"response_minutes":5.2}]}},"TimeRangeConfig":{"properties":{"days":{"items":{"type":"integer"},"type":"array","title":"Days","description":"Days of week (0=Monday, 6=Sunday)"},"start":{"type":"string","title":"Start","description":"Start time in HH:MM format"},"end":{"type":"string","title":"End","description":"End time in HH:MM format"}},"type":"object","required":["days","start","end"],"title":"TimeRangeConfig","description":"Time range for schedule-based activation"},"TokenScopeInfo":{"properties":{"scope":{"type":"string","title":"Scope","description":"Full scope identifier (e.g., 'campaigns:read')"},"action":{"type":"string","title":"Action","description":"Action part of the scope (e.g., 'read')"},"description":{"type":"string","title":"Description","description":"Human-readable description of the scope"}},"type":"object","required":["scope","action","description"],"title":"TokenScopeInfo","description":"Information about a single scope available for token creation"},"ToolsConfig":{"properties":{"search_knowledge":{"type":"boolean","title":"Search Knowledge","default":true},"web_search":{"type":"boolean","title":"Web Search","default":false},"web_search_mode":{"anyOf":[{"type":"string","enum":["off","model","kb_fallback","always"]},{"type":"null"}],"title":"Web Search Mode"},"live_mode":{"anyOf":[{"type":"string","enum":["off","model","kb_fallback","always"]},{"type":"null"}],"title":"Live Mode"},"web_cache_ttl_minutes":{"anyOf":[{"type":"integer","maximum":60.0,"minimum":10.0},{"type":"null"}],"title":"Web Cache Ttl Minutes"},"handoff":{"type":"boolean","title":"Handoff","default":true},"set_state":{"type":"boolean","title":"Set State","default":true},"load_skill":{"type":"boolean","title":"Load Skill","default":true},"read_images":{"type":"boolean","title":"Read Images","default":false},"read_documents":{"type":"boolean","title":"Read Documents","default":false},"max_images_per_message":{"type":"integer","maximum":3.0,"minimum":1.0,"title":"Max Images Per Message","default":3},"max_attachments_per_conversation":{"type":"integer","maximum":50.0,"minimum":1.0,"title":"Max Attachments Per Conversation","default":10}},"additionalProperties":false,"type":"object","title":"ToolsConfig","description":"Which tools the agent may use. Unknown keys are rejected.\n\n``complete_task`` is not configurable: the runtime exposes it only in flow\nmode. ``load_skill`` defaults on (it is how model-selected skills load)."},"TopLink":{"properties":{"short_code":{"type":"string","title":"Short Code"},"short_url":{"type":"string","title":"Short Url"},"original_url":{"type":"string","title":"Original Url"},"label":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Label"},"clicks":{"type":"integer","title":"Clicks"},"first_click_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"First Click At"},"last_click_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Last Click At"}},"type":"object","required":["short_code","short_url","original_url","clicks"],"title":"TopLink","description":"Top performing link"},"TopPerformingLink":{"properties":{"short_code":{"type":"string","title":"Short Code","description":"Short code for the link"},"original_url":{"type":"string","title":"Original Url","description":"Original destination URL"},"clicks":{"type":"integer","title":"Clicks","description":"Total click count"},"label":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Label","description":"Link label/name"},"first_click_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"First Click At","description":"Timestamp of first click (ISO format)"},"last_click_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Last Click At","description":"Timestamp of last click (ISO format)"}},"type":"object","required":["short_code","original_url","clicks"],"title":"TopPerformingLink","description":"Top performing tracked link"},"TrackedLinkCreateManual":{"properties":{"original_url":{"type":"string","title":"Original Url","description":"Full destination URL to shorten"},"custom_alias":{"anyOf":[{"type":"string","maxLength":50,"minLength":3,"pattern":"^[a-z0-9-]+$"},{"type":"null"}],"title":"Custom Alias","description":"Custom short code/alias (e.g., 'summer-sale'). Must be unique. Only lowercase letters (a-z), digits (0-9), and hyphens (-) allowed."},"contact_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Contact Id","description":"Contact UUID for personalized link tracking (optional)"},"label":{"anyOf":[{"type":"string","maxLength":100},{"type":"null"}],"title":"Label","description":"User-friendly label for the link"},"tags":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Tags","description":"Tags for categorization (e.g., ['marketing', 'social'])"},"expires_in_days":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Expires In Days","description":"Days until expiration (default: 365)"},"tracking_domain":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Tracking Domain","description":"Tracking domain to use for this link. If not provided, uses the tenant's default from settings."}},"type":"object","required":["original_url"],"title":"TrackedLinkCreateManual","description":"Request to create a manual tracked link for standalone shortener"},"TrackedLinkDetail":{"properties":{"id":{"type":"string","title":"Id"},"tenant_id":{"type":"string","title":"Tenant Id"},"short_code":{"type":"string","title":"Short Code"},"short_url":{"type":"string","title":"Short Url"},"original_url":{"type":"string","title":"Original Url"},"feature_type":{"type":"string","title":"Feature Type"},"feature_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Feature Id"},"contact_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Contact Id"},"custom_alias":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Custom Alias"},"label":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Label"},"tags":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Tags"},"created_at":{"type":"string","format":"date-time","title":"Created At"},"expires_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Expires At"},"is_active":{"type":"boolean","title":"Is Active"},"click_count":{"type":"integer","title":"Click Count"},"unique_clicks":{"type":"integer","title":"Unique Clicks","description":"Unique clicks (by IP)","default":0},"first_click_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"First Click At"},"last_click_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Last Click At"}},"type":"object","required":["id","tenant_id","short_code","short_url","original_url","feature_type","feature_id","contact_id","custom_alias","label","tags","created_at","expires_at","is_active","click_count"],"title":"TrackedLinkDetail","description":"Full details for a single tracked link with click stats"},"TrackedLinkListItem":{"properties":{"id":{"type":"string","title":"Id"},"short_code":{"type":"string","title":"Short Code"},"short_url":{"type":"string","title":"Short Url"},"original_url":{"type":"string","title":"Original Url"},"feature_type":{"type":"string","title":"Feature Type"},"custom_alias":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Custom Alias"},"label":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Label"},"tags":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Tags"},"created_at":{"type":"string","format":"date-time","title":"Created At"},"expires_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Expires At"},"is_active":{"type":"boolean","title":"Is Active"},"click_count":{"type":"integer","title":"Click Count"}},"type":"object","required":["id","short_code","short_url","original_url","feature_type","custom_alias","label","tags","created_at","expires_at","is_active","click_count"],"title":"TrackedLinkListItem","description":"Summary item for list view"},"TrackedLinkListResponse":{"properties":{"items":{"items":{"$ref":"#/components/schemas/TrackedLinkListItem"},"type":"array","title":"Items"},"total":{"type":"integer","title":"Total"},"page":{"type":"integer","title":"Page"},"page_size":{"type":"integer","title":"Page Size"},"total_pages":{"type":"integer","title":"Total Pages"}},"type":"object","required":["items","total","page","page_size","total_pages"],"title":"TrackedLinkListResponse","description":"Paginated list of tracked links"},"TrackedLinkResponse":{"properties":{"id":{"type":"string","title":"Id"},"tenant_id":{"type":"string","title":"Tenant Id"},"short_code":{"type":"string","title":"Short Code"},"short_url":{"type":"string","title":"Short Url"},"original_url":{"type":"string","title":"Original Url"},"feature_type":{"type":"string","title":"Feature Type"},"feature_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Feature Id"},"contact_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Contact Id"},"created_at":{"type":"string","format":"date-time","title":"Created At"},"expires_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Expires At"},"is_active":{"type":"boolean","title":"Is Active"},"click_count":{"type":"integer","title":"Click Count"}},"type":"object","required":["id","tenant_id","short_code","short_url","original_url","feature_type","feature_id","contact_id","created_at","expires_at","is_active","click_count"],"title":"TrackedLinkResponse","description":"Response for a tracked link"},"TrackedLinkUpdate":{"properties":{"label":{"anyOf":[{"type":"string","maxLength":100},{"type":"null"}],"title":"Label","description":"User-friendly label for the link"},"tags":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Tags","description":"Tags for categorization"},"custom_alias":{"anyOf":[{"type":"string","maxLength":50,"minLength":3,"pattern":"^[a-z0-9-]+$"},{"type":"null"}],"title":"Custom Alias","description":"Custom short code/alias. Must be unique. Only lowercase letters (a-z), digits (0-9), and hyphens (-) allowed."},"is_active":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Is Active","description":"Whether the link is active"}},"type":"object","title":"TrackedLinkUpdate","description":"Request to update a tracked link"},"TrackingDomainSchema":{"properties":{"id":{"type":"string","title":"Id","description":"Unique identifier for the domain"},"domain":{"type":"string","title":"Domain","description":"The tracking domain (e.g., c.sendseven.com)"},"format":{"type":"string","title":"Format","description":"URL format pattern (e.g., /{code})"},"label":{"type":"string","title":"Label","description":"Human-readable label for the domain"},"default":{"type":"boolean","title":"Default","description":"Whether this is the default domain","default":false}},"type":"object","required":["id","domain","format","label"],"title":"TrackingDomainSchema","description":"Schema for a tracking domain option"},"TrackingDomainsResponse":{"properties":{"domains":{"items":{"$ref":"#/components/schemas/TrackingDomainSchema"},"type":"array","title":"Domains","description":"List of available tracking domains"},"default_domain":{"type":"string","title":"Default Domain","description":"The default tracking domain"}},"type":"object","required":["domains","default_domain"],"title":"TrackingDomainsResponse","description":"Schema for available tracking domains response"},"TrackingMode":{"type":"string","enum":["shared","per_recipient"],"title":"TrackingMode","description":"Tracking granularity for a tracked link.\n\n- SHARED: One link shared by all recipients (default, backwards-compatible).\n- PER_RECIPIENT: A template/parent link from which the campaign worker\n  clones one child row per recipient. Each child sets ``parent_link_id``\n  to the template's id, enabling per-recipient click attribution."},"TranscriptExportJobResponse":{"properties":{"job_id":{"type":"string","title":"Job Id","description":"PubSubJob id (UUID) — poll with this."},"conversation_id":{"type":"string","title":"Conversation Id","description":"Conversation this export targets."},"format":{"type":"string","title":"Format","description":"Requested export format ('pdf' | 'zip')."},"status":{"type":"string","title":"Status","description":"Job status: 'queued', 'processing', 'completed', or 'failed'."},"progress_percent":{"type":"integer","title":"Progress Percent","description":"Progress 0-100 (best-effort; may jump straight to 100).","default":0},"message":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Message","description":"User-facing status message (optional)."},"download_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Download Url","description":"Signed download URL for the artifact. Present only when status == 'completed'."},"storage_path":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Storage Path","description":"GCS storage path of the artifact (optional; present when status == 'completed')."},"error_message":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Error Message","description":"Failure reason. Present only when status == 'failed'."},"created_at":{"type":"string","title":"Created At","description":"When the job was created (UTC)."},"updated_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Updated At","description":"When the job was last updated (UTC)."},"completed_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Completed At","description":"When the job finished (UTC); set on completed/failed."}},"type":"object","required":["job_id","conversation_id","format","status","created_at"],"title":"TranscriptExportJobResponse","description":"Async transcript-export job status.\n\nReturned by BOTH the create (POST) and poll (GET) endpoints so the frontend\nconsumes one shape throughout the job lifecycle.\n\nField presence by ``status``:\n  * ``queued`` / ``processing`` — ``download_url``/``storage_path`` are null,\n    ``error_message`` is null. ``progress_percent`` may advance 0..100.\n  * ``completed`` — ``download_url`` is populated (signed URL to the artifact);\n    ``storage_path`` may be populated; ``completed_at`` is set.\n  * ``failed`` — ``error_message`` is populated; download fields are null.","example":{"completed_at":"2026-07-02T10:00:12Z","conversation_id":"conv_abc123","created_at":"2026-07-02T10:00:00Z","download_url":"https://storage.googleapis.com/...signed...","format":"pdf","job_id":"3f9a1c2e-...","message":"Transcript export ready","progress_percent":100,"status":"completed","storage_path":"tenants/t1/transcripts/conv_abc123.pdf","updated_at":"2026-07-02T10:00:12Z"}},"TranscriptExportRequest":{"properties":{"format":{"type":"string","enum":["pdf","zip"],"title":"Format","description":"Export artifact format: 'pdf' (transcript only) or 'zip' (transcript + attachments)."},"wait_for_summary":{"type":"boolean","title":"Wait For Summary","description":"If true, the worker waits for the conversation's AI summary (``conversation.ai_summary``) to be populated before rendering, so the generated transcript can include the summary at the top. Defaults to false (render immediately). Does NOT affect export deduplication — a given (conversation, format) yields the same artifact regardless.","default":false}},"type":"object","required":["format"],"title":"TranscriptExportRequest","description":"Request body for starting a transcript export.","example":{"format":"pdf","wait_for_summary":false}},"TranslateMessageRequest":{"properties":{"target_language":{"type":"string","title":"Target Language","description":"ISO 639-1 language code (e.g. 'en', 'de')"}},"type":"object","required":["target_language"],"title":"TranslateMessageRequest","description":"Request to translate an inbound message"},"TranslateMessageResponse":{"properties":{"original":{"type":"string","title":"Original"},"translated":{"type":"string","title":"Translated"},"language":{"type":"string","title":"Language"},"message_id":{"type":"string","title":"Message Id"}},"type":"object","required":["original","translated","language","message_id"],"title":"TranslateMessageResponse","description":"Response with translated message"},"TranslateTextRequest":{"properties":{"text":{"type":"string","title":"Text","description":"Text to translate"},"target_language":{"type":"string","title":"Target Language","description":"ISO 639-1 code or 'auto'"},"conversation_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Conversation Id","description":"Conversation ID to fetch recent inbound messages for auto-detect"}},"type":"object","required":["text","target_language"],"title":"TranslateTextRequest","description":"Request to translate arbitrary text"},"TranslateTextResponse":{"properties":{"translated":{"type":"string","title":"Translated"},"detected_language":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Detected Language"}},"type":"object","required":["translated"],"title":"TranslateTextResponse","description":"Response with translated text"},"TypingIndicatorRequest":{"properties":{"state":{"type":"string","enum":["on","off"],"title":"State","description":"`on` shows the typing indicator, `off` clears it.","default":"on"},"message_id":{"anyOf":[{"type":"string","maxLength":64},{"type":"null"}],"title":"Message Id","description":"Optional ID of an inbound message in this conversation to attach the indicator to. Only used by channels that tie typing to a received message (WhatsApp); defaults to the latest inbound message."}},"type":"object","title":"TypingIndicatorRequest","description":"Body for `POST /conversations/{conversation_id}/typing`.","example":{"state":"on"}},"UnifiedNoteFeedResponse":{"properties":{"items":{"items":{"$ref":"#/components/schemas/UnifiedNoteItem"},"type":"array","title":"Items","description":"List of items for this page"},"pagination":{"$ref":"#/components/schemas/PaginationMeta","description":"Pagination metadata"}},"type":"object","required":["items","pagination"],"title":"UnifiedNoteFeedResponse","description":"Paginated unified notes feed, newest first."},"UnifiedNoteItem":{"properties":{"id":{"type":"string","title":"Id","description":"Note ID, \"msg_<message_id>\" for an internal note without a notes row, or \"closing_<conversation_id>\" for a closing note"},"note_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Note Id","description":"Notes row ID (null when the item is only a timeline message or a closing note)"},"message_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Message Id","description":"ID of the internal-note message in the timeline, if any"},"kind":{"type":"string","enum":["conversation","contact"],"title":"Kind","description":"Conversation note or contact-level note"},"source":{"type":"string","enum":["note","internal_message","closing_note"],"title":"Source","description":"Where the item comes from"},"conversation_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Conversation Id"},"contact_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Contact Id"},"content":{"type":"string","title":"Content"},"user_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"User Id","description":"Author user ID (null for system/flow notes)"},"user_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"User Name"},"mentioned_user_ids":{"items":{"type":"string"},"type":"array","title":"Mentioned User Ids"},"created_at":{"type":"string","title":"Created At"},"updated_at":{"type":"string","title":"Updated At"},"is_current_conversation":{"type":"boolean","title":"Is Current Conversation","description":"True when conversation_id equals the requested conversation_id","default":false},"editable":{"type":"boolean","title":"Editable","description":"True iff note_id is set (edit/delete via PUT/DELETE /notes/{note_id})","default":false}},"type":"object","required":["id","kind","source","content","created_at","updated_at"],"title":"UnifiedNoteItem","description":"One note in the unified notes feed.\n\nA note is a notes row and/or an internal-note message in the conversation\ntimeline. Notes linked to their timeline message appear exactly once."},"UnlinkOutboundProviderResponse":{"properties":{"success":{"type":"boolean","title":"Success","description":"Whether the operation succeeded"},"message":{"type":"string","title":"Message","description":"Status message"},"connection":{"$ref":"#/components/schemas/EmailConnectionResponse","description":"Updated connection details"}},"type":"object","required":["success","message","connection"],"title":"UnlinkOutboundProviderResponse","description":"Response after unlinking outbound provider"},"UnsubscribeItem":{"properties":{"id":{"type":"string","title":"Id","description":"Unsubscribe token ID"},"email":{"type":"string","title":"Email","description":"Email address that unsubscribed"},"contact_id":{"type":"string","title":"Contact Id","description":"Contact ID"},"campaign_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Campaign Id","description":"Campaign ID if applicable"},"email_campaign_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Email Campaign Id","description":"Email campaign ID if applicable"},"unsubscribed_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Unsubscribed At","description":"When the unsubscribe occurred"},"method":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Method","description":"Unsubscribe method"},"reason":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Reason","description":"Reason for unsubscribing"},"ip_address":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Ip Address","description":"IP address of unsubscribe request"}},"type":"object","required":["id","email","contact_id"],"title":"UnsubscribeItem","description":"Individual unsubscribe item in list response"},"UnsubscribeListResponse":{"properties":{"unsubscribes":{"items":{"$ref":"#/components/schemas/UnsubscribeItem"},"type":"array","title":"Unsubscribes","description":"List of unsubscribes"},"total":{"type":"integer","title":"Total","description":"Total count"},"limit":{"type":"integer","title":"Limit","description":"Items per page"},"offset":{"type":"integer","title":"Offset","description":"Current offset"}},"type":"object","required":["unsubscribes","total","limit","offset"],"title":"UnsubscribeListResponse","description":"Paginated list of unsubscribes"},"UnsubscribeMethod":{"type":"string","enum":["link","keyword","manual","bounce","complaint","api","whatsapp_flow"],"title":"UnsubscribeMethod","description":"How the unsubscription happened"},"UnsubscribeRequest":{"properties":{"reason":{"anyOf":[{"type":"string","maxLength":1000},{"type":"null"}],"title":"Reason","description":"Reason for unsubscribing"},"method":{"$ref":"#/components/schemas/UnsubscribeMethod","description":"Method of unsubscription","default":"manual"}},"type":"object","title":"UnsubscribeRequest","description":"Schema for unsubscribe request","example":{"method":"manual","reason":"Too many messages"}},"UnsubscribeResponse":{"properties":{"success":{"type":"boolean","title":"Success"},"message":{"type":"string","title":"Message"},"subscription":{"anyOf":[{"$ref":"#/components/schemas/SubscriptionResponse"},{"type":"null"}]}},"type":"object","required":["success","message"],"title":"UnsubscribeResponse","description":"Schema for unsubscribe response","example":{"message":"Unsubscribed successfully","success":true}},"UnsubscribeStats":{"properties":{"total_unsubscribes":{"type":"integer","title":"Total Unsubscribes","description":"Total unsubscribes in period"},"period_days":{"type":"integer","title":"Period Days","description":"Number of days analyzed"},"methods":{"additionalProperties":{"type":"integer"},"type":"object","title":"Methods","description":"Breakdown by unsubscribe method","examples":[{"link":23,"one_click":45,"spam_complaint":2}]},"common_reasons":{"items":{"type":"string"},"type":"array","title":"Common Reasons","description":"Most common unsubscribe reasons","default":[]},"unsubscribe_rate_percent":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Unsubscribe Rate Percent","description":"Unsubscribe rate as percentage (if campaign context available)"},"campaign_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Campaign Id"},"email_campaign_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Email Campaign Id"}},"type":"object","required":["total_unsubscribes","period_days","methods"],"title":"UnsubscribeStats","description":"Unsubscribe statistics for reporting"},"UnsubscribedListItem":{"properties":{"list_id":{"type":"string","title":"List Id","description":"Originating list id the contact was unsubscribed from"},"list_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"List Name","description":"List display name, if resolvable"}},"type":"object","required":["list_id"],"title":"UnsubscribedListItem","description":"One list the contact was opted out of by this action."},"UpdateFlowDefaultsRequest":{"properties":{"screen":{"type":"string","maxLength":200,"minLength":1,"title":"Screen","description":"Screen id"},"data":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Data","description":"Default data for the screen; null removes the saved default"}},"type":"object","required":["screen"],"title":"UpdateFlowDefaultsRequest","description":"Save (or clear with data=null) the default data for one flow screen."},"UpdateFlowDefaultsResponse":{"properties":{"success":{"type":"boolean","title":"Success","default":true},"flow_id":{"type":"string","title":"Flow Id"},"defaults":{"additionalProperties":{"additionalProperties":true,"type":"object"},"type":"object","title":"Defaults"}},"type":"object","required":["flow_id"],"title":"UpdateFlowDefaultsResponse"},"UpdateFlowRequest":{"properties":{"name":{"anyOf":[{"type":"string","maxLength":200,"minLength":1},{"type":"null"}],"title":"Name"},"description":{"anyOf":[{"type":"string","maxLength":500},{"type":"null"}],"title":"Description"},"definition":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Definition"},"re_entry_policy":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Re Entry Policy"},"default_language":{"anyOf":[{"type":"string","maxLength":10},{"type":"null"}],"title":"Default Language","description":"Per-flow fallback language (see ``CreateFlowRequest``). Must be in the curated ~40 ``SUPPORTED_TRANSLATION_LOCALES``."},"prefer_originating_channel":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Prefer Originating Channel","description":"When True, outbound nodes attempt to send on the channel the contact originally entered the flow on, falling back to the node's ``channel_chain`` if that channel isn't allowed by the chain or the contact has no method for it. Defaults to True at the DB level."},"manually_triggerable":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Manually Triggerable","description":"When True, the flow appears in the operator's manual-start picker and accepts ``POST /flows/{id}/runs`` requests. Automatically forced True when the trigger kind is changed to ``manual``; never auto-cleared when the trigger is changed away from manual (operator preference is preserved)."},"auto_translate_messages":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Auto Translate Messages","description":"Per-flow opt-in for runtime LLM translation of outbound Send-node messages into the contact's preferred language."},"language_detection_strategy":{"anyOf":[{"type":"string","enum":["cascade","fixed"]},{"type":"null"}],"title":"Language Detection Strategy","description":"How this flow's runs resolve the language (see ``CreateFlowRequest``). ``cascade`` | ``fixed``. Omit on PATCH to preserve the current value; any other value is rejected with 422. Note: because NULL means \"cascade\", this field cannot be explicitly reset to NULL via PATCH — send ``\"cascade\"`` to restore the default behaviour."},"default_channel_chain":{"anyOf":[{"items":{"anyOf":[{"type":"string"},{"$ref":"#/components/schemas/ChannelChainEntry"}]},"type":"array"},{"type":"null"}],"title":"Default Channel Chain","description":"Flow-level fallback channel chain — see ``CreateFlowRequest``. Omitting this field on PATCH preserves the existing value; send an explicit empty list to clear it back to \"no default\" (stored as the empty list, not NULL, when explicitly cleared)."},"stop_on_agent_reply":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Stop On Agent Reply","description":"When True (the default for new flows), the flow engine terminates any active run for the contact + conversation the moment a human agent sends an outbound message. Omit on PATCH to preserve the current setting."},"stop_on_conversation_closed":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Stop On Conversation Closed","description":"When True (the default for new flows), the flow engine terminates any active run scoped to the conversation the moment that conversation is closed. Omit on PATCH to preserve the current setting."},"priority":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Priority","description":"Trigger-arbitration priority among entry flows (LOWER = higher priority, evaluated first). Omit on PATCH to preserve the current value. The bulk reorder endpoint writes this field."},"exclusive":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Exclusive","description":"When True, a running run of this entry flow blocks all OTHER entry flows from starting on the same conversation. Omit on PATCH to preserve the current value."}},"type":"object","title":"UpdateFlowRequest","description":"Request body for ``PATCH /flows/{flow_id}``.\n\nAll fields optional. Updating ``definition`` while the flow is\n``published`` is allowed because the engine snapshots the definition into\n``flow_runs.context.definition_snapshot`` at run-start (plan §5 risk #6)."},"UpdateProfileRequest":{"properties":{"display_name":{"anyOf":[{"type":"string","maxLength":255},{"type":"null"}],"title":"Display Name","description":"User-editable display name"},"chat_nickname":{"anyOf":[{"type":"string","maxLength":100},{"type":"null"}],"title":"Chat Nickname","description":"Chat display nickname"},"live_chat_available":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Live Chat Available","description":"Available for live chat assignment"},"personal_phone":{"anyOf":[{"type":"string","maxLength":50},{"type":"null"}],"title":"Personal Phone","description":"Personal phone for receiving notifications (user-global)"},"preferred_language":{"anyOf":[{"type":"string","maxLength":10},{"type":"null"}],"title":"Preferred Language","description":"Preferred UI language code (e.g., en, de, fr)"},"preferred_translation_language":{"anyOf":[{"type":"string","maxLength":10},{"type":"null"}],"title":"Preferred Translation Language","description":"Preferred language for chat translations (e.g., en, de, fr, es)"},"auto_translate_enabled":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Auto Translate Enabled","description":"Enable automatic translation of incoming messages (user-global). Omit or null to leave unchanged."},"shoutbox_hidden":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Shoutbox Hidden","description":"Hide/deactivate the team-chat Shoutbox widget across all devices (user-global). Omit or null to leave unchanged."},"spoken_languages":{"anyOf":[{"type":"string","maxLength":255},{"type":"null"}],"title":"Spoken Languages","description":"Comma-separated ISO 639-1 codes the user speaks (e.g., 'de,en'). Empty string clears the value; omit or null to leave unchanged."},"phone":{"anyOf":[{"type":"string","maxLength":50},{"type":"null"}],"title":"Phone","description":"Agent's phone number for email signatures (per-tenant)"},"title":{"anyOf":[{"type":"string","maxLength":255},{"type":"null"}],"title":"Title","description":"Agent's job title for email signatures (per-tenant)"},"email_auto_signature_enabled":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Email Auto Signature Enabled","description":"Master toggle for auto-inserting your signature into the composer (per-tenant)"},"email_auto_signature_on_replies":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Email Auto Signature On Replies","description":"Auto-insert your signature when opening a reply composer (per-tenant)"},"email_auto_signature_on_new":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Email Auto Signature On New","description":"Auto-insert your signature when opening a new-email composer (per-tenant)"}},"type":"object","title":"UpdateProfileRequest","description":"Request body for updating current user profile via /users/me/profile or /users/me"},"UpdateScheduledRuleRequest":{"properties":{"name":{"anyOf":[{"type":"string","maxLength":200,"minLength":1},{"type":"null"}],"title":"Name"},"description":{"anyOf":[{"type":"string","maxLength":500},{"type":"null"}],"title":"Description"},"channel_type":{"anyOf":[{"$ref":"#/components/schemas/ChannelType"},{"type":"null"}]},"channel_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Channel Id"},"email_mailbox_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Email Mailbox Id","description":"New pinned EmailMailbox UUID. Only valid when the rule's channel_type is (or is being changed to) ``email``."},"payload":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Payload"},"time_of_day_local":{"anyOf":[{"type":"string","maxLength":8},{"type":"null"}],"title":"Time Of Day Local"},"timezone":{"anyOf":[{"type":"string","maxLength":64},{"type":"null"}],"title":"Timezone"},"recurrence_rule":{"anyOf":[{"type":"string","maxLength":255},{"type":"null"}],"title":"Recurrence Rule","description":"Pass an empty string to clear the rule. Capped at 255 chars."},"segment_filter":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Segment Filter","description":"Audience selector. Two accepted shapes:\n  * NEW: ``{\"match_op\": \"any\"|\"all\", \"rules\": [{\"kind\": \"tag\"|\"list\"|\"language\", \"op\": ..., \"values\": [...]}]}``\n  * LEGACY: ``{\"tags\": [...], \"tags_op\": \"any\"|\"all\"}`` (normalized by the engine)."},"next_evaluation_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Next Evaluation At"},"status":{"anyOf":[{"$ref":"#/components/schemas/ScheduledRuleStatus"},{"type":"null"}],"description":"Only ``active`` <-> ``paused`` transitions allowed via PATCH. DELETE the rule to archive it."}},"type":"object","title":"UpdateScheduledRuleRequest","description":"Request body for ``PATCH /scheduler/rules/{rule_id}``.\n\nAll fields optional; only fields present in the body are mutated. ``status``\ntransitions are constrained: only ``active <-> paused`` may be set via\nPATCH. Use ``DELETE`` to archive (soft-delete) a rule."},"UpdateScheduledSendRequest":{"properties":{"payload":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Payload","description":"Replaces the entire payload dict. Hard-capped at 64 KB JSON-encoded. Per-channel rules are re-checked against the existing row's channel_type: WhatsApp must carry {template_id, variables}; SMS/Telegram/Messenger/Instagram/RCS must NOT carry template_id (but may carry `attachments` — ≤20 attachment UUIDs owned by your workspace); email must carry a valid subject + body or template_id."},"email_mailbox_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Email Mailbox Id","description":"New pinned EmailMailbox UUID. Only valid when the underlying scheduled send has channel_type=email. Pass an empty string to clear."},"send_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Send At","description":"New first-fire timestamp. ``next_run_at`` is updated to match."},"recurrence_rule":{"anyOf":[{"type":"string","maxLength":255},{"type":"null"}],"title":"Recurrence Rule","description":"New iCalendar RRULE. Pass an empty string to clear the rule (turn a recurring send into a one-off). Capped at 255 chars."}},"type":"object","title":"UpdateScheduledSendRequest","description":"Request body for ``PATCH /scheduler/sends/{send_id}``.\n\nOnly allowed while ``status='scheduled'``. All fields optional — only fields\npresent in the body are mutated. Setting a field to ``null`` explicitly is\nnot supported (use exclude_unset semantics)."},"UpdateTemplateRequest":{"properties":{"components":{"anyOf":[{"items":{"$ref":"#/components/schemas/TemplateComponentRequest"},"type":"array"},{"type":"null"}],"title":"Components","description":"Updated template components"},"default_header_media_url":{"anyOf":[{"type":"string","maxLength":500},{"type":"null"}],"title":"Default Header Media Url","description":"Default header media URL for IMAGE/VIDEO/DOCUMENT templates. Used as fallback when callers do not pass `header_media_url` on send. Pass an empty string to clear."},"default_header_document_filename":{"anyOf":[{"type":"string","maxLength":255},{"type":"null"}],"title":"Default Header Document Filename","description":"Default filename presented to the recipient for DOCUMENT-type headers. Used as fallback when callers do not pass `header_document_filename` on send. Pass an empty string to clear; omit the field to leave it unchanged. Only meaningful for DOCUMENT header templates."}},"type":"object","title":"UpdateTemplateRequest","description":"Request to update an existing WhatsApp template"},"UpdateVariableMappingRequest":{"properties":{"variable_mapping":{"additionalProperties":{"type":"string"},"type":"object","title":"Variable Mapping","description":"Map placeholders to contact fields: {'1': 'contact.first_name'}"}},"type":"object","required":["variable_mapping"],"title":"UpdateVariableMappingRequest","description":"Request to update variable mapping for a template"},"UpdateVariableMappingResponse":{"properties":{"id":{"type":"string","title":"Id"},"variable_mapping":{"additionalProperties":{"type":"string"},"type":"object","title":"Variable Mapping"},"updated_at":{"type":"string","format":"date-time","title":"Updated At"}},"type":"object","required":["id","variable_mapping","updated_at"],"title":"UpdateVariableMappingResponse","description":"Response after updating variable mapping"},"UpdateWaFlowRequest":{"properties":{"name":{"anyOf":[{"type":"string","maxLength":200,"minLength":1},{"type":"null"}],"title":"Name"},"description":{"anyOf":[{"type":"string","maxLength":1000},{"type":"null"}],"title":"Description"},"categories":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Categories"},"status":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Status","description":"'archived' archives; 'active' restores"}},"type":"object","title":"UpdateWaFlowRequest"},"UserEffectiveScopesResponse":{"properties":{"user_id":{"type":"string","title":"User Id","description":"User UUID"},"tenant_id":{"type":"string","title":"Tenant Id","description":"Tenant UUID"},"scopes":{"items":{"type":"string"},"type":"array","title":"Scopes","description":"List of effective scopes"},"total":{"type":"integer","title":"Total","description":"Total number of scopes"}},"type":"object","required":["user_id","tenant_id","scopes","total"],"title":"UserEffectiveScopesResponse","description":"Response containing a user's effective scopes","example":{"scopes":["campaigns:read","campaigns:update","contacts:read"],"tenant_id":"uuid-of-tenant","total":3,"user_id":"uuid-of-user"}},"UserInboxAccessResponse":{"properties":{"mode":{"type":"string","enum":["all","restricted"],"title":"Mode"},"inbox_ids":{"items":{"type":"string"},"type":"array","title":"Inbox Ids"},"contact_history_visibility":{"type":"string","enum":["all","accessible_only"],"title":"Contact History Visibility","default":"all"}},"type":"object","required":["mode"],"title":"UserInboxAccessResponse","description":"Current per-user inbox-access settings.\n\nReturned by GET ``/users/{user_id}/inbox-access``.\n\n``contact_history_visibility`` is an INDEPENDENT toggle from ``mode``: it\ncontrols whether the contact detail page's conversation history panel is\nfiltered to channels the user has access to via their inbox memberships.\nDefault 'all' = no filter (preserves legacy behaviour)."},"UserInboxAccessUpdate":{"properties":{"mode":{"type":"string","enum":["all","restricted"],"title":"Mode"},"inbox_ids":{"items":{"type":"string"},"type":"array","maxItems":500,"title":"Inbox Ids"},"contact_history_visibility":{"anyOf":[{"type":"string","enum":["all","accessible_only"]},{"type":"null"}],"title":"Contact History Visibility"}},"type":"object","required":["mode"],"title":"UserInboxAccessUpdate","description":"Request body for PUT ``/users/{user_id}/inbox-access``.\n\nCapped at 500 inbox_ids to match the per-inbox ACL replace endpoint\n(``InboxUsersUpdate``) and to bound EXISTS subquery cost downstream.\n\n``contact_history_visibility`` is optional — when omitted, the existing\nvalue on the row is preserved (allowing legacy clients that don't yet\nship the new toggle to keep PUT-ing only ``mode`` + ``inbox_ids`` without\ninadvertently flipping the new column back to its default)."},"UserInviteRequest":{"properties":{"email":{"type":"string","format":"email","title":"Email","description":"Email address to invite"},"name":{"type":"string","maxLength":255,"title":"Name","description":"User's display name"},"role_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Role Id","description":"RBAC role UUID to assign (system OR this tenant's custom role). Get valid ids from GET /roles. Preferred over the legacy `role` field."},"role":{"$ref":"#/components/schemas/UserRole","description":"DEPRECATED legacy role enum. Ignored when `role_id` is set.","default":"agent"}},"type":"object","required":["email","name"],"title":"UserInviteRequest","description":"Request body for inviting a new user to a tenant.\n\nRole assignment supports BOTH system roles and tenant-created custom roles\nvia ``role_id`` (the RBAC role UUID, as returned by ``GET /roles``). The\nlegacy ``role`` enum field is retained only for backwards compatibility and\nis ignored when ``role_id`` is provided."},"UserInviteResponse":{"properties":{"success":{"type":"boolean","title":"Success","description":"Whether the operation succeeded","default":true},"message":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Message","description":"Optional success message"},"email":{"type":"string","title":"Email","description":"Email address invited"},"note":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Note","description":"Additional information"}},"type":"object","required":["email"],"title":"UserInviteResponse","description":"Response for user invitation","example":{"email":"john@example.com","message":"User invited successfully","note":"User can now log in via Auth0 using this email","success":true}},"UserPermissionsResponseSchema":{"properties":{"user_id":{"type":"string","title":"User Id"},"tenant_id":{"type":"string","title":"Tenant Id"},"roles":{"items":{"$ref":"#/components/schemas/UserRoleResponseSchema"},"type":"array","title":"Roles"},"direct_scopes":{"items":{"$ref":"#/components/schemas/UserScopeResponseSchema"},"type":"array","title":"Direct Scopes"},"effective_scopes":{"items":{"type":"string"},"type":"array","title":"Effective Scopes"},"total_scopes":{"type":"integer","title":"Total Scopes"}},"type":"object","required":["user_id","tenant_id","roles","direct_scopes","effective_scopes","total_scopes"],"title":"UserPermissionsResponseSchema"},"UserResponse":{"properties":{"created_at":{"type":"string","title":"Created At","description":"When the resource was created (UTC)"},"updated_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Updated At","description":"When the resource was last updated (UTC)"},"id":{"type":"string","title":"Id","description":"Unique identifier (UUID)"},"email":{"type":"string","title":"Email","description":"User email address"},"name":{"type":"string","title":"Name","description":"User display name"},"avatar_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Avatar Url","description":"URL to user's avatar image"},"chat_nickname":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Chat Nickname","description":"Chat display nickname"},"live_chat_available":{"type":"boolean","title":"Live Chat Available","description":"Available for live chat assignment","default":true}},"type":"object","required":["created_at","id","email","name"],"title":"UserResponse","description":"Basic user response.\n\nUsed for GET /users/{user_id} and similar endpoints.","example":{"avatar_url":"https://example.com/avatar.jpg","chat_nickname":"JohnD","created_at":"2025-01-15T10:30:00Z","email":"john@example.com","id":"u1a2b3c4-d5e6-f7g8-h9i0-j1k2l3m4n5o6","live_chat_available":true,"name":"John Doe","updated_at":"2025-01-15T14:45:00Z"}},"UserRole":{"type":"string","enum":["owner","admin","agent","viewer"],"title":"UserRole","description":"User roles within a tenant"},"UserRoleAssignmentSchema":{"properties":{"role_id":{"type":"string","title":"Role Id"},"expires_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Expires At"}},"type":"object","required":["role_id"],"title":"UserRoleAssignmentSchema","example":{"role_id":"uuid-of-role"}},"UserRoleResponseSchema":{"properties":{"id":{"type":"string","title":"Id"},"user_id":{"type":"string","title":"User Id"},"tenant_id":{"type":"string","title":"Tenant Id"},"role_id":{"type":"string","title":"Role Id"},"role_name":{"type":"string","title":"Role Name"},"assigned_by_user_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Assigned By User Id"},"assigned_at":{"type":"string","format":"date-time","title":"Assigned At"},"expires_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Expires At"}},"type":"object","required":["id","user_id","tenant_id","role_id","role_name","assigned_by_user_id","assigned_at","expires_at"],"title":"UserRoleResponseSchema"},"UserRoleUpdateRequest":{"properties":{"role":{"$ref":"#/components/schemas/UserRole","description":"New role to assign"}},"type":"object","required":["role"],"title":"UserRoleUpdateRequest","description":"Request body for updating a user's role in a tenant"},"UserRoleUpdateResponse":{"properties":{"success":{"type":"boolean","title":"Success","description":"Whether the operation succeeded","default":true},"message":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Message","description":"Optional success message"},"new_role":{"type":"string","title":"New Role","description":"The new role assigned"}},"type":"object","required":["new_role"],"title":"UserRoleUpdateResponse","description":"Response for role update","example":{"message":"Role updated successfully","new_role":"ADMIN","success":true}},"UserScopeGrantSchema":{"properties":{"scope":{"type":"string","title":"Scope"},"expires_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Expires At"}},"type":"object","required":["scope"],"title":"UserScopeGrantSchema","example":{"expires_at":"2025-12-31T23:59:59Z","scope":"campaigns:update"}},"UserScopeResponseSchema":{"properties":{"id":{"type":"string","title":"Id"},"user_id":{"type":"string","title":"User Id"},"tenant_id":{"type":"string","title":"Tenant Id"},"scope":{"type":"string","title":"Scope"},"granted_by_user_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Granted By User Id"},"granted_at":{"type":"string","format":"date-time","title":"Granted At"},"expires_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Expires At"}},"type":"object","required":["id","user_id","tenant_id","scope","granted_by_user_id","granted_at","expires_at"],"title":"UserScopeResponseSchema"},"UserSignatureResponse":{"properties":{"id":{"type":"string","title":"Id","description":"Signature ID"},"tenant_id":{"type":"string","title":"Tenant Id","description":"Tenant ID"},"user_id":{"type":"string","title":"User Id","description":"User ID who owns this signature"},"content_html":{"type":"string","title":"Content Html","description":"HTML content"},"content_text":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Content Text","description":"Plain text version"},"is_active":{"type":"boolean","title":"Is Active","description":"Whether this signature is active"},"created_at":{"type":"string","title":"Created At","description":"When the signature was created"},"updated_at":{"type":"string","title":"Updated At","description":"When the signature was last updated"}},"type":"object","required":["id","tenant_id","user_id","content_html","is_active","created_at","updated_at"],"title":"UserSignatureResponse","description":"Response schema for user signature","example":{"content_html":"<div><p><strong>John Smith</strong></p></div>","content_text":"John Smith","created_at":"2026-01-16T10:00:00Z","id":"sig-user-123","is_active":true,"tenant_id":"tenant-456","updated_at":"2026-01-16T10:00:00Z","user_id":"user-789"}},"UserSignatureUpdate":{"properties":{"content_html":{"type":"string","maxLength":500000,"minLength":1,"title":"Content Html","description":"HTML content of the personalized signature"},"content_text":{"anyOf":[{"type":"string","maxLength":100000},{"type":"null"}],"title":"Content Text","description":"Plain text version for non-HTML emails"},"is_active":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Is Active","description":"Whether the signature is active. When ``False``, effective-signature resolution will skip this user signature (and the auto-insert effect in the composer will not pre-fill it). Optional on PUT — omitting it leaves the existing flag unchanged."}},"type":"object","required":["content_html"],"title":"UserSignatureUpdate","description":"Schema for creating or updating a user's signature","example":{"content_html":"<div style=\"font-family: Arial, sans-serif;\">\n<p><strong>John Smith</strong></p>\n<p>Senior Support Agent</p>\n<p>Acme Corp</p>\n<p>Phone: +1 555-123-4567</p>\n</div>","content_text":"John Smith\nSenior Support Agent\nAcme Corp\nPhone: +1 555-123-4567"}},"UserTagItem":{"properties":{"id":{"type":"string","title":"Id"},"name":{"type":"string","title":"Name"},"color":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Color"}},"type":"object","required":["id","name"],"title":"UserTagItem","description":"A tag held by a team member."},"UserTagsResponse":{"properties":{"items":{"items":{"$ref":"#/components/schemas/UserTagItem"},"type":"array","title":"Items"}},"type":"object","title":"UserTagsResponse","description":"Tags held by a team member."},"UserTagsUpdate":{"properties":{"tag_ids":{"items":{"type":"string"},"type":"array","maxItems":100,"title":"Tag Ids"}},"type":"object","title":"UserTagsUpdate","description":"Replace the full set of tags held by a team member.\n\nAn empty list removes every tag from the member."},"UserTenantResponse":{"properties":{"id":{"type":"string","title":"Id","description":"Tenant ID"},"name":{"type":"string","title":"Name","description":"Tenant name"},"slug":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Slug","description":"Tenant URL slug"},"role":{"$ref":"#/components/schemas/UserRole","description":"User's role in this tenant"},"joined_at":{"type":"string","format":"date-time","title":"Joined At","description":"When user joined this tenant"},"is_trial":{"type":"boolean","title":"Is Trial","description":"Whether tenant is on trial","default":false},"trial_ends_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Trial Ends At","description":"When trial ends"},"billing_account_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Billing Account Id","description":"Associated billing account ID"},"default_country":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Default Country","description":"Tenant default country (ISO-3166-1 alpha-2) or null"},"default_timezone":{"type":"string","title":"Default Timezone","description":"Tenant default IANA timezone (e.g. 'Europe/Berlin')","default":"UTC"}},"type":"object","required":["id","name","role","joined_at"],"title":"UserTenantResponse","description":"Response for a tenant the user belongs to.\n\nUsed in GET /users/me/tenants endpoint.","example":{"billing_account_id":"b123","default_country":"DE","default_timezone":"Europe/Berlin","id":"t1a2b3c4-d5e6-f7g8-h9i0-j1k2l3m4n5o6","is_trial":false,"joined_at":"2025-01-15T10:30:00Z","name":"Acme Corp","role":"ADMIN","slug":"acme-corp"}},"UserUpdateRequest":{"properties":{"name":{"anyOf":[{"type":"string","maxLength":255},{"type":"null"}],"title":"Name","description":"User's display name"},"avatar_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Avatar Url","description":"URL to user's avatar image"},"chat_nickname":{"anyOf":[{"type":"string","maxLength":100},{"type":"null"}],"title":"Chat Nickname","description":"Chat display nickname"},"live_chat_available":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Live Chat Available","description":"Available for live chat assignment"}},"type":"object","title":"UserUpdateRequest","description":"Request body for updating a user's profile (admin action)"},"ValidateRequest":{"properties":{"builder_doc":{"additionalProperties":true,"type":"object","title":"Builder Doc"}},"type":"object","required":["builder_doc"],"title":"ValidateRequest"},"ValidationError":{"properties":{"loc":{"items":{"anyOf":[{"type":"string"},{"type":"integer"}]},"type":"array","title":"Location"},"msg":{"type":"string","title":"Message"},"type":{"type":"string","title":"Error Type"}},"type":"object","required":["loc","msg","type"],"title":"ValidationError"},"VariableCategory":{"properties":{"label":{"type":"string","title":"Label","description":"Category display name"},"description":{"type":"string","title":"Description","description":"Category description"},"variables":{"items":{"$ref":"#/components/schemas/VariableDefinition"},"type":"array","title":"Variables","description":"Variables in this category"}},"type":"object","required":["label","description","variables"],"title":"VariableCategory","description":"Category of email template variables","example":{"description":"Recipient contact information","label":"Contact","variables":[{"description":"Contact's full name","example":"John Doe","name":"contact.name","preview_filled":false,"required":false}]}},"VariableDefinition":{"properties":{"name":{"type":"string","title":"Name","description":"Variable name (e.g., 'contact.name')"},"description":{"type":"string","title":"Description","description":"Human-readable description"},"example":{"type":"string","title":"Example","description":"Example value for preview"},"required":{"type":"boolean","title":"Required","description":"Whether this variable is required","default":false},"preview_filled":{"type":"boolean","title":"Preview Filled","description":"True if auto-filled from tenant settings in preview","default":false}},"type":"object","required":["name","description","example"],"title":"VariableDefinition","description":"Definition of a single email template variable","example":{"description":"Contact's full name","example":"John Doe","name":"contact.name","preview_filled":false,"required":false}},"VerificationCheck":{"properties":{"code":{"anyOf":[{"type":"string","maxLength":32},{"type":"null"}],"title":"Code","description":"The 6-digit code (OTP methods)"},"pin":{"anyOf":[{"type":"string","maxLength":32},{"type":"null"}],"title":"Pin","description":"The PIN (method 'pin')"}},"additionalProperties":false,"type":"object","title":"VerificationCheck"},"VerificationCheckResponse":{"properties":{"id":{"type":"string","title":"Id"},"status":{"type":"string","title":"Status","description":"approved, pending, expired or locked"},"attempts_remaining":{"type":"integer","title":"Attempts Remaining"},"contact_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Contact Id"}},"type":"object","required":["id","status","attempts_remaining"],"title":"VerificationCheckResponse"},"VerificationCreate":{"properties":{"contact_id":{"anyOf":[{"type":"string","maxLength":36},{"type":"null"}],"title":"Contact Id","description":"Contact to verify (exactly one of contact_id / identifier)"},"identifier":{"anyOf":[{"$ref":"#/components/schemas/VerificationIdentifier"},{"type":"null"}],"description":"Identifier the person claims (exactly one of contact_id / identifier)"},"method":{"type":"string","enum":["email","sms","whatsapp","pin","pin_unlock_auto"],"title":"Method","description":"'pin_unlock_auto' (only with purpose 'pin_unlock') lets SendSeven pick the contact's verified email or phone number"},"purpose":{"type":"string","enum":["identity","pin_unlock"],"title":"Purpose","description":"'identity' verifies the person. 'pin_unlock' sends a code to one of the contact's verified email addresses or phone numbers; approving it unlocks a PIN that is locked until reset (it does not verify identity)","default":"identity"},"conversation_id":{"anyOf":[{"type":"string","maxLength":36},{"type":"null"}],"title":"Conversation Id","description":"Link the result to this conversation"},"sender":{"anyOf":[{"type":"string","enum":["tenant","generic"]},{"type":"null"}],"title":"Sender","description":"Override the configured sender for email/SMS"},"channel_id":{"anyOf":[{"type":"string","maxLength":36},{"type":"null"}],"title":"Channel Id","description":"Sending channel when sender is 'tenant'"},"locale":{"anyOf":[{"type":"string","maxLength":10},{"type":"null"}],"title":"Locale","description":"Language of the code message, e.g. 'de'"}},"additionalProperties":false,"type":"object","required":["method"],"title":"VerificationCreate"},"VerificationIdentifier":{"properties":{"type":{"type":"string","enum":["email","phone","whatsapp"],"title":"Type","description":"Identifier type"},"value":{"type":"string","maxLength":320,"minLength":1,"title":"Value","description":"Email address or phone number"}},"type":"object","required":["type","value"],"title":"VerificationIdentifier"},"VerificationResponse":{"properties":{"id":{"type":"string","title":"Id"},"status":{"type":"string","title":"Status","description":"pending, approved, failed, expired, locked or cancelled"},"method":{"type":"string","title":"Method"},"purpose":{"type":"string","title":"Purpose","description":"identity or pin_unlock","default":"identity"},"expires_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Expires At"},"resend_available_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Resend Available At"},"contact_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Contact Id"},"attempts_remaining":{"type":"integer","title":"Attempts Remaining","default":0},"resend_count":{"type":"integer","title":"Resend Count","default":0},"conversation_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Conversation Id"},"source":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Source"},"approved_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Approved At"},"created_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Created At"}},"type":"object","required":["id","status","method"],"title":"VerificationResponse"},"VerificationSettingsResponse":{"properties":{"email_sender":{"type":"string","title":"Email Sender","description":"'generic' or an email mailbox id"},"sms_sender":{"type":"string","title":"Sms Sender","description":"'generic' or an SMS channel id"},"whatsapp_channel_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Whatsapp Channel Id"},"whatsapp_template_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Whatsapp Template Id"},"whatsapp_template_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Whatsapp Template Name"},"whatsapp_template_status":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Whatsapp Template Status"},"enabled_methods":{"items":{"type":"string"},"type":"array","title":"Enabled Methods"},"allowed_countries":{"items":{"type":"string"},"type":"array","title":"Allowed Countries","description":"Effective destination countries (ISO 3166 alpha-2) for SMS and WhatsApp codes. Defaults to the workspace country."},"allowed_countries_is_default":{"type":"boolean","title":"Allowed Countries Is Default","description":"True when no custom list is stored","default":true},"sms_country_cap":{"items":{"type":"string"},"type":"array","title":"Sms Country Cap","description":"Countries SMS codes can ever be sent to (EU/EEA)"},"eu_countries":{"items":{"type":"string"},"type":"array","title":"Eu Countries","description":"EU member states (for 'Add all EU')"},"agent_new_number_daily_cap":{"type":"integer","title":"Agent New Number Daily Cap","description":"Codes the AI agent may send per day to phone numbers that are not on file (0 = none). Default 10.","default":10},"agent_new_number_daily_cap_is_default":{"type":"boolean","title":"Agent New Number Daily Cap Is Default","description":"True when no custom value is stored","default":true},"updated_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Updated At"}},"type":"object","required":["email_sender","sms_sender","enabled_methods"],"title":"VerificationSettingsResponse"},"VerificationSettingsUpdate":{"properties":{"email_sender":{"anyOf":[{"type":"string","maxLength":36},{"type":"null"}],"title":"Email Sender"},"sms_sender":{"anyOf":[{"type":"string","maxLength":36},{"type":"null"}],"title":"Sms Sender"},"whatsapp_channel_id":{"anyOf":[{"type":"string","maxLength":36},{"type":"null"}],"title":"Whatsapp Channel Id"},"whatsapp_template_id":{"anyOf":[{"type":"string","maxLength":64},{"type":"null"}],"title":"Whatsapp Template Id"},"enabled_methods":{"anyOf":[{"items":{"type":"string","enum":["email","sms","whatsapp","pin"]},"type":"array"},{"type":"null"}],"title":"Enabled Methods"},"allowed_countries":{"anyOf":[{"items":{"type":"string"},"type":"array","maxItems":300},{"type":"null"}],"title":"Allowed Countries","description":"ISO 3166 alpha-2 codes allowed for SMS / WhatsApp codes; null resets to the workspace country. SMS is additionally limited to EU/EEA countries."},"agent_new_number_daily_cap":{"anyOf":[{"type":"integer","maximum":200.0,"minimum":0.0},{"type":"null"}],"title":"Agent New Number Daily Cap","description":"Codes the AI agent may send per day to phone numbers not on file (0-200, 0 = none); null resets to the default of 10. The limit of one per conversation stays fixed."}},"additionalProperties":false,"type":"object","title":"VerificationSettingsUpdate"},"WebSearchModeEnum":{"type":"string","enum":["off","kb_fallback","always"],"title":"WebSearchModeEnum","description":"When a bot is allowed to perform a live web search (Beta).\n\n- ``off`` (default / NULL): today's behaviour — KB-only, never searches\n  the web.\n- ``kb_fallback``: only search the web after the KB returns [NO_ANSWER].\n- ``always``: run the on-topic gate then search the web for every message."},"WebhookDeliveryListResponse":{"properties":{"items":{"items":{"$ref":"#/components/schemas/WebhookDeliveryResponse"},"type":"array","title":"Items"},"total":{"type":"integer","title":"Total"},"page":{"type":"integer","title":"Page"},"page_size":{"type":"integer","title":"Page Size"},"has_more":{"type":"boolean","title":"Has More"}},"type":"object","required":["items","total","page","page_size","has_more"],"title":"WebhookDeliveryListResponse","description":"Paginated list of webhook deliveries"},"WebhookDeliveryResponse":{"properties":{"id":{"type":"string","title":"Id"},"tenant_id":{"type":"string","title":"Tenant Id"},"webhook_endpoint_id":{"type":"string","title":"Webhook Endpoint Id"},"event_type":{"type":"string","title":"Event Type"},"event_id":{"type":"string","title":"Event Id"},"attempt_number":{"type":"integer","title":"Attempt Number"},"status":{"$ref":"#/components/schemas/DeliveryStatus"},"response_status_code":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Response Status Code"},"response_body":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Response Body"},"response_time_ms":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Response Time Ms"},"request_payload":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Request Payload"},"error_message":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Error Message"},"next_retry_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Next Retry At"},"created_at":{"type":"string","title":"Created At"},"completed_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Completed At"},"can_retry":{"type":"boolean","title":"Can Retry","description":"Whether this delivery can be manually retried (failed or dead_letter status). Always false for `queued` deliveries: those are owned by the automatic suspension replay and are re-sent when the endpoint is reactivated.","default":false}},"type":"object","required":["id","tenant_id","webhook_endpoint_id","event_type","event_id","attempt_number","status","created_at"],"title":"WebhookDeliveryResponse","description":"Schema for webhook delivery log response","example":{"attempt_number":1,"can_retry":false,"completed_at":"2025-12-11T10:30:00.150Z","created_at":"2025-12-11T10:30:00Z","event_id":"msg_456def","event_type":"message.received","id":"del_xyz789","response_body":"{\"received\": true}","response_status_code":200,"response_time_ms":150,"status":"success","tenant_id":"tenant_xyz","webhook_endpoint_id":"wh_abc123"}},"WebhookEndpointCreate":{"properties":{"name":{"type":"string","maxLength":255,"minLength":1,"title":"Name","description":"User-friendly name for the webhook"},"url":{"type":"string","maxLength":2048,"minLength":1,"title":"Url","description":"HTTPS URL to receive webhook events"},"authorization_header":{"anyOf":[{"type":"string","maxLength":1024},{"type":"null"}],"title":"Authorization Header","description":"Authorization header value to send with each webhook (e.g., 'Bearer token123')"},"subscribed_events":{"items":{"type":"string"},"type":"array","maxItems":50,"minItems":1,"title":"Subscribed Events","description":"List of event types to subscribe to"},"retry_strategy":{"$ref":"#/components/schemas/RetryStrategy","description":"Retry strategy for failed deliveries","default":"exponential"},"max_retries":{"type":"integer","maximum":15.0,"minimum":0.0,"title":"Max Retries","description":"Maximum number of retry attempts (default: 8, covers ~2 hours with exponential backoff)","default":8},"timeout_seconds":{"type":"integer","maximum":60.0,"minimum":5.0,"title":"Timeout Seconds","description":"Request timeout in seconds","default":30},"source_filter_mode":{"type":"string","pattern":"^(all|selected)$","title":"Source Filter Mode","description":"Source filtering mode: 'all' receives events from all channels, 'selected' only receives events from specified channels/integrations","default":"all"},"filtered_channel_ids":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Filtered Channel Ids","description":"Channel IDs to filter events by (only used when source_filter_mode='selected')"},"filtered_email_integration_ids":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Filtered Email Integration Ids","description":"Email integration IDs to filter events by (only used when source_filter_mode='selected')"}},"type":"object","required":["name","url","subscribed_events"],"title":"WebhookEndpointCreate","description":"Schema for creating a new webhook endpoint","example":{"authorization_header":"Bearer sk_live_xxxxx","max_retries":5,"name":"CRM Integration","retry_strategy":"exponential","source_filter_mode":"all","subscribed_events":["message.received","message.sent"],"timeout_seconds":30,"url":"https://api.mycrm.com/webhooks/sendseven"}},"WebhookEndpointResponse":{"properties":{"id":{"type":"string","title":"Id"},"tenant_id":{"type":"string","title":"Tenant Id"},"name":{"type":"string","title":"Name"},"url":{"type":"string","title":"Url"},"has_authorization_header":{"type":"boolean","title":"Has Authorization Header","description":"Whether an authorization header is configured (value is not exposed)"},"subscribed_events":{"items":{"type":"string"},"type":"array","title":"Subscribed Events"},"source_filter_mode":{"type":"string","title":"Source Filter Mode","default":"all"},"filtered_channel_ids":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Filtered Channel Ids"},"filtered_email_integration_ids":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Filtered Email Integration Ids"},"is_active":{"type":"boolean","title":"Is Active"},"is_verified":{"type":"boolean","title":"Is Verified"},"verification_method":{"type":"string","title":"Verification Method","description":"How this endpoint's URL ownership was established / how it is (re)verified: 'challenge' (echo-challenge handshake, the default) or 'oauth_app' (created by a third-party OAuth app; the OAuth grant is the trust anchor, so the challenge is skipped and reactivation probes via real redelivery). Read-only and derived server-side.","default":"challenge"},"retry_strategy":{"$ref":"#/components/schemas/RetryStrategy"},"max_retries":{"type":"integer","title":"Max Retries"},"timeout_seconds":{"type":"integer","title":"Timeout Seconds"},"last_success_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Last Success At"},"last_failure_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Last Failure At"},"last_error":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Last Error"},"consecutive_failures":{"type":"integer","title":"Consecutive Failures","default":0},"suspended_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Suspended At","description":"Set while the endpoint is suspended by the circuit breaker (events are queued for up to 12h)"},"next_reactivation_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Next Reactivation At","description":"Next scheduled automatic reactivation attempt (UTC)"},"reactivation_pending_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Reactivation Pending At","description":"Set while the endpoint is ON PROBATION: a re-verification challenge succeeded and the suspension is lifted, but the endpoint is not counted as recovered until a real event is delivered successfully. A failed delivery re-suspends it immediately."},"incident_started_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Incident Started At","description":"Start of the current failure incident (anchors the reactivation schedule and the 12h deadline across probation cycles); null when there is no open incident"},"queued_events_count":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Queued Events Count","description":"Number of events currently queued for replay"},"queued_events_dropped":{"type":"integer","title":"Queued Events Dropped","description":"Events dropped because the suspension queue cap was reached","default":0},"created_at":{"type":"string","title":"Created At"},"updated_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Updated At"},"created_by":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Created By"}},"type":"object","required":["id","tenant_id","name","url","has_authorization_header","subscribed_events","is_active","is_verified","retry_strategy","max_retries","timeout_seconds","created_at"],"title":"WebhookEndpointResponse","description":"Schema for webhook endpoint response","example":{"consecutive_failures":0,"created_at":"2025-12-01T12:00:00Z","created_by":"user_123","has_authorization_header":true,"id":"wh_abc123","is_active":true,"is_verified":true,"last_success_at":"2025-12-11T10:00:00Z","max_retries":5,"name":"CRM Integration","retry_strategy":"exponential","source_filter_mode":"all","subscribed_events":["message.received","message.sent"],"tenant_id":"tenant_xyz","timeout_seconds":30,"updated_at":"2025-12-11T10:00:00Z","url":"https://api.mycrm.com/webhooks/sendseven"}},"WebhookEndpointUpdate":{"properties":{"name":{"anyOf":[{"type":"string","maxLength":255,"minLength":1},{"type":"null"}],"title":"Name","description":"User-friendly name for the webhook"},"url":{"anyOf":[{"type":"string","maxLength":2048,"minLength":1},{"type":"null"}],"title":"Url","description":"HTTPS URL to receive webhook events"},"authorization_header":{"anyOf":[{"type":"string","maxLength":1024},{"type":"null"}],"title":"Authorization Header","description":"Authorization header value (set to empty string to remove)"},"subscribed_events":{"anyOf":[{"items":{"type":"string"},"type":"array","maxItems":50,"minItems":1},{"type":"null"}],"title":"Subscribed Events","description":"List of event types to subscribe to"},"is_active":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Is Active","description":"Enable or disable the webhook"},"retry_strategy":{"anyOf":[{"$ref":"#/components/schemas/RetryStrategy"},{"type":"null"}],"description":"Retry strategy for failed deliveries"},"max_retries":{"anyOf":[{"type":"integer","maximum":15.0,"minimum":0.0},{"type":"null"}],"title":"Max Retries","description":"Maximum number of retry attempts (max: 15, covers ~2 hours with exponential backoff)"},"timeout_seconds":{"anyOf":[{"type":"integer","maximum":60.0,"minimum":5.0},{"type":"null"}],"title":"Timeout Seconds","description":"Request timeout in seconds"},"source_filter_mode":{"anyOf":[{"type":"string","pattern":"^(all|selected)$"},{"type":"null"}],"title":"Source Filter Mode","description":"Source filtering mode: 'all' receives events from all channels, 'selected' only receives events from specified channels/integrations"},"filtered_channel_ids":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Filtered Channel Ids","description":"Channel IDs to filter events by (only used when source_filter_mode='selected'). Set to empty list to clear."},"filtered_email_integration_ids":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Filtered Email Integration Ids","description":"Email integration IDs to filter events by (only used when source_filter_mode='selected'). Set to empty list to clear."}},"type":"object","title":"WebhookEndpointUpdate","description":"Schema for updating a webhook endpoint","example":{"filtered_channel_ids":["ch_abc123"],"is_active":true,"max_retries":10,"name":"Updated CRM Webhook","source_filter_mode":"selected","subscribed_events":["message.received","message.sent","conversation.created","conversation.closed"]}},"WebhookEventTypeSchema":{"properties":{"event":{"type":"string","title":"Event","description":"Event type identifier (e.g., 'message.received')"},"description":{"type":"string","title":"Description","description":"Human-readable description of the event"},"category":{"type":"string","title":"Category","description":"Event category (e.g., 'message', 'conversation')"}},"type":"object","required":["event","description","category"],"title":"WebhookEventTypeSchema","description":"Schema for a single webhook event type","example":{"category":"message","description":"Triggered when a new message is received from a contact","event":"message.received"}},"WebhookRefreshResponse":{"properties":{"success":{"type":"boolean","title":"Success","description":"Whether webhook was registered successfully"},"message":{"type":"string","title":"Message","description":"Status message"},"webhook_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Webhook Url","description":"Webhook URL (only on success)"}},"type":"object","required":["success","message"],"title":"WebhookRefreshResponse","description":"Response from refreshing channel webhook"},"WebhookRetryResponse":{"properties":{"success":{"type":"boolean","title":"Success","description":"Whether the retry was queued successfully"},"delivery_id":{"type":"string","title":"Delivery Id","description":"ID of the new delivery attempt"},"message":{"type":"string","title":"Message","description":"Status message"},"previous_delivery_id":{"type":"string","title":"Previous Delivery Id","description":"ID of the original failed delivery"}},"type":"object","required":["success","delivery_id","message","previous_delivery_id"],"title":"WebhookRetryResponse","description":"Schema for manual retry response","example":{"delivery_id":"del_new123","message":"Delivery queued for retry","previous_delivery_id":"del_old456","success":true}},"WebhookSecretResponse":{"properties":{"webhook_id":{"type":"string","title":"Webhook Id"},"secret_key":{"type":"string","title":"Secret Key","description":"HMAC-SHA256 signing key. Store securely - shown only once!"},"message":{"type":"string","title":"Message","default":"Store this secret key securely. It will not be shown again."}},"type":"object","required":["webhook_id","secret_key"],"title":"WebhookSecretResponse","description":"Response containing the webhook secret key (shown only once on creation)"},"WebhookSettingsResponse":{"properties":{"success":{"type":"boolean","title":"Success","description":"Whether settings retrieval succeeded"},"settings":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Settings","description":"Current webhook settings from SendGrid"},"expected_webhook_url":{"type":"string","title":"Expected Webhook Url","description":"The webhook URL that should be configured"}},"type":"object","required":["success","expected_webhook_url"],"title":"WebhookSettingsResponse","description":"Response from getting webhook settings","example":{"expected_webhook_url":"https://api.example.com/api/v1/webhooks/sendgrid/email?tenant_id=xxx&config_id=yyy","settings":{"bounce":true,"click":true,"delivered":true,"enabled":true,"open":true,"url":"https://api.example.com/webhooks/sendgrid/email"},"success":true}},"WebhookTestRequest":{"properties":{"event_type":{"type":"string","title":"Event Type","description":"Event type for test payload","default":"message.received"}},"type":"object","title":"WebhookTestRequest","description":"Schema for sending a test webhook"},"WebhookTestResponse":{"properties":{"success":{"type":"boolean","title":"Success"},"delivery_id":{"type":"string","title":"Delivery Id"},"response_status_code":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Response Status Code"},"response_time_ms":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Response Time Ms"},"error":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Error"},"message":{"type":"string","title":"Message"}},"type":"object","required":["success","delivery_id","message"],"title":"WebhookTestResponse","description":"Schema for test webhook response"},"WebhookVerificationResponse":{"properties":{"webhook_id":{"type":"string","title":"Webhook Id"},"is_verified":{"type":"boolean","title":"Is Verified"},"verification_pending":{"type":"boolean","title":"Verification Pending","description":"Whether verification is in progress"},"verification_expires_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Verification Expires At"},"message":{"type":"string","title":"Message"}},"type":"object","required":["webhook_id","is_verified","verification_pending","message"],"title":"WebhookVerificationResponse","description":"Schema for webhook verification status response","example":{"is_verified":false,"message":"Verification challenge sent. Respond within 5 minutes.","verification_expires_at":"2025-12-11T10:05:00Z","verification_pending":true,"webhook_id":"wh_abc123"}},"WebsiteCrawlJobResponse":{"properties":{"job_id":{"type":"string","title":"Job Id","description":"Unique job ID for tracking"},"status":{"type":"string","title":"Status","description":"Initial status: 'queued'"},"message":{"type":"string","title":"Message","description":"Human-readable status message"},"poll_url":{"type":"string","title":"Poll Url","description":"URL to poll for status updates"}},"type":"object","required":["job_id","status","message","poll_url"],"title":"WebsiteCrawlJobResponse","description":"Response when starting an async website crawl job.\n\nUse the poll_url to check job progress.","example":{"job_id":"job-uuid-123","message":"Website crawl job started. Use the poll_url to check progress.","poll_url":"/api/v1/knowledge-base/documents/crawl/job-uuid-123/status","status":"queued"}},"WebsiteCrawlRequest":{"properties":{"url":{"type":"string","maxLength":2083,"minLength":1,"format":"uri","title":"Url","description":"URL to crawl"},"follow_links":{"type":"boolean","title":"Follow Links","description":"If true, follow links and crawl multiple pages. If false, only crawl the single URL provided.","default":true},"title_prefix":{"anyOf":[{"type":"string","maxLength":100},{"type":"null"}],"title":"Title Prefix","description":"Optional prefix for document titles"},"page_limit":{"anyOf":[{"type":"integer","maximum":20000.0,"minimum":1.0},{"type":"null"}],"title":"Page Limit","description":"Maximum pages to crawl when follow_links=True (default 200). Must not exceed your workspace crawl limit (2000 pages unless raised for your workspace; see GET /knowledge-base/crawl/limits). Larger values are rejected with 422 `crawl_page_limit_exceeded` - contact support@sendseven.com to raise the limit. Smart safeguards prevent duplicate content pages."},"max_depth":{"anyOf":[{"type":"integer","maximum":10.0,"minimum":1.0},{"type":"null"}],"title":"Max Depth","description":"Maximum crawl depth when follow_links=True (default from settings)"},"rate_limit":{"anyOf":[{"type":"number","maximum":10.0,"minimum":0.1},{"type":"null"}],"title":"Rate Limit","description":"Seconds between requests (default from settings)"},"refresh":{"type":"boolean","title":"Refresh","description":"If true, delete existing documents from this domain before crawling","default":false}},"type":"object","required":["url"],"title":"WebsiteCrawlRequest","description":"Request model for starting an async website crawl job.\n\nThe crawler can either scan a single page or discover and index multiple pages.","example":{"follow_links":true,"max_depth":3,"page_limit":50,"rate_limit":1.0,"refresh":false,"title_prefix":"Example Docs","url":"https://docs.example.com"}},"WhatsAppAuthTemplateCreate":{"properties":{"channel_id":{"anyOf":[{"type":"string","maxLength":36},{"type":"null"}],"title":"Channel Id","description":"WhatsApp channel (defaults to the configured one)"},"language":{"type":"string","maxLength":10,"title":"Language","description":"Template language code","default":"en"}},"additionalProperties":false,"type":"object","title":"WhatsAppAuthTemplateCreate"},"WhatsAppAuthTemplateResponse":{"properties":{"whatsapp_template_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Whatsapp Template Id","description":"Stored in settings.whatsapp_template_id"},"template_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Template Id"},"name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Name"},"status":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Status"}},"type":"object","title":"WhatsAppAuthTemplateResponse"},"WhatsAppCardContent":{"properties":{"card_index":{"type":"integer","title":"Card Index"},"header_image_attachment_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Header Image Attachment Id"},"header_video_attachment_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Header Video Attachment Id"},"body_variables":{"anyOf":[{"additionalProperties":{"type":"string"},"type":"object"},{"type":"null"}],"title":"Body Variables"}},"type":"object","required":["card_index"],"title":"WhatsAppCardContent","description":"Per-card content for a WhatsApp CAROUSEL template card.\n\nCarousel-ness lives in the template's ``carousel_cards`` (message_type stays\n\"text\"); this model carries the per-card variable/media bindings the wizard\ncollects so the campaigns worker can build each card's ``components``."},"WhatsAppCarouselCard":{"properties":{"header":{"anyOf":[{"$ref":"#/components/schemas/CarouselCardHeader"},{"type":"null"}],"description":"Card header (image or video)"},"body_parameters":{"items":{"additionalProperties":true,"type":"object"},"type":"array","title":"Body Parameters","description":"Body text parameters (e.g., [{'type': 'text', 'text': 'Value'}])","default":[]},"buttons":{"items":{"$ref":"#/components/schemas/CarouselCardButton"},"type":"array","maxItems":2,"title":"Buttons","description":"Card buttons (max 2 per card)","default":[]}},"type":"object","title":"WhatsAppCarouselCard","description":"A single card in a WhatsApp carousel template.\n\nWhatsApp carousels require a pre-approved template and can have:\n- Header: image or video\n- Body: dynamic text parameters\n- Buttons: up to 2 buttons per card"},"WhatsAppChannelWindowStatus":{"properties":{"in_window":{"type":"boolean","title":"In Window","description":"Whether within 24h messaging window"},"requires_template":{"type":"boolean","title":"Requires Template","description":"Whether template is required to message"},"last_customer_message_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Last Customer Message At","description":"Last message from customer on this channel"},"window_end_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Window End At","description":"When the 24h window ends (if in_window=True)"}},"type":"object","required":["in_window","requires_template"],"title":"WhatsAppChannelWindowStatus","description":"Detailed WhatsApp window status for a channel"},"WhatsAppContent":{"properties":{"channel_ids":{"items":{"type":"string"},"type":"array","title":"Channel Ids"},"template_id":{"type":"string","title":"Template Id"},"template_name":{"type":"string","title":"Template Name"},"template_language":{"type":"string","title":"Template Language","default":"en"},"variable_mapping":{"anyOf":[{"additionalProperties":{"type":"string"},"type":"object"},{"type":"null"}],"title":"Variable Mapping"},"header_image_attachment_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Header Image Attachment Id"},"header_document_filename":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Header Document Filename"},"cards":{"anyOf":[{"items":{"$ref":"#/components/schemas/WhatsAppCardContent"},"type":"array"},{"type":"null"}],"title":"Cards"}},"type":"object","required":["channel_ids","template_id","template_name"],"title":"WhatsAppContent","description":"WhatsApp-specific campaign content using templates."},"WhatsAppProfileResponse":{"properties":{"about":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"About"},"address":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Address"},"description":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Description"},"email":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Email"},"profile_picture_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Profile Picture Url"},"websites":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Websites"},"vertical":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Vertical"}},"type":"object","title":"WhatsAppProfileResponse","description":"Response schema for WhatsApp Business Profile"},"WhatsAppTemplateComponent":{"properties":{"type":{"type":"string","title":"Type","description":"Component type: header, body, button"},"sub_type":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Sub Type","description":"Required for ``type='button'``. One of: ``url``, ``quick_reply``, ``flow``, ``copy_code``, ``voice_call``, ``catalog``. Ignored for header/body."},"index":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Index","description":"Required for ``type='button'`` — the button position as a string (``\"0\"``, ``\"1\"``, ``\"2\"``)."},"parameters":{"items":{"additionalProperties":true,"type":"object"},"type":"array","title":"Parameters","description":"Parameters for this component"}},"additionalProperties":true,"type":"object","required":["type"],"title":"WhatsAppTemplateComponent","description":"Component for WhatsApp template (header, body, button).\n\nFor button components Meta requires both ``sub_type``\n(``url`` / ``quick_reply`` / ``flow`` / ``copy_code`` / ``voice_call`` / ``catalog``)\nand ``index`` (``\"0\"`` / ``\"1\"`` / ``\"2\"``) on the send payload — see\nhttps://developers.facebook.com/docs/whatsapp/cloud-api/guides/send-message-templates#sending-buttons\nWithout them Meta returns ``(#100) Invalid parameter — button: Missing\nparameter sub_type for button component`` (Sentry BACKEND-92).\n\nHeader / body components leave both fields ``None`` and are unaffected."},"WhatsAppTemplateData":{"properties":{"name":{"type":"string","title":"Name","description":"Template name"},"language":{"type":"string","title":"Language","description":"Template language code (e.g., 'en', 'de')"},"components":{"items":{"$ref":"#/components/schemas/WhatsAppTemplateComponent"},"type":"array","title":"Components","description":"Template components with parameters"}},"type":"object","required":["name","language"],"title":"WhatsAppTemplateData","description":"WhatsApp template specification"},"WhatsAppTemplateListResponse":{"properties":{"templates":{"items":{"$ref":"#/components/schemas/WhatsAppTemplateResponse"},"type":"array","title":"Templates"},"total":{"type":"integer","title":"Total"},"channel_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Channel Id"},"status_filter":{"anyOf":[{"$ref":"#/components/schemas/TemplateStatus"},{"type":"null"}]}},"type":"object","required":["templates","total"],"title":"WhatsAppTemplateListResponse","description":"Response for listing templates"},"WhatsAppTemplateResponse":{"properties":{"name":{"type":"string","title":"Name","description":"Template name (e.g., 'order_confirmation')"},"language":{"type":"string","title":"Language","description":"ISO language code","default":"en"},"category":{"$ref":"#/components/schemas/TemplateCategory","description":"Template category"},"header_type":{"$ref":"#/components/schemas/TemplateHeaderType","default":"NONE"},"header_text":{"anyOf":[{"type":"string","maxLength":60},{"type":"null"}],"title":"Header Text","description":"Header text (max 60 chars)"},"body_text":{"type":"string","title":"Body Text","description":"Template body with {{1}}, {{2}} placeholders"},"footer_text":{"anyOf":[{"type":"string","maxLength":60},{"type":"null"}],"title":"Footer Text","description":"Footer text (max 60 chars)"},"buttons":{"anyOf":[{"items":{"$ref":"#/components/schemas/TemplateButton"},"type":"array"},{"type":"null"}],"title":"Buttons","description":"Template buttons"},"variable_mapping":{"anyOf":[{"additionalProperties":{"type":"string"},"type":"object"},{"type":"null"}],"title":"Variable Mapping","description":"Map placeholders to contact fields: {'1': 'contact.first_name', '2': 'contact.company'}"},"id":{"type":"string","title":"Id"},"tenant_id":{"type":"string","title":"Tenant Id"},"channel_id":{"type":"string","title":"Channel Id"},"waba_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Waba Id","description":"WhatsApp Business Account ID (templates are WABA-scoped)"},"template_id":{"type":"string","title":"Template Id","description":"WhatsApp's template ID"},"status":{"$ref":"#/components/schemas/TemplateStatus"},"quality_score":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Quality Score","description":"Quality score: GREEN, YELLOW, RED, UNKNOWN"},"parameter_format":{"$ref":"#/components/schemas/TemplateParameterFormat","description":"How variable placeholders are resolved when sending. `positional` uses numbered placeholders `{{1}}`, `{{2}}`; `named` uses identifier placeholders like `{{first_name}}`. Determines the accepted shape of `variable_values` on POST /send.","default":"positional"},"default_header_media_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Default Header Media Url","description":"Default header media URL for IMAGE/VIDEO/DOCUMENT templates. Used as a fallback when callers do not supply `header_media_url` on send. Set at template creation time and editable via the template editor."},"default_header_document_filename":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Default Header Document Filename","description":"Default filename presented to the recipient for DOCUMENT-type headers. Used as a fallback when callers do not supply `header_document_filename` on send. Only meaningful for DOCUMENT header templates."},"quick_reply_buttons":{"items":{"$ref":"#/components/schemas/QuickReplyButton"},"type":"array","title":"Quick Reply Buttons","description":"Derived, read-only QUICK_REPLY buttons for the Flow Builder ``send_template`` node. Filtered to QUICK_REPLY type only, capped at 3, ``index`` = position among QUICK_REPLY buttons. Computed from ``buttons``; the FE editor + flow validator both consume this so branch wiring (``tplbtn_{index}``) stays consistent."},"carousel_cards":{"anyOf":[{"items":{"additionalProperties":true,"type":"object"},"type":"array"},{"type":"null"}],"title":"Carousel Cards","description":"For CAROUSEL templates: the ordered list of cards, each a `{\"components\": [...]}` object mirroring a mini-template (one IMAGE/VIDEO HEADER, one BODY, optional BUTTONS). `null` for non-carousel templates. Round-trips through sync unchanged, so a carousel created in WhatsApp Manager appears here identically."},"has_call_permission_request":{"type":"boolean","title":"Has Call Permission Request","description":"True when the template includes a call permission request (asks the customer for permission to call them). Meta only delivers these from a number with WhatsApp Calling turned on. False for templates not re-synced since this field was introduced.","default":false},"call_permission_blocked":{"type":"boolean","title":"Call Permission Blocked","description":"True when `has_call_permission_request` is true and Calling is turned off for the template's WhatsApp channel. Sending it is refused with HTTP 400 and `detail.error_code` 138000 until Calling is turned on.","default":false},"channel_is_coexistence":{"type":"boolean","title":"Channel Is Coexistence","description":"True when the template's WhatsApp channel is connected through the WhatsApp Business app (Coexistence). Calling for such a number is turned on in the WhatsApp Business app, not in SendSeven.","default":false},"last_synced_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Last Synced At"},"created_at":{"type":"string","format":"date-time","title":"Created At"},"updated_at":{"type":"string","format":"date-time","title":"Updated At"}},"type":"object","required":["name","category","body_text","id","tenant_id","channel_id","template_id","status","created_at","updated_at"],"title":"WhatsAppTemplateResponse","description":"Response schema for WhatsApp templates"},"WhatsAppTemplateSyncResponse":{"properties":{"synced":{"type":"integer","title":"Synced","description":"Total templates processed"},"added":{"type":"integer","title":"Added","description":"New templates added"},"updated":{"type":"integer","title":"Updated","description":"Existing templates updated"},"removed":{"type":"integer","title":"Removed","description":"Templates removed (no longer in WhatsApp)"},"errors":{"items":{"type":"string"},"type":"array","title":"Errors","description":"Any errors during sync"}},"type":"object","required":["synced","added","updated","removed"],"title":"WhatsAppTemplateSyncResponse","description":"Response for template sync operation"},"WhatsAppUsernameDeleteResponse":{"properties":{"success":{"type":"boolean","title":"Success","description":"Whether the username was released"},"channel_id":{"type":"string","title":"Channel Id","description":"The channel's ID"},"message":{"type":"string","title":"Message","description":"Human-readable status message"}},"type":"object","required":["success","channel_id","message"],"title":"WhatsAppUsernameDeleteResponse","description":"Result of releasing a WhatsApp business username."},"WhatsAppUsernameResponse":{"properties":{"channel_id":{"type":"string","title":"Channel Id","description":"The channel's ID"},"username":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Username","description":"Current username, or null if none is claimed"},"status":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Status","description":"'approved' (live) or 'reserved' (accepted, still propagating)"},"wa_me_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Wa Me Url","description":"Short link customers can use to start a chat"},"suggestions":{"items":{"type":"string"},"type":"array","title":"Suggestions","description":"Available usernames suggested by WhatsApp (empty if unavailable)"}},"type":"object","required":["channel_id"],"title":"WhatsAppUsernameResponse","description":"The username currently held by a WhatsApp channel's phone number."},"WhatsAppUsernameUpdateRequest":{"properties":{"username":{"type":"string","maxLength":35,"minLength":3,"title":"Username","description":"3-35 characters of lowercase letters, numbers, periods and underscores; must contain a letter, may not start/end with a period, contain '..', start with 'www' or end with a domain suffix"},"force_transfer":{"type":"boolean","title":"Force Transfer","description":"Only for retrying after error 147005: moves the username off another phone number in the same Meta portfolio","default":false}},"type":"object","required":["username"],"title":"WhatsAppUsernameUpdateRequest","description":"Claim or change the username on a WhatsApp channel's phone number."},"WhatsAppUsernameUpdateResponse":{"properties":{"success":{"type":"boolean","title":"Success","description":"Whether WhatsApp accepted the username"},"channel_id":{"type":"string","title":"Channel Id","description":"The channel's ID"},"username":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Username","description":"The username that was set"},"status":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Status","description":"'approved' or 'reserved'"},"wa_me_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Wa Me Url","description":"Short link for the username"},"message":{"type":"string","title":"Message","description":"Human-readable status message"}},"type":"object","required":["success","channel_id","message"],"title":"WhatsAppUsernameUpdateResponse","description":"Result of claiming or changing a WhatsApp business username."},"WhatsAppVertical":{"type":"string","enum":["UNDEFINED","OTHER","AUTO","BEAUTY","APPAREL","EDU","ENTERTAIN","EVENT_PLAN","FINANCE","GROCERY","GOVT","HOTEL","HEALTH","NONPROFIT","PROF_SERVICES","RETAIL","TRAVEL","RESTAURANT","NOT_A_BIZ"],"title":"WhatsAppVertical","description":"WhatsApp Business vertical categories"},"WhatsAppWindowStatus":{"properties":{"in_window":{"type":"boolean","title":"In Window","description":"Whether within 24h window"},"remaining_seconds":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Remaining Seconds","description":"Seconds remaining in window (if in_window=True)"}},"type":"object","required":["in_window"],"title":"WhatsAppWindowStatus","description":"WhatsApp 24-hour messaging window status"},"WhatsAppWindowStatusResponse":{"properties":{"in_window":{"type":"boolean","title":"In Window","description":"Whether the conversation is within the active free-form window"},"window_type":{"type":"string","title":"Window Type","description":"Active window kind: 'customer_service' (24h) or 'free_entry_point' (72h)","default":"customer_service"},"remaining_seconds":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Remaining Seconds","description":"Seconds remaining in the window (null if outside window)"},"requires_template":{"type":"boolean","title":"Requires Template","description":"Whether a template message is required to send"},"is_free_entry_point":{"type":"boolean","title":"Is Free Entry Point","description":"Whether this conversation originated from a free entry point (CTWA ad / Page CTA)","default":false},"last_customer_message_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Last Customer Message At","description":"ISO timestamp of last customer message (null if never)"},"window_end_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Window End At","description":"ISO timestamp when the active window expires"},"free_entry_point_anchor_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Free Entry Point Anchor At","description":"ISO timestamp the 72h FEP window is anchored at (the business's first outbound response after the referral). Null when not a FEP conversation or no business response has been sent yet."}},"type":"object","required":["in_window","requires_template"],"title":"WhatsAppWindowStatusResponse","description":"Response for WhatsApp messaging window status check.\n\nTwo window kinds are represented:\n\n- ``customer_service`` (default): standard 24-hour window measured from the\n  last inbound customer message. Once expired, only template messages may\n  be sent (``requires_template`` true).\n- ``free_entry_point``: a 72-hour Free Entry Point (FEP) window for\n  Click-to-WhatsApp / Page-CTA conversations. While open, ANY message type\n  (free-form OR template) is allowed and is free of Meta charge."},"WidgetAnalyticsDailyItem":{"properties":{"date":{"type":"string","title":"Date","description":"Date (YYYY-MM-DD)"},"views":{"type":"integer","title":"Views","description":"Views on this day","default":0},"interactions":{"type":"integer","title":"Interactions","description":"Interactions on this day","default":0}},"type":"object","required":["date"],"title":"WidgetAnalyticsDailyItem","description":"Daily analytics breakdown"},"WidgetAnalyticsResponse":{"properties":{"widget_id":{"type":"string","title":"Widget Id","description":"Widget ID"},"analytics_enabled":{"type":"boolean","title":"Analytics Enabled","description":"Whether analytics is enabled"},"message":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Message","description":"Message if analytics disabled"},"period_days":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Period Days","description":"Number of days in period"},"start_date":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Start Date","description":"Period start date (ISO format)"},"end_date":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"End Date","description":"Period end date (ISO format)"},"totals":{"anyOf":[{"$ref":"#/components/schemas/WidgetAnalyticsTotals"},{"type":"null"}],"description":"Aggregated totals"},"daily":{"anyOf":[{"items":{"$ref":"#/components/schemas/WidgetAnalyticsDailyItem"},"type":"array"},{"type":"null"}],"title":"Daily","description":"Daily breakdown"}},"type":"object","required":["widget_id","analytics_enabled"],"title":"WidgetAnalyticsResponse","description":"Response from getting widget analytics"},"WidgetAnalyticsTotals":{"properties":{"views":{"type":"integer","title":"Views","description":"Total views","default":0},"interactions":{"type":"integer","title":"Interactions","description":"Total interactions","default":0},"channel_clicks":{"type":"integer","title":"Channel Clicks","description":"Total channel clicks","default":0},"live_chat_starts":{"type":"integer","title":"Live Chat Starts","description":"Live chat sessions started","default":0},"live_chat_messages":{"type":"integer","title":"Live Chat Messages","description":"Live chat messages sent","default":0},"newsletter_submits":{"type":"integer","title":"Newsletter Submits","description":"Newsletter form submissions","default":0},"push_subscribes":{"type":"integer","title":"Push Subscribes","description":"Push notification subscriptions","default":0}},"type":"object","title":"WidgetAnalyticsTotals","description":"Aggregated analytics totals"},"WidgetConfigCreate":{"properties":{"primaryColor":{"type":"string","title":"Primarycolor","description":"Primary brand color (hex)","default":"#0066FF"},"backgroundColor":{"type":"string","title":"Backgroundcolor","description":"Background color (hex)","default":"#FFFFFF"},"textColor":{"type":"string","title":"Textcolor","description":"Text color (hex)","default":"#1F2937"},"fontFamily":{"type":"string","title":"Fontfamily","description":"Font family","default":"Inter, sans-serif"},"welcomeMessage":{"type":"string","title":"Welcomemessage","description":"Welcome message","default":"How can we help you?"},"subtitle":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Subtitle","description":"Optional subtitle"},"buttonText":{"type":"string","title":"Buttontext","description":"Button text / CTA text","default":"Chat with us"},"position":{"type":"string","title":"Position","description":"Position: bottom-right, bottom-left, top-right, top-left","default":"bottom-right"},"offsetX":{"type":"integer","title":"Offsetx","description":"Horizontal offset in pixels","default":20},"offsetY":{"type":"integer","title":"Offsety","description":"Vertical offset in pixels","default":20},"logoUrl":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Logourl","description":"Customer logo URL"},"companyName":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Companyname","description":"Company name to display"},"channelIds":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Channelids","description":"List of enabled channel IDs for deep linking"},"categoryIds":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Categoryids","description":"List of newsletter category IDs"},"liveChatEnabled":{"type":"boolean","title":"Livechatenabled","description":"Enable live chat widget","default":false},"liveChatOnlyWhenOnline":{"type":"boolean","title":"Livechatonlywhenonline","description":"Show live chat only when agents online","default":true},"liveChatOfflineMessage":{"type":"string","title":"Livechatofflinemessage","description":"Offline message","default":"We're offline. Leave a message and we'll get back to you!"},"allowChannelSwitch":{"type":"boolean","title":"Allowchannelswitch","description":"Allow visitors to switch to another channel during live chat","default":true},"autoStartLiveChat":{"type":"boolean","title":"Autostartlivechat","description":"Automatically start live chat when widget opens (skip channel selection)","default":false},"liveChatPopOutEnabled":{"type":"boolean","title":"Livechatpopoutenabled","description":"Allow visitors to pop the live chat out into a separate window","default":false},"liveChatSchedule":{"anyOf":[{"$ref":"#/components/schemas/LiveChatSchedule"},{"type":"null"}],"description":"Weekly business-hours gate for the live-chat option"},"fullscreenOnMobile":{"type":"boolean","title":"Fullscreenonmobile","description":"Display the widget fullscreen on mobile devices","default":true},"bubbleLayout":{"type":"string","enum":["classic","panel"],"title":"Bubblelayout","description":"Bubble container layout style","default":"classic"},"cardLayout":{"type":"string","enum":["legacy","inline","hero","grid"],"title":"Cardlayout","description":"Card content layout style","default":"legacy"},"cornerRadius":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Cornerradius","description":"Corner radius in px; omit to let the frontend default apply (no hard backend default)"},"defaultSize":{"anyOf":[{"type":"string","enum":["compact","tall","full"]},{"type":"null"}],"title":"Defaultsize","description":"Default open size of the widget panel"},"showAgentType":{"type":"boolean","title":"Showagenttype","description":"Show the agent type label (bot vs. human) in the widget","default":true},"allowReactions":{"type":"boolean","title":"Allowreactions","description":"Allow visitors to react to messages in the widget","default":true},"launcher_icon":{"anyOf":[{"type":"string","enum":["default","whatsapp","telegram","messenger","instagram","sms","email","message_circle","chat_dots","sparkles","help","headset","phone","life_buoy","hand_wave"]},{"type":"null"}],"title":"Launcher Icon","description":"Icon on the closed bubble (launcher) button. One of: default, whatsapp, telegram, messenger, instagram, sms, email, message_circle, chat_dots, sparkles, help, headset, phone, life_buoy, hand_wave. null/omitted = default icon."},"channelButtonStyle":{"anyOf":[{"type":"string","enum":["outline","filled","minimal","brand"]},{"type":"null"}],"title":"Channelbuttonstyle","description":"Channel list button style: outline | filled | minimal | brand. null/omitted = outline."},"channelDisplayMode":{"anyOf":[{"type":"string","enum":["full","icons-only","tiles"]},{"type":"null"}],"title":"Channeldisplaymode","description":"Channel list display: full | icons-only | tiles. null/omitted = full."},"homeLayout":{"anyOf":[{"type":"string","enum":["menu","composer"]},{"type":"null"}],"title":"Homelayout","description":"Home screen layout: menu (channel list) | composer (message box first). null/omitted = menu."},"launcherMode":{"anyOf":[{"type":"string","enum":["panel","speed_dial"]},{"type":"null"}],"title":"Launchermode","description":"Bubble click behaviour: panel | speed_dial (fan-out of channel buttons). null/omitted = panel."},"desktopHandoff":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Desktophandoff","description":"On desktop, show a QR code + web link step instead of opening messenger apps directly. null/omitted = false."},"prechatMode":{"anyOf":[{"type":"string","enum":["form","composer"]},{"type":"null"}],"title":"Prechatmode","description":"Pre-chat details: form (separate step) | composer (fields inside the message box). null/omitted = form."},"pushEnabled":{"type":"boolean","title":"Pushenabled","description":"Enable browser push notifications","default":false},"pushAutoPrompt":{"type":"boolean","title":"Pushautoprompt","description":"Automatically prompt for push permission on page load","default":false},"pushPromptDelay":{"type":"integer","title":"Pushpromptdelay","description":"Delay in seconds before showing auto-prompt","default":5},"autoStartBotId":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Autostartbotid","description":"UUID of bot to auto-start when live chat session begins"},"autoStartFlowId":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Autostartflowid","description":"UUID of flow to auto-start when live chat session begins"},"customCss":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Customcss","description":"Custom CSS for widget"},"channelId":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Channelid","description":"DEPRECATED: Single channel ID (use channelIds instead)"},"categoryId":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Categoryid","description":"DEPRECATED: Single category ID (use categoryIds instead)"}},"additionalProperties":true,"type":"object","title":"WidgetConfigCreate","description":"Widget configuration schema"},"WidgetCreate":{"properties":{"name":{"type":"string","title":"Name","description":"Internal name for the widget"},"widget_type":{"$ref":"#/components/schemas/WidgetType","description":"Widget display type","default":"bubble"},"use_case":{"$ref":"#/components/schemas/WidgetUseCase","description":"Widget use case","default":"support"},"config":{"$ref":"#/components/schemas/WidgetConfigCreate","description":"Widget configuration"},"privacy_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Privacy Url","description":"Customer's privacy policy URL"},"is_analytics_enabled":{"type":"boolean","title":"Is Analytics Enabled","description":"Enable analytics tracking","default":true},"is_active":{"type":"boolean","title":"Is Active","description":"Widget active status","default":true}},"type":"object","required":["name","config"],"title":"WidgetCreate","description":"Schema for creating a widget"},"WidgetEmbedCodeResponse":{"properties":{"widget_id":{"type":"string","title":"Widget Id","description":"Widget ID"},"embed_code":{"type":"string","title":"Embed Code","description":"HTML/JS code to embed the widget"},"script_url":{"type":"string","title":"Script Url","description":"URL to the widget script"}},"type":"object","required":["widget_id","embed_code","script_url"],"title":"WidgetEmbedCodeResponse","description":"Response from getting widget embed code"},"WidgetIdentitySecretRevealResponse":{"properties":{"secret":{"type":"string","title":"Secret","description":"The current signing secret"}},"type":"object","required":["secret"],"title":"WidgetIdentitySecretRevealResponse","description":"The current signing secret."},"WidgetIdentitySecretRotateResponse":{"properties":{"secret":{"type":"string","title":"Secret","description":"The new signing secret. The previous secret stops working immediately."},"secret_hint":{"type":"string","title":"Secret Hint","description":"Last 4 characters of the new secret"},"secret_rotated_at":{"type":"string","title":"Secret Rotated At","description":"When the secret was generated (UTC)"}},"type":"object","required":["secret","secret_hint","secret_rotated_at"],"title":"WidgetIdentitySecretRotateResponse","description":"The NEW secret. Shown once; store it on your server now."},"WidgetIdentityVerificationResponse":{"properties":{"enabled":{"type":"boolean","title":"Enabled","description":"Whether signed identities are verified for this widget"},"has_secret":{"type":"boolean","title":"Has Secret","description":"Whether a signing secret has been generated"},"secret_hint":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Secret Hint","description":"Last 4 characters of the current secret, or null when there is none"},"secret_rotated_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Secret Rotated At","description":"When the current secret was generated (UTC), or null"},"max_age_seconds":{"type":"integer","title":"Max Age Seconds","description":"Maximum age of a signed identity (`now - issued_at`) in seconds"}},"type":"object","required":["enabled","has_secret","max_age_seconds"],"title":"WidgetIdentityVerificationResponse","description":"Identity verification status of a widget. Never contains the secret."},"WidgetIdentityVerificationUpdate":{"properties":{"enabled":{"anyOf":[{},{"type":"null"}],"title":"Enabled","description":"`true` to enable verification, `false` to disable it"}},"type":"object","title":"WidgetIdentityVerificationUpdate","description":"Body of ``PUT /widgets/{widget_id}/identity-verification``.\n\nStrict JSON boolean only. ``enabled`` is typed ``Any`` and a non-object\nbody is normalised to ``{}`` so Pydantic never raises its own 422 (whose\nbody would echo the input); the endpoint returns its own 422\n``{\"error_code\": \"enabled_required\"}`` without quoting any value."},"WidgetResponse":{"properties":{"id":{"type":"string","title":"Id"},"tenant_id":{"type":"string","title":"Tenant Id"},"name":{"type":"string","title":"Name"},"widget_type":{"$ref":"#/components/schemas/WidgetType"},"use_case":{"$ref":"#/components/schemas/WidgetUseCase"},"config":{"additionalProperties":true,"type":"object","title":"Config"},"privacy_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Privacy Url"},"is_analytics_enabled":{"type":"boolean","title":"Is Analytics Enabled"},"is_active":{"type":"boolean","title":"Is Active"},"created_at":{"type":"string","format":"date-time","title":"Created At"},"updated_at":{"type":"string","format":"date-time","title":"Updated At"}},"type":"object","required":["id","tenant_id","name","widget_type","use_case","config","is_analytics_enabled","is_active","created_at","updated_at"],"title":"WidgetResponse","description":"Schema for widget response"},"WidgetType":{"type":"string","enum":["card","bubble"],"title":"WidgetType","description":"Types of widgets"},"WidgetUpdate":{"properties":{"name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Name"},"config":{"anyOf":[{"$ref":"#/components/schemas/WidgetConfigCreate"},{"type":"null"}]},"privacy_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Privacy Url"},"is_analytics_enabled":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Is Analytics Enabled"},"is_active":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Is Active"}},"type":"object","title":"WidgetUpdate","description":"Schema for updating a widget"},"WidgetUseCase":{"type":"string","enum":["support","newsletter"],"title":"WidgetUseCase","description":"Widget use cases"},"_MergeRequest":{"properties":{"winner_id":{"type":"string","title":"Winner Id","description":"Entry that survives the merge"},"loser_ids":{"items":{"type":"string"},"type":"array","minItems":1,"title":"Loser Ids","description":"Entries folded into the winner then archived"},"dry_run":{"type":"boolean","title":"Dry Run","description":"Preview only (default). Set false to EXECUTE the destructive merge (deletes loser RAG files + GCS blobs, archives losers).","default":true},"keep_loser_questions_as_variants":{"type":"boolean","title":"Keep Loser Questions As Variants","description":"Keep loser phrasings as retrievable alt-questions on the winner","default":true}},"type":"object","required":["winner_id","loser_ids"],"title":"_MergeRequest","description":"Body for ``POST /faq-entries/merge``. ``dry_run`` defaults to TRUE so the\ndefault call previews and writes nothing."},"app__schemas__attachment_schema__AttachmentResponse":{"properties":{"id":{"type":"string","title":"Id","description":"Unique attachment identifier"},"filename":{"type":"string","title":"Filename","description":"Original filename"},"content_type":{"type":"string","title":"Content Type","description":"MIME type of the file"},"file_size":{"type":"integer","title":"File Size","description":"File size in bytes"},"url":{"type":"string","title":"Url","description":"Signed URL for downloading"}},"type":"object","required":["id","filename","content_type","file_size","url"],"title":"AttachmentResponse","description":"Response for attachment metadata.","example":{"content_type":"image/png","file_size":51200,"filename":"image.png","id":"550e8400-e29b-41d4-a716-446655440000","url":"https://storage.googleapis.com/..."}},"app__schemas__campaign_schema__CampaignUnsubscribeItem":{"properties":{"contact_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Contact Id","description":"Contact id, if the opt-out is a known contact"},"contact_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Contact Name","description":"Contact display name, if resolvable"},"channel_type":{"type":"string","title":"Channel Type","description":"Subscribable channel the opt-out happened on (whatsapp/telegram/messenger/sms/...)"},"method":{"type":"string","title":"Method","description":"Opt-out source: 'keyword' (stop-word) or 'link' (unsubscribe surface)"},"attribution":{"type":"string","title":"Attribution","description":"'exact' for link opt-outs; 'heuristic' for keyword opt-outs (last-campaign-on-channel within window)"},"unsubscribed_at":{"type":"string","title":"Unsubscribed At","description":"When the opt-out occurred (event occurred_at, ISO 8601 Z)"},"window_hours":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Window Hours","description":"Heuristic attribution window in hours (keyword only; null for exact/link)"},"matched_sent_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Matched Sent At","description":"Send time of the campaign message the keyword opt-out was matched to (keyword only; null for exact/link)"}},"type":"object","required":["channel_type","method","attribution","unsubscribed_at"],"title":"CampaignUnsubscribeItem","description":"One recipient who opted out attributed to this (messenger) campaign."},"app__schemas__campaign_schema__CampaignUnsubscribeMethodCounts":{"properties":{"link":{"type":"integer","title":"Link","description":"Count of exact link/one-click opt-outs"},"keyword":{"type":"integer","title":"Keyword","description":"Count of heuristic keyword (stop-word) opt-outs"}},"type":"object","required":["link","keyword"],"title":"CampaignUnsubscribeMethodCounts","description":"Opt-out counts broken down by source method (full result set, not page)."},"app__schemas__campaign_schema__CampaignUnsubscribesResponse":{"properties":{"total_unsubscribes":{"type":"integer","title":"Total Unsubscribes","description":"Total opt-outs attributed to this campaign"},"method_counts":{"$ref":"#/components/schemas/app__schemas__campaign_schema__CampaignUnsubscribeMethodCounts"},"attribution_counts":{"$ref":"#/components/schemas/CampaignUnsubscribeAttributionCounts"},"items":{"items":{"$ref":"#/components/schemas/app__schemas__campaign_schema__CampaignUnsubscribeItem"},"type":"array","title":"Items"},"pagination":{"$ref":"#/components/schemas/CampaignUnsubscribePagination"}},"type":"object","required":["total_unsubscribes","method_counts","attribution_counts","items","pagination"],"title":"CampaignUnsubscribesResponse","description":"Campaign-attributed opt-outs for a messenger campaign details page.","example":{"attribution_counts":{"exact":1,"heuristic":3},"items":[{"attribution":"heuristic","channel_type":"whatsapp","contact_id":"c1a2b3","contact_name":"Jane Doe","matched_sent_at":"2026-09-13T09:00:00Z","method":"keyword","unsubscribed_at":"2026-09-14T10:30:00Z","window_hours":72},{"attribution":"exact","channel_type":"whatsapp","contact_id":"d4e5f6","contact_name":"John Roe","method":"link","unsubscribed_at":"2026-09-14T08:15:00Z"}],"method_counts":{"keyword":3,"link":1},"pagination":{"page":1,"page_size":25,"total":4},"total_unsubscribes":4}},"app__schemas__campaign_schema__SendCampaignRequest":{"properties":{"acknowledge_warmup_warning":{"type":"boolean","title":"Acknowledge Warmup Warning","default":false}},"type":"object","title":"SendCampaignRequest","description":"Optional body for ``POST /campaigns/{id}/send``.\n\nThe endpoint also accepts NO body (legacy clients) -- identical to\n``{\"acknowledge_warmup_warning\": false}``."},"app__schemas__canned_response_schema__CategoryListResponse":{"properties":{"categories":{"items":{"type":"string"},"type":"array","title":"Categories","description":"List of unique category names"}},"type":"object","required":["categories"],"title":"CategoryListResponse","description":"List of unique categories for filtering","example":{"categories":["Greetings","Support","Sales","Closing"]}},"app__schemas__email_campaign_schema__CampaignUnsubscribeItem":{"properties":{"contact_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Contact Id","description":"Contact ID, if known"},"contact_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Contact Name","description":"Display name, if resolvable"},"email":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Email","description":"Email address that unsubscribed"},"unsubscribed_at":{"type":"string","format":"date-time","title":"Unsubscribed At","description":"When the opt-out occurred (ISO 8601)"},"method":{"type":"string","title":"Method","description":"Opt-out source. Only 'link' exists today.","default":"link"}},"type":"object","required":["unsubscribed_at"],"title":"CampaignUnsubscribeItem","description":"A single recipient who opted out via this email campaign's link."},"app__schemas__email_campaign_schema__CampaignUnsubscribeMethodCounts":{"properties":{"link":{"type":"integer","title":"Link","description":"Count of link/one-click opt-outs"},"keyword":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Keyword","description":"Reserved; always null for now"}},"type":"object","required":["link"],"title":"CampaignUnsubscribeMethodCounts","description":"Opt-out counts broken down by source method.\n\nOnly link (one-click / unsubscribe-link) opt-outs exist today. ``keyword``\nis reserved (always ``null`` for now) for a future keyword-based source that\nlands once an events table exists."},"app__schemas__email_campaign_schema__CampaignUnsubscribesResponse":{"properties":{"total_unsubscribes":{"type":"integer","title":"Total Unsubscribes","description":"Total opt-outs attributed to this campaign"},"method_counts":{"$ref":"#/components/schemas/app__schemas__email_campaign_schema__CampaignUnsubscribeMethodCounts"},"items":{"items":{"$ref":"#/components/schemas/app__schemas__email_campaign_schema__CampaignUnsubscribeItem"},"type":"array","title":"Items"},"pagination":{"$ref":"#/components/schemas/CampaignUnsubscribePagination"}},"type":"object","required":["total_unsubscribes","method_counts","items","pagination"],"title":"CampaignUnsubscribesResponse","description":"Campaign-attributed opt-outs for an email campaign details page.","example":{"items":[{"contact_id":"c1a2b3","contact_name":"Jane Doe","email":"jane@example.com","method":"link","unsubscribed_at":"2026-09-14T10:30:00"}],"method_counts":{"link":3},"pagination":{"page":1,"page_size":25,"total":3},"total_unsubscribes":3}},"app__schemas__email_campaign_schema__SendCampaignRequest":{"properties":{"send_test":{"type":"boolean","title":"Send Test","description":"Send test to specific addresses","default":false},"test_emails":{"anyOf":[{"items":{"type":"string","format":"email"},"type":"array"},{"type":"null"}],"title":"Test Emails","description":"Test email addresses"}},"type":"object","title":"SendCampaignRequest","description":"Schema for sending/testing a campaign","example":{"send_test":false}},"app__schemas__flow_schema__FlowResponse":{"properties":{"id":{"type":"string","title":"Id"},"tenant_id":{"type":"string","title":"Tenant Id"},"name":{"type":"string","title":"Name"},"description":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Description"},"status":{"type":"string","title":"Status"},"definition":{"additionalProperties":true,"type":"object","title":"Definition"},"re_entry_policy":{"type":"string","title":"Re Entry Policy"},"default_language":{"type":"string","title":"Default Language","default":"en"},"prefer_originating_channel":{"type":"boolean","title":"Prefer Originating Channel","default":true},"manually_triggerable":{"type":"boolean","title":"Manually Triggerable","default":false},"auto_translate_messages":{"type":"boolean","title":"Auto Translate Messages","default":false},"language_detection_strategy":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Language Detection Strategy"},"default_channel_chain":{"anyOf":[{"items":{"anyOf":[{"type":"string"},{"$ref":"#/components/schemas/ChannelChainEntry"}]},"type":"array"},{"type":"null"}],"title":"Default Channel Chain"},"stop_on_agent_reply":{"type":"boolean","title":"Stop On Agent Reply","default":true},"stop_on_conversation_closed":{"type":"boolean","title":"Stop On Conversation Closed","default":true},"priority":{"type":"integer","title":"Priority","default":0},"exclusive":{"type":"boolean","title":"Exclusive","default":false},"created_by_user_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Created By User Id"},"created_at":{"type":"string","title":"Created At"},"updated_at":{"type":"string","title":"Updated At"},"validation":{"anyOf":[{"$ref":"#/components/schemas/FlowValidationStatus"},{"type":"null"}]},"run_count":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Run Count"}},"type":"object","required":["id","tenant_id","name","status","definition","re_entry_policy","created_at","updated_at"],"title":"FlowResponse","description":"Wire format for a Flow row."},"app__schemas__message__AttachmentResponse":{"properties":{"id":{"type":"string","title":"Id"},"filename":{"type":"string","title":"Filename"},"content_type":{"type":"string","title":"Content Type"},"file_size":{"type":"integer","title":"File Size"},"storage_path":{"type":"string","title":"Storage Path"},"created_at":{"type":"string","title":"Created At"},"url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Url"},"ai_summary":{"anyOf":[{"$ref":"#/components/schemas/AttachmentAISummary-Output"},{"type":"null"}],"description":"AI summary of this image/PDF, or null (none yet, or AI features off)."},"ai_summary_available":{"type":"boolean","title":"Ai Summary Available","description":"True when an on-demand summary can be requested via `POST /attachments/{id}/summary` (AI enabled, supported inbound image/PDF, no ok/pending summary).","default":false},"ai_summary_credits":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Ai Summary Credits","description":"AI credits an on-demand summary costs (image 2, PDF 3); null when not available."}},"type":"object","required":["id","filename","content_type","file_size","storage_path","created_at"],"title":"AttachmentResponse","description":"Schema for attachment in message response"},"app__schemas__saved_interactive_message__CategoryListResponse":{"properties":{"categories":{"items":{"type":"string"},"type":"array","title":"Categories","description":"List of unique category names"}},"type":"object","required":["categories"],"title":"CategoryListResponse","description":"Response for listing unique categories"},"app__schemas__whatsapp_flow__FlowResponse":{"properties":{"id":{"type":"string","title":"Id","description":"Flow ID from WhatsApp"},"name":{"type":"string","title":"Name","description":"Flow name"},"status":{"type":"string","title":"Status","description":"Flow status: DRAFT, PUBLISHED, etc."},"categories":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Categories","description":"Flow categories"},"validation_errors":{"anyOf":[{"items":{"additionalProperties":true,"type":"object"},"type":"array"},{"type":"null"}],"title":"Validation Errors","description":"Validation errors if any"},"definition_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Definition Id","description":"SendSeven WhatsApp Flow id when built in SendSeven"},"definition_status":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Definition Status","description":"SendSeven WhatsApp Flow status: active | archived"}},"type":"object","required":["id","name","status"],"title":"FlowResponse","description":"WhatsApp Flow object response"},"RetentionPreviewResponse":{"properties":{"message_effective_days":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Message Effective Days"},"contact_effective_days":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Contact Effective Days"},"messages_to_delete":{"type":"integer","title":"Messages To Delete"},"contacts_to_delete":{"type":"integer","title":"Contacts To Delete"},"conversations_to_delete":{"type":"integer","title":"Conversations To Delete"},"counts_capped":{"type":"boolean","title":"Counts Capped"},"contact_deletion_starts_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Contact Deletion Starts At"},"message_deletion_starts_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Message Deletion Starts At"}},"type":"object","required":["message_effective_days","contact_effective_days","messages_to_delete","contacts_to_delete","conversations_to_delete","counts_capped"],"title":"RetentionPreviewResponse","description":"What the nightly purge would delete if the proposed values were saved."}},"securitySchemes":{"HTTPBearer":{"type":"http","scheme":"bearer"}}},"tags":[{"name":"Agent Suggestions","description":"Get AI-powered reply suggestions for conversations based on context and message history."},{"name":"AI Agents","description":"Beta. Create and manage Conversational Agents: persona, instructions, skills, knowledge sources, handoff rules, test console and turn audit log."},{"name":"AI Skills","description":"Beta. Manage the skills library for Conversational Agents. Every content change creates an immutable version; import skills in bulk."},{"name":"Alerts","description":"Configure channel-health and delivery alerts and manage alert notification settings and history."},{"name":"Analytics","description":"Access messaging analytics, conversation metrics, and channel performance data."},{"name":"API Tokens","description":"Create and manage scoped API tokens for programmatic access to the SendSeven API."},{"name":"Attachments","description":"Upload, retrieve, and manage file attachments (images, documents, videos, audio) for messages."},{"name":"Audit Log","description":"Retrieve audit and activity events for API requests and user actions."},{"name":"Automation","description":"Configure automated workflows and message routing rules triggered by events."},{"name":"Bounce Handling","description":"Monitor and manage email bounces, complaints, and delivery failures."},{"name":"Campaigns","description":"Create and manage messaging campaigns for WhatsApp, SMS, and other channels. Schedule sends and track delivery."},{"name":"Canned Responses","description":"Manage pre-saved quick reply templates for agents. Supports personal and shared responses with shortcut triggers."},{"name":"Channel Connect","description":"Connect new messaging channels (WhatsApp, Telegram, Instagram, etc.) via guided setup wizards with token-based authentication."},{"name":"Channels","description":"Manage connected messaging channels (WhatsApp, Telegram, Messenger, Instagram, SMS, Email). View capabilities, profiles, and configuration."},{"name":"Comment Rules","description":"Configure automated rules that reply to or act on incoming social comments."},{"name":"Comments","description":"Manage inbound social comments (Facebook, Instagram) and send public or private replies."},{"name":"Company Profile","description":"Manage your organization's profile information displayed to customers across channels."},{"name":"Contact Bulk Actions","description":"Perform bulk operations on contacts such as batch tagging, deletion, and export."},{"name":"Contacts","description":"Create, update, search, and manage customer contacts across all channels. Supports tags, custom fields, and subscription management."},{"name":"Conversations","description":"Manage customer conversations across all channels. Assign agents, close/reopen, and track conversation state."},{"name":"Custom Fields","description":"Define and manage custom data fields for contacts to store business-specific information."},{"name":"Custom Inboxes","description":"Create and manage custom conversation inboxes with filtered views and per-user access control."},{"name":"Double Opt-In","description":"Manage double opt-in confirmation flows for email and messaging subscriptions."},{"name":"Double Opt-In Templates","description":"Create and customize confirmation message templates for double opt-in flows."},{"name":"Email Campaigns","description":"Create, schedule, and send email marketing campaigns using layout templates and content blocks. Track opens and clicks."},{"name":"Email Connections","description":"Test and validate email server connections (SMTP/IMAP) for email integrations."},{"name":"Email Contacts","description":"Manage email-specific contact data including subscription preferences and email addresses."},{"name":"Email Contents","description":"Create and edit email content blocks that populate layout template slots for campaigns."},{"name":"Email Integrations","description":"Connect email providers (SendGrid, Mailgun, SMTP/IMAP, Gmail) for sending and receiving emails."},{"name":"Email Mailboxes","description":"Manage connected email mailboxes used for sending and receiving email conversations."},{"name":"Email Providers","description":"Manage email delivery provider configurations and DNS verification for custom domains."},{"name":"Email Settings","description":"Configure email-specific settings including sender defaults, reply-to addresses, and footer preferences."},{"name":"Email Signatures","description":"Create and manage HTML email signatures appended to outgoing emails."},{"name":"Email Templates","description":"Design and manage reusable MJML-based email templates with configurable content slots for campaigns."},{"name":"FAQ Management","description":"Manage FAQ entries in the knowledge base for AI-powered customer self-service."},{"name":"Flows","description":"Build and execute visual automation flows. Triggers, multi-channel sends, conditions, delays, tag updates, tracked links, and webhook nodes. Start runs via API or from contact profiles."},{"name":"HubSpot Integration","description":"Manage the HubSpot integration including contact sync, segments, and embedded cards."},{"name":"Integrations","description":"Manage third-party integration connections and their configuration."},{"name":"Invitations","description":"Invite new team members to your tenant with role-based access control."},{"name":"Invoices","description":"View and download billing invoices, payment history, and usage breakdowns."},{"name":"KB Folders","description":"Organize knowledge base articles into folders for structured AI retrieval."},{"name":"Knowledge Base","description":"Manage knowledge base articles, website crawls, and FAQ entries used by the AI assistant for answering customer queries."},{"name":"Link Tracking","description":"Track link clicks in messages and campaigns with per-recipient analytics."},{"name":"Link Tracking Settings","description":"Configure link tracking behavior including custom domains and tracking pixel settings."},{"name":"Lists","description":"Create and manage contact lists for targeted campaign sends and audience segmentation."},{"name":"Live Chat","description":"Embeddable live chat widget for websites. Manage widget configuration, appearance, and visitor conversations."},{"name":"Messages","description":"Send and receive messages across all channels (WhatsApp, Telegram, SMS, Email, Instagram, Messenger). Supports text, media, interactive messages, reactions, and contact cards."},{"name":"Notes","description":"Add internal notes to conversations visible only to team members, not sent to the customer."},{"name":"OAuth2 Apps","description":"Register and manage OAuth2 client applications for third-party integrations with SendSeven."},{"name":"Roles & Permissions","description":"Manage RBAC roles, permissions, and scope assignments for team members and API tokens."},{"name":"Saved Interactive Messages","description":"Save and reuse interactive message templates (button menus, list menus) across conversations."},{"name":"Scheduler","description":"Schedule one-off and recurring messages. Birthday reminders, anniversaries, cron-style recurring sends with audience filters across all channels."},{"name":"SMS","description":"Manage SMS provisioning, sender verification, and account balance."},{"name":"Social Posts","description":"Create, schedule, and manage social media posts across connected channels."},{"name":"Subscriptions","description":"Manage contact subscription preferences across channels including opt-in/opt-out status."},{"name":"Tags","description":"Create color-coded tags and apply them to contacts and conversations for organization and filtering."},{"name":"Tenant Email Settings","description":"Configure tenant-level email settings including sending defaults and delivery preferences."},{"name":"Tenants","description":"Manage tenant (workspace) settings, branding, and multi-tenant configuration."},{"name":"Unsubscribe","description":"Public-facing unsubscribe endpoints for one-click email and messaging opt-out."},{"name":"User Inbox Access","description":"Manage which team members can access specific custom inboxes."},{"name":"Users","description":"Manage team member profiles, preferences, chat nicknames, and notification settings."},{"name":"Webhooks","description":"Configure HTTPS webhook endpoints to receive real-time event notifications (messages, conversations, contacts)."},{"name":"WhatsApp","description":"WhatsApp Business API management including business profile, phone number settings, and platform-specific features."},{"name":"WhatsApp Templates","description":"Create, sync, preview, and send WhatsApp Business API message templates. Manage variable mappings and template translations."},{"name":"Widgets","description":"Configure embeddable website widgets for live chat, newsletter signup, and customer engagement."}],"servers":[{"url":"https://api.sendseven.com","description":"Production"}]}