{"openapi":"3.0.0","paths":{"/contacts/{id}/notes":{"patch":{"operationId":"ContactController_updateContactNotes","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"number"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateContactNotesDto"}}}},"responses":{"200":{"description":""}},"tags":[],"security":[{"bearerToken":[]}]}},"/contacts/by-ids":{"get":{"operationId":"ContactController_getContactsByIds","parameters":[{"name":"ids","required":true,"in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ContactDto"}}}}}},"tags":["Contact"],"security":[{"bearerToken":[]}]}},"/contacts/{id}":{"get":{"operationId":"ContactController_findContactById","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"number"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ContactDto"}}}}},"tags":["Contact"],"security":[{"bearerToken":[]}]},"delete":{"operationId":"ContactController_deleteContact","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"number"}}],"responses":{"200":{"description":""}},"tags":[],"security":[{"bearerToken":[]}]},"patch":{"operationId":"ContactController_editContact","summary":"Update contact details","description":"Updates contact fields in strict REST mode. Supported fields are firstName, lastName and email.","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"number"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateContactDto"},"examples":{"update_name":{"summary":"Update contact name fields","value":{"firstName":"Jane","lastName":"Doe"}},"update_email":{"summary":"Update email only","value":{"email":"jane.doe@example.com"}}}}}},"responses":{"200":{"description":"Contact updated successfully.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ContactDto"}}}},"404":{"description":"Contact not found for this workspace.","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/ApiErrorEnvelopeDto"}],"example":{"statusCode":404,"message":"Contact not found","error":"Not Found"}}}}}},"tags":["Contact"],"security":[{"bearerToken":[]}]}},"/contacts/{contactId}/conversations":{"get":{"operationId":"ContactController_listContactConversations","summary":"List a contact's WhatsApp conversations","description":"The WhatsApp numbers this contact has a conversation on, newest first, with the conversation id to open in Spur Embed. Only numbers with messages are listed unless `include=all`. Read-only: no token is minted. An Instagram or Facebook conversation is a separate contact and is not listed.","parameters":[{"name":"contactId","required":true,"in":"path","schema":{"type":"number"}},{"name":"include","required":false,"in":"query","description":"Default lists only numbers the contact has messages on. `all` also lists every other WhatsApp number in the workspace (`hasMessages: false`).","schema":{"enum":["all"],"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ContactConversationsResponseDto"}}}},"404":{"description":"WhatsApp contact not found"}},"tags":["Contact"],"security":[{"bearerToken":[]}]}},"/contacts/inbox/{inboxId}":{"get":{"operationId":"ContactController_findContactByInboxId","parameters":[{"name":"inboxId","required":true,"in":"path","schema":{"type":"number"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ContactByInboxDto"}}}},"default":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ContactByInboxDto"}}}}},"tags":[],"security":[{"bearerToken":[]}]}},"/contacts":{"post":{"operationId":"ContactController_addContact","summary":"Create contact (strict REST)","description":"Creates a WhatsApp contact using strict REST semantics. Returns 4xx exceptions for validation/duplication errors.","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateContactDto"},"examples":{"create_contact":{"summary":"Create a new WhatsApp contact","value":{"firstName":"Jane","lastName":"Doe","email":"jane.doe@example.com","number":"919876543210","name":"Jane Doe"}}}}}},"responses":{"201":{"description":"Contact created successfully.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ContactDto"}}}},"400":{"description":"Invalid phone number format.","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/ApiErrorEnvelopeDto"}]},"examples":{"invalid_phone":{"value":{"statusCode":400,"message":"Invalid phone number format.","error":"Bad Request"}}}}}},"404":{"description":"WhatsApp channel not connected for workspace.","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/ApiErrorEnvelopeDto"}]},"examples":{"whatsapp_not_connected":{"value":{"statusCode":404,"message":"Whatsapp not connected","error":"Not Found"}}}}}},"409":{"description":"Duplicate contact exists.","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/DuplicateContactErrorDto"}]},"examples":{"duplicate_contact":{"value":{"statusCode":409,"message":"A contact with this phone number already exists.","error":"Conflict","existingContactId":42}}}}}}},"tags":["Contact"],"security":[{"bearerToken":[]}]},"get":{"operationId":"ContactController_listContacts","parameters":[{"name":"limit","required":false,"in":"query","description":"The number of contacts to return.","schema":{"default":20,"type":"number"}},{"name":"query","required":false,"in":"query","description":"Searches (case insensitive) for contacts with name, email, number, username, firstName, lastName","schema":{"type":"string"}},{"name":"channel","required":false,"in":"query","description":"Comma separated channel names to filter contacts by. E.g. \"ig,fb,wa\"","schema":{"type":"string"}},{"name":"getOrdersShopifyAndTagInfo","required":false,"in":"query","description":"Whether to fetch Shopify orders and tag information","schema":{"default":false,"type":"boolean"}},{"name":"hasEmail","required":false,"in":"query","description":"Only return contacts with a non-empty email address, regardless of channel. Use this instead of channel=email to list every contact that can be emailed.","schema":{"default":false,"type":"boolean"}},{"name":"getActiveContacts","required":false,"in":"query","description":"Whether to filter for active contacts","schema":{"default":false,"type":"boolean"}},{"name":"activeContactsFrom","required":false,"in":"query","description":"Filter contacts by date added (required when getActiveContacts is true)","schema":{"type":"string"}},{"name":"activeContactsTo","required":false,"in":"query","description":"Filter contacts by date added (required when getActiveContacts is true)","schema":{"type":"string"}},{"name":"cursor","required":false,"in":"query","schema":{"type":"number"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ContactDto"}}}}}},"tags":["Contact"],"security":[{"bearerToken":[]}]}},"/contacts/{id}/tags":{"post":{"operationId":"ContactController_addTagToContact","parameters":[{"name":"id","required":true,"in":"path","description":"Contact ID. You can find the Contact ID by searching for contacts using the GET /contacts endpoint.","schema":{"type":"number"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"oneOf":[{"$ref":"#/components/schemas/TagNameDto"},{"$ref":"#/components/schemas/TagIdDto"}]}}}},"responses":{"201":{"description":"Tags after addition","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ContactTag"}}}}}},"tags":["Tags"],"security":[{"bearerToken":[]}]}},"/contacts/{id}/custom-fields":{"post":{"operationId":"ContactController_addCustomFieldToContact","parameters":[{"name":"id","required":true,"in":"path","description":"Contact ID. You can find the Contact ID by searching for contacts using the GET /contacts endpoint.","schema":{"type":"number"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AddCustomFieldDto"}}}},"responses":{"201":{"description":""}},"tags":["Custom Fields"],"security":[{"bearerToken":[]}]}},"/contacts/{id}/custom-fields/update":{"post":{"operationId":"ContactController_updateCustomFieldOfContact","parameters":[{"name":"id","required":true,"in":"path","description":"Contact ID. You can find the Contact ID by searching for contacts using the GET /contacts endpoint.","schema":{"type":"number"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AddCustomFieldDto"}}}},"responses":{"201":{"description":""}},"tags":["Custom Fields"],"security":[{"bearerToken":[]}]}},"/contacts/{id}/custom-fields/update-many":{"post":{"operationId":"ContactController_updateManyCustomFieldsOfContact","parameters":[{"name":"id","required":true,"in":"path","description":"Contact ID. You can find the Contact ID by searching for contacts using the GET /contacts endpoint.","schema":{"type":"number"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/AddCustomFieldDto"}}}}},"responses":{"201":{"description":""}},"tags":["Custom Fields"],"security":[{"bearerToken":[]}]}},"/contacts/{id}/custom-fields/delete":{"post":{"operationId":"ContactController_deleteCustomFieldFromContact","parameters":[{"name":"id","required":true,"in":"path","description":"Contact ID. You can find the Contact ID by searching for contacts using the GET /contacts endpoint.","schema":{"type":"number"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeleteCustomFieldDto"}}}},"responses":{"201":{"description":""}},"tags":["Custom Fields"],"security":[{"bearerToken":[]}]}},"/contacts/custom-fields/bulk-update":{"post":{"operationId":"ContactController_bulkUpdateCustomFields","summary":"Update custom fields by phone, email, or contact ID","description":"\nUpdate custom fields for a contact using any of the following identifiers:\n- **contactId**: Direct contact ID (takes highest priority)\n- **phone**: Phone number with country code (e.g., \"919876543210\")\n- **email**: Email address (case-insensitive lookup)\n\nAt least one identifier must be provided. If multiple are provided, the lookup priority is: contactId > phone > email.\n\nThis endpoint is useful when you don't have the contact ID readily available but have the phone number or email from your system.\n        ","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BulkUpdateCustomFieldsDto"},"examples":{"Update by phone number":{"summary":"Look up contact by phone and update custom fields","value":{"phone":"919876543210","customFields":[{"name":"loyalty_points","value":500},{"name":"membership_tier","value":"gold"}]}},"Update by email":{"summary":"Look up contact by email and update custom fields","value":{"email":"user@example.com","customFields":[{"name":"last_purchase_date","value":"2025-01-08"},{"name":"total_orders","value":15}]}},"Update by contact ID":{"summary":"Update custom fields using contact ID directly","value":{"contactId":12345,"customFields":[{"name":"vip_status","value":true}]}}}}}},"responses":{"201":{"description":""}},"tags":["Custom Fields"],"security":[{"bearerToken":[]}]}},"/contacts/tag/{id}/delete-contacts":{"get":{"operationId":"ContactController_deleteContactsWithTag","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"number"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"object"}}}}},"tags":[],"security":[{"bearerToken":[]}]}},"/contacts/add":{"post":{"operationId":"ContactController_createContact","summary":"Create or update contact (legacy status contract)","description":"Legacy endpoint used by current frontend flows. Returns status payloads (`success`, `exists`, `failed`) instead of strict exception-only semantics.","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AddContactInput"}}}},"responses":{"201":{"description":"Status response for create/update operation.","content":{"application/json":{"schema":{"oneOf":[{"$ref":"#/components/schemas/AddContactSuccessResponseDto"},{"$ref":"#/components/schemas/AddContactExistsResponseDto"},{"$ref":"#/components/schemas/AddContactFailedResponseDto"}]},"examples":{"success_created":{"value":{"status":"success","message":"Contact added successfully","contactId":9812}},"exists_conflict":{"value":{"status":"exists","message":"Phone number/Email already exists for another contact","contactId":9812}},"failed_generic":{"value":{"status":"failed","message":"Something went wrong"}}}}}},"400":{"description":"Input validation failure (e.g. invalid number).","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/ApiErrorEnvelopeDto"}],"example":{"statusCode":400,"message":"Invalid phone number format.","error":"Bad Request"}}}}},"404":{"description":"WhatsApp channel not connected for workspace.","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/ApiErrorEnvelopeDto"}],"example":{"statusCode":404,"message":"Whatsapp not connected","error":"Not Found"}}}}}},"tags":["Contact"],"security":[{"bearerToken":[]}]}},"/v2/ticket/ticket-conversations":{"get":{"operationId":"TicketV2Controller_getAllTicketConversations","summary":"List workspace inbox conversations","description":"Returns a cursor-paginated list of 1:1, email, and WhatsApp group conversations. For the next page, send back both cursor and lastId from the previous response.","parameters":[{"name":"channelId","required":false,"in":"query","schema":{"type":"string"}},{"name":"ticketStatus","required":false,"in":"query","schema":{"nullable":true,"type":"string"}},{"name":"query","required":false,"in":"query","schema":{"type":"string"}},{"name":"cursor","required":false,"in":"query","schema":{"type":"string"}},{"name":"lastId","required":false,"in":"query","schema":{"type":"string"}},{"name":"channels","required":false,"in":"query","schema":{"type":"string"}},{"name":"unread","required":false,"in":"query","schema":{"type":"string"}},{"name":"scroll","required":false,"in":"query","schema":{"type":"string"}},{"name":"isAssigned","required":false,"in":"query","schema":{"type":"string"}},{"name":"teamId","required":false,"in":"query","schema":{"type":"string"}},{"name":"userId","required":false,"in":"query","schema":{"type":"string"}},{"name":"hours","required":false,"in":"query","schema":{"type":"string"}},{"name":"days","required":false,"in":"query","schema":{"type":"string"}},{"name":"sortBy","required":false,"in":"query","schema":{"type":"string"}},{"name":"requestId","required":false,"in":"query","schema":{"type":"string"}},{"required":false,"name":"channelId","in":"query","schema":{"type":"string"}},{"required":false,"name":"ticketStatus","in":"query","schema":{"nullable":true,"type":"string"}},{"required":false,"name":"query","in":"query","schema":{"type":"string"}},{"required":false,"name":"cursor","in":"query","schema":{"type":"string"}},{"required":false,"name":"lastId","in":"query","schema":{"type":"string"}},{"required":false,"name":"channels","in":"query","schema":{"type":"string"}},{"required":false,"name":"unread","in":"query","schema":{"type":"string"}},{"required":false,"name":"scroll","in":"query","schema":{"type":"string"}},{"required":false,"name":"isAssigned","in":"query","schema":{"type":"string"}},{"required":false,"name":"teamId","in":"query","schema":{"type":"string"}},{"required":false,"name":"userId","in":"query","schema":{"type":"string"}},{"required":false,"name":"hours","in":"query","schema":{"type":"string"}},{"required":false,"name":"days","in":"query","schema":{"type":"string"}},{"required":false,"name":"sortBy","in":"query","schema":{"type":"string"}},{"required":false,"name":"requestId","in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TicketConversationV2Response"}}}},"default":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TicketConversationV2Response"}}}}},"tags":["Ticket V2"],"security":[{"bearerToken":[]}]}},"/conversations/{conversationId}/assign":{"post":{"operationId":"ConversationAssignController_assign","summary":"Hand a conversation between the AI agent and a person","description":"`{ \"to\": \"user\", \"userId\": 318 }` assigns a person and stops the AI agent on this conversation. `{ \"to\": \"ai\" }` clears the person; the AI agent replies to the next inbound message. Assigning the current assignee again returns the current state. Assignment doesn’t change the conversation status, except that assigning a person to a closed conversation reopens it. Call with your workspace API key.","parameters":[{"name":"conversationId","required":true,"in":"path","description":"The conversation (inboxId).","schema":{"type":"number"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AssignConversationDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AssignConversationResponseDto"}}}},"400":{"description":"Missing or invalid `to` or `userId`, or the user is not in the workspace"},"401":{"description":"Invalid API key"},"404":{"description":"Conversation not found in this workspace"},"409":{"description":"The user hasn't accepted their invite"}},"tags":["Conversations"],"security":[{"bearerToken":[]}]}},"/whatsapp/message/template/draft":{"post":{"operationId":"WhatsAppTemplateController_createDraftMessageTemplate","summary":"Create WhatsApp template draft","description":"Creates a draft template in Spur only. Use submit-for-review endpoint to send this template to Meta.","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateDraftTemplateDto"},"examples":{"Default":{"summary":"Create utility template draft","value":{"name":"order_status_update_v1","channelId":25,"category":"UTILITY","format":"SINGLE","language":"en_US","components":[{"type":"BODY","text":"Hi {{1}}, your order {{2}} is now {{3}}.","example":{"body_text":[["Rohan","#1001","shipped"]]}}]}}}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateDraftTemplateResponse"}}}},"400":{"description":"Invalid template draft payload.","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/ApiErrorEnvelopeDto"}],"example":{"statusCode":400,"message":"Validation failed","error":"Validation failed"}}}}},"404":{"description":"Channel not found for workspace.","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/ApiErrorEnvelopeDto"}],"example":{"statusCode":404,"message":"Channel not found","error":"Channel not found"}}}}}},"tags":["WhatsApp Templates"],"security":[{"bearerToken":[]}]}},"/whatsapp/message/template/from-library":{"post":{"operationId":"WhatsAppTemplateController_createDraftFromLibrary","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateDraftFromLibraryDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateDraftTemplateResponse"}}}},"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateDraftTemplateResponse"}}}}},"tags":["WhatsApp Templates"],"security":[{"bearerToken":[]}]}},"/whatsapp/message/template/{id}":{"delete":{"operationId":"WhatsAppTemplateController_deleteTemplate","summary":"Delete a template by internal Spur template id","description":"Deletes the template from Spur database and attempts Meta template deletion if a linked templateId exists.","parameters":[{"name":"id","required":true,"in":"path","description":"Internal Spur MessageTemplate ID.","schema":{"example":89,"type":"string"}}],"responses":{"200":{"description":"Template deleted from Spur. If linked with Meta, provider response is returned when available.","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","nullable":true,"example":true},"message":{"type":"string","nullable":true,"example":"Template deleted locally; no linked Meta template id found."}},"additionalProperties":true}}}},"404":{"description":"Template or workspace not found.","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/ApiErrorEnvelopeDto"}],"example":{"statusCode":404,"message":"Template not found","error":"Not Found"}}}}}},"tags":["WhatsApp Templates"],"security":[{"bearerToken":[]}]}},"/whatsapp/message/template":{"post":{"operationId":"WhatsAppTemplateController_createTemplate","summary":"Submit draft template for Meta review","description":"Submits a saved draft template to Meta. Provider-level error fields are preserved for troubleshooting.","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SubmitTemplateForReviewDto"},"examples":{"Default":{"value":{"templateId":1234}}}}}},"responses":{"201":{"description":"Template submitted successfully.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MessageTemplateEntity"}}}},"400":{"description":"Template submission rejected by Meta or payload validation failed. Common subcodes include 2388124 (edit cooldown), 2388047 (body format), and 2388153 (button text constraints). Flow templates can also fail with error_user_title \"WhatsApp Store not configured\" when flow_id is missing.","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/ApiErrorEnvelopeDto"}]},"examples":{"edit_cooldown_2388124":{"summary":"Template edit cooldown in Meta","value":{"statusCode":400,"message":"Invalid parameter","error":{"message":"Invalid parameter","type":"OAuthException","code":100,"error_subcode":2388124,"error_user_title":"Message template can't be edited","error_user_msg":"You can only edit an active template once in 24 hours.","fbtrace_id":"Aa6LJRrhtxH_utcEI9YAQ7o"},"providerError":{"code":100,"error_subcode":2388124,"error_user_title":"Message template can't be edited","error_user_msg":"You can only edit an active template once in 24 hours.","fbtrace_id":"Aa6LJRrhtxH_utcEI9YAQ7o"}}},"body_format_2388047":{"summary":"Template body format rejected by Meta","value":{"statusCode":400,"message":"Invalid parameter","error":{"message":"Invalid parameter","type":"OAuthException","code":100,"error_subcode":2388047,"error_user_title":"Message Body Format is Incorrect","error_user_msg":"Message body can't have more than two consecutive newline characters.","fbtrace_id":"AXB-3cfBXm9U6yoF7OTsiMD","error_data":{"details":"Message body can't have more than two consecutive newline characters."}}}},"button_text_2388153":{"summary":"Button text mismatch","value":{"statusCode":400,"message":"Invalid parameter","error":{"message":"Invalid parameter","type":"OAuthException","code":100,"error_subcode":2388153,"error_user_title":"Button text is invalid","error_user_msg":"Button text does not match approved template button text.","fbtrace_id":"AZrm8aYeM9S1i-4qS6HYnax"}}},"flow_id_missing_store_not_configured":{"summary":"FLOW button submission blocked: flow_id missing","value":{"statusCode":400,"message":"Request failed","error":{"error_user_title":"WhatsApp Store not configured","error_user_msg":"Please configure your WhatsApp Store first to get a flow_id. Go to Marketing Tools > WhatsApp Store and submit your flow."},"providerError":{"error_user_title":"WhatsApp Store not configured","error_user_msg":"Please configure your WhatsApp Store first to get a flow_id. Go to Marketing Tools > WhatsApp Store and submit your flow."}}}}}}},"404":{"description":"Workspace or template not found.","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/ApiErrorEnvelopeDto"}]},"examples":{"template_not_found":{"value":{"statusCode":404,"message":"Template not found","error":"Template not found"}},"workspace_not_found":{"value":{"statusCode":404,"message":"User profile not found","error":"User profile not found"}}}}}}},"tags":["WhatsApp Templates"],"security":[{"bearerToken":[]}]}},"/whatsapp/templates/all":{"get":{"operationId":"WhatsAppTemplateController_getAllMessageTemplate","summary":"Search workspace template library","description":"Returns workspace WhatsApp templates with optional query, status, category, channel, and cursor filters.","parameters":[{"name":"query","required":false,"in":"query","description":"Case-insensitive substring match on template name/components.","schema":{"example":"order_update","type":"string"}},{"name":"cursor","required":false,"in":"query","description":"Cursor for pagination (returns templates with id < cursor).","schema":{"example":1024,"type":"string"}},{"name":"limit","required":false,"in":"query","description":"Maximum templates to return. Default is 25.","schema":{"example":25,"type":"string"}},{"name":"status","required":false,"in":"query","description":"Template status filter (case-insensitive).","schema":{"example":"APPROVED","type":"string"}},{"name":"channelId","required":false,"in":"query","description":"Filter templates by connected WhatsApp channel ID.","schema":{"example":12,"type":"string"}},{"name":"category","required":false,"in":"query","description":"Category filter (MARKETING, UTILITY, AUTHENTICATION).","schema":{"example":"UTILITY","type":"string"}},{"name":"groupsCompatible","required":false,"in":"query","description":"When true, filters to templates Meta permits in group threads: no QUICK_REPLY buttons, no LIST / PRODUCT_LIST / CATALOG / FLOW components, no call permission request. URL / PHONE_NUMBER / COPY_CODE buttons remain allowed.","schema":{"example":true,"type":"boolean"}},{"name":"callPermission","required":false,"in":"query","description":"When true, only templates that ask the customer for permission to call them (a call_permission_request component), for calling customers outside the 24-hour window.","schema":{"example":true,"type":"boolean"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MessageTemplateResponse"}}}},"400":{"description":"Invalid query params (cursor/limit/channelId must be numeric).","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/ApiErrorEnvelopeDto"}],"example":{"statusCode":400,"message":"Invalid query parameter","error":"Bad Request"}}}}}},"tags":["WhatsApp Templates"],"security":[{"bearerToken":[]}]}},"/whatsapp/message/templates/{metaTemplateId}":{"get":{"operationId":"WhatsAppTemplateController_getMessageTemplateInfo","parameters":[{"name":"metaTemplateId","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WhatsAppMessageTemplate"}}}}},"tags":["WhatsApp Templates"],"security":[{"bearerToken":[]}]}},"/whatsapp/templates/{id}":{"get":{"operationId":"WhatsAppTemplateController_getMessageTemplateById","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"number"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MessageTemplateEntity"}}}}},"tags":["WhatsApp Templates"],"security":[{"bearerToken":[]}]}},"/whatsapp/message/template/refresh/{id}":{"post":{"operationId":"WhatsAppTemplateController_refreshTemplateStatus","summary":"Refresh template status by Spur template ID","description":"Fetches latest status/components from Meta for an existing template record and updates database state.","parameters":[{"name":"id","required":true,"in":"path","description":"Internal Spur MessageTemplate ID.","schema":{"example":1234,"type":"number"}}],"responses":{"200":{"description":"Template refreshed successfully.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MessageTemplateEntity"}}}},"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MessageTemplateEntity"}}}},"404":{"description":"Template not found or WhatsApp channel/token is unavailable.","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/ApiErrorEnvelopeDto"}],"example":{"statusCode":404,"message":"Something went wrong, please refresh whatsapp from Settings ","error":"Not Found"}}}}}},"tags":["WhatsApp Templates"],"security":[{"bearerToken":[]}]}},"/whatsapp/message/template/refresh-by-meta-id/{metaTemplateId}":{"post":{"operationId":"WhatsAppTemplateController_refreshTemplateByMetaId","summary":"Refresh template status by Meta template ID","description":"Refreshes a template by Meta template ID, then updates any broadcasts that reference the same template.","parameters":[{"name":"metaTemplateId","required":true,"in":"path","description":"Meta template ID (waba template identifier).","schema":{"example":"1208259160474051","type":"string"}}],"responses":{"200":{"description":"Template refreshed successfully.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MessageTemplateEntity"}}}},"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MessageTemplateEntity"}}}},"404":{"description":"Template not found in workspace.","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/ApiErrorEnvelopeDto"}],"example":{"statusCode":404,"message":"Template not found","error":"Not Found"}}}}}},"tags":["WhatsApp Templates"],"security":[{"bearerToken":[]}]}},"/whatsapp/message/template/{id}/duplicate":{"post":{"operationId":"WhatsAppTemplateController_duplicateTemplate","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"number"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateDraftTemplateResponse"}}}},"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateDraftTemplateResponse"}}}}},"tags":["WhatsApp Templates"],"security":[{"bearerToken":[]}]}},"/whatsapp/message/template/{id}/duplicate-with-changes":{"post":{"operationId":"WhatsAppTemplateController_duplicateTemplateWithChanges","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"number"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DuplicateTemplateWithChangesDto"}}}},"responses":{"200":{"description":"Returns template ID if not submitted, or full template object if auto-submitted","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MessageTemplateEntity"}}}},"201":{"description":"","content":{"application/json":{"schema":{"type":"object"}}}}},"tags":["WhatsApp Templates"],"security":[{"bearerToken":[]}]}},"/whatsapp/magic-message/config":{"get":{"operationId":"WhatsAppTemplateController_getMagicMessageConfig","parameters":[{"name":"channelId","required":true,"in":"query","description":"WhatsApp channel ID","schema":{"example":25,"type":"number"}}],"responses":{"200":{"description":"Get the magic message template configuration for a channel","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MagicMessageConfigResponseDto"}}}}},"tags":["WhatsApp Templates"],"security":[{"bearerToken":[]}]}},"/whatsapp/magic-message/configure":{"post":{"operationId":"WhatsAppTemplateController_setMagicMessageTemplate","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConfigureMagicMessageDto"}}}},"responses":{"200":{"description":"Configure the magic message template for a channel","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConfigureMagicMessageResponseDto"}}}},"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConfigureMagicMessageResponseDto"}}}}},"tags":["WhatsApp Templates"],"security":[{"bearerToken":[]}]}},"/whatsapp/magic-message/ensure-template":{"post":{"operationId":"WhatsAppTemplateController_ensureMagicMessageTemplate","parameters":[{"name":"channelId","required":true,"in":"query","description":"WhatsApp channel ID","schema":{"example":25,"type":"number"}}],"responses":{"200":{"description":"Ensures the configured magic message template exists and is approved for 24hr bypass feature","content":{"application/json":{"schema":{"$ref":"#/components/schemas/EnsureMagicMessageTemplateResponseDto"}}}},"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/EnsureMagicMessageTemplateResponseDto"}}}}},"tags":["WhatsApp Templates"],"security":[{"bearerToken":[]}]}},"/v2/inbox/messages":{"get":{"operationId":"InboxV2Controller_messages","summary":"Get messages (v2 - uses inboxId param)","parameters":[{"name":"inboxId","required":false,"in":"query","schema":{"type":"number"}},{"name":"contactId","required":false,"in":"query","description":"Deprecated: use inboxId instead","schema":{"type":"number"}},{"name":"cursor","required":false,"in":"query","description":"Load older messages before this sentDateTime","schema":{"type":"string"}},{"name":"afterCursor","required":false,"in":"query","description":"Load newer messages after this sentDateTime","schema":{"type":"string"}},{"name":"aroundMessageId","required":false,"in":"query","description":"Jump to a specific message: returns ~15 messages before it + messages after it. Falls back to normal pagination if not found.","schema":{"type":"number"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InboxMessagesV2Response"}}}}},"tags":["Inbox V2"],"security":[{"bearerToken":[]}]}},"/v2/inbox/conversations/{inboxId}/notes":{"post":{"operationId":"InboxV2Controller_createNote","summary":"Add an internal note to a conversation","description":"Adds an agent-only note to a 1:1 conversation timeline, the same as adding a note in the inbox. The note is never sent to the contact and does not change assignment or automation.","parameters":[{"name":"inboxId","required":true,"in":"path","description":"ContactInbox id.","schema":{"type":"number"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateInboxNoteBody"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateInboxNoteResponseDto"}}}},"400":{"description":"Empty text, or the conversation has no messages yet."}},"tags":["Inbox V2"],"security":[{"bearerToken":[]}]}},"/v2/inbox/search":{"get":{"operationId":"InboxV2Controller_search","summary":"Sectioned search across contacts and messages","parameters":[{"name":"q","required":true,"in":"query","description":"Search query string","schema":{"type":"string"}},{"name":"sections","required":false,"in":"query","description":"Comma-separated sections: contacts,groups,messages. The contacts section covers native 1:1 conversations and email threads (matched on contact name/email or any participant address).","schema":{"default":"contacts,groups,messages","type":"string"}},{"name":"contactsLimit","required":false,"in":"query","schema":{"default":5,"type":"number"}},{"name":"messagesLimit","required":false,"in":"query","schema":{"default":10,"type":"number"}},{"name":"contactsOffset","required":false,"in":"query","description":"Offset for contacts section (for load more)","schema":{"default":0,"type":"number"}},{"name":"messagesOffset","required":false,"in":"query","description":"Offset for messages section (for load more)","schema":{"default":0,"type":"number"}},{"name":"groupsLimit","required":false,"in":"query","schema":{"default":5,"type":"number"}},{"name":"groupsOffset","required":false,"in":"query","description":"Offset for groups section (for load more)","schema":{"default":0,"type":"number"}},{"name":"conversationType","required":false,"in":"query","description":"Scope search to the active view: all | assigned | unassigned | team | ai-agent","schema":{"type":"string"}},{"name":"userId","required":false,"in":"query","description":"Assigned-agent id (assigned/mine views)","schema":{"type":"number"}},{"name":"teamId","required":false,"in":"query","description":"Assigned-team id (team view)","schema":{"type":"number"}},{"name":"channelId","required":false,"in":"query","description":"Comma-separated channel ids to scope to","schema":{"type":"string"}},{"name":"ticketStatus","required":false,"in":"query","description":"OPEN | CLOSED","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SearchResponse"}}}}},"tags":["Inbox V2"],"security":[{"bearerToken":[]}]}},"/send-message":{"post":{"operationId":"SendMessageController_sendMessage","summary":"Send a message to a contact or a WhatsApp group","description":"\nSend a message to a contact via WhatsApp, Instagram, or Facebook, or to a WhatsApp group.\n\n## Important Notes\n\n### Sending to a WhatsApp group\nSet `to` to `group:<id>` using the group id from `GET /v2/groups`, for example `group:123`.\n\n```json\n{ \"to\": \"group:123\", \"channel\": \"whatsapp\", \"content\": { \"type\": \"text\", \"text\": { \"body\": \"Call summary attached\" } } }\n```\n\n- Groups accept `text`, `image`, `video`, `audio`, `document` and `template` content.\n- Media must be a public **https** URL in `link`. Groups cannot resolve an uploaded media id.\n- Templates are billed by Meta as **one conversation per delivered participant**.\n- Meta does not accept QUICK_REPLY, LIST, PRODUCT_LIST, CATALOG or FLOW components in groups. URL, phone number and copy code buttons are fine.\n- Groups are WhatsApp only, and a group is always messaged from the channel that owns it, so omit `options.from`.\n- A group send never changes who the conversation is assigned to. DeliveryBoost does not apply, since it retries a single recipient.\n- The per-contact marketing opt-out is not applied to groups, because a group has many participants and no single opt-out state. Check your own audience before sending marketing templates to a group.\n\n### Channel Selection (options.from)\n⚠️ **If you have multiple WhatsApp channels connected**, you MUST specify `options.from` with the phone number of the channel you want to send from. Otherwise, messages may be routed through the wrong channel.\n\n### Template Messages\n- **Template name must be EXACT**: The template name must exactly match what's approved in your WhatsApp Business Manager. Typos will cause \"template not found\" errors.\n- **All template variables are MANDATORY**: Every placeholder in your template must have a corresponding parameter. Empty or null values will cause the message to fail. Use fallback values like \"User\" or \"Customer\" if you don't have the actual data.\n- **Template must be approved**: The template must be approved in the specific WhatsApp Business Account you're sending from.\n\n### Content Format\nFor WhatsApp, the content follows the [WhatsApp Cloud API format](https://developers.facebook.com/docs/whatsapp/cloud-api/reference/messages). You can omit `to` and `messaging_product` fields as they are populated automatically.\n\n### DeliveryBoost (Marketing Sends)\nMarketing template messages are **automatically retried** when they fail due to WhatsApp's per-user marketing limit (Meta error `131049`). The first retry fires 10 hours after the failure, then every 5 hours, for up to 12 attempts across a 72-hour window.\n\n**DeliveryBoost is ON by default for marketing sends.** When enabled and the message is a marketing template, the response includes a `deliveryBoost` object confirming retries are scheduled.\n\n**To disable retries**, pass `\"deliveryBoost\": false` in the request body. See the *\"Marketing send with DeliveryBoost disabled\"* example below. This is useful for time-sensitive campaigns where a delayed delivery would be worse than no delivery.\n        ","parameters":[],"requestBody":{"required":true,"description":"Message payload. For WhatsApp, content follows the Cloud API format. See operation description for important notes about channel selection and template requirements.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SendMessageDto"},"examples":{"Template Message (with channel selection)":{"summary":"Send template message via specific WhatsApp channel","description":"Note: options.from specifies which WhatsApp channel to use. Template name must exactly match the approved template.","value":{"channel":"whatsapp","to":"910123456789","content":{"type":"template","template":{"name":"abandoned_cart_recovery_reminder","components":[{"type":"header","parameters":[{"type":"image","image":{"link":"https://cdn.example.com/banners/sale-banner.jpg"}}]},{"type":"body","parameters":[{"text":"Jane","type":"text"},{"text":"Doe","type":"text"},{"text":"Your cart is waiting!","type":"text"}]},{"type":"button","index":0,"sub_type":"url","parameters":[{"type":"text","text":"SUMMER25"}]}],"language":{"code":"en"}}},"options":{"from":"919876543210"}}},"Template with fallback for missing name":{"summary":"Use fallback value when first_name is not available","description":"All template variables are mandatory. Use \"User\" or \"Customer\" as fallback when actual data is not available.","value":{"channel":"whatsapp","to":"910123456789","content":{"type":"template","template":{"name":"bill_settlement","components":[{"type":"body","parameters":[{"text":"User","type":"text"},{"text":"500","type":"text"}]}],"language":{"code":"en"}}},"options":{"from":"919876543210"}}},"Free Form Message":{"summary":"Send a free-form text message","description":"Free-form messages can only be sent within 24 hours of the last customer message.","value":{"channel":"whatsapp","to":"910123456789","content":{"recipient_type":"individual","type":"text","text":{"preview_url":true,"body":"Hi, test message"}},"options":{"from":"919876543210"}}},"Marketing send with DeliveryBoost disabled":{"summary":"Opt out of automatic retries for a marketing template send","description":"DeliveryBoost is on by default for marketing templates. Pass \"deliveryBoost\": false to disable automatic retries on WhatsApp per-user limit failures (error 131049). Use this when a delayed delivery would be worse than no delivery at all.","value":{"channel":"whatsapp","to":"910123456789","content":{"type":"template","template":{"name":"abandoned_cart_recovery_reminder","components":[{"type":"body","parameters":[{"text":"Jane","type":"text"},{"text":"Doe","type":"text"},{"text":"Your cart is waiting!","type":"text"}]}],"language":{"code":"en"}}},"options":{"from":"919876543210"},"deliveryBoost":false}}}}}},"responses":{"201":{"description":"Message accepted and persisted.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SendMessageResponseDto"}}}},"400":{"description":"Validation error or provider-level bad request.","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/ApiErrorEnvelopeDto"}]},"examples":{"provider_body_format_invalid":{"summary":"Meta rejected body format (error_subcode 2388047)","value":{"statusCode":400,"message":"Invalid parameter","error":{"message":"Invalid parameter","type":"OAuthException","code":100,"error_subcode":2388047,"error_user_title":"Message Body Format is Incorrect","error_user_msg":"Message body can't have more than two consecutive newline characters.","fbtrace_id":"AXB-3cfBXm9U6yoF7OTsiMD","error_data":{"messaging_product":"whatsapp","details":"Message body can't have more than two consecutive newline characters."}},"providerError":{"message":"Invalid parameter","type":"OAuthException","code":100,"error_subcode":2388047,"error_user_title":"Message Body Format is Incorrect","error_user_msg":"Message body can't have more than two consecutive newline characters.","fbtrace_id":"AXB-3cfBXm9U6yoF7OTsiMD"}}},"provider_template_variable_invalid":{"summary":"Meta rejected template variables","value":{"statusCode":400,"message":"Invalid parameter","error":{"message":"Invalid parameter","type":"OAuthException","code":100,"error_subcode":2494073,"error_user_title":"Template parameter is invalid","error_user_msg":"One or more template parameters are missing or empty.","fbtrace_id":"Adf91WbH9V_fQea4FmQw3c4","error_data":{"messaging_product":"whatsapp","details":"The number of parameters does not match the placeholders in the template body."}}}},"provider_channel_payload_invalid":{"summary":"Provider rejected payload for selected channel","value":{"statusCode":400,"message":"Invalid parameter","error":{"message":"Invalid parameter","type":"OAuthException","code":100,"error_user_title":"Unsupported request payload","error_user_msg":"Recipient/channel payload is invalid for the selected destination.","fbtrace_id":"Aah4W-Gx_2pfq2qkwkJe6xY"}}}}}}},"402":{"description":"Subscription inactive or wallet balance insufficient.","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/ApiErrorEnvelopeDto"}]},"examples":{"inactive_subscription":{"summary":"Spur subscription inactive","value":{"statusCode":402,"message":"Your Spur subscription is not active. Please activate or renew your subscription at https://use.spurnow.com/app/settings/plans-usage to continue sending messages.","error":"Your Spur subscription is not active. Please activate or renew your subscription at https://use.spurnow.com/app/settings/plans-usage to continue sending messages."}},"wallet_frozen":{"summary":"WhatsApp channels frozen due to low balance","value":{"statusCode":402,"message":"Your WhatsApp channels are frozen due to insufficient wallet balance. Please top up your wallet.","error":"Your WhatsApp channels are frozen due to insufficient wallet balance. Please top up your wallet."}},"wallet_low":{"summary":"Wallet below send threshold","value":{"statusCode":402,"message":"Your WhatsApp wallet balance is too low to send this message. Please top up your wallet at https://use.spurnow.com/app/settings/plans-usage/wallet to continue messaging.","error":"Your WhatsApp wallet balance is too low to send this message. Please top up your wallet at https://use.spurnow.com/app/settings/plans-usage/wallet to continue messaging."}}}}}},"404":{"description":"Channel not found or destination contact not found.","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/ApiErrorEnvelopeDto"}]},"examples":{"channel_not_found":{"summary":"No channel found for workspace/from phone","value":{"statusCode":404,"message":"Channel not found","error":"Channel not found"}},"contact_not_found":{"summary":"Instagram/Facebook contact not found","value":{"statusCode":404,"message":"Contact id: 88711 not found","error":"Not Found"}}}}}}},"tags":["Message"],"security":[{"bearerToken":[]}]}},"/send-message/direct-send":{"post":{"operationId":"SendMessageController_directSend","summary":"Send a utility text outside the 24-hour window with Meta Direct Send","description":"\nSend one plain-text WhatsApp message to one contact. Outside WhatsApp's 24-hour window it goes out through **Meta Direct Send**: a normal text message that Meta delivers as a **utility** message. You don't create or get a template approved: Meta creates (or reuses) one for each message, named `auto_generated_…`, visible in WhatsApp Manager and not editable or deletable. Inside the window it is an ordinary message.\n\n## ⚠️ Read before you send\n\n- **Utility content only:** replies, updates about an order, booking or account, and support. **No marketing or promotions**, no discount codes, no offers.\n- **Misuse gets your whole WhatsApp Business Account, every number on it, restricted or banned by Meta.** Meta warns first, then caps utility messages, then blocks Direct Send for 7 days, then for 30 days, then removes access. **Spur cannot reverse or help with Meta's enforcement.**\n- **Delivery can take a few minutes, and Meta may hold a message.** Direct Send messages are subject to Meta's business portfolio pacing: a message can be held for quality review (the response then carries `held: true`) and delivered later or dropped. A drop is reported only by the status webhook, with code 135000, which Spur's message-delivery webhook forwards. Don't send the same message again just because it hasn't arrived yet.\n- **Every message is billed at Meta's utility rate.**\n\n## How it works\n\n- Direct Send is never converted to a Magic Message or a template. When it can't be used, the request fails with a clear error carrying `directSend.unavailable`, and nothing is sent. Use `POST /send-message` with an approved template instead.\n- Meta has to enable Direct Send for your WhatsApp Business Account (Meta generally requires a messaging limit of 100,000 or unlimited). Spur learns Meta's answer from the first send; there is no way to ask Meta in advance. Spur does not gate this endpoint per workspace: only Spur's platform-wide kill switch can pause it.\n- Only contacts who have messaged this number before. A number with no prior conversation is refused, and so are contacts who opted out.\n- Only WhatsApp Cloud API and Onextel numbers. Text only, up to 1,024 characters. One contact per request; never loop this endpoint for campaigns (use broadcasts). Spur does not cap how often a contact is messaged: that judgement is yours, and Meta's enforcement lands on your account.\n\n## Response\n\n`deliveryPath` says how the message went out: `direct_send` or, inside the window, `plain`. A Direct Send response also carries `notice`, and `held: true` when Meta is holding the message for quality review.\n\n## When it fails\n\nEvery refusal has one shape: `error: \"Direct Send unavailable\"`, a `message` you can show as is, and `directSend.unavailable` to switch on. Alongside it, `directSend` carries the number's Direct Send standing: `status` (UNKNOWN, ENABLED, NOT_ENABLED or BLOCKED), `wabaId`, `phoneNumberId`, `nextCheckAt`, `blockedUntil`, `rateLimitedUntil` and `requestAccessUrl` (Meta's Direct Send access request form, prefilled with the WABA id), plus `errorCode` (Meta's error code) when Meta answered with one.\n\n- 403 `not_turned_on`: Spur has paused Direct Send for everyone.\n- 400 `unsupported_number`: the number is not on WhatsApp Cloud API or Onextel, or has no WhatsApp Business Account id yet.\n- 400 `too_long`: the text is over 1,024 characters.\n- 400 `rejected`: Meta refused this message. `errorCode` is Meta's code and `message` explains it: for example 131026 (the customer can't receive WhatsApp messages), 131056 (too many messages to this customer in a short time), 132015 and 132016 (Meta paused or disabled the template it matched to this text).\n- 409 `not_enabled`: Meta hasn't enabled Direct Send for this WhatsApp Business Account. Ask Meta through `requestAccessUrl`; Spur asks Meta again after `nextCheckAt`.\n- 409 `blocked`: Meta has paused Direct Send for this account (code 139200, utility misuse) until `blockedUntil`.\n- 409 `no_prior_inbound`: the contact has never messaged this number.\n- 409 `opted_out`: the contact opted out.\n- 429 `rate_limited`: Meta is limiting utility messages from this number (code 131064) until `rateLimitedUntil`.\n- 503 `checking`: Spur is asking Meta whether Direct Send is enabled for this account; try again in a minute.\n- 503 `unavailable`: Spur couldn't read the number's Direct Send state; try again shortly.\n- 504 with `directSend.ambiguous: true` and no `unavailable`: Meta hasn't confirmed the message yet. It may still arrive, so don't resend straight away.\n- 500 with `directSend.accepted: true` and no `unavailable`: WhatsApp accepted the message but Spur couldn't save it. Don't send it again.\n- After a send, a message Meta held and then dropped fails on the status webhook with code 135000.\n","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DirectSendMessageDto"}}}},"responses":{"201":{"description":"Sent. `deliveryPath` is `direct_send` outside the window, `plain` inside it.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DirectSendMessageResponseDto"}}}},"400":{"description":"The text is empty or longer than 1,024 characters, the recipient is a group, the number is not on Cloud API or Onextel, or Meta refused the message (`directSend.errorCode` is Meta’s code).","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/ApiErrorEnvelopeDto"}]},"examples":{"unsupported_number":{"summary":"The number cannot use Direct Send","value":{"statusCode":400,"message":"Direct Send only works on WhatsApp Cloud API and Onextel numbers. Send an approved template instead.","error":"Direct Send unavailable","directSend":{"unavailable":"unsupported_number","reason":"UNSUPPORTED_PROVIDER"}}},"rejected":{"summary":"Meta refused this message","value":{"statusCode":400,"message":"WhatsApp didn't accept this message through Direct Send (Meta error 131026). This customer can't receive WhatsApp messages: the number may not be on WhatsApp, may not have accepted WhatsApp's latest terms, or uses a very old app.","error":"Direct Send unavailable","directSend":{"unavailable":"rejected","reason":"DIRECT_SEND_REJECTED","errorCode":131026,"status":"ENABLED","wabaId":"3019009511622567","phoneNumberId":"1172982672572085"}}}}}}},"403":{"description":"Spur has paused Direct Send for everyone (the platform-wide kill switch).","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/ApiErrorEnvelopeDto"}]},"example":{"statusCode":403,"message":"Direct Send is paused across Spur right now. Send an approved template instead.","error":"Direct Send unavailable","directSend":{"unavailable":"not_turned_on","reason":"KILL_SWITCH","status":"ENABLED","wabaId":"3019009511622567","phoneNumberId":"1172982672572085"}}}}},"409":{"description":"Meta has not enabled Direct Send for this WhatsApp Business Account, Meta has paused it, the contact has never messaged this number, or the contact opted out. `directSend` carries the number’s standing: the WABA id, the dates and Meta’s access form.","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/ApiErrorEnvelopeDto"}]},"examples":{"not_enabled":{"summary":"Meta has not enabled Direct Send","value":{"statusCode":409,"message":"Meta hasn't enabled Direct Send for this WhatsApp Business Account (WABA 3019009511622567). Meta generally requires a messaging limit of 100,000 or unlimited. Spur asks Meta again after 23 Oct 2026, 10:00 UTC. To request access, use Meta's form: https://docs.google.com/forms/…","error":"Direct Send unavailable","directSend":{"unavailable":"not_enabled","reason":"COOLDOWN","status":"NOT_ENABLED","nextCheckAt":"2026-10-23T10:00:00.000Z","wabaId":"3019009511622567","phoneNumberId":"1172982672572085","requestAccessUrl":"https://docs.google.com/forms/…"}}},"blocked":{"summary":"Meta has paused Direct Send","value":{"statusCode":409,"message":"Meta has paused Direct Send for this WhatsApp Business Account until 23 Oct 2026, 10:00 UTC, because messages sent through it were judged to be marketing, not utility.","error":"Direct Send unavailable","directSend":{"unavailable":"blocked","reason":"BLOCKED","status":"BLOCKED","blockedUntil":"2026-10-23T10:00:00.000Z","wabaId":"3019009511622567"}}},"no_prior_inbound":{"summary":"The contact has never messaged this number","value":{"statusCode":409,"message":"This contact has never messaged this WhatsApp number, so Spur doesn't send them automated Direct Send messages. Send an approved template instead.","error":"Direct Send unavailable","directSend":{"unavailable":"no_prior_inbound","status":"ENABLED","wabaId":"3019009511622567"}}},"opted_out":{"summary":"The contact opted out","value":{"statusCode":409,"message":"This contact has opted out, so Spur doesn't send them automated Direct Send messages.","error":"Direct Send unavailable","directSend":{"unavailable":"opted_out","status":"ENABLED","wabaId":"3019009511622567"}}}}}}},"429":{"description":"Meta is limiting utility messages from this number (code 131064).","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/ApiErrorEnvelopeDto"}]},"example":{"statusCode":429,"message":"Meta is limiting utility messages from this number until 17 Oct 2026, 09:00 UTC, because some messages sent as utility looked like marketing.","error":"Direct Send unavailable","directSend":{"unavailable":"rate_limited","reason":"RATE_LIMITED","rateLimitedUntil":"2026-10-17T09:00:00.000Z","wabaId":"3019009511622567"}}}}},"503":{"description":"Spur is checking with Meta whether Direct Send is enabled for this account, or could not check it. Try again in a minute."},"504":{"description":"Meta didn't confirm the message in time. It may still arrive, so don't send it again straight away."}},"tags":["Message"],"security":[{"bearerToken":[]}]}},"/send-message/bulk":{"post":{"operationId":"SendMessageController_bulkSendMessage","summary":"Send multiple messages to different contacts","description":"\nSend multiple messages in a single API call. Each message in the array follows the same format as the single send-message endpoint.\n\n## Important Notes\n- All the same rules from the single message endpoint apply here\n- **Specify options.from for each message** if you have multiple WhatsApp channels\n- **All template variables are mandatory** - use fallback values like \"User\" if data is missing\n- Messages are processed in sequence; failures in one message don't affect others\n        ","parameters":[],"requestBody":{"required":true,"description":"Array of messages to send. Each message follows the same format as the single send-message endpoint.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BulkSendMessageDto"},"examples":{"Bulk Template Messages":{"summary":"Send template messages to multiple contacts","description":"Each message should have options.from specified if you have multiple channels. All template variables must have values.","value":{"messages":[{"channel":"whatsapp","to":"910123456789","content":{"type":"template","template":{"name":"abandoned_cart_recovery_reminder","components":[{"type":"body","parameters":[{"text":"John","type":"text"}]}],"language":{"code":"en"}}},"options":{"from":"919876543210"}},{"channel":"whatsapp","to":"910987654321","content":{"type":"template","template":{"name":"abandoned_cart_recovery_reminder","components":[{"type":"body","parameters":[{"text":"User","type":"text"}]}],"language":{"code":"en"}}},"options":{"from":"919876543210"}}]}},"Bulk Text Messages":{"summary":"Send free-form text messages to multiple contacts","description":"Free-form messages can only be sent within 24 hours of the last customer message.","value":{"messages":[{"channel":"whatsapp","to":"910123456789","content":{"recipient_type":"individual","type":"text","text":{"preview_url":true,"body":"Hi John, test message"}},"options":{"from":"919876543210"}},{"channel":"whatsapp","to":"910987654321","content":{"recipient_type":"individual","type":"text","text":{"preview_url":true,"body":"Hi Jane, test message"}},"options":{"from":"919876543210"}}]}}}}}},"responses":{"201":{"description":""}},"tags":["Message"],"security":[{"bearerToken":[]}]}},"/embed/tokens":{"post":{"operationId":"EmbedTokensController_mint","summary":"Mint an embed token","description":"Call from your server with a workspace API key. Returns a single-use `embedUrl` (valid 60 seconds, only from `origin`) that opens one WhatsApp conversation in an iframe as the given Spur user, for an 8-hour session. Creates the contact from `contact.number` if needed. `channelId` (or `conversationId`) is required unless the workspace has exactly one WhatsApp number. Rate limit: 600 tokens per minute per workspace.","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateEmbedTokenDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/EmbedTokenResponseDto"}}}},"400":{"description":"Missing contact or conversation, missing channelId with more than one WhatsApp number, invalid number, or origin not on the allowed list"},"401":{"description":"Invalid or deleted API key"},"403":{"description":"The user's permissions don't cover the conversation"},"404":{"description":"User, contact, conversation or channel not found"},"409":{"description":"The user hasn't accepted their invite"},"429":{"description":"600 tokens per minute per workspace"}},"tags":["Embed"],"security":[{"bearerToken":[]}]}},"/v2/groups/eligibility":{"get":{"operationId":"WhatsAppGroupsController_getEligibility","summary":"Check WhatsApp Groups API eligibility","description":"Checks each fully connected WhatsApp number in the workspace against Meta's read-only groups endpoint. Results are cached per number. Error 131215 is ineligible; transient failures use a stale result when available.","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GroupEligibilityResponseDto"}}}}},"tags":["WhatsApp Groups"],"security":[{"bearerToken":[]}]}},"/v2/groups":{"post":{"operationId":"WhatsAppGroupsController_createGroup","summary":"Create a WhatsApp group (asynchronous)","description":"Starts group creation at Meta and returns the pending conversation in the `creating` state. The group is finalized asynchronously by the group_lifecycle_update webhook (or the polling reconciler), so poll `GET /v2/groups/{id}` until `lifecycleStatus` is `active` before inviting anyone or reading `inviteLink`. Note the shape changes: this response carries `lifecycleStatus` at the top level, while the GET nests it as `group.lifecycleStatus`. Reusing the path from this response makes a poll read nothing and time out on a group that is already active.\n\nSend an `idempotencyKey` to make the call repeat-safe: the first request creates the group, and any later request carrying the same key returns that same group instead of creating a second one. Without a key, two requests produce two groups, since WhatsApp itself offers no idempotency.\n\nA repeat returns the group exactly as it already is, so the `subject` and `description` sent with the repeat are ignored, not applied. Use `POST /v2/groups/{id}/settings` to change either after creation.","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateGroupBody"}}}},"responses":{"201":{"description":"The new group. When the `idempotencyKey` was already used, the group that key created, unchanged.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GroupConversationDto"}}}},"400":{"description":"Validation failure or a mapped Meta error (e.g. number not eligible)."},"403":{"description":"The workspace has reached its live-group cap (early access: 1 by default, raisable per workspace by Spur support; exempt workspaces are not subject to the cap at all). The body carries `code: \"GROUP_LIMIT_REACHED\"` and the effective `limit` so the dashboard can offer the \"contact support\" path instead of a generic error."},"404":{"description":"WhatsApp channel not found in this workspace."}},"tags":["WhatsApp Groups"],"security":[{"bearerToken":[]}]},"get":{"operationId":"WhatsAppGroupsController_listConversations","summary":"List WhatsApp group conversations","parameters":[{"name":"cursor","required":false,"in":"query","description":"Opaque cursor returned by the previous page.","schema":{"type":"string"}},{"name":"limit","required":false,"in":"query","description":"Page size from 1 to 100. Defaults to 50.","schema":{"type":"number"}},{"name":"subject","required":false,"in":"query","description":"Return only groups whose subject matches this exactly. Useful for checking whether a group you name yourself already exists.","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GroupConversationListResponseDto"}}}}},"tags":["WhatsApp Groups"],"security":[{"bearerToken":[]}]}},"/v2/groups/{id}":{"get":{"operationId":"WhatsAppGroupsController_getThread","summary":"Get a WhatsApp group thread with messages and participants","parameters":[{"name":"id","required":true,"in":"path","description":"Internal group conversation id.","schema":{"type":"number"}},{"name":"cursor","required":false,"in":"query","description":"Oldest group message id returned by the previous page.","schema":{"type":"number"}},{"name":"limit","required":false,"in":"query","description":"Page size from 1 to 100. Defaults to 50.","schema":{"type":"number"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GroupThreadResponseDto"}}}},"404":{"description":"Group conversation not found in this workspace."}},"tags":["WhatsApp Groups"],"security":[{"bearerToken":[]}]},"delete":{"operationId":"WhatsAppGroupsController_deleteGroup","summary":"Delete a WhatsApp group","description":"Deletes the group at Meta — all participants including the business are removed and WhatsApp fires group_lifecycle_update (group_delete). The conversation row is kept with `lifecycleStatus: deleted`: the thread stays in the inbox read-only and history is never destroyed. Idempotent for already-deleted groups.","parameters":[{"name":"id","required":true,"in":"path","description":"Internal group conversation id.","schema":{"type":"number"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GroupConversationDto"}}}},"400":{"description":"Group still being created, channel not connected, or a mapped Meta error."},"404":{"description":"Group conversation not found in this workspace."}},"tags":["WhatsApp Groups"],"security":[{"bearerToken":[]}]}},"/v2/groups/{id}/invites":{"post":{"operationId":"WhatsAppGroupsController_inviteParticipants","summary":"Invite people to a group by phone number or contact id","description":"Sends each person a 1:1 utility template message carrying the group invite link (Meta resolves the group id to the link at delivery). The template is auto-provisioned from Meta's Template Library on first use per channel. Joins arrive asynchronously on the group_participants_update webhook, so someone invited will not appear in `participants` until they accept.\n\nIdentify people by `phones` or by `contactIds`, but not both in the same request. Numbers must be in international format including the country code (`+919958135900`); bare local digits will not match a contact. Every number must already be a WhatsApp contact in this workspace. Nothing is sent unless all of them resolve, and the response names any that did not, so you can create those contacts with `POST /contacts` and invite again. Creating a contact raises `new_contact_created`, so any workflow listening for a new contact will run. The group must be `active`.\n\nEach result carries the `contactId` it applies to, plus the `phone` it came from when the request used `phones`, so a failure can be traced back to the number you sent. The number is echoed normalised, without the leading `+`, so match on digits rather than on the exact string you sent.","parameters":[{"name":"id","required":true,"in":"path","description":"Internal group conversation id.","schema":{"type":"number"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/InviteParticipantsBody"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GroupInviteResponseDto"}}}},"400":{"description":"Neither `phones` nor `contactIds` was provided, or both were; a value is not a valid phone number; a number or contact is not a WhatsApp contact in this workspace; the group is not active; or the selection exceeds the remaining participant slots."},"404":{"description":"Group conversation not found in this workspace."},"409":{"description":"The group-invite template is still pending WhatsApp approval."}},"tags":["WhatsApp Groups"],"security":[{"bearerToken":[]}]}},"/v2/groups/{id}/notes":{"post":{"operationId":"WhatsAppGroupsController_createNote","summary":"Add an internal note to a WhatsApp group thread","description":"Persists an agent-only note in the group timeline. The note is never sent to WhatsApp, so it remains available when the group messaging window is closed or the group itself cannot receive messages.","parameters":[{"name":"id","required":true,"in":"path","description":"Internal group conversation id.","schema":{"type":"number"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateGroupNoteBody"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GroupMessageDto"}}}},"400":{"description":"The note has neither text nor attachments."},"404":{"description":"Group conversation not found in this workspace."}},"tags":["WhatsApp Groups"],"security":[{"bearerToken":[]}]}},"/v2/groups/{id}/settings":{"post":{"operationId":"WhatsAppGroupsController_updateSettings","summary":"Update the subject and/or description of a WhatsApp group (asynchronous)","description":"Asks Meta to update the group settings. Meta only acknowledges the request; the authoritative per-field result arrives on the group_settings_update webhook, which persists successes. The response carries the current (pre-update) conversation — clients should update optimistically and reconcile on the webhook-driven groupSettingsUpdate socket event.","parameters":[{"name":"id","required":true,"in":"path","description":"Internal group conversation id.","schema":{"type":"number"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateGroupSettingsBody"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GroupConversationDto"}}}},"400":{"description":"Validation failure, group not active on WhatsApp, or a mapped Meta error."},"404":{"description":"Group conversation not found in this workspace."}},"tags":["WhatsApp Groups"],"security":[{"bearerToken":[]}]}},"/whatsapp-flows/{wabaId}/flows":{"get":{"operationId":"WhatsappFlowsController_getFlows","parameters":[{"name":"wabaId","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"object"}}}}},"tags":["Whatsapp Flows"],"security":[{"bearerToken":[]}]},"post":{"operationId":"WhatsappFlowsController_createFlow","parameters":[{"name":"wabaId","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateFlowDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"type":"object"}}}}},"tags":["Whatsapp Flows"],"security":[{"bearerToken":[]}]}},"/whatsapp-flows/{wabaId}/flows/migrate":{"post":{"operationId":"WhatsappFlowsController_migrateFlows","parameters":[{"name":"wabaId","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MigrateFlowsDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"type":"object"}}}}},"tags":["Whatsapp Flows"],"security":[{"bearerToken":[]}]}},"/whatsapp-flows/flows/{flowId}/assets":{"get":{"operationId":"WhatsappFlowsController_getFlowAssets","parameters":[{"name":"flowId","required":true,"in":"path","schema":{"type":"string"}},{"name":"wabaId","required":true,"in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"object"}}}}},"tags":["Whatsapp Flows"],"security":[{"bearerToken":[]}]},"post":{"operationId":"WhatsappFlowsController_updateFlowJson","parameters":[{"name":"flowId","required":true,"in":"path","schema":{"type":"string"}},{"name":"wabaId","required":true,"in":"query","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/FlowAssetDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"type":"object"}}}}},"tags":["Whatsapp Flows"],"security":[{"bearerToken":[]}]}},"/whatsapp-flows/flows/{flowId}/json":{"get":{"operationId":"WhatsappFlowsController_getFlowJson","parameters":[{"name":"flowId","required":true,"in":"path","schema":{"type":"string"}},{"name":"wabaId","required":true,"in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"object"}}}}},"tags":["Whatsapp Flows"],"security":[{"bearerToken":[]}]}},"/whatsapp-flows/flows/{flowId}":{"post":{"operationId":"WhatsappFlowsController_updateFlowMetadata","parameters":[{"name":"flowId","required":true,"in":"path","schema":{"type":"string"}},{"name":"wabaId","required":true,"in":"query","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateFlowDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"type":"object"}}}}},"tags":["Whatsapp Flows"],"security":[{"bearerToken":[]}]}},"/whatsapp-flows/flows/{flowId}/publish":{"post":{"operationId":"WhatsappFlowsController_publishFlow","parameters":[{"name":"flowId","required":true,"in":"path","schema":{"type":"string"}},{"name":"wabaId","required":true,"in":"query","schema":{"type":"string"}}],"responses":{"201":{"description":"","content":{"application/json":{"schema":{"type":"object"}}}}},"tags":["Whatsapp Flows"],"security":[{"bearerToken":[]}]}},"/whatsapp-flows/flows/{flowId}/deprecate":{"post":{"operationId":"WhatsappFlowsController_deprecateFlow","parameters":[{"name":"flowId","required":true,"in":"path","schema":{"type":"string"}},{"name":"wabaId","required":true,"in":"query","schema":{"type":"string"}}],"responses":{"201":{"description":"","content":{"application/json":{"schema":{"type":"object"}}}}},"tags":["Whatsapp Flows"],"security":[{"bearerToken":[]}]}},"/whatsapp-flows/flows/{flowId}/preview":{"get":{"operationId":"WhatsappFlowsController_getFlowPreview","parameters":[{"name":"flowId","required":true,"in":"path","schema":{"type":"string"}},{"name":"wabaId","required":true,"in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"object"}}}}},"tags":["Whatsapp Flows"],"security":[{"bearerToken":[]}]}},"/whatsapp-flows/send":{"post":{"operationId":"WhatsappFlowsController_sendFlow","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SendFlowDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"type":"object"}}}}},"tags":["Whatsapp Flows"],"security":[{"bearerToken":[]}]}}},"info":{"title":"Spur APIs","description":"APIs containing auth and many other things in spur.\n\n## Authentication\n\nThis API uses **Bearer Token Authentication**. To authenticate:\n\n1. Click the **Authorize** button at the top of this page\n2. Enter your workspace API key in the format: `Bearer <your-api-key>`\n3. Click **Authorize** to save\n\nAll subsequent requests will include it in the `Authorization` header:\n\n```\nAuthorization: Bearer <64-character key>\n```\n\nCreate and delete API keys under **Settings → API keys**. A logged-in user's\nJWT is accepted in the same header, but it expires and is tied to that person,\nso an automation should use a key.\n\nEndpoints marked with a 🔒 lock icon require authentication.\n","version":"0.1.0","contact":{}},"tags":[],"servers":[{"url":"https://api.spurnow.com","description":"Production"},{"url":"https://api.local.dev","description":"Development"}],"components":{"securitySchemes":{"bearerToken":{"scheme":"bearer","bearerFormat":"API key","description":"Enter your workspace API key, created in Settings under API keys. A logged-in user JWT is accepted in the same header.","type":"http"}},"schemas":{"UpdateContactNotesDto":{"type":"object","properties":{"notes":{"type":"string"}},"required":["notes"]},"ContactDto":{"type":"object","properties":{"id":{"type":"number"},"firstName":{"type":"string"},"lastName":{"type":"string"},"email":{"type":"string"},"number":{"type":"string"}},"required":["id","firstName","lastName","email","number"]},"ContactConversationDto":{"type":"object","properties":{"conversationId":{"type":"number","nullable":true,"example":49205067,"description":"The conversation (inboxId) on this number. Null when the contact has no conversation on it yet: mint a token with `contact.id` and `channelId` to start one."},"channelId":{"type":"number","example":1042,"description":"Which of your WhatsApp numbers this is."},"type":{"type":"string","example":"wa","enum":["wa"]},"number":{"type":"string","nullable":true,"example":"+911234567890","description":"Your business number, never the contact’s."},"name":{"type":"string","nullable":true,"example":"Acme Support","description":"The number’s display name."},"conversationStatus":{"type":"string","enum":["OPEN","CLOSED"],"nullable":true},"lastMessageAt":{"type":"string","nullable":true,"format":"date-time"},"hasMessages":{"type":"boolean"}},"required":["conversationId","channelId","type","number","name","conversationStatus","lastMessageAt","hasMessages"]},"ContactConversationsResponseDto":{"type":"object","properties":{"contactId":{"type":"number","example":9812},"conversations":{"description":"Newest first.","type":"array","items":{"$ref":"#/components/schemas/ContactConversationDto"}}},"required":["contactId","conversations"]},"ShopifyCustomerDto":{"type":"object","properties":{"shopifyCustomerId":{"format":"int64","type":"integer"},"adminUrl":{"type":"string"}},"required":["shopifyCustomerId","adminUrl"]},"TagDto":{"type":"object","properties":{"id":{"type":"number"},"title":{"type":"string"}},"required":["id","title"]},"InboxDto":{"type":"object","properties":{"id":{"type":"number"},"assignedAgentId":{"type":"number","nullable":true},"assignedTeamId":{"type":"number","nullable":true},"conversationStatus":{"type":"string","enum":["OPEN","CLOSED"]},"channelId":{"type":"number"},"automationEnabled":{"type":"boolean"}},"required":["id","assignedAgentId","assignedTeamId","conversationStatus","channelId","automationEnabled"]},"ContactByInboxDto":{"type":"object","properties":{"id":{"type":"number"},"createdAt":{"format":"date-time","type":"string"},"updatedAt":{"format":"date-time","type":"string"},"lastMessageAt":{"format":"date-time","type":"string","nullable":true},"state":{"type":"string"},"scopedId":{"type":"string"},"channel":{"type":"string","enum":["fb","ig","wa","lc","email"]},"optedIn":{"type":"boolean"},"name":{"type":"string","nullable":true},"firstName":{"type":"string","nullable":true},"lastName":{"type":"string","nullable":true},"username":{"type":"string","nullable":true},"number":{"type":"string","nullable":true},"email":{"type":"string","nullable":true},"profilePicUrl":{"type":"string","nullable":true},"isUserFollowBusiness":{"type":"boolean","nullable":true},"isBusinessFollowUser":{"type":"boolean","nullable":true},"followerCount":{"type":"number","nullable":true},"isVerifiedUser":{"type":"boolean","nullable":true},"defaultReplySent":{"type":"boolean"},"customFields":{"type":"object","nullable":true},"userProfileId":{"type":"number"},"platform":{"type":"string","enum":["SHOPIFY","WOO_COMMERCE","BIG_COMMERCE","MAGENTO2","PRESTASHOP","WIX","SQUARE","FYND","CUSTOM"]},"shopifyCustomerId":{"type":"number","nullable":true},"shopifyCustomer":{"nullable":true,"allOf":[{"$ref":"#/components/schemas/ShopifyCustomerDto"}]},"tags":{"type":"array","items":{"$ref":"#/components/schemas/TagDto"}},"inbox":{"$ref":"#/components/schemas/InboxDto"},"lastUserMessageAt":{"type":"string","nullable":true},"notes":{"type":"string","nullable":true}},"required":["id","createdAt","updatedAt","lastMessageAt","state","scopedId","channel","optedIn","name","firstName","lastName","username","number","email","profilePicUrl","isUserFollowBusiness","isBusinessFollowUser","followerCount","isVerifiedUser","defaultReplySent","customFields","userProfileId","platform","shopifyCustomerId","shopifyCustomer","tags","inbox","lastUserMessageAt","notes"]},"ProviderErrorDataDto":{"type":"object","properties":{"messaging_product":{"type":"string","description":"Messaging product returned by the provider, usually \"whatsapp\".","example":"whatsapp"},"details":{"type":"string","description":"Provider supplied details for the failure.","example":"Template parameter count does not match placeholders in body text."}}},"ProviderErrorDto":{"type":"object","properties":{"message":{"type":"string","description":"Provider error message.","example":"Invalid parameter"},"type":{"type":"string","description":"Provider error type.","example":"OAuthException"},"code":{"type":"number","description":"Provider primary error code.","example":100},"error_subcode":{"type":"number","description":"Provider subcode for granular troubleshooting.","example":2388124},"error_user_title":{"type":"string","description":"Provider user-facing error title.","example":"Message template can't be edited"},"error_user_msg":{"type":"string","description":"Provider user-facing error message.","example":"You can only edit an active template once in 24 hours."},"fbtrace_id":{"type":"string","description":"Provider trace identifier helpful for support escalation.","example":"Aa6LJRrhtxH_utcEI9YAQ7o"},"is_transient":{"type":"boolean","description":"Provider transient flag when available.","example":false},"error_data":{"description":"Nested provider error data.","allOf":[{"$ref":"#/components/schemas/ProviderErrorDataDto"}]}}},"ApiErrorEnvelopeDto":{"type":"object","properties":{"statusCode":{"type":"number","description":"HTTP status code for the error.","example":400},"message":{"type":"string","description":"Error message returned by the API.","example":"Request failed"},"error":{"oneOf":[{"type":"string"},{"$ref":"#/components/schemas/ProviderErrorDto"}],"description":"Original error body from service/controller. This can be a string or a provider error object."},"providerError":{"description":"Normalized provider error payload when available.","allOf":[{"$ref":"#/components/schemas/ProviderErrorDto"}]},"raw":{"type":"object","description":"Raw provider or upstream payload for advanced troubleshooting.","additionalProperties":true},"source":{"type":"string","description":"Who rejected the request. \"meta\" when the error body comes from the Meta Graph API (use providerError.fbtrace_id as the support reference); \"spur\" when Spur itself rejected the request, even if the body uses the provider error shape.","enum":["meta","spur"],"example":"meta"}},"required":["statusCode","message"]},"UpdateContactDto":{"type":"object","properties":{"firstName":{"type":"string","description":"Updated first name.","example":"Jane"},"lastName":{"type":"string","description":"Updated last name.","example":"Doe"},"email":{"type":"string","description":"Updated email.","example":"jane.doe@example.com"}}},"DuplicateContactErrorDto":{"type":"object","properties":{"statusCode":{"type":"number","description":"HTTP status code for the error.","example":400},"message":{"type":"string","description":"Error message returned by the API.","example":"Request failed"},"error":{"oneOf":[{"type":"string"},{"$ref":"#/components/schemas/ProviderErrorDto"}],"description":"Original error body from service/controller. This can be a string or a provider error object."},"providerError":{"description":"Normalized provider error payload when available.","allOf":[{"$ref":"#/components/schemas/ProviderErrorDto"}]},"raw":{"type":"object","description":"Raw provider or upstream payload for advanced troubleshooting.","additionalProperties":true},"source":{"type":"string","description":"Who rejected the request. \"meta\" when the error body comes from the Meta Graph API (use providerError.fbtrace_id as the support reference); \"spur\" when Spur itself rejected the request, even if the body uses the provider error shape.","enum":["meta","spur"],"example":"meta"},"existingContactId":{"type":"number","description":"ID of the existing WhatsApp contact in the current workspace.","example":42}},"required":["statusCode","message","existingContactId"]},"CreateContactDto":{"type":"object","properties":{"firstName":{"type":"string","description":"Contact first name.","example":"Jane"},"lastName":{"type":"string","description":"Contact last name.","example":"Doe"},"email":{"type":"string","description":"Contact email.","example":"jane.doe@example.com"},"number":{"type":"string","description":"Phone number with country code, digits only.","example":"919876543210"},"name":{"type":"string","description":"Optional full name override.","example":"Jane Doe"}},"required":["firstName","lastName","number"]},"TagIdDto":{"type":"object","properties":{"id":{"type":"number","description":"Tag ID"}},"required":["id"]},"TagNameDto":{"type":"object","properties":{"name":{"type":"string","description":"Tag name"}},"required":["name"]},"ContactTag":{"type":"object","properties":{"id":{"type":"number"},"title":{"type":"string"},"createdAt":{"format":"date-time","type":"string"},"updatedAt":{"format":"date-time","type":"string"}},"required":["id","title","createdAt","updatedAt"]},"AddCustomFieldDto":{"type":"object","properties":{"value":{"description":"Value can be any type of data","oneOf":[{"type":"string","nullable":true},{"type":"number","nullable":true},{"type":"string","format":"date-time","nullable":true}]},"name":{"type":"string"}},"required":["name"]},"DeleteCustomFieldDto":{"type":"object","properties":{"name":{"type":"string"}},"required":["name"]},"BulkUpdateCustomFieldsDto":{"type":"object","properties":{"contactId":{"type":"number","description":"Contact ID. Takes priority over phone and email if provided. You can find the Contact ID by searching for contacts using the GET /contacts endpoint.","example":12345},"phone":{"type":"string","description":"Phone number with country code, no spaces (e.g., \"919876543210\"). Used to look up the contact if contactId is not provided.","example":"919876543210"},"email":{"type":"string","description":"Email address. Used to look up the contact if contactId and phone are not provided.","example":"user@example.com"},"customFields":{"description":"Array of custom fields to update","type":"array","items":{"$ref":"#/components/schemas/AddCustomFieldDto"}}},"required":["customFields"]},"AddContactSuccessResponseDto":{"type":"object","properties":{"status":{"type":"string","example":"success"},"message":{"type":"string","example":"Contact added successfully"},"contactId":{"type":"number","example":9812,"description":"Present when a contact was created in this call."}},"required":["status","message"]},"AddContactExistsResponseDto":{"type":"object","properties":{"status":{"type":"string","example":"exists"},"message":{"type":"string","example":"Phone number/Email already exists for another contact"},"contactId":{"type":"number","example":9812,"description":"Existing contact id when duplicate is found."}},"required":["status","message"]},"AddContactFailedResponseDto":{"type":"object","properties":{"status":{"type":"string","example":"failed"},"message":{"type":"string","example":"Something went wrong"}},"required":["status","message"]},"AddContactInput":{"type":"object","properties":{"firstName":{"type":"string"},"lastName":{"type":"string"},"email":{"type":"string"},"number":{"type":"string"},"tags":{"type":"array","items":{"type":"number"}},"update":{"type":"boolean"},"id":{"type":"number"}},"required":["firstName","lastName","email","number","tags","update","id"]},"MConversationV2":{"type":"object","properties":{"id":{"oneOf":[{"type":"number"},{"type":"string"}],"description":"Numeric for native channels; `eml_<n>` string for email threads."},"content":{"type":"object","additionalProperties":true,"nullable":true,"description":"Last-message payload; channel-specific shape. Null for email threads."},"lastMessagePreview":{"type":"string","description":"Plaintext snippet of last message, max 150 chars"},"lastMessageType":{"type":"string","enum":["text","image","video","document","audio","voice","sticker","template","contacts","location","reaction","call","unknown"],"description":"Type of the last message"},"lastMessageSentByUs":{"type":"boolean","description":"Whether the last message was sent by us (true) or the customer (false)"},"lastMessageStatus":{"type":"string","enum":["sent","delivered","read"],"description":"Delivery status of the last message"},"lastMessageIsNote":{"type":"boolean","description":"Whether the last message is a note"},"lastMessageIsDeleted":{"type":"boolean","description":"Whether the last message was deleted (content cleared by the Meta webhook)."},"subject":{"type":"string","nullable":true,"description":"Email threads and WhatsApp groups only: the thread/group subject, shown as the list-row title."},"isGroup":{"type":"boolean","description":"True for WhatsApp group conversations (id is `grp_<id>`); absent for 1:1 rows."},"groupLifecycleStatus":{"type":"string","enum":["creating","active","suspended","deleted"],"description":"WhatsApp groups only: lifecycle state of the group. `creating` marks a pending group still waiting for Meta to confirm creation."},"assignedAgentId":{"type":"number","nullable":true,"description":"Id of the agent this conversation is assigned to, or null when unassigned. Surfaced so the Team view can group the queue by assignee."},"assignedTeamId":{"type":"number","nullable":true,"description":"Id of the team this conversation is assigned to, or null when unassigned."},"ticketId":{"type":"number"},"contactId":{"type":"number"},"automationEnabled":{"type":"boolean"},"name":{"type":"string"},"profilePicUrl":{"type":"string"},"username":{"type":"string","nullable":true},"channel":{"type":"string"},"sentDateTime":{"format":"date-time","type":"string"},"createdAt":{"format":"date-time","type":"string"},"unreadCount":{"type":"number"},"channelId":{"type":"number"}},"required":["id","lastMessagePreview","lastMessageType","lastMessageSentByUs","lastMessageStatus","lastMessageIsNote","automationEnabled","username","channel","createdAt"]},"TicketConversationV2Response":{"type":"object","properties":{"conversations":{"type":"array","items":{"$ref":"#/components/schemas/MConversationV2"}},"cursor":{"type":"string"},"lastId":{"type":"number"},"requestId":{"type":"string"},"hasMore":{"type":"boolean","description":"Whether more results are available"}},"required":["conversations","hasMore"]},"AssignConversationDto":{"type":"object","properties":{"to":{"type":"string","enum":["user","ai"],"description":"`user`: hand the conversation to a person; the AI agent stops on it. `ai`: clear the person assigned; the AI agent replies to the next inbound message."},"userId":{"type":"number","minimum":1,"example":318,"description":"Required with `to: \"user\"`. Must be an active member of the workspace."}},"required":["to"]},"AssignConversationResponseDto":{"type":"object","properties":{"conversationId":{"type":"number","example":49205067,"description":"The conversation (inboxId)."},"assignedUserId":{"type":"number","nullable":true,"example":318,"description":"The person assigned (assignedAgentId), or null."},"aiAgentActive":{"type":"boolean","description":"True when no person is assigned and the number has an AI agent connected: the AI agent answers the next inbound message."},"conversationStatus":{"type":"string","enum":["OPEN","CLOSED"]}},"required":["conversationId","assignedUserId","aiAgentActive","conversationStatus"]},"CreateDraftTemplateDto":{"type":"object","properties":{"name":{"type":"string"},"channelId":{"type":"number"},"category":{"type":"string","enum":["MARKETING","UTILITY","AUTHENTICATION"]},"format":{"type":"string","enum":["SINGLE","CAROUSEL"]},"language":{"type":"string"},"components":{"type":"object"},"status":{"type":"string"},"subCategory":{"type":"string","enum":["CUSTOM","LTO","CATALOG","MPM","PRODUCT_CAROUSEL"]}},"required":["name","channelId","category","format","language","components"]},"CreateDraftTemplateResponse":{"type":"object","properties":{"id":{"type":"number"}},"required":["id"]},"CreateDraftFromLibraryDto":{"type":"object","properties":{"name":{"type":"string"},"channelId":{"type":"number"},"category":{"type":"string","enum":["MARKETING","UTILITY","AUTHENTICATION"]},"format":{"type":"string","enum":["SINGLE","CAROUSEL"]},"language":{"type":"string"},"components":{"type":"object"},"subCategory":{"type":"string","enum":["CUSTOM","LTO","CATALOG","MPM","PRODUCT_CAROUSEL"]}},"required":["name","channelId","category","format","language","components"]},"Example":{"type":"object","properties":{"body_text":{"items":{"type":"array"},"type":"array"}},"required":["body_text"]},"Button":{"type":"object","properties":{"type":{"enum":["QUICK_REPLY","URL","CATALOG","MPM"],"type":"string"},"text":{"type":"string"},"url":{"type":"string"}},"required":["type","text"]},"Component":{"type":"object","properties":{"type":{"enum":["BODY","BUTTONS","FOOTER","HEADER","CAROUSEL","LIMITED_TIME_OFFER"],"type":"string"},"text":{"type":"string"},"example":{"$ref":"#/components/schemas/Example"},"buttons":{"type":"array","items":{"$ref":"#/components/schemas/Button"}},"format":{"enum":["DOCUMENT","IMAGE","TEXT","VIDEO","GIF","LOCATION"],"type":"string"}},"required":["type"]},"WhatsAppMessageTemplate":{"type":"object","properties":{"name":{"type":"string"},"components":{"type":"array","items":{"$ref":"#/components/schemas/Component"}},"language":{"enum":["en","en_US","es","id","pt_BR"],"type":"string"},"status":{"enum":["APPROVED","REJECTED","PENDING","PAUSED","DISABLED","IN_APPEAL"],"type":"string"},"rejected_reason":{"type":"string"},"quality_score":{"type":"object"},"category":{"type":"string"},"id":{"type":"string"},"last_updated_time":{"type":"string"},"source":{"type":"string"}},"required":["name","components","language","status","category","id"]},"SubmitTemplateForReviewDto":{"type":"object","properties":{"templateId":{"type":"number","description":"Draft template ID in Spur to submit for Meta review.","example":1234}},"required":["templateId"]},"MessageTemplateQualityScore":{"type":"object","properties":{"score":{"enum":["GREEN","YELLOW","RED","UNKNOWN"],"type":"string"}},"required":["score"]},"MessageTemplateEntity":{"type":"object","properties":{"name":{"type":"string"},"templateId":{"type":"string","nullable":true},"components":{"type":"object"},"category":{"type":"string","enum":["MARKETING","UTILITY","AUTHENTICATION"]},"subCategory":{"type":"string","nullable":true,"enum":["CUSTOM","LTO","CATALOG","MPM","PRODUCT_CAROUSEL"]},"status":{"type":"string","enum":["APPROVED","REJECTED","PENDING","DRAFT","PAUSED","DISABLED","IN_APPEAL"]},"format":{"type":"string","enum":["SINGLE","CAROUSEL"]},"language":{"type":"string"},"rejectedReason":{"type":"string","nullable":true},"qualityScore":{"nullable":true,"allOf":[{"$ref":"#/components/schemas/MessageTemplateQualityScore"}]},"lastUpdatedTime":{"format":"date-time","type":"string","nullable":true},"workspaceId":{"type":"number"},"createdAt":{"format":"date-time","type":"string"},"updatedAt":{"format":"date-time","type":"string"},"channelId":{"type":"number","nullable":true},"id":{"type":"number"}},"required":["name","templateId","components","category","subCategory","status","format","language","rejectedReason","qualityScore","lastUpdatedTime","workspaceId","createdAt","updatedAt","channelId","id"]},"MessageTemplateResponse":{"type":"object","properties":{"templates":{"type":"array","items":{"$ref":"#/components/schemas/MessageTemplateEntity"}}},"required":["templates"]},"DuplicateTemplateWithChangesDto":{"type":"object","properties":{"templateId":{"type":"number","description":"ID of the template to duplicate","example":123},"changes":{"type":"object","description":"Current template changes to apply to the duplicate"},"submitForReview":{"type":"boolean","default":true,"description":"Automatically submit template for review after duplication","example":true}},"required":["templateId"]},"MagicMessageConfigResponseDto":{"type":"object","properties":{"templateName":{"type":"string","nullable":true,"example":"support_ticket_update_spur"},"isConfigured":{"type":"boolean","example":true}},"required":["templateName","isConfigured"]},"ConfigureMagicMessageDto":{"type":"object","properties":{"channelId":{"type":"number","description":"WhatsApp channel ID","example":25},"templateName":{"type":"string","description":"Template name to use for magic messages","example":"support_ticket_update_spur"},"templateLanguage":{"type":"string","description":"Template language code (defaults to \"en\")","example":"en"}},"required":["channelId","templateName"]},"ConfigureMagicMessageResponseDto":{"type":"object","properties":{"success":{"type":"boolean","example":true},"message":{"type":"string","example":"Magic message template configured successfully"}},"required":["success","message"]},"DirectSendStatusDto":{"type":"object","properties":{"supported":{"type":"boolean","description":"Whether Spur tries Direct Send for this number at all: the provider must be Cloud API or Onextel and the platform switch on. An inbox reply also needs the workspace on the rollout list while one is set; the API and MCP skip the list (PRO-2453)."},"unsupportedReason":{"type":"string","nullable":true,"enum":["NO_WABA","PROVIDER","KILL_SWITCH","NOT_ALLOWLISTED"],"description":"Why `supported` is false; null when it is true."},"status":{"type":"string","enum":["UNKNOWN","ENABLED","NOT_ENABLED","BLOCKED"],"description":"What Meta last told us about this WhatsApp Business Account. UNKNOWN until the first outside-window text tries; Meta exposes no eligibility API."},"lastCheckedAt":{"type":"string","nullable":true,"description":"ISO time of the last Direct Send attempt for this WABA."},"nextCheckAt":{"type":"string","nullable":true,"description":"NOT_ENABLED only: ISO time after which the next outside-window text tries Direct Send again."},"blockedUntil":{"type":"string","nullable":true,"description":"BLOCKED only: ISO time Meta’s block is expected to lift."},"rateLimitedUntil":{"type":"string","nullable":true,"description":"Set while Meta rate-limits utility sends for this WABA (error 131064); replies go out as Magic Messages until then."},"wabaId":{"type":"string","nullable":true,"description":"The WABA id, for Meta’s access form."},"phoneNumberId":{"type":"string","nullable":true,"description":"The number’s Meta phone number id, to quote to Meta or support."},"requestAccessUrl":{"type":"string","description":"Meta’s Direct Send access request form, prefilled with the WABA id and business name."}},"required":["supported","unsupportedReason","status","lastCheckedAt","nextCheckAt","blockedUntil","rateLimitedUntil","wabaId","phoneNumberId","requestAccessUrl"]},"EnsureMagicMessageTemplateResponseDto":{"type":"object","properties":{"exists":{"type":"boolean","example":true},"approved":{"type":"boolean","example":true},"status":{"type":"string","enum":["APPROVED","PENDING","REJECTED","NOT_FOUND"],"example":"APPROVED"},"message":{"type":"string","example":"Template is approved and ready to use"},"templateName":{"type":"string","example":"support_ticket_update_spur"},"directSend":{"description":"How a reply outside the 24-hour window leaves this number: Meta Direct Send when the WABA is enabled, this template otherwise (PRO-2250).","allOf":[{"$ref":"#/components/schemas/DirectSendStatusDto"}]}},"required":["exists","approved","status","message"]},"InboxMessageReactionDto":{"type":"object","properties":{"actorType":{"type":"string","enum":["business","participant"]},"contactId":{"type":"number","nullable":true},"participantWaId":{"type":"string","nullable":true},"userId":{"type":"number","nullable":true},"reaction":{"type":"string"},"reactedAt":{"type":"string","format":"date-time"},"eventMessageId":{"type":"string"}},"required":["actorType","contactId","participantWaId","userId","reaction","reactedAt","eventMessageId"]},"InboxMessageDto":{"type":"object","properties":{"ticketId":{"type":"string"},"id":{"type":"string"},"content":{"type":"object"},"sentDateTime":{"format":"date-time","type":"string"},"senderId":{"type":"string"},"userId":{"type":"string"},"recipientId":{"type":"string"},"isDeleted":{"type":"boolean"},"type":{"type":"string"},"recordType":{"type":"string"},"name":{"type":"string"},"messageReadTime":{"format":"date-time","type":"string"},"deliveredTime":{"format":"date-time","type":"string"},"sentViaSpur":{"type":"boolean"},"metaMessageId":{"type":"string"},"reaction":{"type":"string","nullable":true,"description":"Current emoji reaction attached to this message."},"reactions":{"description":"Active participant and business reactions on this message.","type":"array","items":{"$ref":"#/components/schemas/InboxMessageReactionDto"}},"errorTimestamp":{"format":"date-time","type":"string"},"errorStatusCode":{"type":"string"},"aiAgentId":{"type":"number"},"workflowExecutionId":{"type":"string"},"broadcastId":{"type":"string"},"conversationOriginType":{"type":"string"},"replyContent":{"type":"object"},"replyToMessageId":{"type":"number","description":"DB id of the parent message this one replies to (WA/IG reply_to lookup). Used by the frontend to make the reply-quote clickable → jumps to the referenced bubble."}},"required":["id","content","recordType"]},"InboxMessagesV2Response":{"type":"object","properties":{"messages":{"type":"array","items":{"$ref":"#/components/schemas/InboxMessageDto"}},"hasMoreBefore":{"type":"boolean","description":"Whether more older messages are available above"},"hasMoreAfter":{"type":"boolean","description":"Whether more newer messages are available below"},"cursor":{"type":"string","description":"Cursor to load older messages. Use with cursor param."},"afterCursor":{"type":"string","description":"Cursor to load newer messages. Use with afterCursor param."}},"required":["messages","hasMoreBefore","hasMoreAfter"]},"CreateInboxNoteBody":{"type":"object","properties":{"text":{"type":"string","minLength":1,"example":"Draft reply: Hi! The template was rejected because…","maxLength":4096,"description":"Internal note text. Visible to agents only; never sent to the contact."}},"required":["text"]},"CreateInboxNoteResponseDto":{"type":"object","properties":{"id":{"type":"number","description":"Internal Message id of the note."},"inboxId":{"type":"number","description":"ContactInbox id the note was added to."},"text":{"type":"string"},"sentDateTime":{"type":"string","format":"date-time"}},"required":["id","inboxId","text","sentDateTime"]},"SearchSection":{"type":"object","properties":{"results":{"type":"array","items":{"type":"object"}},"total":{"type":"number"},"hasMore":{"type":"boolean"}},"required":["results","total","hasMore"]},"SearchResponse":{"type":"object","properties":{"contacts":{"$ref":"#/components/schemas/SearchSection"},"groups":{"$ref":"#/components/schemas/SearchSection"},"messages":{"$ref":"#/components/schemas/SearchSection"}}},"SendMessageResponseDto":{"type":"object","properties":{"id":{"type":"number","description":"Internal Spur message ID.","example":981273},"metaMessageId":{"type":"string","description":"Provider message id (Meta/Infobip) when available.","example":"wamid.HBgMOTE5ODc2NTQzMjEwFQIAERgSNkQ5MjhEMUE5QkQ0NTM0RkI1AA=="},"sentDateTime":{"format":"date-time","type":"string","description":"Time when message was sent from Spur.","example":"2026-02-14T10:20:30.000Z"},"content":{"type":"object","description":"Channel/content payload saved with the message.","additionalProperties":true},"deliveryPath":{"type":"string","enum":["plain","direct_send","magic_message","template"],"description":"How the message actually went out: `plain` (a normal message), `direct_send` (a Meta Direct Send utility message), `magic_message` (outside the 24-hour window, the text was sent as a Magic Message template) or `template`.","example":"plain"},"warning":{"type":"string","description":"Additional delivery warning for specific template categories.","example":"Marketing messages are not guaranteed delivery due to WhatsApp per-user marketing limits."},"deliveryBoost":{"type":"object","properties":{"enabled":{"required":true,"type":"boolean"},"maxRetries":{"required":true,"type":"number"},"retryWindowHours":{"required":true,"type":"number"},"message":{"required":true,"type":"string"}}}},"required":["id","deliveryBoost"]},"SendMessageOptionsDto":{"type":"object","properties":{"from":{"type":"string","description":"⚠️ IMPORTANT: Phone number to send from (with country code, no spaces). This field is CRITICAL when you have multiple WhatsApp channels connected. If not specified, the system will use the default channel which may not be the intended one. Always specify this to ensure messages are routed through the correct WhatsApp Business Account.","example":"919876543210"}}},"SendMessageDto":{"type":"object","properties":{"to":{"type":"string","description":"For WhatsApp: phone number with country code, no spaces (e.g., \"919876543210\"), or a BSUID (e.g., \"IN.abc123\"), or a WhatsApp group as \"group:<id>\" (e.g., \"group:123\") using the group id from GET /v2/groups. For Instagram & Facebook: the Spur contact ID.","example":"919876543210"},"channel":{"enum":["whatsapp","instagram","facebook"],"type":"string","description":"The messaging channel to use","example":"whatsapp"},"content":{"type":"object","description":"Message content following WhatsApp Cloud API format. See https://developers.facebook.com/docs/whatsapp/cloud-api/reference/messages for full specification."},"options":{"description":"Optional configuration for message sending. Use options.from to specify which WhatsApp channel to send from when you have multiple channels.","allOf":[{"$ref":"#/components/schemas/SendMessageOptionsDto"}]},"deliveryBoost":{"type":"boolean","description":"Enable DeliveryBoost for marketing messages. When true (default), if a marketing message fails due to WhatsApp per-user limits (error 131049), it will be retried up to 12 times over the next 72 hours. Set to false to opt out.","default":true,"example":true}},"required":["to","channel","content"]},"DirectSendMessageResponseDto":{"type":"object","properties":{"id":{"type":"number","description":"Internal Spur message ID.","example":981273},"metaMessageId":{"type":"string","description":"Provider message id (Meta/Infobip) when available.","example":"wamid.HBgMOTE5ODc2NTQzMjEwFQIAERgSNkQ5MjhEMUE5QkQ0NTM0RkI1AA=="},"sentDateTime":{"format":"date-time","type":"string","description":"Time when message was sent from Spur.","example":"2026-02-14T10:20:30.000Z"},"content":{"type":"object","description":"Channel/content payload saved with the message.","additionalProperties":true},"deliveryPath":{"type":"string","enum":["plain","direct_send","magic_message","template"],"description":"How the message actually went out: `plain` (a normal message), `direct_send` (a Meta Direct Send utility message), `magic_message` (outside the 24-hour window, the text was sent as a Magic Message template) or `template`.","example":"plain"},"warning":{"type":"string","description":"Additional delivery warning for specific template categories.","example":"Marketing messages are not guaranteed delivery due to WhatsApp per-user marketing limits."},"deliveryBoost":{"type":"object","properties":{"enabled":{"required":true,"type":"boolean"},"maxRetries":{"required":true,"type":"number"},"retryWindowHours":{"required":true,"type":"number"},"message":{"required":true,"type":"string"}}},"notice":{"type":"string","description":"Present when the message went out through Direct Send: delivery can take a few minutes, Meta may hold it for quality review (see `held`), so do not send it again just because it has not arrived yet.","example":"Meta accepted this Direct Send message. It is billed at Meta's utility rate, and delivery can take a few minutes. Don't send it again just because it hasn't arrived yet; Spur's message-delivery webhook reports when it is sent, delivered, read or failed."},"held":{"type":"boolean","description":"True when Meta answered `held_for_quality_assessment`: business portfolio pacing is holding the message. It may be delivered later or dropped (status webhook code 135000). Don't send it again.","example":false}},"required":["id","deliveryBoost"]},"DirectSendMessageDto":{"type":"object","properties":{"to":{"type":"string","description":"WhatsApp recipient: a phone number with country code and no spaces (e.g. \"919876543210\") or a BSUID (e.g. \"IN.abc123\"). WhatsApp groups are not supported.","example":"919876543210"},"text":{"type":"string","description":"The message, as plain text, up to 1,024 characters. Utility content only: a reply, or an update about an order, booking, account or support request. No marketing or promotions.","maxLength":1024,"example":"Hi Priya, your order #4512 shipped today and should reach you by Friday. Reply here if anything looks wrong."},"options":{"description":"Use options.from to pick the WhatsApp number to send from when more than one is connected.","allOf":[{"$ref":"#/components/schemas/SendMessageOptionsDto"}]}},"required":["to","text"]},"BulkSendMessageDto":{"type":"object","properties":{"messages":{"description":"Array of messages to send","type":"array","items":{"$ref":"#/components/schemas/SendMessageDto"}}},"required":["messages"]},"EmbedTokenUserRefDto":{"type":"object","properties":{"id":{"type":"number","minimum":1,"example":318,"description":"Spur user id. Send this or `email`."},"email":{"type":"string","example":"crm@acme.com","description":"Email of the Spur user the frame acts as. Send this or `id`."}}},"EmbedTokenContactRefDto":{"type":"object","properties":{"number":{"type":"string","maxLength":32,"example":"+919876543210","description":"Phone number in international format: + and the country code, e.g. +919876543210 (00919876543210 also works). If no contact exists, one is created. Send this or `id`."},"id":{"type":"number","minimum":1,"example":9812,"description":"Spur contact id from an earlier response. Send this or `number`."},"firstName":{"type":"string","maxLength":120,"example":"Rahul","description":"Only used when a contact is created."},"lastName":{"type":"string","maxLength":120,"example":"Sharma","description":"Only used when a contact is created."},"email":{"type":"string","example":"rahul@example.com","description":"Only used when a contact is created."}}},"CreateEmbedTokenDto":{"type":"object","properties":{"user":{"description":"The Spur user the frame acts as. Must be an active member of the workspace; the session inherits that user's role, inbox permissions and number masking.","allOf":[{"$ref":"#/components/schemas/EmbedTokenUserRefDto"}]},"contact":{"description":"The contact, by number or Spur id. One of `contact` or `conversationId` is required.","allOf":[{"$ref":"#/components/schemas/EmbedTokenContactRefDto"}]},"conversationId":{"type":"number","minimum":1,"example":49205067,"description":"Open this conversation directly. It already identifies the WhatsApp number."},"channelId":{"type":"number","minimum":1,"example":1042,"description":"Which of your WhatsApp numbers to open, with `contact`. Required when the workspace has more than one WhatsApp number."},"origin":{"type":"string","maxLength":255,"example":"https://crm.acme.com","description":"Origin of the page that will show the frame. Must be on the allowed list."},"codeChallenge":{"type":"string","pattern":"^[A-Za-z0-9_-]{43}$","example":"E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM","description":"BASE64URL(SHA-256(codeVerifier)), 43 characters (PKCE, S256). The CRM page makes a random `codeVerifier` for each frame and sends only this hash to your server; when the frame posts `spur:ready`, the page answers with `spur:verify` and the verifier. Without it the link opens nothing, so a leaked `embedUrl` is useless."},"ttl":{"type":"number","minimum":1,"maximum":60,"example":60,"description":"Token lifetime in seconds. Default and maximum 60. This is the token's life, not the session's."}},"required":["user","origin","codeChallenge"]},"EmbedTokenResponseDto":{"type":"object","properties":{"token":{"type":"string","example":"emb_…","description":"Single-use token. Already part of `embedUrl`."},"embedUrl":{"type":"string","example":"https://embed.spurnow.com/auth?token=emb_…","description":"Set this as the iframe src. Works once, within 60 seconds, from `origin`, and only with the code verifier."},"expiresAt":{"type":"string","format":"date-time","example":"2026-10-20T10:31:04Z"},"userId":{"type":"number","example":318},"contactId":{"type":"number","example":9812,"description":"Store this on your CRM record and mint with it next time."},"conversationId":{"type":"number","example":49205067,"description":"The conversation the frame opens (inboxId)."},"channelId":{"type":"number","example":1042,"description":"The WhatsApp number the conversation is on."},"sessionTtl":{"type":"number","example":28800,"description":"Session length in seconds once the frame loads."}},"required":["token","embedUrl","expiresAt","userId","contactId","conversationId","channelId","sessionTtl"]},"GroupChannelEligibilityDto":{"type":"object","properties":{"channelId":{"type":"number","description":"Connected WhatsApp conversation channel id."},"eligible":{"type":"boolean","description":"True only when Meta confirms this phone number holds Official Business Account status, which is what gates Groups API access."},"obaStatus":{"type":"string","description":"Status of an OBA application submitted through the API (e.g. NOT_STARTED, PENDING, APPROVED, REJECTED). Informational only — numbers granted OBA in WhatsApp Manager still report NOT_STARTED, so never use this to decide eligibility."}},"required":["channelId","eligible"]},"GroupEligibilityResponseDto":{"type":"object","properties":{"eligible":{"type":"boolean","description":"True when at least one connected WhatsApp number in the workspace is eligible."},"channels":{"type":"array","items":{"$ref":"#/components/schemas/GroupChannelEligibilityDto"}}},"required":["eligible","channels"]},"CreateGroupBody":{"type":"object","properties":{"channelId":{"type":"number","minimum":1,"description":"WhatsApp channel to create the group on."},"subject":{"type":"string","minLength":1,"maxLength":128,"description":"Group subject (trimmed, 1-128 characters)."},"description":{"type":"string","maxLength":2048,"description":"Optional group description (trimmed, up to 2048 characters)."},"idempotencyKey":{"type":"string","minLength":1,"maxLength":255,"description":"Your own key identifying what this group is for, unique per workspace. Repeating a request with the same key returns the group created the first time instead of creating a second one, which makes retries and duplicate form submissions safe. Use something stable and specific, such as `partner:acme@example.com`."}},"required":["channelId","subject"]},"GroupConversationDto":{"type":"object","properties":{"id":{"type":"number"},"metaGroupId":{"type":"string","nullable":true},"channelId":{"type":"number"},"requestId":{"type":"string","nullable":true},"subject":{"type":"string","nullable":true},"description":{"type":"string","nullable":true},"notes":{"type":"string","nullable":true,"description":"Internal group-level notes used as AI context. Never sent to WhatsApp."},"inviteLink":{"type":"string","nullable":true},"profilePictureUrl":{"type":"string","nullable":true,"description":"S3 URL of the group icon Meta accepted. Rendered by the UI; Meta stores its own copy for delivery to WhatsApp. Null if never set or if a subsequent update was rejected by Meta."},"lifecycleStatus":{"type":"string","enum":["creating","active","suspended","deleted"]},"conversationStatus":{"type":"string","enum":["OPEN","CLOSED"]},"assignedAgentId":{"type":"number","nullable":true},"assignedTeamId":{"type":"number","nullable":true},"lastMessageAt":{"type":"string","nullable":true,"format":"date-time"},"agentLastSeenAt":{"type":"string","nullable":true,"format":"date-time"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"}},"required":["id","metaGroupId","channelId","requestId","subject","description","notes","inviteLink","profilePictureUrl","lifecycleStatus","conversationStatus","assignedAgentId","assignedTeamId","lastMessageAt","agentLastSeenAt","createdAt","updatedAt"]},"GroupConversationListResponseDto":{"type":"object","properties":{"groups":{"type":"array","items":{"$ref":"#/components/schemas/GroupConversationDto"}},"hasMore":{"type":"boolean"},"cursor":{"type":"string","nullable":true}},"required":["groups","hasMore","cursor"]},"GroupMessageReactionDto":{"type":"object","properties":{"actorType":{"type":"string","enum":["business","participant"]},"contactId":{"type":"number","nullable":true},"participantWaId":{"type":"string","nullable":true},"userId":{"type":"number","nullable":true},"reaction":{"type":"string","description":"The active emoji reaction from this actor."},"reactedAt":{"type":"string","format":"date-time"},"eventMessageId":{"type":"string","description":"Meta message id for the reaction action."}},"required":["actorType","contactId","participantWaId","userId","reaction","reactedAt","eventMessageId"]},"GroupMessageDto":{"type":"object","properties":{"id":{"type":"number"},"direction":{"type":"string","enum":["inbound","outbound"]},"participantWaId":{"type":"string","nullable":true},"sendingUserId":{"type":"number","nullable":true},"aiAgentId":{"type":"number","nullable":true},"metaMessageId":{"type":"string","nullable":true},"content":{"type":"object","additionalProperties":true,"description":"Raw inbound Meta WhatsApp message object with its type-specific payload."},"errorCode":{"type":"number","nullable":true},"errorMessage":{"type":"string","nullable":true,"description":"Human-readable mapping of errorCode (group-specific 1312xx codes and common send failures). Null when the message has no error."},"errorTimestamp":{"type":"string","nullable":true,"format":"date-time"},"sentAt":{"type":"string","nullable":true,"format":"date-time"},"deliveredAt":{"type":"string","nullable":true,"format":"date-time"},"readAt":{"type":"string","nullable":true,"format":"date-time"},"pinnedAt":{"type":"string","nullable":true,"format":"date-time"},"pinnedUntil":{"type":"string","nullable":true,"format":"date-time"},"participantReceipts":{"type":"object","additionalProperties":true,"description":"Receipt timestamps keyed by participant wa_id: { \"<wa_id>\": { deliveredAt, readAt } }."},"reactions":{"description":"Active participant and business reactions. Removal tombstones are not exposed.","type":"array","items":{"$ref":"#/components/schemas/GroupMessageReactionDto"}},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"}},"required":["id","direction","participantWaId","sendingUserId","metaMessageId","content","errorCode","errorMessage","errorTimestamp","sentAt","deliveredAt","readAt","pinnedAt","pinnedUntil","participantReceipts","reactions","createdAt","updatedAt"]},"GroupParticipantContactDto":{"type":"object","properties":{"id":{"type":"number"},"name":{"type":"string","nullable":true},"number":{"type":"string","nullable":true},"profilePicUrl":{"type":"string","nullable":true}},"required":["id","name","number","profilePicUrl"]},"GroupParticipantDto":{"type":"object","properties":{"id":{"type":"number"},"waId":{"type":"string"},"contactId":{"type":"number","nullable":true},"contact":{"nullable":true,"allOf":[{"$ref":"#/components/schemas/GroupParticipantContactDto"}]},"joinedAt":{"type":"string","nullable":true,"format":"date-time"},"removedAt":{"type":"string","nullable":true,"format":"date-time"},"removedBy":{"type":"string","nullable":true,"enum":["business","participant"],"description":"Who initiated the removal: `business` (removed from Spur / by the business) or `participant` (left on their own). Null while the participant is active or when the initiator is unknown."}},"required":["id","waId","contactId","contact","joinedAt","removedAt","removedBy"]},"GroupTicketEventDto":{"type":"object","properties":{"id":{"type":"number"},"type":{"type":"string","enum":["OPEN","CLOSE","REOPEN","ASSIGNMENT"]},"data":{"type":"object","description":"Rendered event payload — `{ msg, agentMessage?, teamMessage?, agentId?, teamId? }` — the same shape TicketEvents rows carry for 1:1 threads. Rendered inline in the timeline."},"timestamp":{"type":"string","format":"date-time"},"authorId":{"type":"number","nullable":true}},"required":["id","type","data","timestamp","authorId"]},"GroupThreadResponseDto":{"type":"object","properties":{"group":{"$ref":"#/components/schemas/GroupConversationDto"},"lastInboundMessageAt":{"type":"string","nullable":true,"format":"date-time","description":"Latest inbound participant message timestamp. The WhatsApp group customer service window is measured from this value."},"messages":{"type":"array","items":{"$ref":"#/components/schemas/GroupMessageDto"}},"pinnedMessages":{"description":"Currently active pinned messages, ordered by parent-message chronology newest first.","type":"array","items":{"$ref":"#/components/schemas/GroupMessageDto"}},"participants":{"type":"array","items":{"$ref":"#/components/schemas/GroupParticipantDto"}},"ticketEvents":{"description":"All lifecycle events (open, close, reopen, assignment) recorded for this group, oldest → newest. Volume is intentionally low so pagination is not needed; new events after load arrive via the `groupAssignmentUpdate` socket event.","type":"array","items":{"$ref":"#/components/schemas/GroupTicketEventDto"}},"hasMoreBefore":{"type":"boolean"},"cursor":{"type":"number","nullable":true}},"required":["group","lastInboundMessageAt","messages","pinnedMessages","participants","ticketEvents","hasMoreBefore","cursor"]},"InviteParticipantsBody":{"type":"object","properties":{"contactIds":{"minItems":1,"maxItems":8,"description":"Existing WhatsApp contact ids to invite. Each receives a 1:1 utility template message carrying the group invite link. Provide this or `phones`.","type":"array","items":{"type":"number","minimum":1}},"phones":{"minItems":1,"maxItems":8,"description":"Phone numbers to invite, in international format including the country code. Each must already be a WhatsApp contact in this workspace; nothing is sent unless all of them resolve, and the response names any that did not, so you can create the contact and invite again. Provide this or `contactIds`, not both.","type":"array","items":{"type":"string"}}}},"GroupInviteResultDto":{"type":"object","properties":{"contactId":{"type":"number"},"phone":{"type":"string","description":"The number this result is for, echoed back when the request identified people by `phones`, so a failure can be matched to the number you sent."},"status":{"type":"string","enum":["sent","failed"]},"error":{"type":"string","description":"Human-readable failure reason when status is failed."}},"required":["contactId","status"]},"GroupInviteResponseDto":{"type":"object","properties":{"results":{"type":"array","items":{"$ref":"#/components/schemas/GroupInviteResultDto"}}},"required":["results"]},"GroupNoteMediaBody":{"type":"object","properties":{"type":{"type":"string","enum":["image","video","audio","document"]},"url":{"type":"string","description":"Public https URL of the note attachment."}},"required":["type","url"]},"CreateGroupNoteBody":{"type":"object","properties":{"text":{"type":"string","minLength":1,"maxLength":4096,"description":"Internal note text. Optional when the note contains at least one attachment."},"media":{"maxItems":10,"description":"Internal note attachments. These are stored in Spur and never sent to Meta.","type":"array","items":{"$ref":"#/components/schemas/GroupNoteMediaBody"}}}},"UpdateGroupSettingsBody":{"type":"object","properties":{"subject":{"type":"string","minLength":1,"maxLength":128,"description":"New group subject (trimmed, 1-128 characters)."},"description":{"type":"string","maxLength":2048,"description":"New group description (trimmed, up to 2048 characters). An empty string clears the description."}}},"CreateFlowDto":{"type":"object","properties":{"name":{"type":"string"},"categories":{"type":"array","items":{"type":"string","enum":["SIGN_UP","SIGN_IN","APPOINTMENT_BOOKING","LEAD_GENERATION","CONTACT_US","CUSTOMER_SUPPORT","SURVEY","OTHER"]}},"flow_json":{"type":"string"},"publish":{"type":"boolean"},"clone_flow_id":{"type":"string"},"endpoint_uri":{"type":"string"}},"required":["name","categories"]},"MigrateFlowsDto":{"type":"object","properties":{"source_waba_id":{"type":"string"},"source_flow_names":{"type":"array","items":{"type":"string"}}},"required":["source_waba_id"]},"UpdateFlowDto":{"type":"object","properties":{"name":{"type":"string"},"categories":{"type":"array","items":{"type":"string","enum":["SIGN_UP","SIGN_IN","APPOINTMENT_BOOKING","LEAD_GENERATION","CONTACT_US","CUSTOMER_SUPPORT","SURVEY","OTHER"]}},"endpoint_uri":{"type":"string"},"application_id":{"type":"string"}}},"FlowAssetDto":{"type":"object","properties":{"jsonEscaped":{"type":"string"}},"required":["jsonEscaped"]},"SendFlowDto":{"type":"object","properties":{"wabaId":{"type":"string"},"flowId":{"type":"string"},"to":{"type":"string"},"messageTemplate":{"type":"string"}},"required":["wabaId","flowId","to"]}}}}