# API reference Source: https://docs.kapso.ai/api/introduction Three APIs, one API key Kapso exposes three APIs. They are documented separately because they have different base URLs and different jobs, but a single project API key authenticates all of them. Send and read messages, templates, and media. Mirrors Meta's Graph API shapes, so existing Cloud API code ports over with a base URL change. ``` https://api.kapso.ai/meta/whatsapp/v24.0 ``` [Browse endpoints](/api/meta/whatsapp/messages/list-messages) Everything around the messages: onboarding customers, provisioning numbers, generating setup links, running broadcasts, and managing webhooks. ``` https://api.kapso.ai/platform/v1 ``` [Browse endpoints](/api/platform/v1/customers/list-customers) Trigger and inspect automation runs, read execution history, and deploy serverless functions. ``` https://api.kapso.ai/platform/v1 ``` Shares a base URL with the Platform API and is the same service. They are split here only to keep the reference navigable. [Browse endpoints](/api/platform/v1/functions/workflows/list-workflows) ## Authentication Send your project API key in the `X-API-Key` header: ```bash theme={null} curl https://api.kapso.ai/platform/v1/customers \ -H "X-API-Key: YOUR_API_KEY" ``` The WhatsApp API also accepts a bearer token, which is what lets Meta Cloud API code work unchanged: ```bash theme={null} curl https://api.kapso.ai/meta/whatsapp/v24.0/{phone_number_id}/messages \ -H "Authorization: Bearer YOUR_TOKEN" ``` ## Response shape Most endpoints return a `data` object: ```json theme={null} { "data": { "id": "..." } } ``` List endpoints also include a `meta` object with pagination state. ## Before you start * All requests are HTTPS. Plain HTTP is rejected * Requests and responses are JSON unless the endpoint handles media * [Rate limits](/api/rate-limits) apply per API key and depend on your plan # Block users Source: https://docs.kapso.ai/api/meta/whatsapp/block-users/block-users /api/meta/whatsapp/openapi-whatsapp.yaml post /{phone_number_id}/block_users Block one or more users for a given phone number. **Proxy endpoint**: Proxies directly to Meta Graph API. # List blocked users Source: https://docs.kapso.ai/api/meta/whatsapp/block-users/list-blocked-users /api/meta/whatsapp/openapi-whatsapp.yaml get /{phone_number_id}/block_users Retrieve the list of users blocked for a given phone number. **Proxy endpoint**: Proxies directly to Meta Graph API. # Unblock users Source: https://docs.kapso.ai/api/meta/whatsapp/block-users/unblock-users /api/meta/whatsapp/openapi-whatsapp.yaml delete /{phone_number_id}/block_users Unblock one or more previously blocked users for a given phone number. **Proxy endpoint**: Proxies directly to Meta Graph API. # Get business profile Source: https://docs.kapso.ai/api/meta/whatsapp/business-profile/get-business-profile /api/meta/whatsapp/openapi-whatsapp.yaml get /{phone_number_id}/whatsapp_business_profile Retrieve the WhatsApp Business profile information. WhatsApp users can view your business profile by clicking your business's name or number in a WhatsApp message thread. **Proxy endpoint**: Proxies directly to Meta Graph API. # Update business profile Source: https://docs.kapso.ai/api/meta/whatsapp/business-profile/update-business-profile /api/meta/whatsapp/openapi-whatsapp.yaml post /{phone_number_id}/whatsapp_business_profile Update WhatsApp business profile information. **Proxy endpoint**: Proxies directly to Meta Graph API /PHONE_NUMBER_ID/whatsapp_business_profile. Use this endpoint to update: - About text (1-139 characters, appears below profile image) - Business address and description - Contact email - Profile picture (via handle from resumable upload) - Business category (vertical) - Website links (max 2) **Restrictions**: - About text: 1-139 chars, rendered emojis supported, hyperlinks won't be clickable, no markdown - Address: max 256 characters - Description: max 512 characters - Email: max 128 characters, valid email format - Websites: max 2 URLs, max 256 chars each, must include http:// or https:// **Note**: Sandbox configurations are blocked (returns 403). # Get call permission state Source: https://docs.kapso.ai/api/meta/whatsapp/calls/get-call-permission-state /api/meta/whatsapp/openapi-whatsapp.yaml get /{phone_number_id}/call_permissions Get the call permission state for a business phone number with a specific WhatsApp user. **Proxy endpoint**: Proxies directly to Meta Graph API. Returns the current permission status and available actions with their limits. Permission can be: - **no_permission**: No calling permission granted - **temporary**: Temporary permission with expiration time Actions include: - **send_call_permission_request**: Send permission request message - **start_call**: Initiate a call Each action has time-based limits (e.g., max 2 permission requests per 24 hours). # List calls Source: https://docs.kapso.ai/api/meta/whatsapp/calls/list-calls /api/meta/whatsapp/openapi-whatsapp.yaml get /{phone_number_id}/calls Retrieve a paginated list of WhatsApp voice calls. **Kapso Extension**: This endpoint returns call records stored in Kapso's database, not Meta's API. Supports filtering by direction, status, and time range. Uses cursor-based pagination. # Perform call action Source: https://docs.kapso.ai/api/meta/whatsapp/calls/perform-call-action /api/meta/whatsapp/openapi-whatsapp.yaml post /{phone_number_id}/calls Perform various call actions via the WhatsApp Calling API. **Proxy endpoint**: Proxies directly to Meta Graph API. Supports the following actions: - **connect**: Initiate an outbound call to a WhatsApp user - **pre_accept**: Pre-establish WebRTC connection before accepting call - **accept**: Accept an inbound call from a WhatsApp user - **reject**: Reject an inbound call - **terminate**: End an active call # Get contact details Source: https://docs.kapso.ai/api/meta/whatsapp/contacts/get-contact-details /api/meta/whatsapp/openapi-whatsapp.yaml get /{phone_number_id}/contacts/{wa_id} Retrieve detailed information about a specific contact. # List contacts Source: https://docs.kapso.ai/api/meta/whatsapp/contacts/list-contacts /api/meta/whatsapp/openapi-whatsapp.yaml get /{phone_number_id}/contacts Retrieve a paginated list of WhatsApp contacts for your project. Supports filtering by WhatsApp ID, customer association, and more. # Get conversation details Source: https://docs.kapso.ai/api/meta/whatsapp/conversations/get-conversation-details /api/meta/whatsapp/openapi-whatsapp.yaml get /{phone_number_id}/conversations/{conversation_id} Retrieve detailed information about a specific conversation. ## Kapso Extensions The response includes: - Full contact information - Message statistics - Conversation metadata # List conversations Source: https://docs.kapso.ai/api/meta/whatsapp/conversations/list-conversations /api/meta/whatsapp/openapi-whatsapp.yaml get /{phone_number_id}/conversations Retrieve a paginated list of WhatsApp conversations for a phone number. Conversations are ordered by last activity (most recent first). Supports filtering by status, activity time range, and phone number. ## Kapso Extensions The response includes Kapso-specific conversation metadata: - Message counts (total and unread) - Associated contact information - Conversation status and timestamps # Create flow Source: https://docs.kapso.ai/api/meta/whatsapp/flows/create-flow /api/meta/whatsapp/openapi-whatsapp.yaml post /{business_account_id}/flows Create a new WhatsApp Flow. **Proxy endpoint**: Proxies directly to Meta Graph API. # Create flow (phone number scoped) Source: https://docs.kapso.ai/api/meta/whatsapp/flows/create-flow-phone-number-scoped /api/meta/whatsapp/openapi-whatsapp.yaml post /{phone_number_id}/flows Create a new WhatsApp Flow for a phone number. **Proxy endpoint**: Proxies directly to Meta Graph API. **Note**: Requires WhatsappConfig with matching phone_number_id. # Delete flow Source: https://docs.kapso.ai/api/meta/whatsapp/flows/delete-flow /api/meta/whatsapp/openapi-whatsapp.yaml delete /flows/{flow_id} Delete a draft flow. This action is not reversible. **Only DRAFT flows can be deleted.** Published flows cannot be deleted but can be deprecated. **Proxy endpoint**: Proxies directly to Meta Graph API. **Note**: This endpoint uses the `/flows/` prefix. It is an alias provided for better developer experience. # Deprecate flow Source: https://docs.kapso.ai/api/meta/whatsapp/flows/deprecate-flow /api/meta/whatsapp/openapi-whatsapp.yaml post /{flow_id}/deprecate Deprecate a published flow. **Proxy endpoint**: Proxies directly to Meta Graph API. # Get flow assets Source: https://docs.kapso.ai/api/meta/whatsapp/flows/get-flow-assets /api/meta/whatsapp/openapi-whatsapp.yaml get /{flow_id}/assets Get flow JSON assets and URLs. **Proxy endpoint**: Proxies directly to Meta Graph API. # Get flow details Source: https://docs.kapso.ai/api/meta/whatsapp/flows/get-flow-details /api/meta/whatsapp/openapi-whatsapp.yaml get /{flow_id} Retrieve detailed information about a specific flow. By default returns: id, name, status, categories, validation_errors. Use `fields` parameter to request additional information like preview URLs, health status, metrics, etc. **Proxy endpoint**: Proxies directly to Meta Graph API. # List flows Source: https://docs.kapso.ai/api/meta/whatsapp/flows/list-flows /api/meta/whatsapp/openapi-whatsapp.yaml get /{business_account_id}/flows List all WhatsApp Flows for a business account. **Proxy endpoint**: Proxies directly to Meta Graph API. # List flows (phone number scoped) Source: https://docs.kapso.ai/api/meta/whatsapp/flows/list-flows-phone-number-scoped /api/meta/whatsapp/openapi-whatsapp.yaml get /{phone_number_id}/flows List all WhatsApp Flows for a phone number. **Proxy endpoint**: Proxies directly to Meta Graph API. **Note**: Requires WhatsappConfig with matching phone_number_id. # Publish flow Source: https://docs.kapso.ai/api/meta/whatsapp/flows/publish-flow /api/meta/whatsapp/openapi-whatsapp.yaml post /{flow_id}/publish Publish a flow. This action is not reversible. Once published, the flow and its assets become immutable. **Proxy endpoint**: Proxies directly to Meta Graph API. # Update flow metadata Source: https://docs.kapso.ai/api/meta/whatsapp/flows/update-flow-metadata /api/meta/whatsapp/openapi-whatsapp.yaml post /{flow_id} Update flow name, categories, endpoint_uri, or application_id. **Proxy endpoint**: Proxies directly to Meta Graph API. # Upload flow JSON Source: https://docs.kapso.ai/api/meta/whatsapp/flows/upload-flow-json /api/meta/whatsapp/openapi-whatsapp.yaml post /{flow_id}/assets Upload or update flow JSON definition. The file must be attached as multipart/form-data. Returns validation errors in the Flow JSON, if any. **Proxy endpoint**: Proxies directly to Meta Graph API. # Delete media Source: https://docs.kapso.ai/api/meta/whatsapp/media/delete-media /api/meta/whatsapp/openapi-whatsapp.yaml delete /{media_id} Delete a media file from WhatsApp. You can optionally provide `phone_number_id` query parameter to verify the media belongs to that phone number before deletion. # Download media file Source: https://docs.kapso.ai/api/meta/whatsapp/media/download-media-file /api/meta/whatsapp/openapi-whatsapp.yaml get /media_download Download a media file using a short-lived authenticated token. Tokens are returned in the `download_url` field of the [Get media URL](#operation/getMediaUrl) response. They expire 4 minutes after issue. No `X-API-Key` header is needed — authentication is embedded in the token. # Get media URL Source: https://docs.kapso.ai/api/meta/whatsapp/media/get-media-url /api/meta/whatsapp/openapi-whatsapp.yaml get /{media_id} Retrieve the download URL for a media file. **Important:** The returned URL is temporary and expires after 5 minutes. # Upload media Source: https://docs.kapso.ai/api/meta/whatsapp/media/upload-media /api/meta/whatsapp/openapi-whatsapp.yaml post /{phone_number_id}/media Upload media files to WhatsApp. The media ID returned can be used when sending messages. **Supported formats and size limits:** **Images** (jpeg, png) - Max size: 5MB **Videos** (mp4, 3gp) - Max size: 16MB **Audio** (aac, mp3, ogg, opus) - Max size: 16MB **Documents** (pdf, doc, docx, ppt, pptx, xls, xlsx) - Max size: 100MB **Stickers** (webp) - Static: max 100KB - Animated: max 500KB # Get message by ID Source: https://docs.kapso.ai/api/meta/whatsapp/messages/get-message-by-id /api/meta/whatsapp/openapi-whatsapp.yaml get /{phone_number_id}/messages/{message_id} Retrieve a single WhatsApp message by its message ID. Returns the message with all its fields, including Kapso extensions like status, direction, and processing state. # List messages Source: https://docs.kapso.ai/api/meta/whatsapp/messages/list-messages /api/meta/whatsapp/openapi-whatsapp.yaml get /{phone_number_id}/messages Retrieve a paginated list of WhatsApp messages for a phone number. Supports filtering by conversation, direction, status, and time range. Uses cursor-based pagination for efficient scrolling through large result sets. # Send a marketing message Source: https://docs.kapso.ai/api/meta/whatsapp/messages/send-a-marketing-message /api/meta/whatsapp/openapi-whatsapp.yaml post /{phone_number_id}/marketing_messages Send a WhatsApp marketing template message. Use `to` for phone numbers. Use `recipient` for a BSUID or parent BSUID. If both are present, the phone number in `to` takes precedence. # Send a message Source: https://docs.kapso.ai/api/meta/whatsapp/messages/send-a-message /api/meta/whatsapp/openapi-whatsapp.yaml post /{phone_number_id}/messages Send a WhatsApp message to a recipient. Use `to` for phone numbers. Use `recipient` for a BSUID or parent BSUID. If both are present, the phone number in `to` takes precedence. Supports all WhatsApp message types: - **text**: Plain text messages with optional URL preview - **image**: Images with optional caption - **video**: Videos with optional caption - **audio**: Audio files - **document**: Documents with optional caption and filename - **sticker**: Stickers - **location**: Location sharing - **contacts**: Contact cards - **interactive**: Interactive messages (buttons, lists, flows) - **template**: Message templates - **reaction**: Emoji reactions to messages # Get phone number details Source: https://docs.kapso.ai/api/meta/whatsapp/phone-numbers/get-phone-number-details /api/meta/whatsapp/openapi-whatsapp.yaml get /{phone_number_id} Retrieve detailed information about a specific phone number. By default returns basic information. Use `fields` parameter to request additional data like throughput limits, account mode, certificate status, etc. **Proxy endpoint**: Proxies directly to Meta Graph API. # List phone numbers Source: https://docs.kapso.ai/api/meta/whatsapp/phone-numbers/list-phone-numbers /api/meta/whatsapp/openapi-whatsapp.yaml get /{business_account_id}/phone_numbers List all phone numbers associated with a business account. Returns basic phone number information including verification status and quality rating. **Proxy endpoint**: Proxies directly to Meta Graph API. # Update phone number settings Source: https://docs.kapso.ai/api/meta/whatsapp/phone-numbers/update-phone-number-settings /api/meta/whatsapp/openapi-whatsapp.yaml post /{phone_number_id} Update phone number settings. Common use case is updating the two-step verification PIN. **Proxy endpoint**: Proxies directly to Meta Graph API. **Note**: Two-step verification is required for WhatsApp Business API. The PIN must be 6 digits. # Create or update message template Source: https://docs.kapso.ai/api/meta/whatsapp/templates/create-or-update-message-template /api/meta/whatsapp/openapi-whatsapp.yaml post /{business_account_id}/message_templates Create a new WhatsApp message template, or update an existing one. - Omit `hsm_id` to create a new template. - Include `hsm_id` as a query parameter to update an existing template. Templates must be approved by WhatsApp before they can be used. After creation, templates enter a PENDING state until reviewed. **Side effect**: Enqueues a template sync job on success to update Kapso's local template cache. # Delete message template Source: https://docs.kapso.ai/api/meta/whatsapp/templates/delete-message-template /api/meta/whatsapp/openapi-whatsapp.yaml delete /{business_account_id}/message_templates Delete a WhatsApp message template. **Specify either `name` or `hsm_id` to identify the template to delete.** # Get message template by ID Source: https://docs.kapso.ai/api/meta/whatsapp/templates/get-message-template-by-id /api/meta/whatsapp/openapi-whatsapp.yaml get /{business_account_id}/message_templates/{template_id} Retrieve a single WhatsApp message template by its ID. Use this endpoint to fetch full details of a specific template when you already know its ID. For listing templates or searching by name, use the list endpoint instead. # List message templates Source: https://docs.kapso.ai/api/meta/whatsapp/templates/list-message-templates /api/meta/whatsapp/openapi-whatsapp.yaml get /{business_account_id}/message_templates Retrieve a list of approved message templates for this phone number. Templates must be approved by WhatsApp before they can be used. # Claim or change username Source: https://docs.kapso.ai/api/meta/whatsapp/usernames/claim-or-change-username /api/meta/whatsapp/openapi-whatsapp.yaml post /{phone_number_id}/username # Delete username Source: https://docs.kapso.ai/api/meta/whatsapp/usernames/delete-username /api/meta/whatsapp/openapi-whatsapp.yaml delete /{phone_number_id}/username # Get current username Source: https://docs.kapso.ai/api/meta/whatsapp/usernames/get-current-username /api/meta/whatsapp/openapi-whatsapp.yaml get /{phone_number_id}/username # Get reserved username suggestions Source: https://docs.kapso.ai/api/meta/whatsapp/usernames/get-reserved-username-suggestions /api/meta/whatsapp/openapi-whatsapp.yaml get /{phone_number_id}/username_suggestions # List API logs Source: https://docs.kapso.ai/api/platform/v1/api-logs/list-api-logs /api/platform/v1/openapi-platform.yaml get /api_logs Returns logs of external API calls made by your project, most recent first. # Add recipients Source: https://docs.kapso.ai/api/platform/v1/broadcasts/add-recipients /api/platform/v1/openapi-platform.yaml post /whatsapp/broadcasts/{broadcast_id}/recipients Add up to 1000 recipients to a draft broadcast. Duplicates are skipped. Recipients use Meta's component syntax with body, header, and button components. # Cancel scheduled broadcast Source: https://docs.kapso.ai/api/platform/v1/broadcasts/cancel-scheduled-broadcast /api/platform/v1/openapi-platform.yaml post /whatsapp/broadcasts/{broadcast_id}/cancel Cancel a scheduled broadcast and return it to draft status. Only works for broadcasts in scheduled status. # Clear recipients Source: https://docs.kapso.ai/api/platform/v1/broadcasts/clear-recipients /api/platform/v1/openapi-platform.yaml delete /whatsapp/broadcasts/{broadcast_id}/recipients Remove all recipients from a draft or scheduled broadcast. Clearing recipients from a scheduled broadcast also returns it to draft and clears `scheduled_at`. # Create broadcast Source: https://docs.kapso.ai/api/platform/v1/broadcasts/create-broadcast /api/platform/v1/openapi-platform.yaml post /whatsapp/broadcasts Create a broadcast campaign in draft mode. Workflow: create broadcast → add recipients → send. Broadcasts stay in draft until you call the send endpoint. # Get broadcast Source: https://docs.kapso.ai/api/platform/v1/broadcasts/get-broadcast /api/platform/v1/openapi-platform.yaml get /whatsapp/broadcasts/{broadcast_id} # List broadcasts Source: https://docs.kapso.ai/api/platform/v1/broadcasts/list-broadcasts /api/platform/v1/openapi-platform.yaml get /whatsapp/broadcasts Get broadcast campaigns, most recent first. # List recipients Source: https://docs.kapso.ai/api/platform/v1/broadcasts/list-recipients /api/platform/v1/openapi-platform.yaml get /whatsapp/broadcasts/{broadcast_id}/recipients Get recipients for this broadcast with delivery status. # Schedule broadcast Source: https://docs.kapso.ai/api/platform/v1/broadcasts/schedule-broadcast /api/platform/v1/openapi-platform.yaml post /whatsapp/broadcasts/{broadcast_id}/schedule Schedule a broadcast to send at a future time. The broadcast must be in draft status and have recipients. # Send broadcast Source: https://docs.kapso.ai/api/platform/v1/broadcasts/send-broadcast /api/platform/v1/openapi-platform.yaml post /whatsapp/broadcasts/{broadcast_id}/send Start sending messages immediately. This is asynchronous - use GET /broadcasts/{id} to monitor progress. # Update broadcast status Source: https://docs.kapso.ai/api/platform/v1/broadcasts/update-broadcast-status /api/platform/v1/openapi-platform.yaml patch /whatsapp/broadcasts/{broadcast_id} Update a broadcast status. Set `status` to `stopped` to stop a broadcast that is currently sending. Pending recipients remain pending and no new sends are started. Set `status` to `draft` to cancel a scheduled broadcast and clear its schedule. # Create contact Source: https://docs.kapso.ai/api/platform/v1/contacts/create-contact /api/platform/v1/openapi-platform.yaml post /whatsapp/contacts Create a new WhatsApp contact. # Erase contact Source: https://docs.kapso.ai/api/platform/v1/contacts/erase-contact /api/platform/v1/openapi-platform.yaml delete /whatsapp/contacts/{identifier} Permanently erase a WhatsApp contact and all associated data (conversations, messages, media). The erasure is processed asynchronously. A `204 No Content` response confirms the erasure job was queued. The `identifier` can be the contact UUID, the WhatsApp phone number (E.164 format), or a business-scoped user ID. # Get contact Source: https://docs.kapso.ai/api/platform/v1/contacts/get-contact /api/platform/v1/openapi-platform.yaml get /whatsapp/contacts/{identifier} Retrieve a WhatsApp contact by UUID, phone number, or business-scoped user ID. # Get marketing preference Source: https://docs.kapso.ai/api/platform/v1/contacts/get-marketing-preference /api/platform/v1/openapi-platform.yaml get /whatsapp/contacts/{identifier}/marketing_preferences/{phone_number_id} Get the contact's marketing message preference on one WhatsApp number. A 404 with `"Marketing preference not found"` means the contact has no recorded preference on that number — subscribed. Check the error string before treating a 404 as subscribed: a mistyped identifier returns `"WhatsApp contact not found"` and an unknown number returns `"WhatsApp configuration not found"`. # List contacts Source: https://docs.kapso.ai/api/platform/v1/contacts/list-contacts /api/platform/v1/openapi-platform.yaml get /whatsapp/contacts Retrieve a paginated list of WhatsApp contacts for your project. # List marketing preferences Source: https://docs.kapso.ai/api/platform/v1/contacts/list-marketing-preferences /api/platform/v1/openapi-platform.yaml get /whatsapp/contacts/{identifier}/marketing_preferences List the contact's marketing message preference on each of your WhatsApp numbers. One entry per number the contact has stopped or resumed marketing on; an empty list means the contact never changed their preference and is subscribed everywhere. Preferences are read-only. Only the contact can change them, inside WhatsApp. While a preference is `stopped`, marketing template sends to the contact on that number are refused with error code `marketing_preference_stopped`. # Update contact Source: https://docs.kapso.ai/api/platform/v1/contacts/update-contact /api/platform/v1/openapi-platform.yaml patch /whatsapp/contacts/{identifier} Update a WhatsApp contact's profile or metadata. # Create conversation assignment Source: https://docs.kapso.ai/api/platform/v1/conversations/create-conversation-assignment /api/platform/v1/openapi-platform.yaml post /whatsapp/conversations/{conversation_id}/assignments Assign a conversation to a team member. Only one active assignment is allowed per conversation. The user must be a member of the project. # Get conversation Source: https://docs.kapso.ai/api/platform/v1/conversations/get-conversation /api/platform/v1/openapi-platform.yaml get /whatsapp/conversations/{conversation_id} Retrieve a single conversation with metadata. # Get conversation assignment Source: https://docs.kapso.ai/api/platform/v1/conversations/get-conversation-assignment /api/platform/v1/openapi-platform.yaml get /whatsapp/conversations/{conversation_id}/assignments/{id} Retrieve a specific assignment by ID. # List conversation assignments Source: https://docs.kapso.ai/api/platform/v1/conversations/list-conversation-assignments /api/platform/v1/openapi-platform.yaml get /whatsapp/conversations/{conversation_id}/assignments Get all assignments for a conversation, most recent first. # List conversations Source: https://docs.kapso.ai/api/platform/v1/conversations/list-conversations /api/platform/v1/openapi-platform.yaml get /whatsapp/conversations Query WhatsApp conversations with filters. Results are returned by latest activity first and use cursor pagination. # Update conversation assignment Source: https://docs.kapso.ai/api/platform/v1/conversations/update-conversation-assignment /api/platform/v1/openapi-platform.yaml patch /whatsapp/conversations/{conversation_id}/assignments/{id} Update an assignment's notes, reassign to another user, or deactivate (unassign). Set `active: false` to unassign without deleting the assignment record. # Update conversation status Source: https://docs.kapso.ai/api/platform/v1/conversations/update-conversation-status /api/platform/v1/openapi-platform.yaml patch /whatsapp/conversations/{conversation_id} Close completed conversations or reopen them for follow-ups. # Create customer Source: https://docs.kapso.ai/api/platform/v1/customers/create-customer /api/platform/v1/openapi-platform.yaml post /customers # Delete customer Source: https://docs.kapso.ai/api/platform/v1/customers/delete-customer /api/platform/v1/openapi-platform.yaml delete /customers/{customer_id} # Get customer Source: https://docs.kapso.ai/api/platform/v1/customers/get-customer /api/platform/v1/openapi-platform.yaml get /customers/{customer_id} # List customers Source: https://docs.kapso.ai/api/platform/v1/customers/list-customers /api/platform/v1/openapi-platform.yaml get /customers Returns customers in your project, most recent first. # Update customer Source: https://docs.kapso.ai/api/platform/v1/customers/update-customer /api/platform/v1/openapi-platform.yaml patch /customers/{customer_id} # List display name requests Source: https://docs.kapso.ai/api/platform/v1/display-names/list-display-name-requests /api/platform/v1/openapi-platform.yaml get /whatsapp/phone_numbers/{phone_number_id}/display_name_requests View all display name change requests for this number, most recent first. # Retrieve display name request Source: https://docs.kapso.ai/api/platform/v1/display-names/retrieve-display-name-request /api/platform/v1/openapi-platform.yaml get /whatsapp/phone_numbers/{phone_number_id}/display_name_requests/{request_id} Check status of a display name change request. Poll this endpoint to monitor Meta's review progress. # Submit display name request Source: https://docs.kapso.ai/api/platform/v1/display-names/submit-display-name-request /api/platform/v1/openapi-platform.yaml post /whatsapp/phone_numbers/{phone_number_id}/display_name_requests Request a display name change. Meta reviews most changes within 24-48 hours. Some names may be approved instantly. # Create or update a project event definition Source: https://docs.kapso.ai/api/platform/v1/events/create-or-update-a-project-event-definition /api/platform/v1/openapi-platform.yaml post /event-definitions Creates a definition for an event type. If a definition with the same name already exists, Kapso updates its editable metadata and returns it. If the existing definition is archived, this request restores it. Use `/events` when you want to emit an actual event occurrence. # Delete a project event definition and its events Source: https://docs.kapso.ai/api/platform/v1/events/delete-a-project-event-definition-and-its-events /api/platform/v1/openapi-platform.yaml delete /event-definitions/{id} Permanently deletes the event definition and all recorded event emissions associated with it. This action cannot be undone. The endpoint returns `202 Accepted` while a background job performs the deletion; new events with the same name are rejected while deletion is pending. To retain historical events while hiding the definition from new tools and workflow selectors, archive it with the PATCH endpoint instead. # Emit a project event Source: https://docs.kapso.ai/api/platform/v1/events/emit-a-project-event /api/platform/v1/openapi-platform.yaml post /events Stores one timestamped project event. Event names must be lowercase and may contain optional dot-separated segments; each segment starts with a lowercase letter and may contain lowercase letters, numbers, and underscores. `conversation_id` is optional; include it when the event belongs to a WhatsApp conversation. # List project event definitions Source: https://docs.kapso.ai/api/platform/v1/events/list-project-event-definitions /api/platform/v1/openapi-platform.yaml get /event-definitions Returns the event definitions registered for the project associated with your API key. Definitions describe event names, meanings, and property schemas. Emitting a new event through `/events` can create a minimal definition automatically, but use this endpoint when you want to manage definition metadata directly. # List project events Source: https://docs.kapso.ai/api/platform/v1/events/list-project-events /api/platform/v1/openapi-platform.yaml get /events Returns project-scoped events for the project associated with your API key, newest first. Use filters to narrow by event name, linked WhatsApp conversation, or occurrence time. Use `limit`, `after`, and `before` for cursor pagination. Cursor-paginated responses include `paging`; legacy offset responses include `meta`. # Retrieve a project event definition Source: https://docs.kapso.ai/api/platform/v1/events/retrieve-a-project-event-definition /api/platform/v1/openapi-platform.yaml get /event-definitions/{id} # Update a project event definition Source: https://docs.kapso.ai/api/platform/v1/events/update-a-project-event-definition /api/platform/v1/openapi-platform.yaml patch /event-definitions/{id} Updates editable metadata for an event definition. Event names cannot be changed after events have been recorded for the definition. Set `archived` to true to hide the definition from event tools and workflow selectors while retaining historical events; set it to false to restore it. # Dismiss a finding Source: https://docs.kapso.ai/api/platform/v1/findings/dismiss-a-finding /api/platform/v1/openapi-platform.yaml post /findings/{finding_id}/dismiss Removes the finding from the list. Both `reason` and `note` are required. A dismissed finding can reopen if monitoring detects the problem again, and it stays readable through `GET /findings/{finding_id}`. # Get a finding Source: https://docs.kapso.ai/api/platform/v1/findings/get-a-finding /api/platform/v1/openapi-platform.yaml get /findings/{finding_id} Returns one finding with its metrics, related findings, latest verification, and latest investigation including causes and suggested fixes. Unlike listing, this reads any finding in the project — quiet and dismissed findings included. `related_findings` is capped at 24 entries. # Get finding evidence Source: https://docs.kapso.ai/api/platform/v1/findings/get-finding-evidence /api/platform/v1/openapi-platform.yaml get /findings/{finding_id}/evidence Returns the evidence behind a finding: daily history, source events, affected and comparison conversation IDs, co-occurring events, and the same data for any grouped findings. Evidence is bounded to 100 source events, 50 affected conversations, 20 comparison conversations, 20 co-occurring event types, and 25 grouped findings. Those budgets are shared across the finding and its grouped findings, not applied per finding. The `coverage` object reports what was returned against those limits. When evidence cannot be read for the finding's source, this still returns `200` with an object carrying `error` instead of the evidence fields. # List findings Source: https://docs.kapso.ai/api/platform/v1/findings/list-findings /api/platform/v1/openapi-platform.yaml get /findings Returns the findings for the project associated with your API key, most recently qualified first. Only findings with status `candidate` or `open` are listed. Quiet findings, dismissed findings, and findings from sources Kapso does not read evidence from yet are omitted. To read a quiet or dismissed finding, fetch it by ID. Findings that belong to the same group are returned together on the same page, so a page can hold slightly more than `limit` items. # Mark a finding as addressed Source: https://docs.kapso.ai/api/platform/v1/findings/mark-a-finding-as-addressed /api/platform/v1/openapi-platform.yaml post /findings/{finding_id}/mark_addressed Records the current metrics as a baseline and starts monitoring the finding. Requires a completed investigation that covers the finding's current evidence; otherwise this returns `422`. # Start an investigation Source: https://docs.kapso.ai/api/platform/v1/findings/start-an-investigation /api/platform/v1/openapi-platform.yaml post /findings/{finding_id}/start_investigation Queues a Kapso Agent Investigator run for the finding. The investigation is asynchronous; poll the finding to read the result. Returns `422` when the finding already has an active investigation, is not currently eligible for one, or the investigator is not configured for the project, and `409` when another dispatch is in flight. # Create function Source: https://docs.kapso.ai/api/platform/v1/functions/functions/create-function /api/platform/v1/openapi-workflows.yaml post /functions Create a new serverless function in draft status. The function will be saved but not deployed to the runtime platform. After creating a function: 1. Review and test the code locally 2. Deploy using POST /functions/{id}/deploy 3. Invoke using POST /functions/{id}/invoke Choose function_type based on your deployment requirements: - `cloudflare_worker`: Fast global edge deployment with standard JavaScript - `supabase_function`: Deno runtime with built-in Supabase client The function slug will be auto-generated from the name if not provided (lowercase with hyphens). # Create function secret Source: https://docs.kapso.ai/api/platform/v1/functions/functions/create-function-secret /api/platform/v1/openapi-workflows.yaml post /functions/{function_id}/secrets Create a secret for this function. Secrets are injected as environment variables when your function executes. Use secrets to store sensitive data without hardcoding in function code: - API keys (Stripe, OpenAI, Twilio, etc.) - External service connection strings - OAuth credentials - Service account tokens Requirements: - Function must be in 'deployed' status (422 if not) - Secret name must be uppercase alphanumeric with underscores (e.g., STRIPE_API_KEY) - Secret name must be unique within the function Secret types are automatically detected: - String values → text type - Object/array values → json type Important: The secret value is only returned in the creation response. It cannot be retrieved later. Store the value securely after creation if needed. After creating a secret, it's immediately available in your function as an environment variable with the specified name. # Delete function Source: https://docs.kapso.ai/api/platform/v1/functions/functions/delete-function /api/platform/v1/openapi-workflows.yaml delete /functions/{function_id} Permanently delete a serverless function. This will also remove the function from the runtime platform and delete all associated secrets. After deletion: - Function will be removed from Cloudflare Workers or Supabase Edge Functions - All function secrets are automatically deleted - Function endpoint URL will return 404 - Function invocation records are preserved for audit history This operation cannot be undone. Make sure to backup function code if needed before deletion. # Delete function secret Source: https://docs.kapso.ai/api/platform/v1/functions/functions/delete-function-secret /api/platform/v1/openapi-workflows.yaml delete /functions/{function_id}/secrets/{secret_name} Permanently delete a secret from this function. The secret will be removed from the runtime environment and will no longer be available as an environment variable. Requirements: - Function must be in 'deployed' status (422 if not) - Secret must exist (404 if not found) After deletion: - Secret is immediately removed from function runtime environment - Function can no longer access the secret value - Any function invocations referencing the deleted secret will fail This operation cannot be undone. Make sure the secret is no longer needed before deletion. If the function code still references the deleted secret, invocations may fail with undefined variable errors. # Deploy function Source: https://docs.kapso.ai/api/platform/v1/functions/functions/deploy-function /api/platform/v1/openapi-workflows.yaml post /functions/{function_id}/deploy Deploy a function to the serverless runtime platform asynchronously. Deployment happens in the background and may take 10-60 seconds. You'll receive a 202 Accepted response immediately. To check deployment status: 1. Poll GET /functions/{id} to monitor status field 2. Wait for status to change from 'draft' to 'deployed' (success) or 'error' (failure) 3. Check last_deployed_at timestamp to confirm deployment completion Deployment process: - For `cloudflare_worker`: Code is deployed to Cloudflare's global edge network - For `supabase_function`: Code is deployed to Supabase Edge Functions with Deno runtime Deployment will fail (status='error') if: - Function code has syntax errors - Function type is 'supabase_function' but no Supabase project is configured - Runtime platform API is unavailable After successful deployment, the function is immediately available for invocation via POST /functions/{id}/invoke. # Invoke function Source: https://docs.kapso.ai/api/platform/v1/functions/functions/invoke-function /api/platform/v1/openapi-workflows.yaml post /functions/{function_id}/invoke Execute a deployed serverless function with a custom JSON payload. The request body is forwarded directly to your function code. Request payload structure is completely flexible - send any valid JSON that your function expects. Your function receives the payload as the request body and can parse it however needed. Successful response behavior depends on `invoke_response_mode`: - New functions default to `passthrough`, so Kapso forwards the function response body, success status code, and `Content-Type` directly - Legacy wrapped functions can keep `wrapped`, which preserves the legacy API shape and returns successful JSON results under `data` The function must be in 'deployed' status to be invoked. If the function is in 'draft' or 'error' status, the request will fail with 422 validation error. Authentication: - Private functions require `X-API-Key` - Public Cloudflare functions (`public_endpoint=true`) can be invoked without an API key - Private and unknown functions both return `404` from this route Execution tracking: - All invocations are tracked with timing and request/response data - Failed invocations return an `invocation_id` for debugging - Check function invocation history via GET /functions/{id} (extended view) Error handling: - Function execution errors are captured and returned with details - Errors are truncated to 512 characters for storage - Original error context is preserved in invocation record # List function invocations Source: https://docs.kapso.ai/api/platform/v1/functions/functions/list-function-invocations /api/platform/v1/openapi-workflows.yaml get /functions/{function_id}/invocations Retrieve recent invocation history for a function, including console logs captured during execution. Returns the most recent invocations (up to 20 by default) with: - Request/response payloads - Status codes and execution duration - Console logs (info, warn, error messages) - Error details for failed invocations Use the `status` parameter to filter by success or failure. # List function secrets Source: https://docs.kapso.ai/api/platform/v1/functions/functions/list-function-secrets /api/platform/v1/openapi-workflows.yaml get /functions/{function_id}/secrets Retrieve all secret names configured for this function. Secret values are NEVER included in list responses for security. Secrets are injected as environment variables when your function executes. Use this endpoint to: - Audit which secrets are configured - Verify secret names before creating new ones - Check secret types (text, json, inherited) Important: If the function is not deployed, this endpoint returns an empty array. Secrets can only be managed for deployed functions. Secret values are only returned once when creating a secret via POST /functions/{id}/secrets. After creation, values cannot be retrieved - only the secret name and type are visible. # List functions Source: https://docs.kapso.ai/api/platform/v1/functions/functions/list-functions /api/platform/v1/openapi-workflows.yaml get /functions Retrieve all serverless functions for your project. Functions are custom JavaScript code that runs on-demand in response to API invocations. Use this endpoint to: - List all configured functions - Review function deployment status - Audit function creation and updates # Retrieve function Source: https://docs.kapso.ai/api/platform/v1/functions/functions/retrieve-function /api/platform/v1/openapi-workflows.yaml get /functions/{function_id} Get complete details for a serverless function including code, configuration, deployment status, and computed URLs. Use this endpoint to: - Review function code before updating - Check deployment status and version - Get endpoint URL for external invocation - Retrieve runtime configuration # Update function Source: https://docs.kapso.ai/api/platform/v1/functions/functions/update-function /api/platform/v1/openapi-workflows.yaml patch /functions/{function_id} Update function metadata or code. Supports partial updates - only include fields you want to change. Common updates: - Update function code (requires redeployment to take effect) - Change function name or description - Update runtime configuration - Modify function slug Important: Code updates are saved immediately but do NOT automatically deploy. After updating code, you must call POST /functions/{id}/deploy for changes to take effect in production. # List conversation workflow executions Source: https://docs.kapso.ai/api/platform/v1/functions/whatsapp-conversations/list-conversation-workflow-executions /api/platform/v1/openapi-workflows.yaml get /whatsapp/conversations/{conversation_id}/flow_executions Retrieve workflow executions associated with a WhatsApp conversation. Executions are returned in reverse chronological order (most recent first). Use this endpoint to map a conversation to its workflow executions and track automation history for a specific conversation. **Response**: Returns summary data (id, status, tracking_id, timestamps, workflow, current_step). Does not include execution_context or events. Use GET /workflow_executions/{id} to retrieve full execution details. **Pagination**: Use `limit`, `after`, and `before`. Pagination cursors are returned in the `paging` object. # Create workflow trigger Source: https://docs.kapso.ai/api/platform/v1/functions/workflow-triggers/create-workflow-trigger /api/platform/v1/openapi-workflows.yaml post /workflows/{workflow_id}/triggers Create a new trigger for a workflow. Triggers define when the workflow should automatically execute. Trigger types: - `inbound_message`: Workflow starts when WhatsApp messages arrive at specified phone number (requires phone_number_id) - `api_call`: Workflow starts only via POST /workflows/{id}/executions (no phone_number_id needed) After creating a trigger: - For inbound_message: Messages to the phone_number_id will start workflow executions - For api_call: Workflow can only be started via API endpoint Note: A workflow can have multiple triggers (e.g., multiple phone numbers, or both inbound_message and api_call). # Delete workflow trigger Source: https://docs.kapso.ai/api/platform/v1/functions/workflow-triggers/delete-workflow-trigger /api/platform/v1/openapi-workflows.yaml delete /triggers/{trigger_id} Permanently delete a workflow trigger. This will stop the workflow from executing automatically based on this trigger. After deletion: - For inbound_message triggers: Messages to the phone_number_id will no longer start this workflow - For api_call triggers: The workflow can still be started manually via POST /workflows/{id}/executions This operation cannot be undone. If you need to temporarily disable a trigger, use PATCH to set active=false instead. # List workflow triggers Source: https://docs.kapso.ai/api/platform/v1/functions/workflow-triggers/list-workflow-triggers /api/platform/v1/openapi-workflows.yaml get /workflows/{workflow_id}/triggers Retrieve all triggers configured for a workflow. Triggers define when and how a workflow should automatically execute. Use this endpoint to: - Review configured triggers for a workflow - Audit which phone numbers trigger this workflow - Check trigger active status # Replace workflow triggers Source: https://docs.kapso.ai/api/platform/v1/functions/workflow-triggers/replace-workflow-triggers /api/platform/v1/openapi-workflows.yaml put /workflows/{workflow_id}/triggers Atomically replace the full set of triggers for a workflow. All existing triggers are deleted and the supplied set is created in a single transaction — if any trigger in the request is invalid, no changes are applied. Use this when syncing trigger configuration from an external source of truth. For per-trigger CRUD, use POST/PATCH/DELETE instead. # Update workflow trigger Source: https://docs.kapso.ai/api/platform/v1/functions/workflow-triggers/update-workflow-trigger /api/platform/v1/openapi-workflows.yaml patch /triggers/{trigger_id} Update a workflow trigger. Currently only the 'active' status can be modified. Use this to: - Temporarily disable a trigger without deleting it (set active=false) - Re-enable a previously disabled trigger (set active=true) Note: To change trigger type or phone_number_id, delete the trigger and create a new one. # Create workflow Source: https://docs.kapso.ai/api/platform/v1/functions/workflows/create-workflow /api/platform/v1/openapi-workflows.yaml post /workflows Create a new workflow in draft status. You can provide a minimal definition (just a start node) and build out the workflow later, or provide a complete workflow definition with all nodes and edges. After creating a workflow, you can: 1. Update the definition to add more nodes/edges 2. Create triggers to specify when the workflow should execute 3. Activate the workflow by changing status to 'active' # Get workflow variables Source: https://docs.kapso.ai/api/platform/v1/functions/workflows/get-workflow-variables /api/platform/v1/openapi-workflows.yaml get /workflows/{workflow_id}/variables Retrieve all variables available in a workflow, including both fixed system variables and variables discovered from execution history. Returns two categories of variables: - **Fixed variables**: Built-in system and context variables always available (flow_id, started_at, channel, phone_number, etc.) - **Discovered variables**: User-defined variables observed during workflow executions, with sample values and usage statistics Use this endpoint to: - Understand what variables are available for use in workflow steps - Review variable types and sample values for debugging - Build autocomplete for variable references in workflow editors # List workflow execution events Source: https://docs.kapso.ai/api/platform/v1/functions/workflows/list-workflow-execution-events /api/platform/v1/openapi-workflows.yaml get /workflow_executions/{execution_id}/events Retrieve events for a workflow execution in reverse chronological order (most recent first). Use `limit`, `after`, and `before` for cursor pagination. Cursors are returned in the `paging` object. # List workflow executions Source: https://docs.kapso.ai/api/platform/v1/functions/workflows/list-workflow-executions /api/platform/v1/openapi-workflows.yaml get /workflows/{workflow_id}/executions Retrieve execution history for a workflow. Executions are returned in reverse chronological order (most recent first). Use query parameters to filter by status, time range, or cursor. **Response**: Returns summary data (id, status, tracking_id, timestamps, workflow, current_step). Does not include execution_context or events. Use GET /workflow_executions/{id} to retrieve full execution details. **Pagination**: Use `limit`, `after`, and `before`. Pagination cursors are returned in the `paging` object. Common use cases: - Monitor active executions for a workflow - Review failed executions for debugging - Audit execution history over time - Find waiting executions that need user input # List workflows Source: https://docs.kapso.ai/api/platform/v1/functions/workflows/list-workflows /api/platform/v1/openapi-workflows.yaml get /workflows Retrieve all workflows for your project. Workflows are returned ordered by creation time (newest first). Use query parameters to filter by status, name, or creation date. Common use cases: - List all active workflows ready for execution - Find workflows by partial name match - Audit workflow creation over time # Resume waiting workflow execution Source: https://docs.kapso.ai/api/platform/v1/functions/workflows/resume-waiting-workflow-execution /api/platform/v1/openapi-workflows.yaml post /workflow_executions/{execution_id}/resume Resume a workflow execution that is in 'waiting' status. Workflows enter waiting status when they reach a wait_for_response step or are explicitly paused. Send a message with: - `kind`: Message type, defaults to "payload" if omitted - `data`: The actual payload - can be a string for simple text responses (e.g., "yes", "no") or an object for structured data (e.g., button clicks, form submissions) Optionally include variables to update the execution context: - `variables`: Key-value pairs to merge into the execution context. These will be available in subsequent workflow steps as `{{var_name}}`. Existing variables with the same key will be overwritten. After resuming, the workflow will continue processing from the waiting step with the provided message data and updated variables. **Response**: Returns minimal execution data (id, status, tracking_id, timestamps, workflow, current_step). Does not include execution_context or events. Use GET /workflow_executions/{id} to retrieve full execution details. # Retrieve workflow Source: https://docs.kapso.ai/api/platform/v1/functions/workflows/retrieve-workflow /api/platform/v1/openapi-workflows.yaml get /workflows/{workflow_id} Get workflow metadata, status, and execution stats for a specific workflow. This endpoint does not include the expanded canvas definition payload. Use this endpoint to: - Retrieve workflow metadata before a simple update - Check workflow status and execution count - Inspect timestamps, lock version, and execution activity Use `GET /workflows/{workflow_id}/definition` when you need the editor payload with nodes, edges, embedded project data, and WhatsApp configs. # Retrieve workflow definition Source: https://docs.kapso.ai/api/platform/v1/functions/workflows/retrieve-workflow-definition /api/platform/v1/openapi-workflows.yaml get /workflows/{workflow_id}/definition Get the editor-oriented workflow payload for a specific workflow. This endpoint returns everything needed to load the canvas: - Base workflow metadata - `definition.nodes` built from `flow_steps` - `definition.edges` built from `flow_edges` Response details: - `definition.nodes[].id` is the workflow step identifier, not the database ID - `definition.edges[].id` is the persisted edge UUID - `definition.nodes[].data.node_type` is the canonical backend node type - `definition.nodes[].data.config` changes shape by node type - Config fields are returned in snake_case - Function and call-workflow references are returned as IDs in the Platform API; the Kapso CLI can export them as slugs for local source repos Use this endpoint when building or syncing a visual workflow editor, exporting a workflow graph, or cloning an existing workflow definition. # Retrieve workflow execution Source: https://docs.kapso.ai/api/platform/v1/functions/workflows/retrieve-workflow-execution /api/platform/v1/openapi-workflows.yaml get /workflow_executions/{execution_id} Get complete details for a workflow execution including current status, execution context, variables, and full event history. **This is the only endpoint that returns the full execution_context** (vars, system, context, metadata) along with the complete event history. Use this endpoint to: - Monitor execution progress and current step - Debug failed executions with full event log - Review execution variables and context - Check error details when status is 'failed' The response includes: - Current status and step - Complete event chronology (step transitions, agent actions, variable updates) - Full execution_context with standard structure (vars, system, context, metadata) - Error details if execution failed # Start workflow execution Source: https://docs.kapso.ai/api/platform/v1/functions/workflows/start-workflow-execution /api/platform/v1/openapi-workflows.yaml post /workflows/{workflow_id}/executions Start a new execution of a workflow asynchronously. The workflow will begin processing in the background. You'll receive a 202 Accepted response with a tracking_id immediately. Use this tracking_id to: - Poll GET /workflow_executions with tracking_id filter to check status - Correlate execution events with your own systems The execution will fail if: - The workflow is not in 'active' status - Both phone_number and recipient are missing, or recipient is not a valid BSUID or parent BSUID - recipient is used with a sandbox WhatsApp number - The workflow definition is malformed Use cases: - Start workflow from external trigger (API, webhook, scheduled job) - Test workflow with specific initial variables - Retry failed execution with same parameters **Burst rate limit**: This endpoint has an additional per-workflow burst limiter on top of the general platform API rate limits. The burst counter is scoped by API key and workflow ID and resets every second. - `legacy` / `free`: 5 requests per second - `pro`: 15 requests per second - `enterprise` / `platform`: 30 requests per second Successful responses include `X-Burst-RateLimit-Limit` and `X-Burst-RateLimit-Remaining` headers. If the burst limit is exceeded, the API returns `429 Too Many Requests` with `Retry-After: 1`. # Update workflow Source: https://docs.kapso.ai/api/platform/v1/functions/workflows/update-workflow /api/platform/v1/openapi-workflows.yaml patch /workflows/{workflow_id} Update workflow metadata or replace workflow definition collections. Common updates: - Change workflow name or description - Update workflow definition nodes and edges - Activate workflow by setting status to 'active' - Archive workflow by setting status to 'archived' Definition update semantics: - Omit `definition` to update metadata only. - Omit `definition.nodes` to leave nodes unchanged. - Include `definition.nodes` only when sending the complete desired node set. Nodes omitted from that array are removed. - Omit `definition.edges` to leave edges unchanged. - Include `definition.edges` only when sending the complete desired edge set. Edges omitted from that array are removed. - Use snake_case config keys in API payloads. Successful responses contain workflow metadata, including the new `lock_version`. They do not include the expanded graph. Call `GET /workflows/{workflow_id}/definition` after updating when you need the saved nodes and edges. You can update the definition of an active workflow. Changes take effect immediately for new executions; running executions continue with the snapshot they started with. # Update workflow execution status Source: https://docs.kapso.ai/api/platform/v1/functions/workflows/update-workflow-execution-status /api/platform/v1/openapi-workflows.yaml patch /workflow_executions/{execution_id} Manually update the status of a workflow execution. This is useful for programmatically controlling workflow lifecycle from external systems. **Allowed status transitions:** - `ended` - End the execution immediately - `handoff` - Transfer execution to human agent - `waiting` - Pause execution until resumed **Use cases:** - End workflows based on external events - Transfer complex queries to human agents - Implement custom timeout logic - Coordinate workflows with external systems Invalid transitions (e.g., transitioning from a terminal state) will return a 422 error with details about why the transition is not allowed. **Response**: Returns full execution data including the updated status. Use GET /workflow_executions/{id} to retrieve execution context and event history. # Create inbox embed Source: https://docs.kapso.ai/api/platform/v1/inbox-embeds/create-inbox-embed /api/platform/v1/openapi-platform.yaml post /inbox_embeds Create an embeddable inbox access link. The response includes `token` and `embed_url` once. Store the embed URL when you create it; list, get, and update responses omit the secret. # Get inbox embed Source: https://docs.kapso.ai/api/platform/v1/inbox-embeds/get-inbox-embed /api/platform/v1/openapi-platform.yaml get /inbox_embeds/{inbox_embed_id} Returns an inbox embed without the raw token or embed URL. # List inbox embeds Source: https://docs.kapso.ai/api/platform/v1/inbox-embeds/list-inbox-embeds /api/platform/v1/openapi-platform.yaml get /inbox_embeds Returns inbox embed access links for your project, most recent first. Raw tokens and embed URLs are not returned after creation. # Revoke inbox embed Source: https://docs.kapso.ai/api/platform/v1/inbox-embeds/revoke-inbox-embed /api/platform/v1/openapi-platform.yaml delete /inbox_embeds/{inbox_embed_id} Revokes the embed token immediately. # Update inbox embed Source: https://docs.kapso.ai/api/platform/v1/inbox-embeds/update-inbox-embed /api/platform/v1/openapi-platform.yaml patch /inbox_embeds/{inbox_embed_id} Updates mutable settings. Scope cannot be changed; create a new embed for a different scope. # Get a mode Source: https://docs.kapso.ai/api/platform/v1/kapso-agent/agent-modes/get-a-mode /api/platform/v1/openapi-kapso-agent.yaml get /kapso-agent/modes/{mode} # List modes Source: https://docs.kapso.ai/api/platform/v1/kapso-agent/agent-modes/list-modes /api/platform/v1/openapi-kapso-agent.yaml get /kapso-agent/modes Every mode. Only modes whose `available_invocations` includes `api` accept runs. # Approve a tool call Source: https://docs.kapso.ai/api/platform/v1/kapso-agent/agent-runs/approve-a-tool-call /api/platform/v1/openapi-kapso-agent.yaml post /kapso-agent/runs/{id}/approvals/{approval_id}/approve # Cancel a run Source: https://docs.kapso.ai/api/platform/v1/kapso-agent/agent-runs/cancel-a-run /api/platform/v1/openapi-kapso-agent.yaml post /kapso-agent/runs/{id}/cancel # Create a run Source: https://docs.kapso.ai/api/platform/v1/kapso-agent/agent-runs/create-a-run /api/platform/v1/openapi-kapso-agent.yaml post /kapso-agent/runs Triggers an agent run asynchronously. Poll the returned `status_url` or subscribe to `kapso_agent.run.*` project webhooks. # Get a run Source: https://docs.kapso.ai/api/platform/v1/kapso-agent/agent-runs/get-a-run /api/platform/v1/openapi-kapso-agent.yaml get /kapso-agent/runs/{id} Only runs created via the API with the same API key are retrievable. # Pause a run Source: https://docs.kapso.ai/api/platform/v1/kapso-agent/agent-runs/pause-a-run /api/platform/v1/openapi-kapso-agent.yaml post /kapso-agent/runs/{id}/pause # Reject a tool call Source: https://docs.kapso.ai/api/platform/v1/kapso-agent/agent-runs/reject-a-tool-call /api/platform/v1/openapi-kapso-agent.yaml post /kapso-agent/runs/{id}/approvals/{approval_id}/reject # Resume a run Source: https://docs.kapso.ai/api/platform/v1/kapso-agent/agent-runs/resume-a-run /api/platform/v1/openapi-kapso-agent.yaml post /kapso-agent/runs/{id}/resume # Get a session Source: https://docs.kapso.ai/api/platform/v1/kapso-agent/agent-sessions/get-a-session /api/platform/v1/openapi-kapso-agent.yaml get /kapso-agent/sessions/{id} Returns a session and its API runs, ordered from newest to oldest. The session must belong to the authenticating API key. Pagination parameters apply to the `runs` array. Use `limit`, `after`, and `before` for cursor pagination. # List mode sessions Source: https://docs.kapso.ai/api/platform/v1/kapso-agent/agent-sessions/list-mode-sessions /api/platform/v1/openapi-kapso-agent.yaml get /kapso-agent/modes/{mode}/sessions Lists sessions for a built-in or custom mode, ordered by most recent activity. Results include only API runs created with the authenticating API key. Use `limit`, `after`, and `before` for cursor pagination. Without these parameters, the endpoint returns legacy offset pagination. # Get log search catalog Source: https://docs.kapso.ai/api/platform/v1/log-search/get-log-search-catalog /api/platform/v1/openapi-platform.yaml get /log_search/catalog Returns the log source list, supported filter keys, and detail fields for the project associated with your API key. # Search log events Source: https://docs.kapso.ai/api/platform/v1/log-search/search-log-events /api/platform/v1/openapi-platform.yaml get /log_search Search log events for the project associated with your API key. Returns the same event payload shape used by the logs UI. Use POST when sending filters. # Search log events with filters Source: https://docs.kapso.ai/api/platform/v1/log-search/search-log-events-with-filters /api/platform/v1/openapi-platform.yaml post /log_search Search log events with the same project-scoped data and filter catalog used by the logs UI. # Upload media Source: https://docs.kapso.ai/api/platform/v1/media/upload-media /api/platform/v1/openapi-platform.yaml post /whatsapp/media Upload media files for WhatsApp messaging from public URLs. Supports two delivery methods: - `meta_media`: Standard upload to Meta's media endpoint (30-day lifetime) - `meta_resumable_asset`: Resumable upload flow for profile pictures and large files **Security**: SSRF-protected - blocks private IPs, localhost, and metadata endpoints **Size limits**: - Images: 5 MB - Audio/Video: 16 MB - Documents: 100 MB Requests exceeding these limits fail immediately. # Get message Source: https://docs.kapso.ai/api/platform/v1/messages/get-message /api/platform/v1/openapi-platform.yaml get /whatsapp/messages/{message_id} Retrieve a single message by its WhatsApp message ID (WAMID). # List messages Source: https://docs.kapso.ai/api/platform/v1/messages/list-messages /api/platform/v1/openapi-platform.yaml get /whatsapp/messages Query WhatsApp messages across all conversations. Results are returned newest first. Use cursor pagination (`limit`, `after`, `before`) to traverse large message histories efficiently. # Create a shared email destination Source: https://docs.kapso.ai/api/platform/v1/notifications/create-a-shared-email-destination /api/platform/v1/openapi-platform.yaml post /notifications/destinations Creates a `shared_email` destination with status `pending` and sends a verification email to the address. The link expires after 7 days. The destination cannot receive notifications until the recipient confirms. Addresses belonging to project members are rejected; those members manage their own personal notification preferences in the Kapso app. Slack channels are added from the Kapso app under **Notifications**. Once connected they appear in `GET /notifications/destinations` and can be routed like any other destination. Requesting `kind: slack_channel` here returns `400`. Creating a destination for an address that was previously removed reuses the existing destination: it returns to `pending` and a new verification email is sent. # Create or update a route Source: https://docs.kapso.ai/api/platform/v1/notifications/create-or-update-a-route /api/platform/v1/openapi-platform.yaml put /notifications/routes Routes an event type to a team destination. Every event is delivered immediately. Called again for the same `destination_id` and `event_key`, it returns the existing route. The destination may be `pending`; deliveries start once it is `active`. Unknown event keys and internal events return `400`. # Delete a route Source: https://docs.kapso.ai/api/platform/v1/notifications/delete-a-route /api/platform/v1/openapi-platform.yaml delete /notifications/routes/{id} Deletes the route and cancels its pending deliveries. # List notification event types Source: https://docs.kapso.ai/api/platform/v1/notifications/list-notification-event-types /api/platform/v1/openapi-platform.yaml get /notifications/events The catalog of events you can route, ordered by category and name. Use `key` when creating a route. Internal events such as the test notification are not listed. # List routes Source: https://docs.kapso.ai/api/platform/v1/notifications/list-routes /api/platform/v1/openapi-platform.yaml get /notifications/routes Event types currently routed to team destinations, oldest first. # List team destinations Source: https://docs.kapso.ai/api/platform/v1/notifications/list-team-destinations /api/platform/v1/openapi-platform.yaml get /notifications/destinations Returns the project's shared email destinations and the Slack channel destinations connected from the Kapso app, including `pending` and `unhealthy` ones. Removed destinations and personal member destinations are never returned. # Remove a team destination Source: https://docs.kapso.ai/api/platform/v1/notifications/remove-a-team-destination /api/platform/v1/openapi-platform.yaml delete /notifications/destinations/{destination_id} Disables a shared email or Slack channel destination, deletes its routes, and cancels its pending deliveries. The destination stops appearing in `GET /notifications/destinations`; its delivery history is kept. Personal member destinations are not addressable and return `404`. # Check phone health Source: https://docs.kapso.ai/api/platform/v1/phone-numbers/check-phone-health /api/platform/v1/openapi-platform.yaml get /whatsapp/phone_numbers/{phone_number_id}/health Health check via Meta APIs and Kapso services. Results are cached for 3 minutes per phone number, so repeated calls can return the same payload. Use `timestamp` to tell when the check actually ran. The cache is invalidated early when the number's configuration changes. If a fresh check is already running for the same number and does not finish within 15 seconds, the response is `status: error` with an `error` message asking you to retry shortly. When a check keeps failing to read the phone number from Meta because of an access or token error, the result is held for longer: 5, then 15, then 30, then 60 minutes. During that window the response repeats the last payload and adds `retry_after`. Rate limits, payment errors and transport failures do not trigger this. A successful check clears `retry_after` and resets the delay, as does reconnecting the number or changing its credentials. # Connect phone number Source: https://docs.kapso.ai/api/platform/v1/phone-numbers/connect-phone-number /api/platform/v1/openapi-platform.yaml post /customers/{customer_id}/whatsapp/phone_numbers Connect a WhatsApp number to this customer using Meta credentials. Get credentials from Meta's App Dashboard after completing embedded signup or manual setup. # Delete phone number Source: https://docs.kapso.ai/api/platform/v1/phone-numbers/delete-phone-number /api/platform/v1/openapi-platform.yaml delete /whatsapp/phone_numbers/{phone_number_id} # Get phone number Source: https://docs.kapso.ai/api/platform/v1/phone-numbers/get-phone-number /api/platform/v1/openapi-platform.yaml get /whatsapp/phone_numbers/{phone_number_id} # List phone numbers Source: https://docs.kapso.ai/api/platform/v1/phone-numbers/list-phone-numbers /api/platform/v1/openapi-platform.yaml get /whatsapp/phone_numbers Get WhatsApp numbers in your project, most recent first. # Update phone number Source: https://docs.kapso.ai/api/platform/v1/phone-numbers/update-phone-number /api/platform/v1/openapi-platform.yaml patch /whatsapp/phone_numbers/{phone_number_id} # List provider models Source: https://docs.kapso.ai/api/platform/v1/provider-models/list-provider-models /api/platform/v1/openapi-platform.yaml get /provider_models Returns available AI provider models. # Create setup link Source: https://docs.kapso.ai/api/platform/v1/setup-links/create-setup-link /api/platform/v1/openapi-platform.yaml post /customers/{customer_id}/setup_links Generate an onboarding setup for customers to connect their WhatsApp number. Open the returned URL as a Kapso-hosted page, or use the returned token with `@kapso/sdk` when your Tech Provider app belongs to an active Kapso Multi-partner Solution. The setup can optionally provision a new number. # List setup links Source: https://docs.kapso.ai/api/platform/v1/setup-links/list-setup-links /api/platform/v1/openapi-platform.yaml get /customers/{customer_id}/setup_links Get WhatsApp onboarding links for a customer, most recent first. # Update setup link Source: https://docs.kapso.ai/api/platform/v1/setup-links/update-setup-link /api/platform/v1/openapi-platform.yaml patch /customers/{customer_id}/setup_links/{setup_link_id} # List project users Source: https://docs.kapso.ai/api/platform/v1/users/list-project-users /api/platform/v1/openapi-platform.yaml get /users Returns all users who are members of your project. # List webhook deliveries Source: https://docs.kapso.ai/api/platform/v1/webhook-deliveries/list-webhook-deliveries /api/platform/v1/openapi-platform.yaml get /webhook_deliveries Returns webhook delivery attempts for your project, most recent first. # Create project webhook Source: https://docs.kapso.ai/api/platform/v1/webhooks/create-project-webhook /api/platform/v1/openapi-platform.yaml post /whatsapp/webhooks Create a webhook for this project. Two scoping options: - **Project-scoped**: Omit `phone_number_id` to receive project events only - **Number-scoped**: Include `phone_number_id` to receive message and conversation events for that number Project webhooks do not receive message or conversation events. Use a number-scoped webhook (or `POST /whatsapp/phone_numbers/{phone_number_id}/webhooks`) for those. Subscribing to `project.event` requires project events to be available on your plan. Two webhook types: - **kapso**: Event-based webhooks with filtered events, buffering support, and Kapso payload format - **meta**: Raw Meta webhook forwarding - receives the exact payload Meta sends (requires `phone_number_id`) # Create webhook Source: https://docs.kapso.ai/api/platform/v1/webhooks/create-webhook /api/platform/v1/openapi-platform.yaml post /whatsapp/phone_numbers/{phone_number_id}/webhooks Subscribe to WhatsApp events for this number. Two webhook types available: - **kapso**: Event-based webhooks with filtered events, buffering support, and Kapso payload format - **meta**: Raw Meta webhook forwarding - receives the exact payload Meta sends, with X-Idempotency-Key header Use buffering (kapso only) to batch high-volume events like inbound messages. Without buffering, each message triggers an immediate webhook delivery. # Delete project webhook Source: https://docs.kapso.ai/api/platform/v1/webhooks/delete-project-webhook /api/platform/v1/openapi-platform.yaml delete /whatsapp/webhooks/{webhook_id} # Delete webhook Source: https://docs.kapso.ai/api/platform/v1/webhooks/delete-webhook /api/platform/v1/openapi-platform.yaml delete /whatsapp/phone_numbers/{phone_number_id}/webhooks/{webhook_id} # Get project webhook Source: https://docs.kapso.ai/api/platform/v1/webhooks/get-project-webhook /api/platform/v1/openapi-platform.yaml get /whatsapp/webhooks/{webhook_id} # Get webhook Source: https://docs.kapso.ai/api/platform/v1/webhooks/get-webhook /api/platform/v1/openapi-platform.yaml get /whatsapp/phone_numbers/{phone_number_id}/webhooks/{webhook_id} # List project webhooks Source: https://docs.kapso.ai/api/platform/v1/webhooks/list-project-webhooks /api/platform/v1/openapi-platform.yaml get /whatsapp/webhooks Get all webhooks for the project (both project-scoped and phone number-scoped), most recent first. # List webhooks Source: https://docs.kapso.ai/api/platform/v1/webhooks/list-webhooks /api/platform/v1/openapi-platform.yaml get /whatsapp/phone_numbers/{phone_number_id}/webhooks Get webhooks for this number, most recent first. # Test project webhook Source: https://docs.kapso.ai/api/platform/v1/webhooks/test-project-webhook /api/platform/v1/openapi-platform.yaml post /whatsapp/webhooks/{webhook_id}/test Send a test payload to the webhook endpoint. Optionally specify an `event_type` to test with a specific event payload. The event type must be one of the events the webhook is configured to receive. # Update project webhook Source: https://docs.kapso.ai/api/platform/v1/webhooks/update-project-webhook /api/platform/v1/openapi-platform.yaml patch /whatsapp/webhooks/{webhook_id} Update a project webhook. Subscribing to `project.event` requires project events to be available on your plan. # Update webhook Source: https://docs.kapso.ai/api/platform/v1/webhooks/update-webhook /api/platform/v1/openapi-platform.yaml patch /whatsapp/phone_numbers/{phone_number_id}/webhooks/{webhook_id} # Create flow Source: https://docs.kapso.ai/api/platform/v1/whatsapp-flows/create-flow /api/platform/v1/openapi-platform.yaml post /whatsapp/flows Create a new WhatsApp Flow. By default creates a draft flow with a basic welcome screen. # Create flow version Source: https://docs.kapso.ai/api/platform/v1/whatsapp-flows/create-flow-version /api/platform/v1/openapi-platform.yaml post /whatsapp/flows/{flow_id}/versions Upload new flow JSON to create a new version. Syncs with Meta's API. # Create/update data endpoint Source: https://docs.kapso.ai/api/platform/v1/whatsapp-flows/createupdate-data-endpoint /api/platform/v1/openapi-platform.yaml post /whatsapp/flows/{flow_id}/data_endpoint Create or update the data endpoint function code. The function handles dynamic data for your flow. # Deploy data endpoint Source: https://docs.kapso.ai/api/platform/v1/whatsapp-flows/deploy-data-endpoint /api/platform/v1/openapi-platform.yaml post /whatsapp/flows/{flow_id}/data_endpoint/deploy Deploy the data endpoint function to Cloudflare Workers. # Get data endpoint Source: https://docs.kapso.ai/api/platform/v1/whatsapp-flows/get-data-endpoint /api/platform/v1/openapi-platform.yaml get /whatsapp/flows/{flow_id}/data_endpoint Get the data endpoint function configuration for a flow. # Get flow Source: https://docs.kapso.ai/api/platform/v1/whatsapp-flows/get-flow /api/platform/v1/openapi-platform.yaml get /whatsapp/flows/{flow_id} # Get flow version Source: https://docs.kapso.ai/api/platform/v1/whatsapp-flows/get-flow-version /api/platform/v1/openapi-platform.yaml get /whatsapp/flows/{flow_id}/versions/{version_id} Returns version details including the flow JSON. # Get function invocations Source: https://docs.kapso.ai/api/platform/v1/whatsapp-flows/get-function-invocations /api/platform/v1/openapi-platform.yaml get /whatsapp/flows/{flow_id}/function_invocations Get recent invocations of the data endpoint function. # Get function logs Source: https://docs.kapso.ai/api/platform/v1/whatsapp-flows/get-function-logs /api/platform/v1/openapi-platform.yaml get /whatsapp/flows/{flow_id}/function_logs Get logs from the data endpoint function. # List flow versions Source: https://docs.kapso.ai/api/platform/v1/whatsapp-flows/list-flow-versions /api/platform/v1/openapi-platform.yaml get /whatsapp/flows/{flow_id}/versions Returns versions for a flow, most recent first. # List flows Source: https://docs.kapso.ai/api/platform/v1/whatsapp-flows/list-flows /api/platform/v1/openapi-platform.yaml get /whatsapp/flows Returns WhatsApp flows in your project, most recent first. # Publish flow Source: https://docs.kapso.ai/api/platform/v1/whatsapp-flows/publish-flow /api/platform/v1/openapi-platform.yaml post /whatsapp/flows/{flow_id}/publish Publish a draft flow to make it available for use. Published flows cannot be unpublished. # Register data endpoint with Meta Source: https://docs.kapso.ai/api/platform/v1/whatsapp-flows/register-data-endpoint-with-meta /api/platform/v1/openapi-platform.yaml post /whatsapp/flows/{flow_id}/data_endpoint/register Register the deployed data endpoint URL with Meta. Requires flows encryption to be configured. # Setup encryption Source: https://docs.kapso.ai/api/platform/v1/whatsapp-flows/setup-encryption /api/platform/v1/openapi-platform.yaml post /whatsapp/flows/{flow_id}/setup_encryption Set up flows encryption for the WABA associated with this flow. Required for data endpoints. # Rate limits Source: https://docs.kapso.ai/api/rate-limits Per-minute request limits by plan, and how to handle 429s Rate limits apply to every endpoint across the WhatsApp, Platform, and Workflows APIs. They are counted per API key, or per IP when no key is present. ## Requests per minute | Plan | Requests / minute | | ---------- | ----------------- | | Free | 100 | | Legacy | 100 | | Pro | 500 | | Platform | 1000 | | Enterprise | 2000 | The window is a fixed minute, not a rolling one. A project with no active plan gets the free limit. ## Response headers Every response carries your current state: ``` X-RateLimit-Limit: 500 X-RateLimit-Remaining: 487 ``` When you exceed the limit, Kapso returns `429` with: ``` X-RateLimit-Limit: 500 X-RateLimit-Remaining: 0 Retry-After: 60 ``` ```json theme={null} { "error": "Rate limit exceeded", "message": "You have exceeded the rate limit. Please try again later." } ``` Read `X-RateLimit-Remaining` and back off before you hit zero, rather than waiting for the 429. ## Workflow execution burst limit Starting workflow executions has a second, tighter limit measured **per second and per workflow**. It stops a single runaway workflow from consuming your whole per-minute budget. | Plan | Executions / second / workflow | | ---------- | ------------------------------ | | Free | 5 | | Legacy | 5 | | Pro | 15 | | Platform | 30 | | Enterprise | 30 | Exceeding it returns `429` with its own headers: ``` X-Burst-RateLimit-Limit: 15 X-Burst-RateLimit-Remaining: 0 Retry-After: 1 ``` ## Meta's limits are separate The WhatsApp API proxies Meta. Meta enforces its own throughput and messaging limits on top of Kapso's, so a request can pass Kapso's limit and still be rejected upstream: ```json theme={null} { "error": "Meta API rate limit exceeded. Retry after 60 seconds" } ``` Your WABA messaging tier controls how many business-initiated conversations you can start per day. See [Meta's messaging limits](https://developers.facebook.com/docs/whatsapp/messaging-limits/). Broadcasts are not affected by this. Kapso paces them internally to stay within throughput limits, so you do not need to throttle a broadcast yourself. ## Handling 429 Retry with exponential backoff, honoring `Retry-After`: ```typescript theme={null} async function callWithRetry(url: string, options: RequestInit, attempt = 0) { const res = await fetch(url, options); if (res.status !== 429 || attempt >= 5) return res; const wait = Number(res.headers.get('Retry-After') ?? 60) * 1000; await new Promise((r) => setTimeout(r, wait * 2 ** attempt)); return callWithRetry(url, options, attempt + 1); } ``` # Build with AI Source: https://docs.kapso.ai/docs/build-with-ai Give your AI agent the tools and context it needs to work with Kapso. If you're building with Codex, Cursor, Claude, or another AI agent, start here. This page gives your agent the fastest path into Kapso: * the CLI for live project operations * Project MCP to operate your WhatsApp numbers without shell access * `llms.txt` for documentation context * Kapso agent skills * docs MCP for interactive browsing * direct links to the most common starting points ## Prerequisite A human still needs to create a Kapso project first. After that, your agent can use the CLI, Project MCP, APIs, SDKs, and docs integrations. ## Kapso CLI For live project operations, start with the CLI. ```bash theme={null} curl -fsSL https://kapso.ai/install.sh | bash kapso setup kapso whatsapp numbers list kapso whatsapp messages send --phone-number "" --to 15551234567 --text "Hello" ``` Use the CLI when the agent needs to inspect project state, create setup links, manage webhooks, send messages, or work against a real Kapso project from a terminal session. See the [full CLI reference](/docs/whatsapp/cli) for all available commands. ## Project MCP Use Project MCP when your agent supports MCP and should operate your WhatsApp numbers without shell access. For Codex with browser sign-in: ```bash theme={null} codex mcp add kapso --url https://api.kapso.ai/mcp codex mcp login kapso ``` Project MCP can send messages, read conversations and messages, manage templates, configure webhooks, provision and inspect WhatsApp numbers, and manage customers and setup links. See the [Project MCP guide](/docs/whatsapp/mcp) for browser sign-in and API key setup. ## Agent skills If the agent is working inside a codebase, install [Kapso agent skills](https://github.com/gokapso/agent-skills): ```bash theme={null} npx skills add gokapso/agent-skills ``` Use skills when you want the agent to follow Kapso-specific workflows, scripts, and integration patterns while editing code. ## Docs Use the docs when the agent needs current product and API context rather than live project access. ### llms.txt Give your agent the Kapso docs in one file: ```text theme={null} https://docs.kapso.ai/llms.txt ``` This is the fastest way to give an agent current documentation context. ### MCP If your agent supports MCP, connect the docs MCP server for interactive documentation browsing: ```bash Codex theme={null} codex mcp add kapso-docs --url https://docs.kapso.ai/mcp ``` ```bash Claude Code theme={null} claude mcp add --transport http kapso-docs https://docs.kapso.ai/mcp ``` ```json Cursor theme={null} { "mcpServers": { "kapso-docs": { "url": "https://docs.kapso.ai/mcp" } } } ``` Use the same `https://docs.kapso.ai/mcp` endpoint in any MCP client that supports streamable HTTP. Docs MCP only searches documentation. Use [Project MCP](/docs/whatsapp/mcp) for live project operations. ## More pages Let Kapso's own agent build workflows, debug APIs, and operate your project from the dashboard, Slack, or the API. Connect your own WhatsApp number to your own AI agent. Let AI agents operate your WhatsApp numbers through MCP. Start sending text, media, templates, and interactive messages. Receive WhatsApp events through Kapso webhooks or forwarded Meta payloads. Use Kapso for support, operations, broadcasts, and inbox workflows. Let your customers connect their own WhatsApp accounts to your product. # AI fields Source: https://docs.kapso.ai/docs/flows/ai-fields Dynamic content generation using AI at runtime AI fields generate dynamic content using AI models during workflow execution. Instead of static text, you provide prompts that are resolved at runtime with context and variables. ## How it works 1. **Define prompt**: Provide your generation prompt 2. **Runtime resolution**: AI generates content based on prompt and workflow context 3. **Variable access**: Use `{{variable_name}}` to include workflow variables 4. **Context aware**: AI has access to conversation history and user data ## Variable interpolation Access workflow variables in your prompts using `{{variable_name}}` syntax. Available variables: * Workflow variables: `{{variable_name}}` * User context: `{{phone_number}}`, `{{conversation_id}}` * System info: `{{flow_id}}`, `{{current_time}}` ## Requirements * **AI model required**: Must specify `provider_model_name` when using AI fields * **Credits consumption**: AI field resolution consumes processing credits * **Runtime resolution**: Fields are resolved during workflow execution, not at build time # Edges Source: https://docs.kapso.ai/docs/flows/edges Connect workflow nodes to define execution paths Edges connect nodes in your workflow, defining the execution path from one step to another. Each edge has a source node, target node, and label. ## Edge labels * **Default label**: "next" - used by most nodes * **Custom labels**: Required for decide nodes to match condition labels * **Unique labels**: Each outgoing edge from a node must have a unique label ## Node-specific edges For function-mode DecideNodes, the condition labels are automatically provided to your function in the `available_edges` array, ensuring your function knows which edges can be taken. ## Validation rules * Source and target node identifiers must exist in the workflow definition * Exactly one Start node must be present and have at least one outgoing edge * Node identifiers must be unique across the workflow * Workflow must include at least one node * Decide node edge labels must match condition labels exactly in both AI and function modes (otherwise execution cannot route) # Workflow execution events Source: https://docs.kapso.ai/docs/flows/events Workflow execution tracking and debugging Workflow execution events automatically track every step of execution - from start to finish, including decisions, actions, and errors. Use them to debug workflows and understand what happened during execution. > Workflow execution events are internal logs for a workflow run. To record durable custom facts or trigger workflows, use [project events](/docs/platform/events). ## Event categories **Execution events** - Workflow lifecycle * `execution_started` - Workflow begins * `execution_ended` - Workflow completes * `execution_failed` - Workflow crashes **Step events** - Step transitions * `step_entered` - Entering a step * `step_completed` - Step finishes successfully * `step_failed` - Step encounters error **Action events** - Node actions * `action_preparing` - Setting up action * `action_executing` - Running action * `action_performed` - Action completes * `decision_evaluated` - Decide node chooses path ## Accessing events ### Test workflows Events appear in the Workflow Events tab when testing workflows. They show step-by-step execution with timestamps and details. ### Production workflows View events in execution logs dashboard: * Project → Execution logs → Click execution → Events tab * Real-time updates for active workflows * Full execution history ## Key event types **Decision tracking** * `decision_evaluating` - AI evaluating conditions * `decision_evaluated` - Path chosen with reasoning **Variable changes** * `variables_set` - Variables updated * Shows old/new values and source **AI field resolution** * `ai_field_resolving` - Processing AI prompt * `ai_field_resolved` - AI response received **User interactions** * `user_input_received` - User responded * `timeout_scheduled` - Timeout scheduled for wait step * `timeout_cancelled` - Timeout cancelled (user responded before timeout) * `timeout_triggered` - Timeout fired (no user response received) **Errors** * `step_failed` - Step error with details * `action_failed` - Action error ## Event structure Each event includes: ```json theme={null} { "id": "uuid", "eventType": "step_entered", "createdAt": "2024-01-01T12:00:00Z", "step": { "identifier": "welcome_step", "stepableType": "FlowSendTextStep" }, "edgeLabel": "next", "payload": { "step_identifier": "welcome_step", "duration_ms": 150 } } ``` **Core fields** * `eventType` - What happened * `createdAt` - Exact timestamp * `step` - Associated step (if any) * `edgeLabel` - Transition taken * `payload` - Event-specific data **Payload examples** * Decision events include prompt, response, model used * Action events show parameters and results * Error events contain error message and stack trace * Variable events show before/after values Events provide complete audit trail of workflow execution for debugging and monitoring. # Use project events in workflows Source: https://docs.kapso.ai/docs/flows/project-events Trigger workflows from project events and emit events from workflow runs Project events are durable custom records, such as `lead.qualified` or `conversation.csat_scored`. Use them in workflows to react to product events or record an outcome for later querying, analysis, or automation. For the API and event data model, see [Project events](/docs/platform/events). ## Trigger workflows Use a Project event trigger to start a workflow when a project event is emitted. ### Configure a trigger Select an event definition in the dashboard. Create the definition first when you need to configure a trigger before real events exist; events that have already been emitted also appear in the picker. You can optionally filter on one event property: * `eq` for exact matches * `lt`, `lte`, `gt`, `gte` for numeric comparisons Numeric filters match only numeric property values. ### Observer mode and context Project-event-triggered workflows run in observer mode. They can read event data, but send steps do not send WhatsApp messages unless the step explicitly overrides the destination phone number. ```text theme={null} {{system.trigger_type}} # "project_event" {{system.project_event_id}} # Event ID {{system.observer_mode}} # true {{system.allow_outbound}} # false {{system.workflow_id}} # Current workflow ID {{context.channel}} # "project_event" or "whatsapp" {{context.conversation_id}} # Conversation ID, when linked {{context.event.id}} # Event ID {{context.event.name}} # "conversation.csat_scored" {{context.event.occurred_at}} # Event timestamp {{context.event.properties}} # Event properties {{context.event.properties.score}} # Event property ``` Project events must be available on your plan. A workflow triggered by a project event cannot emit project events, which prevents trigger loops. ## Emit project events Workflows can emit project events in three ways: * An [Emit event node](/docs/flows/step-types/emit-event-node) * The Agent node's [`emit_event` tool](/docs/flows/step-types/agent-node#emitting-project-events) * A [Function node response](/docs/flows/step-types/function-node) Function responses include events in `project_events`: ```json theme={null} { "vars": { "csat_score": 4 }, "project_events": [ { "name": "conversation.csat_scored", "properties": { "score": 4, "reason": "Issue resolved in one reply", "source": "workflow" } } ] } ``` ### Limits and test mode * A workflow execution can emit at most 10 project events. * A workflow execution can emit at most 3 events with the same name. * A Function node response can emit at most 5 project events. * In test mode, emitted events are validated and appear in workflow execution events, but project events are not stored. # Agent node Source: https://docs.kapso.ai/docs/flows/step-types/agent-node AI agent that can use tools and hold conversations Embedded AI agent that can use tools, access data, and have multi-turn conversations with users. ## Execution behavior Unlike other nodes that execute and immediately advance, agent nodes maintain workflow execution at the node until the agent explicitly calls the `complete_task` tool. This allows: * **Multi-turn conversations**: Agent can exchange multiple messages with the user * **Stateful execution**: Maintains conversation context throughout * **Tool orchestration**: Decides when to call tools, send messages, or complete * **Dynamic input**: New user messages are automatically injected into the agent's conversation * **Controlled completion**: Workflow only advances when agent determines task is done ## Configuration * `id`: Unique node identifier * `system_prompt`: Instructions for the agent's behavior * `provider_model_id`: AI model to use * `temperature`: Model creativity, 0.0-1.0 (default: 0.0) * `max_iterations`: Maximum tool calls/responses (default: 80) * `max_tokens`: Maximum tokens per response (default: 8192) * `reasoning_effort`: For o1 models - low, medium, high (optional) * `prompt_cache_ttl`: Prompt cache lifetime, `5m` or `1h` (default: `5m`) * `flow_agent_webhooks`: Custom API tools (optional) * `flow_agent_function_tools`: Deployed functions as tools (optional) * `flow_agent_mcp_servers`: MCP server tools (optional, HTTP streamable only) * `enabled_default_tools`: Built-in tools enabled for the agent node * `default_tool_configs`: Optional configuration for built-in tools * `sandbox_enabled`: Enables the remote sandbox for this node * `sandbox_network_mode`: Sandbox outbound policy, `allow_all` or `allow_list` * `sandbox_allowed_outbound_hosts`: Extra outbound hosts allowed when using `allow_list` * `flow_agent_resources`: Repository resources mounted into the sandbox * `observer_prompt_mode`: Behavior when outbound disabled (advanced, see below) * `message_delivery_mode`: Message delivery behavior, `auto_send_assistant_text` or `tool_only` ### Prompt caching `prompt_cache_ttl` controls how long reusable prompt content stays cached between agent turns. * `5m`: Default. * `1h`: Longer cache lifetime with a higher cache-write cost. Only accepted for Anthropic models, including Anthropic models served through OpenRouter. Setting it on any other model is rejected. Check `supported_prompt_cache_ttls` on `GET /platform/v1/provider_models` to see which values a model accepts. ### Message delivery mode Use `message_delivery_mode` to control whether normal assistant text is sent to the WhatsApp user. * `auto_send_assistant_text`: Normal assistant text is sent automatically. This is the default. * `tool_only`: Normal assistant text is kept internal. The agent must call `send_notification_to_user` for every user-visible message, including questions. When using `tool_only`, enable `send_notification_to_user` and `enter_waiting`. Call `enter_waiting` after sending a question that needs a reply. ### Emitting project events Enable the built-in `emit_event` tool when an agent should record outcomes, labels, scores, or other project events during its work. In the dashboard, you can choose which event definitions the agent may emit: * Select one event definition when the agent should only fill properties for that event. * Select multiple event definitions to give the agent a small allowlist. * Leave the selection empty to allow the agent to emit any valid project event. When using the Platform API directly, include `emit_event` in `enabled_default_tools`. To restrict the agent to specific event definitions, also set `default_tool_configs.emit_event.event_definition_ids`: ```json theme={null} { "node_type": "agent", "config": { "system_prompt": "Score the conversation and record the result.", "provider_model_id": "uuid", "enabled_default_tools": ["emit_event", "complete_task"], "default_tool_configs": { "emit_event": { "event_definition_ids": ["880e8400-e29b-41d4-a716-446655440003"] } } } } ``` Agent-emitted events follow the same validation, plan, usage, and loop-prevention rules as other workflow event emission. See [Events](/docs/platform/events) for limits and behavior. ## Remote sandbox Enable the remote sandbox when the agent needs a disposable workspace to inspect or modify repository files during execution. When sandbox access is enabled and at least one repository has valid credentials, the agent gets these repository tools: * `bash` * `read` * `list_dir` * `write` * `edit` Configured repositories are cloned into `/workspace/repos/` before those tools run. Remote sandbox is in beta. Sandbox usage is free during the beta. Pricing may change later. ### Repository resources Repository resources are separate from agent tools. They are mounted only when `sandbox_enabled` is `true`. * v1 supports GitHub repositories only * Use a repository root URL like `https://github.com/org/repo` or `git@github.com:org/repo.git` * GitHub file, subdirectory, and `tree/...` URLs are not valid here * Saved responses never echo credentials back; they return metadata such as `auth_type`, `has_pat`, `has_github_app`, and `github_app_installation_id` Set `auth_type` to choose how the sandbox authenticates: * `pat`: send a GitHub Personal Access Token in `pat`. Required for a new PAT-backed resource unless one is already stored. * `github_app`: connect a GitHub App to the project first, then send `github_app_installation_id` for an active connection that grants access to the repository. * `public`: no credentials; clears any stored PAT or App connection. Manage which repositories the App can access in GitHub. Duplicating or importing a workflow across projects does not carry App connections over: the resource comes back with `imported_missing_github_app: true` and needs a connection selected before the workflow runs. ### Sandbox network Use `sandbox_network_mode` to control outbound network access from the sandbox: * `allow_all`: allow all outbound hosts * `allow_list`: only allow `sandbox_allowed_outbound_hosts` When GitHub repositories are attached, Kapso automatically adds the GitHub hosts needed to clone and read them. You only need to add extra hosts your workflow depends on. ### Example ```json theme={null} { "node_type": "agent", "config": { "system_prompt": "Inspect the repository and summarize the architecture.", "provider_model_id": "uuid", "max_iterations": 20, "message_delivery_mode": "auto_send_assistant_text", "sandbox_enabled": true, "sandbox_network_mode": "allow_list", "sandbox_allowed_outbound_hosts": ["api.example.com"], "flow_agent_resources": [ { "resource_type": "github_repository", "repo_url": "https://github.com/org/repo", "branch": "main", "auth_type": "pat", "pat": "github_pat_..." } ] } } ``` ## Custom tools Extend agent capabilities with external integrations. ### Webhook tools Call external APIs during agent execution. Configure URL, method, headers, and body with variable interpolation. ### Function tools Call deployed functions (Cloudflare Workers) as agent tools. Each function tool has: * **Name**: Tool identifier the agent calls (letters, numbers, underscores, dashes) * **Description**: Tells the agent when to use this tool * **Function**: Select a deployed function * **Input Schema**: Define the arguments the agent can pass **Payload structure:** ```json theme={null} { "input": { ... }, // tool arguments from the agent "execution_context": { ... }, // flow vars, system, context, metadata "flow_info": { ... }, // flow id, name, step_id "flow_events": [ ... ], // most recent 10 events "whatsapp_context": { ... } // present for WhatsApp runs } ``` The agent only controls `input`. Kapso automatically injects the rest. Read it in your function like this: ```javascript theme={null} async function handler(request, env) { const body = await request.json(); const input = body.input || {}; const executionContext = body.execution_context || {}; const vars = executionContext.vars || {}; return new Response(JSON.stringify({ vars: { customer_email: input.email || vars.customer_email || null } }), { headers: { "Content-Type": "application/json" } }); } ``` **Response format:** Return JSON. Include a `vars` object to update flow variables: ```json theme={null} { "vars": { "lead_saved": true, "lead_id": "abc123" } } ``` ### MCP servers MCP server URLs and headers support variable substitution: ``` # URL with variables https://api.example.com/mcp/{{system.customer.external_customer_id}} # Headers with env and context Authorization: Bearer ${ENV:MCP_API_KEY} X-Customer-Id: {{system.customer.id}} X-Phone: {{context.phone_number}} ``` Supported: `{{vars.*}}`, `{{system.*}}`, `{{context.*}}`, `${ENV:KEY}` URLs resolving to localhost or private IPs will fail in production (SSRF protection). ## Observer mode When a workflow runs with outbound messages disabled (`allow_outbound: false`), the agent operates in "observer mode". The `observer_prompt_mode` setting controls how the agent behaves: * `interactive_chat` (default): Agent chats with the operator via the Workflow Chat sidebar in the inbox. Use for workflows where human review or input is needed. * `analysis_only`: Agent runs non-interactively with no chat interface. Use for background analysis or logging workflows. The Workflow Chat sidebar appears in the inbox when viewing conversations with active observer-mode executions. ## Built-in tools Send a message to the user without waiting for a response. **Parameters:** * `message` (string, required): The text message to send **Usage:** Send progress updates, confirmations, or notifications Send media files to the user via WhatsApp. **Parameters:** * `media_url` (string, required): URL of the media file * `media_type` (string, required): "image", "video", "audio", or "document" * `caption` (string, optional): Caption for the media **Usage:** Share images, documents, or other media content Access flow execution context and variables. **Parameters:** None **Returns:** Flow variables, execution context, and metadata **Usage:** Access stored data and flow state information Get WhatsApp conversation details. **Parameters:** None **Returns:** Phone number, conversation ID, and contact information **Usage:** Access user contact details for personalization List or read older WhatsApp conversations for the current contact. **Parameters:** * `action` (string, required): Use `"list"` to return older conversations, then `"read"` to inspect one conversation * `conversation_id` (string, required for `read`): Conversation ID returned by `list` * `limit` (number, optional): Limits conversations for `list` or messages for `read` **Returns:** Matching contact details, prior conversation IDs, statuses, timestamps, and message snippets **Usage:** Give an agent context from earlier support conversations before it answers a returning WhatsApp contact Store data for use in later flow steps. **Parameters:** * `variable_name` (string, required): Variable name to save * `value` (string, required): Value to store. The tool receives this as a string and parses it using `value_type` * `value_type` (string, required): One of `string`, `integer`, `float`, `boolean`, `array`, or `object` **Type rules:** * Use `integer` for whole numbers, e.g. `value: "42"` * Use `float` for decimals, e.g. `value: "19.99"` * Use `boolean` with `value: "true"` or `value: "false"`; `yes`/`no`, `1`/`0`, and `on`/`off` are also accepted * Use `array` with a valid JSON array string, e.g. `value: "[\"small\", \"medium\"]"` * Use `object` with a valid JSON object string, e.g. `value: "{\"plan\":\"pro\",\"seats\":3}"` * Do not use the legacy `key` parameter **Usage:** Save user data, API responses, or calculated values Retrieve previously stored data. **Parameters:** * `variable_name` (string, required): Variable name to retrieve. Use `"*"` to retrieve all variables * `include_metadata` (boolean, optional): Include inferred type and size metadata Do not use the legacy `key` parameter. **Usage:** Access data saved in earlier steps Get the current date and time. **Parameters:** None **Returns:** Current timestamp in ISO format **Usage:** Time-based logic and timestamp generation Complete the agent's task and continue the flow. **Parameters:** None **Usage:** Signal task completion and advance to next step Transfer the conversation to a human agent. **Parameters:** * `reason` (string, optional): Reason for handoff **Usage:** Escalate complex issues to human support Pause the workflow execution and enter waiting state. The conversation can resume when the user sends a new message. **Parameters:** None **Usage:** Wait for user input mid-conversation without completing the task **How it works:** * Agent pauses execution at the current node * Workflow enters "waiting" state * When user sends a new message, workflow resumes and the agent continues from where it left off * Agent retains full conversation context after resuming **Note:** This tool is required by default for new workflows (created after Feb 5, 2026). Legacy workflows can enable it as an optional tool. Analyze files and answer questions about their content. Supports PDFs, images, text files, and Office documents (.docx, .xlsx, .pptx). **Parameters:** * `file_url` (string, required): Kapso file URL (use WhatsApp `media_data.url` from `get_whatsapp_context`) * `question` (string, required): What you want to know about the file **Returns:** Answer text, filename, MIME type **Limits:** * Max file size: 30MB * Office docs: Text extracted (DOCX paragraphs, XLSX first 10 sheets/50 rows, PPTX first 40 slides) * Legacy formats (.doc/.xls/.ppt) not supported - convert to modern format first **Usage:** Summarize documents, extract data from spreadsheets, analyze images Custom tools for external API integration. **Parameters:** Defined by webhook configuration **Usage:** Call external APIs, fetch data, trigger actions ## External inputs When a workflow is resumed via API or receives input from non-WhatsApp sources (e.g., Slack replies, API payloads), the agent automatically tags these as external inputs to distinguish them from direct user messages. **How it works:** External inputs are wrapped in `` tags when presented to the agent: ```xml theme={null} {"status": "approved", "comments": "Looks good"} ``` The agent's system prompt includes context that these inputs are from internal teams or external systems, not the WhatsApp user. This helps the agent: * Understand the input source * Adapt its tone (e.g., acknowledge internal team input differently than user messages) * Make better decisions about what to communicate to the end user **Triggering external inputs:** External inputs are automatically created when: * Resuming a workflow via the Platform API resume endpoint with a payload * Using Slack integration to provide internal team responses * Triggering workflows via API with initial data **Example workflow:** ```mermaid theme={null} graph LR A[API trigger] --> B[Agent
Process order] B --> C[Wait for approval] C --> D[Slack reply
External input] D --> B B --> E[Send confirmation] ``` When the Slack reply arrives, it's tagged as an external input so the agent knows it's from your team, not the customer. ## How it works 1. **Starts conversation**: Uses system prompt and conversation history 2. **Tool access**: Can call built-in tools and custom webhooks 3. **Multi-turn**: Continues until calls `complete_task` or needs user input 4. **Message injection**: New user messages are automatically injected during conversation 5. **External input tagging**: API payloads and non-WhatsApp inputs are wrapped in `` tags 6. **Workflow control**: Returns `next` edge when task completed, `wait` when needs input ## Usage patterns **Support workflow** ```mermaid theme={null} graph LR A[Wait for response] --> B[Agent
Handle request] B --> C[Send text
Resolution] B --> D[Handoff
To human] ``` **Data processing** ```mermaid theme={null} graph LR A[Agent
Get order info] --> B[Function
Update database] B --> C[Send text
Confirmation] ``` ## Workflow library example ```javascript theme={null} workflow.addNode("support_agent", { type: "agent", systemPrompt: "Help the customer resolve their support request. Be concise.", providerModel: "gpt-5", temperature: 0.1, maxIterations: 20, enabledDefaultTools: ["get_whatsapp_context", "contact_conversations", "complete_task"], functionTools: [ { name: "lookup_order", description: "Look up an order by order ID", functionSlug: "lookup-order", inputSchema: { type: "object", properties: { order_id: { type: "string" } }, required: ["order_id"] } } ], webhooks: [ { name: "create_ticket", description: "Create a support ticket", url: "https://api.example.com/tickets", method: "POST", headers: { "X-API-Key": "${ENV:SUPPORT_API_KEY}" }, bodyTemplate: { message: "{{last_user_input}}" } } ] }); ``` # Call workflow node Source: https://docs.kapso.ai/docs/flows/step-types/call-workflow-node Execute another workflow and return to continue execution Execute another workflow as a subroutine. The parent workflow pauses while the child workflow runs, then resumes with merged variables when the child completes. ## Configuration * `id`: Unique node identifier * `workflow_id`: ID of the workflow to call * `workflow_name`: Display name of called workflow (read-only) * `save_error_to`: Variable name to store error details if child workflow fails (optional, defaults to `subworkflow_error`) ## How it works 1. **Creates child execution**: Starts a new execution of the called workflow with copied context 2. **Pauses parent**: Parent workflow enters waiting state until child completes 3. **Merges variables**: When child finishes, its variables merge into parent's `vars` 4. **Handles errors**: If child fails, error details saved to configured variable 5. **Continues execution**: Parent resumes from next step after child completes ## Execution context The child workflow receives: * Copy of parent's `vars` (workflow variables) * Copy of parent's `system` variables * Copy of parent's `context` (phone\_number, channel, etc) * Same WhatsApp conversation if applicable Variable changes in the child automatically merge back to parent on completion. ## Error handling If the child workflow fails, error details are stored in the configured variable: ```json theme={null} { "error": "Workflow call cycle detected", "workflow_id": "abc123", "workflow_name": "Order Processing", "call_stack": ["flow1", "flow2", "flow3"] } ``` Common error scenarios: * **Cycle detection**: Workflow calls itself directly or indirectly * **Max depth exceeded**: Call stack exceeds 10 levels * **Non-executable workflow**: Called workflow is not published/active * **Insufficient credits**: Not enough credits to execute child workflow ## Safeguards **Recursion protection** * Detects circular calls (workflow A → workflow B → workflow A) * Maximum call depth of 10 workflows (prevents infinite recursion) * Errors stored in variables instead of failing entire workflow **Execution isolation** * Each call creates independent execution record * Parent and child executions visible in execution history * Child execution shows in parent's execution stack ## Usage patterns **Reusable order validation** ```mermaid theme={null} graph LR A[Wait for order] --> B[Call workflow
Validate Order] B --> C[Decide
Check error] C --> D[Send text
Success] C --> E[Send text
Fix errors] ``` **Multi-step authentication** ```mermaid theme={null} graph LR A[Start] --> B[Call workflow
Verify Phone] B --> C[Call workflow
Check Account] C --> D[Send template
Welcome] ``` **Conditional sub-processes** ```mermaid theme={null} graph LR A[Decide
User type] --> B[Call workflow
Premium Flow] A --> C[Call workflow
Basic Flow] B --> D[Send text
Complete] C --> D ``` ## Execution stack When workflows call other workflows, the platform maintains an execution stack visible in the UI and API responses: ```json theme={null} { "id": "parent_exec_123", "flow": { "name": "Main Workflow" }, "execution_stack": [ { "id": "parent_exec_123", "flow": { "name": "Main Workflow" }, "status": "waiting", "current_step": { "identifier": "call_order_validation" } }, { "id": "child_exec_456", "flow": { "name": "Order Validation" }, "status": "running", "current_step": { "identifier": "check_inventory" } } ] } ``` The execution stack shows the full call chain from root to currently executing workflow. ## Best practices **Design reusable workflows**: Create focused workflows that handle single responsibilities (validation, notifications, data processing). **Handle errors explicitly**: Always check the error variable after calling workflows that might fail. **Avoid deep nesting**: Keep call depth under 3-4 levels for maintainability. **Share via variables**: Use `vars` to pass data between workflows - changes in child workflows automatically merge back. **Test call chains**: Use test mode to verify the full execution path including all child workflows. ## Workflow library example Use workflow slugs in local workflow source. `kapso push` resolves the slug to the Platform API `workflow_id`. ```javascript theme={null} workflow.addNode("run_support_flow", { type: "call", workflowSlug: "support-flow", saveErrorTo: "support_flow_error" }); ``` # Decide node Source: https://docs.kapso.ai/docs/flows/step-types/decide-node Route workflows with AI or custom logic Routes the workflow to different paths based on AI analysis or your own routing logic. ## Decision modes Choose between two decision types: * **AI-Powered**: AI interprets user intent from conversation * **Function (Custom Code)**: Run your own routing logic - business rules, data checks, API calls, etc. ## Configuration ### Common parameters * `id`: Unique node identifier * `conditions`: List of possible paths with labels and descriptions ### AI-powered mode * `decision_type`: Set to `"ai"` (default) * `provider_model_name`: AI model to use for decision making * `llm_temperature`: Model creativity, 0.0-1.0 (default: 0.0) * `llm_max_tokens`: Maximum tokens for response (default: 10000) ### Function mode * `decision_type`: Set to `"function"` * `function_id`: ID of the deployed Kapso Function to execute ## How it works ### AI-powered mode 1. **Analyzes conversation**: Reviews recent WhatsApp message history 2. **Evaluates conditions**: Uses AI to match user intent against condition descriptions 3. **Returns label**: Chooses the best matching condition label for routing 4. **Routes workflow**: Uses the label to follow the matching outgoing edge 5. **Fallback**: Uses first condition if AI evaluation fails ### Function mode 1. **Executes function**: Calls your deployed Kapso Function with current workflow state 2. **Your logic runs**: Function executes your custom logic - check data, call APIs, apply rules, etc. 3. **Returns decision**: Function returns JSON with `next_edge` set to a condition label 4. **Routes workflow**: Uses the returned label to follow the matching outgoing edge 5. **Updates variables**: Optionally updates workflow variables if function returns `vars` **Important**: Condition labels must exactly match your outgoing edge labels for proper routing in both modes. ## Function decision contract When using function mode, your function receives the standard execution payload plus an `available_edges` array containing all possible edge labels from your conditions. ### Request payload ```typescript theme={null} { execution_context: { vars: {...}, // Workflow variables system: {...}, // System data context: {...}, // Execution context metadata: {...} // Metadata }, flow_events: [...], // Recent workflow events (last 10) flow_info: { id: "flow_abc123", // Flow ID name: "My Flow", // Flow name step_id: "step_xyz" // Current step ID }, available_edges: [ // Condition labels from your DecideNode "premium", "standard", "trial" ], whatsapp_context: { // Only present when triggered by WhatsApp conversation: { id: "conv_123", phone_number: "+1234567890", status: "open", last_active_at: "2024-01-15T10:30:00Z", whatsapp_config_id: "config_abc", metadata: {}, created_at: "2024-01-10T08:00:00Z", updated_at: "2024-01-15T10:30:00Z" }, messages: [ // All messages, ordered by created_at (oldest first) { id: "msg_001", message_type: "text", // text, image, video, document, audio, location, interactive, template, reaction, contacts content: "Hello!", direction: "inbound", // inbound (from user), outbound (from bot) status: "delivered", processing_status: "processed", whatsapp_message_id: "wamid_abc", origin: "cloud_api", phone_number: "+1234567890", has_media: false, reply_option_id: null, // Button/list ID when user clicks interactive reply reply_option_title: null, // Button/list title when user clicks interactive reply interactive_type: null, // button_reply, list_reply, nfm_reply (for interactive responses) interactive_data: null, // Full interactive payload created_at: "2024-01-15T10:00:00Z", updated_at: "2024-01-15T10:00:00Z" } ] } } ``` ### Response format Your function must return JSON with `next_edge` set to one of the labels from `available_edges`: ```typescript theme={null} { next_edge: "premium", // REQUIRED: Must match a condition label vars: { // OPTIONAL: Updated workflow variables customer_tier: "premium", discount_applied: true } } ``` ### Example function ```javascript theme={null} async function handler(request, env) { const body = await request.json(); const availableEdges = body?.available_edges || []; const executionContext = body?.execution_context || {}; const vars = executionContext.vars || {}; const context = executionContext.context || {}; // Your custom routing logic - can be anything: // 1. Business rules const accountAge = vars.account_age_days || 0; const totalSpent = vars.lifetime_value || 0; // 2. External API check (optional) // const res = await fetch(`https://api.example.com/users/${context.phone_number}/tier`); // const userTier = await res.json(); // 3. Time-based logic // const hour = new Date().getHours(); // if (hour < 9 || hour > 17) nextEdge = "closed"; let nextEdge = availableEdges[0] || "default"; if (totalSpent > 1000 && accountAge > 365) { nextEdge = "premium"; } else if (accountAge > 30) { nextEdge = "standard"; } else { nextEdge = "trial"; } return new Response(JSON.stringify({ next_edge: nextEdge, vars: { customer_tier: nextEdge, routing_timestamp: new Date().toISOString() } }), { headers: { "Content-Type": "application/json" } }); } ``` ### Accessing WhatsApp data When your flow is triggered by a WhatsApp message, use `whatsapp_context` to access conversation data: ```javascript theme={null} async function handler(request, env) { const body = await request.json(); const availableEdges = body?.available_edges || []; const whatsappContext = body?.whatsapp_context; // Guard: ensure WhatsApp context exists if (!whatsappContext) { return new Response(JSON.stringify({ next_edge: availableEdges[0] }), { headers: { "Content-Type": "application/json" } }); } const { conversation, messages } = whatsappContext; // Get user's phone number const userPhone = conversation.phone_number; // Get the last message const lastMessage = messages[messages.length - 1]; const lastContent = lastMessage?.content || ""; const lastDirection = lastMessage?.direction; // "inbound" or "outbound" // Count inbound messages const userMessages = messages.filter(m => m.direction === "inbound"); // Check for media const hasMedia = messages.some(m => m.has_media); // Access interactive reply data (button/list clicks) const buttonId = lastMessage?.reply_option_id; const buttonTitle = lastMessage?.reply_option_title; // Route based on conversation state let nextEdge = "returning_user"; if (userMessages.length < 2) { nextEdge = "new_user"; } return new Response(JSON.stringify({ next_edge: nextEdge, vars: { user_phone: userPhone } }), { headers: { "Content-Type": "application/json" } }); } ``` ### Routing by button ID Common pattern: route based on which button the user clicked. ```javascript theme={null} async function handler(request, env) { const body = await request.json(); const availableEdges = body?.available_edges || []; const messages = body?.whatsapp_context?.messages || []; // Get the last inbound message const lastInbound = [...messages].reverse().find(m => m.direction === "inbound"); // Get button ID from interactive reply const buttonId = lastInbound?.reply_option_id; // Route by button ID (button IDs often match edge labels) let nextEdge = availableEdges[0]; if (buttonId && availableEdges.includes(buttonId)) { nextEdge = buttonId; } return new Response(JSON.stringify({ next_edge: nextEdge, vars: { selected_button: buttonId } }), { headers: { "Content-Type": "application/json" } }); } ``` **Tip**: Set your button `id` values to match your condition labels for direct routing. ## Usage patterns **Question → Wait → Decide → Route** ```mermaid theme={null} graph LR A[Send text
Ask question] --> B[Wait for response] B --> C[Decide
Route by intent] C --> D[Send text
Support path] C --> E[Send text
Sales path] ``` **Interactive → Wait → Decide** ```mermaid theme={null} graph LR A[Send interactive
Menu options] --> B[Wait for response] B --> C[Decide
Route by selection] ``` ## Workflow library example AI decision: ```javascript theme={null} workflow.addNode("classify_intent", { type: "decide", decisionType: "ai", providerModel: "gpt-5-mini", conditions: [ { label: "sales", description: "The user wants to buy or compare plans" }, { label: "support", description: "The user needs help with an existing account" } ] }); workflow.addEdge("classify_intent", "sales_reply", { label: "sales" }); workflow.addEdge("classify_intent", "support_reply", { label: "support" }); ``` Function decision: ```javascript theme={null} workflow.addNode("route_with_code", { type: "decide", decisionType: "function", functionSlug: "classify-message", conditions: [ { label: "urgent", description: "Needs immediate human attention" }, { label: "normal", description: "Can be handled automatically" } ] }); ``` # Emit event node Source: https://docs.kapso.ai/docs/flows/step-types/emit-event-node Record a project event from a workflow Records a project event and then advances through the `next` edge. Use this node for outcomes, scores, labels, or workflow-derived facts that should be queryable later or trigger other workflows. ## Configuration * `event_name`: Project event name to emit * `properties`: Flat scalar JSON properties to store with the event * `occurred_at`: Optional ISO 8601 timestamp or workflow variable. Leave blank to use the current time. Event names can be any non-empty string. We recommend lowercase dotted snake case, such as `conversation.csat_scored`, to keep names consistent. Properties can contain strings, numbers, booleans, or `null`. ## API payload shape When using the Platform API directly, use snake\_case keys: ```json theme={null} { "node_type": "emit_event", "config": { "event_name": "conversation.csat_scored", "properties": { "score": 5, "source": "workflow" }, "occurred_at": "{{vars.scored_at}}" } } ``` ## Limits and behavior * A workflow execution can emit at most 10 project events. * A workflow execution can emit at most 3 events with the same name. * Workflows started by project event triggers cannot emit project events. * In test mode, event emission is validated and shown in workflow events, but project events are not stored. See [Events](/docs/platform/events) for event format, retention, querying, webhooks, and plan limits. ## Usage patterns **Record conversation outcome** ```mermaid theme={null} graph LR A[Function
Score conversation] --> B[Emit event
conversation.csat_scored] B --> C[End] ``` **Start a follow-up workflow** ```mermaid theme={null} graph LR A[Decide
Qualified?] --> B[Emit event
lead.qualified] B --> C[Send text
Confirmation] ``` ## Workflow library example ```javascript theme={null} workflow.addNode("record_csat", { type: "emit_event", eventName: "conversation.csat_scored", properties: { score: "{{vars.csat_score}}", source: "workflow" } }); ``` # Function node Source: https://docs.kapso.ai/docs/flows/step-types/function-node Execute custom JavaScript functions in your workflow Execute custom JavaScript functions deployed on Cloudflare Workers. Functions can process data, integrate with external APIs, and update workflow variables. ## Configuration * `id`: Unique node identifier * `function_id`: ID of deployed function to execute * `save_response_to`: Variable name to store function response (optional) ## Function context Kapso sends this JSON body to your function. Read it with `await request.json()`: ```javascript theme={null} { execution_context: { vars: {}, // Workflow variables system: {}, // System info (flow_id, started_at, etc) context: {} // Channel info (phone_number, etc) }, flow_events: [], // Recent workflow events (last 10) flow_info: { id: "flow_123", name: "Customer Support", step_id: "current_step_456" }, whatsapp_context: { // Only if WhatsApp workflow conversation: {}, messages: [] } } ``` Your function still uses the standard runtime signature: ```javascript theme={null} async function handler(request, env) { const body = await request.json(); const executionContext = body.execution_context || {}; const vars = executionContext.vars || {}; return new Response(JSON.stringify({ vars: { normalized_email: (vars.email || "").trim().toLowerCase() } }), { headers: { "Content-Type": "application/json" } }); } ``` ## Function response Functions can return JSON to update the workflow: ```javascript theme={null} return new Response(JSON.stringify({ vars: { user_score: 85, validated: true }, next_edge: "success" // Optional: suggest next workflow path })) ``` Functions can also emit project events: ```javascript theme={null} return new Response(JSON.stringify({ vars: { lead_score: 92 }, project_events: [ { name: "lead.qualified", properties: { score: 92, source: "workflow" } } ] })) ``` Project events follow the same validation rules as the Events API. A Function node response can emit at most 5 project events. See [Events](/docs/platform/events) for limits and caveats. ## How it works 1. **Invokes function**: Calls your deployed function with workflow context 2. **Processes response**: Updates workflow variables from function response 3. **Continues workflow**: Advances to next step via `next` edge 4. **Saves data**: Optionally stores full response in specified variable ## Usage patterns **Data validation** ```mermaid theme={null} graph LR A[Wait for response] --> B[Function
Validate input] B --> C[Send text
Confirmation] B --> D[Send text
Error message] ``` **API integration** ```mermaid theme={null} graph LR A[Function
Get user data] --> B[Function
Process order] B --> C[Send template
Order confirmation] ``` **Conditional processing** ```mermaid theme={null} graph LR A[Function
Calculate score] --> B[Decide
Score check] B --> C[Send text
Approved] B --> D[Send text
Rejected] ``` ## Workflow library example Use function slugs in local workflow source. `kapso push` resolves the slug to the Platform API `function_id`. ```javascript theme={null} workflow.addNode("normalize_phone", { type: "function", functionSlug: "normalize-phone", saveResponseTo: "normalized_phone" }); ``` # Handoff node Source: https://docs.kapso.ai/docs/flows/step-types/handoff-node Transfer workflow execution to human agents Immediately stops workflow execution and transfers the conversation to human agents. Sets the workflow status to 'handoff' and prevents further automated processing. ## Configuration * `id`: Unique node identifier No other configuration needed - handoff nodes simply trigger the transfer. ## How it works 1. **Stops execution**: Immediately halts automated workflow processing 2. **Sets status**: Changes workflow execution status from 'running' to 'handoff' 3. **Prevents messages**: Blocks further automated message processing 4. **Requires intervention**: Human agents must manually handle the conversation ## Usage patterns **Escalation workflow** ```mermaid theme={null} graph LR A[Wait for response] --> B[Decide
Issue complexity] B --> C[Send text
Solution] B --> D[Handoff
To human] ``` ## Workflow library example ```javascript theme={null} workflow.addNode("handoff_to_team", { type: "handoff", reason: "needs_human", contextData: { priority: "{{vars.priority}}" } }); ``` # Send interactive Source: https://docs.kapso.ai/docs/flows/step-types/send-interactive-node Send interactive WhatsApp messages Sends interactive messages (buttons, lists, CTAs) to users via WhatsApp. Free-form carousel interactive messages are not available in workflow `send_interactive` nodes. Use the Meta proxy or TypeScript SDK message API for `interactive.type = "carousel"`. ## Configuration * `whatsapp_config_id`: WhatsApp connection to use (optional; falls back to the conversation) * `phone_number_id`: WhatsApp Business phone number ID to use (preferred for local/API source) * `interactive_type`: button, list, cta\_url, flow, product, product\_list, catalog\_message, location\_request\_message * `body_text`: Message content (string or AIField) * `header_type`: none, text, image, video, document (optional) * `header_text`: Header text for text headers (optional) * `header_media_url`: Media URL for image/video/document headers (optional) * `footer_text`: Footer text (optional) * `provider_model_name`: Required when using AIField * `to_phone_number`: Override destination phone number (optional; required for observer mode flows) Type-specific options: * `buttons`: For button type * `list_button_text`, `list_sections`: For list type * `cta_display_text`, `cta_url`: For cta\_url type * `flow_id`, `flow_cta`, `flow_token`, `flow_action`, `flow_action_payload`: For flow type ## Interactive types * **button**: Up to 3 reply buttons * **list**: Dropdown menu with sections * **cta\_url**: Single button that opens URL * **flow**: WhatsApp Flow for data collection * **product**: Single product from catalog * **product\_list**: Multiple products from catalog * **catalog\_message**: Full catalog browser * **location\_request\_message**: Request user's current location ## Usage patterns **Decision collection** ```mermaid theme={null} graph LR A[Send interactive
Button choices] --> B[Wait for Response] B --> C[Decide
Route by selection] ``` **Menu system** ```mermaid theme={null} graph LR A[Send interactive
Main menu] --> B[Wait] --> C[Send interactive
Submenu] ``` ## Workflow library example ```javascript theme={null} workflow.addNode("show_menu", { type: "send_interactive", interactiveType: "button", phoneNumberId: "", bodyText: "What do you need help with?", buttons: [ { id: "billing", title: "Billing" }, { id: "support", title: "Support" } ] }); ``` ### Flow type options * `flow_id`: The WhatsApp Flow ID to send * `flow_cta`: Button text to open the flow * `flow_token`: Correlation ID for tracking responses (supports variable interpolation; defaults to flow\_id if not provided) * `flow_action`: Action to perform when opening the flow - `navigate` (default) or `data_exchange` * `flow_action_payload`: Initial screen and data to pass to the flow (supports variable interpolation) Example with dynamic data: ```json theme={null} { "node_type": "send_interactive", "config": { "interactive_type": "flow", "body_text": "Complete your checkout", "flow_id": "123456789012345", "flow_cta": "Continue", "flow_token": "order_{{vars.order_id}}", "flow_action": "navigate", "flow_action_payload": { "screen": "CHECKOUT", "data": { "phone_number": "{{context.phone_number}}", "order_total": "{{vars.cart_total}}" } } } } ``` Variables like `{{context.phone_number}}`, `{{vars.cart_total}}`, and `{{vars.order_id}}` are automatically substituted with values from the workflow execution context. Use dynamic `flow_token` values to correlate flow responses with specific workflow instances or business entities. # Send template Source: https://docs.kapso.ai/docs/flows/step-types/send-template-node Send WhatsApp template messages Sends WhatsApp template messages. Templates must be pre-approved by Meta. ## Configuration * `whatsapp_config_id`: WhatsApp connection to use (optional; falls back to the conversation) * `phone_number_id`: WhatsApp Business phone number ID to use (preferred for local/API source) * `template_id`: WhatsApp template identifier * `parameters`: Template parameter values (see formats below) * `provider_model_name`: Required when using AIField in parameters * `to_phone_number`: Override destination phone number (optional; required for observer mode flows) ## Parameter formats The `parameters` field supports two formats: ### Meta components format (recommended) Use Meta's native components structure for full control over template parameters: ```json theme={null} [ { "type": "BODY", "parameters": [ { "type": "text", "text": "{{user_name}}" }, { "type": "text", "text": "{{order_id}}" } ] } ] ``` **Multi-component example** (header + body + buttons): ```json theme={null} [ { "type": "HEADER", "parameters": [ { "type": "text", "text": "{{customer_name}}" } ] }, { "type": "BODY", "parameters": [ { "type": "text", "text": "{{order_number}}" }, { "type": "text", "text": "{{order_total}}" } ] }, { "type": "BUTTON", "sub_type": "URL", "index": "0", "parameters": [ { "type": "text", "text": "{{tracking_id}}" } ] } ] ``` **Named parameters**: ```json theme={null} [ { "type": "BODY", "parameters": [ { "type": "text", "parameter_name": "customer_name", "text": "{{user_name}}" }, { "type": "text", "parameter_name": "order_id", "text": "{{order_number}}" } ] } ] ``` Alternative hash format using `components` key: ```json theme={null} { "components": [...] } ``` ### Legacy format Simple array or hash format (still supported): ```json theme={null} ["{{user_name}}", "{{order_id}}"] ``` Or with explicit template\_params key: ```json theme={null} { "template_params": ["{{user_name}}", "{{order_id}}"] } ``` ## Template setup Templates must be created and approved in Meta Business Manager first. Common templates: * Welcome messages * Order confirmations * Appointment reminders * Support ticket updates ## Usage patterns **Order workflow** ```mermaid theme={null} graph LR A[Function
Get order] --> B[Send template
Order confirmation] B --> C[Wait for Response] ``` **Notification workflow** ```mermaid theme={null} graph LR A[API Trigger] --> B[Send template
Alert message] ``` ## Workflow library example ```javascript theme={null} workflow.addNode("send_order_template", { type: "send_template", templateId: "", phoneNumberId: "", parameters: [ { type: "BODY", parameters: [ { type: "text", text: "{{vars.customer_name}}" }, { type: "text", text: "{{vars.order_id}}" } ] } ] }); ``` # Send text Source: https://docs.kapso.ai/docs/flows/step-types/send-text-node Send WhatsApp text messages Sends text messages to users via WhatsApp. ## Configuration * `whatsapp_config_id`: WhatsApp connection to use (optional; falls back to the conversation) * `phone_number_id`: WhatsApp Business phone number ID to use (preferred for local/API source) * `message`: Text content (string or AIField) * `provider_model_name`: Required when using AIField * `to_phone_number`: Override destination phone number (optional; required for observer mode flows) ## Variable interpolation Use variables in messages: ``` Hi {{customer_name}}, your order {{order_id}} is ready! ``` ## Usage patterns **Welcome message** ```mermaid theme={null} graph LR A[Start] --> B[Send Text
Welcome] B --> C[Wait for Response] ``` **Response after decision** ```mermaid theme={null} graph LR A[Decide] --> B[Send Text
Support info] A --> C[Send Text
Sales info] ``` ## Workflow library example ```javascript theme={null} workflow.addNode("send_intro", { type: "send_text", message: "Hi {{vars.customer_name}}, how can we help?", phoneNumberId: "" }); ``` # Set variable node Source: https://docs.kapso.ai/docs/flows/step-types/set-variable-node Store a workflow variable during execution Sets or replaces a workflow variable and then advances through the `next` edge. ## Configuration * `variable_name`: Variable name to set * `variable_value`: Value to store * `value_type`: string, number, boolean, or json ## API payload shape When using the Platform API directly, use snake\_case keys: ```json theme={null} { "node_type": "set_variable", "config": { "variable_name": "priority", "variable_value": "high", "value_type": "string" } } ``` ## Usage patterns **Store routing state** ```mermaid theme={null} graph LR A[Decide] --> B[Set variable
priority = high] B --> C[Handoff] ``` **Prepare data for a function** ```mermaid theme={null} graph LR A[Set variable] --> B[Function] ``` ## Workflow library example ```javascript theme={null} workflow.addNode("mark_priority", { type: "set_variable", variableName: "priority", variableValue: "high", valueType: "string" }); ``` # Start Source: https://docs.kapso.ai/docs/flows/step-types/start-node Entry point for every workflow Entry point of the workflow. Automatically created with new workflows. ## Configuration No configuration required. ## Behavior * Always the first node in a workflow * Automatically advances to the next connected node * Cannot be deleted * Only one Start node per workflow ## Usage Connect the Start node to your first action: ```mermaid theme={null} graph LR A[Start] --> B[Send Text] B --> C[Wait for Response] ``` The Start node triggers when: * User sends first message (WhatsApp trigger) * API call initiates workflow (API trigger) ## Workflow library example ```javascript theme={null} import { START, Workflow } from "@kapso/workflows"; const workflow = new Workflow("welcome-flow", { name: "Welcome Flow" }); workflow.addNode(START, { position: { x: 100, y: 100 } }); export default workflow; ``` # Wait for response Source: https://docs.kapso.ai/docs/flows/step-types/wait-for-response-node Wait for user input before continuing Pauses workflow execution until the user sends a message. Optionally configure a timeout to automatically continue if no response is received. ## Configuration * `id`: Unique node identifier * `save_response_to`: Variable name to store user response (optional) * `has_timeout`: Enable automatic timeout (optional) * `timeout_seconds`: Timeout duration in seconds, between 10 and 604800 (7 days) ## Storing responses User responses are automatically saved to `{{last_user_input}}`. Use `save_response_to` to store the response in a custom variable. Response content is saved as text. ## Timeout behavior When timeout is enabled: * Workflow automatically continues after the specified duration if no user response is received * `{{system.last_resume.reason}}` is set to `"timeout"` (vs `"user_input"` for normal responses) * No value is written to `{{last_user_input}}` or custom response variables on timeout * Use a Decide node after the wait to branch based on `{{system.last_resume.reason}}` ### Detecting timeouts Check why the workflow resumed: ``` {{system.last_resume.reason}} # "user_input" or "timeout" ``` If a user responds before the timeout, the timeout is automatically cancelled. ## Usage patterns **Question → Wait → Route** ```mermaid theme={null} graph LR A[Send message] --> B[Wait for response] --> C[Decide] ``` **Menu → Wait → Process** ```mermaid theme={null} graph LR A[Send interactive] --> B[Wait for response] --> C[Function] ``` **Custom variable** ```mermaid theme={null} graph LR A[Send text
Ask for name] --> B[Wait
save_response_to: user_name] --> C[Function
Process {{user_name}}] ``` ## Workflow library example ```javascript theme={null} workflow.addNode("wait_for_reply", { type: "wait_for_response", saveResponseTo: "latest_reply", timeoutSeconds: 300 }); ``` # Webhook node Source: https://docs.kapso.ai/docs/flows/step-types/webhook-node Call external HTTP APIs from a workflow Calls an external HTTP endpoint, optionally stores the response, and then advances through the `next` edge. ## Configuration * `url`: HTTP endpoint to call * `method`: HTTP method, defaults to `POST` * `headers`: Header object * `body_template`: JSON body template * `save_response_to`: Variable name to store the response (optional) * `provider_model_name`: Required when using AIField * `ai_field_config`: AI field-resolution settings ## API payload shape When using the Platform API directly, use snake\_case keys: ```json theme={null} { "node_type": "webhook", "config": { "url": "https://api.example.com/tickets", "method": "POST", "headers": { "X-API-Key": "${ENV:SUPPORT_API_KEY}" }, "body_template": { "phone": "{{context.phone_number}}", "message": "{{last_user_input}}" }, "save_response_to": "ticket" } } ``` ## Usage patterns **Create CRM record** ```mermaid theme={null} graph LR A[Wait for response] --> B[Webhook
Create lead] B --> C[Send text
Confirmation] ``` **Notify another system** ```mermaid theme={null} graph LR A[Decide
Urgent?] --> B[Webhook
Page team] B --> C[Handoff] ``` ## Workflow library example ```javascript theme={null} workflow.addNode("create_ticket", { type: "webhook", url: "https://api.example.com/tickets", method: "POST", headers: { "X-API-Key": "${ENV:SUPPORT_API_KEY}" }, bodyTemplate: { phone: "{{context.phone_number}}", message: "{{last_user_input}}" }, saveResponseTo: "ticket" }); ``` # Triggers Source: https://docs.kapso.ai/docs/flows/triggers Start workflow execution from WhatsApp, project events, or API calls Triggers define how workflows are initiated. Workflows can be triggered by incoming WhatsApp messages, project events, or external API calls. ## Trigger types **WhatsApp message trigger** * Starts workflow when message received on specific WhatsApp number * Intercepts messages before they reach agents * Provides message content and user context to workflow **WhatsApp event trigger** * Starts workflow when WhatsApp events occur (message and conversation lifecycle) * Runs in observer mode (read-only, no outbound messages unless overridden) * Access event data, conversation state, and message details **API trigger** * Starts workflow via HTTP POST request * Programmatic workflow execution from external systems * Pass custom variables and context data **Project event trigger** * Starts workflow when a custom project event is emitted * Optionally filters by one event property * Runs in observer mode (read-only, no outbound messages unless overridden) For configuration, available context, and caveats, see [Use project events in workflows](/docs/flows/project-events). ## WhatsApp message trigger When active, starts the workflow when messages are received on the configured WhatsApp number. **Configuration** * Select WhatsApp configuration (phone number) * Only one workflow can have an active WhatsApp trigger per number **Available context** ``` # Access in workflow via variables {{context.phone_number}} # User's WhatsApp number {{last_user_input}} # The received message text {{context.channel}} # "whatsapp" {{context.conversation_id}} # WhatsApp conversation ID ``` ## WhatsApp event trigger Starts workflows when WhatsApp message and conversation events occur. Workflows run in observer mode: they can read event data but cannot send messages unless a step explicitly overrides the destination phone number. **Configuration** * Select event type (message or conversation event) * Optionally scope to specific WhatsApp number * Only one workflow per event type per number **Available events** Message events: * `whatsapp.message.received` - New message from customer * `whatsapp.message.sent` - Message sent to WhatsApp * `whatsapp.message.failed` - Message delivery failed Conversation events: * `whatsapp.conversation.created` - New conversation initiated * `whatsapp.conversation.ended` - Conversation closed **Observer mode** Event-triggered workflows run in observer mode: * Cannot send outbound messages by default * Send steps (text, template, interactive) are skipped * To send messages: configure "To phone number" in send step **Available context** ``` # System variables {{system.trigger_type}} # "whatsapp_event" {{system.observer_mode}} # true {{system.allow_outbound}} # false # Event data {{system.event.type}} # "whatsapp.message.sent" {{system.event.payload}} # Full event payload {{system.event.conversation.id}} # Conversation ID {{system.event.conversation.phone_number}} # Customer phone number {{system.event.message.id}} # Message ID (for message events) {{system.event.message.text.body}} # Message text (when applicable) ``` **Testing event triggers** Test event triggers from the Flow Test modal: * Select trigger type: "WhatsApp Event" * Choose conversation ID * Select event type * Test execution runs in observer mode * Use phone number override in send steps to test outbound messages ## API trigger Execute workflows programmatically via HTTP API. Perfect for integrating workflows with external systems, webhooks, or custom applications. **Endpoint** ``` POST /platform/v1/workflows/{workflow_id}/executions X-API-Key: {api_key} Content-Type: application/json ``` **Request parameters** ```json theme={null} { "workflow_execution": { "phone_number": "+1234567890", // Required "phone_number_id": "123456789012345", // Optional - WhatsApp phone number ID (preferred) "whatsapp_config_id": 123, // Optional - Deprecated, use phone_number_id "variables": { // Optional workflow variables "customer_name": "John Doe", "order_id": "ORDER-123", "priority": "high" }, "context": { // Optional context data "source": "website", "campaign": "summer_promo" }, "initial_data": { // Optional initial data "user_preferences": {"language": "en"} } } } ``` **phone\_number\_id validation** * Must belong to your project (direct or via customer) * Returns 422 error if invalid or doesn't belong to project * When omitted, uses project's default WhatsApp config * Preferred over deprecated `whatsapp_config_id` **Response** ```json theme={null} { "data": { "message": "Workflow execution initiated", "workflow_id": "workflow_uuid", "id": "execution_uuid", "tracking_id": "tracking_uuid" } } ``` **Burst rate limit** * This endpoint has an additional burst limiter scoped by API key and `workflow_id` * `legacy` / `free`: 5 requests per second * `pro`: 15 requests per second * `enterprise` / `platform`: 30 requests per second * Successful responses include `X-Burst-RateLimit-Limit` and `X-Burst-RateLimit-Remaining` * If exceeded, the API returns `429 Too Many Requests` with `Retry-After: 1` * General platform API rate limits still apply **Example API call** ```bash theme={null} curl -X POST "https://api.kapso.ai/platform/v1/workflows/your-workflow-id/executions" \ -H "X-API-Key: your-api-key" \ -H "Content-Type: application/json" \ -d '{ "workflow_execution": { "phone_number": "+1234567890", "phone_number_id": "123456789012345", "variables": { "customer_name": "Alice Smith", "order_total": 149.99 }, "context": { "source": "checkout_flow" } } }' ``` ## Workflow context Triggered workflows receive context data based on the trigger type: **WhatsApp message trigger context** ``` # System variables {{system.trigger_type}} # "inbound_message" {{system.trigger_whatsapp_config_id}} # WhatsApp config ID {{system.workflow_id}} # Current workflow ID # Context variables {{context.phone_number}} # "+1234567890" {{context.channel}} # "whatsapp" {{context.conversation_id}} # WhatsApp conversation ID # Workflow variables {{last_user_input}} # The received message text ``` **WhatsApp event trigger context** ``` # System variables {{system.trigger_type}} # "whatsapp_event" {{system.observer_mode}} # true {{system.allow_outbound}} # false {{system.workflow_id}} # Current workflow ID # Event data {{system.event.type}} # "whatsapp.message.sent" {{system.event.payload}} # Full event payload {{system.event.conversation.id}} # Conversation ID {{system.event.conversation.phone_number}} # "+1234567890" {{system.event.conversation.status}} # "active" or "ended" {{system.event.message.id}} # Message ID (for message events) {{system.event.message.type}} # "text", "image", etc. {{system.event.message.text.body}} # Message text (when applicable) ``` **API trigger context** ``` # System variables {{system.trigger_type}} # "api_call" {{system.tracking_id}} # Unique execution tracking ID {{system.api_key_id}} # API key used {{system.workflow_id}} # Current workflow ID {{system.trigger_whatsapp_config_id}} # WhatsApp config used (if provided) # Context variables {{context.phone_number}} # "+1234567890" (from request, normalized) {{context.channel}} # "api" # Custom variables (from request) {{your_variable_name}} # From request variables object {{vars.your_variable_name}} # Explicit vars namespace # Request metadata (in metadata namespace) {{metadata.request_ip}} # API caller IP {{metadata.request_timestamp}} # Request time ``` ## Managing triggers Triggers are managed via the web interface: 1. Go to Workflow settings 2. Add a WhatsApp, project event, or API trigger 3. Configure trigger settings 4. Activate/deactivate as needed See [Use project events in workflows](/docs/flows/project-events) for project event triggers. See [Project events](/docs/platform/events) for emitting and querying events through the API. # Variables and context Source: https://docs.kapso.ai/docs/flows/variables-and-context Data management throughout workflow execution Every workflow execution maintains data in four organized namespaces. This data flows between nodes, gets updated by user interactions, and drives workflow logic. ## Data structure **`vars` namespace** - User-defined variables * Read/write access * Store custom data, user responses, API results * Persists throughout workflow execution **`system` namespace** - System-managed data * Read-only access * Workflow metadata, timing, execution details * Automatically maintained by the platform **`context` namespace** - Contextual information * Mostly read-only (set at workflow start) * Channel info, user details, trigger context * Provides execution environment details **`metadata` namespace** - Request metadata * Read-only access * API request details, timestamps, caller info * Available for API-triggered workflows ## Initial data **WhatsApp trigger workflow starts with:** ``` # System variables {{system.trigger_type}} # "inbound_message" {{system.workflow_id}} # Workflow UUID {{system.started_at}} # ISO timestamp # WhatsApp config data {{system.whatsapp_config.id}} # WhatsApp config ID {{system.whatsapp_config.phone_number_id}} # Meta phone number ID {{system.whatsapp_config.display_phone_number}} # Host phone number (formatted) {{system.whatsapp_config.display_phone_number_normalized}} # Host phone (normalized) {{system.whatsapp_config.business_account_id}} # WhatsApp Business Account ID {{system.whatsapp_config.name}} # Config name {{system.whatsapp_config.kind}} # "production" or "sandbox" # Customer data (when WhatsApp config is linked to a Customer) {{system.customer.id}} # Customer UUID {{system.customer.external_customer_id}} # Your external customer ID {{system.customer.name}} # Customer name # Context variables {{context.phone_number}} # User's phone number "+1234567890" {{context.whatsapp_business_scoped_user_id}} # User BSUID (may be null) {{context.whatsapp_parent_business_scoped_user_id}} # Parent BSUID (may be null) {{context.whatsapp_username}} # WhatsApp username (may be null) {{context.channel}} # "whatsapp" {{context.conversation_id}} # WhatsApp conversation ID # Contact data {{context.contact.id}} # Contact UUID (when known) {{context.contact.wa_id}} # Normalized WhatsApp ID {{context.contact.business_scoped_user_id}} # User BSUID (may be null) {{context.contact.parent_business_scoped_user_id}} # Parent BSUID (may be null) {{context.contact.username}} # WhatsApp username (may be null) {{context.contact.name}} # Contact name (or phone number fallback) {{context.contact.profile_name}} # WhatsApp profile name (may be null) {{context.contact.display_name}} # Display name (may be null) # Workflow variables {{last_user_input}} # Initial message content ``` **API trigger workflow starts with:** ``` # System variables {{system.trigger_type}} # "api_call" {{system.tracking_id}} # Unique execution ID {{system.api_key_id}} # API key used {{system.workflow_id}} # Workflow UUID {{system.trigger_whatsapp_config_id}} # WhatsApp config ID (if provided in request) # Context variables {{context.phone_number}} # From request (normalized) {{context.channel}} # "api" # Custom variables (from API request) {{your_variable}} # From variables object {{another_variable}} # From variables object # Metadata {{metadata.request_ip}} # Caller IP {{metadata.request_timestamp}} # Request time ``` **WhatsApp event trigger workflow starts with:** ``` # System variables {{system.trigger_type}} # "whatsapp_event" {{system.observer_mode}} # true {{system.allow_outbound}} # false {{system.workflow_id}} # Workflow UUID {{system.started_at}} # ISO timestamp # Event data {{system.event.type}} # Event type (e.g. "whatsapp.message.sent") {{system.event.payload}} # Full event payload # Conversation snapshot {{system.event.conversation.id}} # Conversation ID {{system.event.conversation.phone_number}} # Customer phone "+1234567890" {{system.event.conversation.business_scoped_user_id}} # User BSUID (may be null) {{system.event.conversation.parent_business_scoped_user_id}} # Parent BSUID (may be null) {{system.event.conversation.username}} # WhatsApp username (may be null) {{system.event.conversation.status}} # "active" or "ended" {{system.event.conversation.phone_number_id}} # Meta phone number ID # Message snapshot (for message events) {{system.event.message.id}} # WhatsApp message ID {{system.event.message.type}} # "text", "image", "video", etc. {{system.event.message.timestamp}} # Unix timestamp {{system.event.message.from_user_id}} # Inbound BSUID when available {{system.event.message.to_user_id}} # Outbound BSUID when available {{system.event.message.username}} # WhatsApp username when available {{system.event.message.text.body}} # Message text (when applicable) {{system.event.message.kapso.direction}} # "inbound" or "outbound" {{system.event.message.kapso.status}} # Message status ``` When `phone_number_id` is provided in the API request: * Automatically stored in `{{system.trigger_whatsapp_config_id}}` * Phone number normalized and stored in `{{context.phone_number}}` * Guarantees all messages use the same WhatsApp configuration ## Accessing variables Use `{{variable_name}}` syntax in messages, templates, and AI fields to access variables. **Direct access** (looks in vars namespace): ``` Hello {{customer_name}} ``` **Explicit namespace access**: ``` Your number is {{context.phone_number}} Workflow started at {{system.started_at}} ``` ## Resume tracking After a workflow resumes from a wait step, these system variables are available: ``` {{system.last_resume.reason}} # "user_input" or "timeout" {{system.last_resume.at}} # When workflow resumed (ISO timestamp) {{system.last_resume.step_id}} # Step ID where workflow was waiting {{system.last_resume.step_identifier}} # Step identifier where workflow was waiting ``` Use `{{system.last_resume.reason}}` to detect if a wait step timed out: ```javascript theme={null} // In a decide node or function if (system.last_resume.reason === "timeout") { // Handle timeout scenario } else { // Handle normal user response } ``` While a wait step with timeout is active, these variables are available: ``` {{system.pending_timeout.job_id}} # Background job ID {{system.pending_timeout.timeout_seconds}} # Timeout duration {{system.pending_timeout.scheduled_for}} # When timeout will fire (ISO timestamp) {{system.pending_timeout.step_id}} # Step the timeout applies to ``` ## Data flow between nodes **Send nodes** (SendTextNode, SendTemplateNode, SendInteractiveNode) * **Read**: All variables for message content and parameters * **Write**: Nothing **Wait for response node** * **Read**: Nothing (just waits) * **Write**: Sets `{{last_user_input}}` when user responds (not set on timeout). Sets `{{system.last_resume}}` metadata on resume. Sets `{{system.pending_timeout}}` when timeout is scheduled **Decide node** * **Read**: All variables to evaluate conditions * **Write**: Nothing (just routes workflow) **Function node** * **Read**: Sends entire execution context to function * **Write**: Can set variables via `save_response_to` or function return **Agent node** * **Read**: Full access to all variables via `get_variable` tool * **Write**: Can set any variable via `save_variable` tool **Handoff node** * **Read**: Nothing * **Write**: Nothing (just stops execution) ## Variable naming * Use lowercase with underscores: `user_name`, `order_total` * Be descriptive: `last_user_input` not `input` * Avoid system reserved names: `flow_id`, `started_at` ## Environment variables Store sensitive values like API keys as environment variables. They use a distinct syntax to avoid conflicts with workflow variables. **Syntax**: `${ENV:VARIABLE_NAME}` Each variable has two values: * **Development**: Used during test runs * **Production**: Baked into published workflows ### Scopes **Project-level** — Available across all workflows in the project. **Access**: Project Settings → Environment variables (or click the key icon in the workflow canvas toolbar). **Flow-level** — Scoped to a single workflow. Flow-level variables **override** project-level variables with the same key. **Access**: Workflow Settings → Environment variables. ### Example usage In webhook URLs: ``` https://api.example.com/v1/data?key=${ENV:API_KEY} ``` In agent MCP headers: ``` Authorization: Bearer ${ENV:MCP_API_KEY} ``` ### Key differences from workflow variables | | Workflow variables | Environment variables | | ------ | ------------------ | ------------------------ | | Syntax | `{{var_name}}` | `${ENV:VAR_NAME}` | | Scope | Single execution | Project or flow | | Set by | Workflow runtime | Settings | | Values | Dynamic | Static (per environment) | # Functions Source: https://docs.kapso.ai/docs/functions/overview Deploy serverless JavaScript functions with Kapso Kapso Functions run on Cloudflare Workers. Use them for webhooks, custom business logic, workflow steps, and agent tools. ## Create and deploy 1. Open **Functions** in your project 2. Create or edit a function 3. Deploy it 4. Attach it to a workflow node, an agent tool, or call its endpoint directly The Kapso CLI does not manage functions yet. Use the dashboard or API for function create/update/deploy. ## Invoke endpoints Deployed Cloudflare functions expose a Kapso-hosted invoke URL: ```text theme={null} https://api.kapso.ai/platform/v1/functions/{function_id}/invoke ``` * `public_endpoint: false` (default) keeps the invoke URL private. Send `X-API-Key` with the request. * `public_endpoint: true` allows invoke requests without an API key. This is only supported for Cloudflare functions. * Private and unknown functions both return `404` from the invoke route. * Newly created functions use `invoke_response_mode: passthrough`. Successful invoke responses preserve the function body, success status code, and `Content-Type`. * Older wrapped functions can be updated to `passthrough` if you need to remove the legacy `{ data: ... }` wrapper. ## Runtime contract Kapso Cloudflare Worker functions must define: ```javascript theme={null} async function handler(request, env) { const body = await request.json().catch(() => ({})); return new Response(JSON.stringify({ ok: true, received: body }), { headers: { "Content-Type": "application/json" } }); } ``` Kapso wraps your code and calls `handler(request, env)`. Do not use `export default`. ## Bindings and env ```javascript theme={null} async function handler(request, env) { const body = await request.json().catch(() => ({})); await env.KV.put("last-request", JSON.stringify(body)); const { results } = await env.DB.prepare( "SELECT id, email FROM customers ORDER BY created_at DESC LIMIT 5" ).all(); return new Response(JSON.stringify({ recentCustomers: results }), { headers: { "Content-Type": "application/json" } }); } ``` * `fetch()` - Make HTTP requests * `Request`/`Response` - Handle HTTP * `URL`/`URLSearchParams` - Parse URLs * `crypto.randomUUID()` - Generate IDs * `TextEncoder`/`TextDecoder` - Text encoding * `env.KV` - Persistent key-value storage * `env.DB` - Project database bound as Cloudflare D1 * `env.YOUR_SECRET` - Access encrypted secrets set in the function page * Standard JavaScript APIs ## Secrets Secrets are available as string keys on `env`: ```javascript theme={null} async function handler(request, env) { const apiKey = env.API_KEY; const webhookSecret = env.WEBHOOK_SECRET; const response = await fetch("https://api.example.com/data", { headers: { Authorization: `Bearer ${apiKey}` } }); return new Response("Success"); } ``` * Secrets must be set in the Kapso web app (function page → Secrets tab) * Secret names should use UPPERCASE\_WITH\_UNDERSCORES * Values are encrypted and never exposed after creation * Functions must be deployed before adding secrets ## KV storage Each project has its own KV namespace for persistent data storage: ```javascript theme={null} await env.KV.put("user:123", JSON.stringify(userData)); await env.KV.put("session", token, { expirationTtl: 3600 }); const user = await env.KV.get("user:123", { type: "json" }); const session = await env.KV.get("session"); await env.KV.delete("user:123"); const list = await env.KV.list({ prefix: "user:" }); ``` ## Functions in flows Kapso sends workflow data in the JSON request body. Parse it with `await request.json()`. ### Function node ```javascript theme={null} async function handler(request, env) { const body = await request.json(); const executionContext = body.execution_context || {}; const vars = executionContext.vars || {}; const context = executionContext.context || {}; const user = { phoneNumber: context.phone_number, plan: vars.plan || "free" }; return new Response(JSON.stringify({ vars: { user } }), { headers: { "Content-Type": "application/json" } }); } ``` Payload keys: * `execution_context` * `flow_info` * `flow_events` * `whatsapp_context` when the run comes from WhatsApp ### Decide node ```javascript theme={null} async function handler(request, env) { const body = await request.json(); const availableEdges = body.available_edges || []; const vars = body.execution_context?.vars || {}; let nextEdge = availableEdges[0] || "default"; if ((vars.customer_tier || "").toLowerCase() === "vip" && availableEdges.includes("vip")) { nextEdge = "vip"; } return new Response(JSON.stringify({ next_edge: nextEdge }), { headers: { "Content-Type": "application/json" } }); } ``` `next_edge` is only used by decide nodes. ### Agent function tools Agent tool arguments are inside `body.input`, not at the root: ```javascript theme={null} async function handler(request, env) { const body = await request.json(); const input = body.input || {}; const vars = body.execution_context?.vars || {}; return new Response(JSON.stringify({ vars: { last_lookup_email: input.email || null } }), { headers: { "Content-Type": "application/json" } }); } ``` Payload keys: * `input` - tool arguments chosen by the agent * `execution_context` * `flow_info` * `flow_events` * `whatsapp_context` when the run comes from WhatsApp ## Common patterns ### WhatsApp CRM integration with session tracking ```javascript theme={null} async function handler(request, env) { const webhook = await request.json(); // Only process message received events if (request.headers.get('X-Webhook-Event') !== 'whatsapp.message.received') { return new Response('OK'); } // Extract customer info const { message, conversation } = webhook; const customerPhone = message.phone_number; // Track customer session in KV const sessionKey = `session:${customerPhone}`; const session = await env.KV.get(sessionKey, { type: 'json' }) || { firstContact: new Date().toISOString(), messageCount: 0 }; session.lastMessage = message.content; session.lastContact = new Date().toISOString(); session.messageCount++; // Store session with 24-hour expiration await env.KV.put(sessionKey, JSON.stringify(session), { expirationTtl: 86400 // 24 hours }); // Create or update CRM contact with session data await fetch('https://api.hubspot.com/contacts/v1/contact', { method: 'POST', headers: { 'Authorization': `Bearer ${env.HUBSPOT_API_KEY}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ properties: [ { property: 'phone', value: customerPhone }, { property: 'last_whatsapp_message', value: message.content }, { property: 'whatsapp_conversation_id', value: conversation.id }, { property: 'total_messages', value: session.messageCount }, { property: 'first_contact_date', value: session.firstContact } ] }) }); return new Response('OK'); } ``` # Bring your own SIM troubleshooting Source: https://docs.kapso.ai/docs/how-to/whatsapp/bring-your-own-sim-troubleshooting Fix common issues when connecting your own phone number to Kapso Bring your own SIM uses Meta embedded signup to connect a phone number you control as a dedicated Cloud API number. For the setup walkthrough, see [Connect WhatsApp](/docs/how-to/whatsapp/connect-whatsapp#bring-your-own-sim). Use this page if: * Meta does not show the phone number or profile you expected * you chose or are considering **Display name only** option in Meta's flow * SMS or voice verification fails * Meta says the number is already linked somewhere else * Kapso says the number already exists * Meta says the WABA is unverified or under account review * Meta succeeds but Kapso does not show the number as connected ## Before you retry Bring your own SIM is for a number that can be dedicated to Cloud API automation. The number should not stay active in the WhatsApp Business App after setup. Before restarting setup, check: * you are logged into the correct Facebook account * you have admin or sufficient access to the right Meta Business Portfolio * the selected Business Portfolio has capacity for another WhatsApp phone number * [WhatsApp phone numbers](https://business.facebook.com/latest/whatsapp_manager/phone_numbers) does not show unresolved number-status or capacity issues * [WhatsApp account settings](https://business.facebook.com/latest/settings/whatsapp_account) does not show unresolved WABA status, business verification, billing, or payment prompts * the number can receive SMS or voice calls during setup * the number is not already connected to another Kapso project * the number is not still attached to another WABA, previous provider, or WhatsApp Business App account * two-step verification is disabled in the source WABA if Meta requires it ## Common issues ### Business Portfolio, WABA, or profile is missing This usually means the wrong Facebook account is logged in, the WABA belongs to another Business Portfolio, or your Meta user does not have enough permissions. What to do: 1. Confirm you are logged into the Facebook account that owns or manages the business. 2. Open Meta Business Settings and verify the WABA exists under that business. 3. Ask a Meta business admin to grant access if you cannot see the business or WABA. 4. If the portfolio and WABA are correct but your number is missing, choose **Create a new WhatsApp Business profile**. 5. Retry Bring your own SIM after access and asset selection are corrected. ### Meta only shows unrelated WhatsApp Business profiles Meta may show existing profiles under the selected business account. That list is scoped to the selected Meta business context; it is not a Kapso list of every number you control. What to do: 1. Do not select an unrelated profile. 2. Choose **Create a new WhatsApp Business profile** if Meta offers it. 3. Continue to phone-number entry and verify the intended number. 4. If the correct Business Portfolio is missing, restart while logged into a Facebook account with full admin access. ### You chose Display name only Do not use **Display name only** for Bring your own SIM. That Meta option can create a WhatsApp profile with a generic, limited Meta-managed number, often a `+1 555...` number, instead of registering and verifying the phone number you control. These numbers can stop working after about 5 messages and require display-name approval before you can keep using them. Approval in these cases might also take significantly longer. What to do: 1. If you are still in the Meta popup, cancel it. 2. Restart Bring your own SIM from Kapso. 3. Choose or create the WhatsApp Business profile for your business. 4. Enter the phone number you want to connect and complete SMS or voice verification. 5. If you already completed setup with **Display name only**, disconnect or delete that profile if needed before restarting Bring your own SIM. 6. If you cannot reach the phone-number verification path, contact support with screenshots from the Meta popup and the phone number you are trying to connect. ### Business Portfolio cannot add another phone number Meta enforces phone-number capacity at the Business Portfolio level. New portfolios usually start with capacity for **2 registered WhatsApp business phone numbers**. Meta can later raise this to **20** after business verification or after the portfolio reaches a 2,000 messaging limit. When the portfolio has no capacity left, Meta's embedded signup can fail without a clear inline error. The UI can show only a red indicator on the phone-number step, disable **Add phone number**, or show capacity text such as `1 of 2 added`. What to do: 1. Open [WhatsApp phone numbers](https://business.facebook.com/latest/whatsapp_manager/phone_numbers). 2. Review all phone numbers across every WABA under the selected Business Portfolio. 3. Remove unused, pending, or stale numbers if you have Meta admin access. 4. Complete Meta Business Verification if you need higher phone-number limits. 5. Retry after Meta shows capacity is available. ### Kapso says the number already exists If Kapso says `A WhatsApp config with the same display phone number already exists`, the number is already connected to another Kapso account or project. What to do: 1. Disconnect the number from the Kapso account where it is currently connected. 2. If you own both accounts, contact support with proof that you control the number and both accounts. 3. Do not keep retrying the same setup. The number cannot be connected twice. ### Meta says the number is already linked elsewhere Meta can block setup when the number is still attached to another WABA, previous provider, WhatsApp Business App registration, or stale ownership state. What to do: 1. Remove the number from old WABAs in WhatsApp Manager where you have admin access. 2. Disconnect old Business Platform or partner links. 3. Remove the number from the WhatsApp Business App path if it was used there. 4. Disable two-step verification in the source WABA if Meta requires it. 5. Wait a few minutes for Meta cleanup to settle. 6. Retry through one path only. ### SMS or voice verification fails SMS or voice verification is expected for Bring your own SIM. It proves you control the phone line. What to do: 1. Confirm the phone number and country code. 2. Use voice call if the number is a landline or cannot receive SMS. 3. Stop retrying if the resend timer keeps resetting or Meta keeps rejecting the code. 4. For persistent pending or reset loops, wait 72 hours with no verification, registration, deregistration, SMS, or voice attempts. 5. Before retrying, remove stale WABA, WhatsApp Business App, and previous-provider attachments where applicable. 6. After the quiet window, do one clean retry through Kapso. ### Business profile step is stuck If Meta stops on a business profile step and **Next** is disabled, the business profile data may not pass Meta validation. What to do: 1. Use a real public HTTPS website. 2. Confirm the website loads in an incognito browser. 3. Make sure the legal name, address, website, and phone number match the business. 4. Restart the Meta popup after updating the information. ### WABA review blocks registration Every newly created WABA goes through a Meta review before it is fully onboarded. During this period, SMS or voice verification may succeed, but the number can still stay pending while Meta finishes the WABA review. In Kapso, an issue may appear on the connected number raising this issue. What to do: 1. Open [WhatsApp phone numbers](https://business.facebook.com/latest/whatsapp_manager/phone_numbers) and confirm the number status. 2. Make sure the Meta Business Portfolio has complete business information, including legal name, address, business phone number, and a public HTTPS website. 3. Complete any visible Meta-requested action if one appears. 4. If no action appears, wait for Meta to finish the WABA review. Kapso saves the WABA review status and shows it in the UI while the number is pending. When Meta sends the WABA review update, Kapso automatically retries phone registration. You do not need to keep restarting Bring your own SIM unless support asks you to. ### WABA, business, or payment restrictions block setup Meta can block setup or later sending even after the number appears connected. Common causes: * WABA is banned, restricted, or under a compliance review * Business Portfolio is under compliance review * payment eligibility or billing information is incomplete * display name is still pending or rejected * country restrictions apply What to do: 1. Open [WhatsApp phone numbers](https://business.facebook.com/latest/whatsapp_manager/phone_numbers) and check for WABA or phone-number warnings. 2. If Meta points to account-level setup, open [WhatsApp account settings](https://business.facebook.com/latest/settings/whatsapp_account) and check WABA status, business verification, and billing or payment prompts. 3. Complete any requested business verification, review, or billing information. 4. Retry only after Meta clears the restriction. 5. Send a production test message before treating the number as fully ready. ### Meta succeeds but Kapso stays pending Meta can show success while Kapso still cannot finish creating the connected number. What to do: 1. Confirm Meta showed success. 2. Confirm whether Kapso still shows setup as pending or failed. 3. Retry once in a clean browser session if no number was created. 4. If Meta success repeats but Kapso still cannot create the number, contact support. ## Still stuck? Contact support with: * your Kapso project URL * the phone number you are trying to connect * the Business Portfolio and WABA you selected * the exact Meta error text * screenshots from the Meta popup * screenshots from WhatsApp account settings or WhatsApp phone numbers if they show warnings # Coexistence troubleshooting Source: https://docs.kapso.ai/docs/how-to/whatsapp/coexistence-troubleshooting Fix common issues when connecting WhatsApp Business App to Kapso Coexistence lets you use the **WhatsApp Business App** and Kapso at the same time. It is convenient, but less stable than a dedicated Cloud API connection. Use this page if: * your existing WhatsApp Business App number does not connect correctly * your WABA does not appear in Meta embedded signup * the number was previously connected to another provider * you chose or are considering **Display name only** option in Meta's flow * you see Meta errors like `3441041` or `2655093` * Meta says the phone number is not eligible and needs more WhatsApp Business App activity ## Before you start Coexistence does **not** remove or bypass: * display-name approval requirements * business verification requirements * payment-method restrictions * template restrictions If you need maximum stability for production, use [Connect WhatsApp](/docs/how-to/whatsapp/connect-whatsapp) and choose the **dedicated** / Cloud API path instead. ## Standard coexistence setup 1. Go to **Connected numbers → Connect new number** in Kapso. 2. Choose **WhatsApp Business App** and open Meta embedded signup. 3. Log in with the Facebook account that administers your Business Portfolio. 4. Follow the Business App connection flow and complete app pairing when prompted. 5. Finish the Meta flow and return to Kapso. Meta is rolling out two versions of this flow. One offers **Connect a WhatsApp Business App** before asking for the number. Another asks for the number first, then detects that it is active in WhatsApp Business App and routes to coexistence. Entering your number is normal in either version; the screens do not always appear in the same order. ## Connection issues ### Number is already linked to another account or provider Use these steps when you want to **keep using WhatsApp Business App** and Meta says the number or WhatsApp Business Account (WABA) is already connected, occupied, or shared with another partner. If the number is currently a dedicated Cloud API connection, contact support to plan the change to coexistence first. Do not remove a working connection as routine troubleshooting. 1. If you used another provider, request disconnection from its **website or app** first. Check **WhatsApp Business App → Settings → Account → Business platform** for any remaining connection. Disconnection does not guarantee that the provider's credit line was revoked. 2. Open [Meta WhatsApp account settings](https://business.facebook.com/latest/settings/whatsapp_account), select the correct Business Portfolio, and select the affected WABA. 3. If the WABA is visible, open **Summary → Payment method**. If it shows **Credit line**, note the provider name. An empty **Partners** tab alone does not confirm that billing was disconnected. 4. If the previous provider's credit line remains, or its removal is uncertain, ask that provider to **revoke the credit-line allocation for this WABA** and confirm completion. If it shows **Kapso**, contact Kapso Support so we can review and revoke our allocation. Tell us if the number is still active in another Kapso project before disconnecting it. 5. Once funding is cleared, remove the affected **WhatsApp account from the Business Portfolio** in the same Meta settings page. A coexistence WABA belongs to the Business App and contains its single number; you cannot add other numbers to it. Preserve any templates you need before removing it. 6. Wait **5 minutes**, then reconnect through Kapso's **WhatsApp Business App** flow. Let the flow connect the Business App from scratch and create a new WABA. You do not need to find a separate **Create WABA** button or create a new portfolio. Removing the WABA from the Business Portfolio is different from deleting your WhatsApp Business App account. Do **not** use **Settings → Account → Delete account** in the phone app to recover coexistence. If you have never used another provider, start with the WABA check in step 2. A residual WABA association can still need removal. If the WABA is not visible, check your other accessible portfolios and admin access. For a persistent partner-sharing error such as `2655093`, ask your previous provider to confirm credit-line revocation even if you cannot inspect the WABA yourself. If there was no previous provider, or revocation is already confirmed, contact Kapso Support with the evidence below. If funding is cleared but Meta blocks WABA removal, or the same error remains after removal, the five-minute wait, and one reconnect, contact Kapso Support. We can escalate the case to Meta. Do not repeat cleanup or wait 72 hours just for a residual partner/WABA association. ### Existing WABA does not appear in embedded signup This usually means one of these: * you selected the wrong Business Portfolio in Meta * the WABA is still controlled by another partner * the number is still tied to an older WABA or app assignment Confirm the Facebook account, admin access, and Business Portfolio. For a stale association, follow [the recovery steps above](#number-is-already-linked-to-another-account-or-provider), including the payment-method check. A coexistence reconnect creates a new WABA through the Business App flow; selecting the old WABA is not required. ### Meta shows the wrong onboarding option During onboarding, choose the path for connecting the **WhatsApp Business App** through Kapso. Choosing the wrong option can leave the number attached to the wrong app or with missing webhook subscriptions. Do not choose **Display name only**. That Meta option can create a WhatsApp profile with a generic, limited Meta-managed number instead of pairing the WhatsApp Business App number you already use. These numbers can stop working after about 5 messages and require display-name approval before you can keep using them. If Meta actually asks you to receive or enter an **SMS or voice verification code** instead of Business App pairing, stop and send Kapso Support screenshots or a recording of the full flow. Include the option selected in Kapso and what Meta showed next. Number entry alone is not SMS verification, and the newer flow may not show an explicit Business App selector first. If you already completed setup with **Display name only**, contact support to identify that unintended profile before disconnecting it and reconnecting your Business App number. ### Phone number is not eligible or needs more app activity Meta can block the phone-number step with a message that the number is not eligible to connect to the WhatsApp Business Platform, or that more activity in the WhatsApp Business App is needed to determine eligibility. Make sure the number is active in **WhatsApp Business App**, not personal WhatsApp, and that the app is updated. Follow any activity requirements Meta displays. There is no guaranteed wait period or account reset that makes a number eligible. Do not delete and recreate the Business App account to bypass eligibility. If the message persists, contact support with the exact text and screenshots. Use the partner/WABA cleanup above only when there is evidence of an existing association. ### Error `3441041` This error indicates that the number is not associated with the business selected in the flow. Confirm the selected Business Portfolio and number first. If an old portfolio or WABA association remains, use [the coexistence recovery steps](#number-is-already-linked-to-another-account-or-provider). ### Error `2655093` This error indicates that the WABA is already shared with another partner and this flow cannot switch partners. It does **not** prove a credit line remains attached. Use [the coexistence recovery steps](#number-is-already-linked-to-another-account-or-provider) to distinguish the provider connection, credit-line allocation, and WABA association. A provider saying “disconnected” or disappearing from **Partners** is not sufficient proof of credit-line revocation. ### Switching to dedicated Cloud API If you deliberately want to stop using WhatsApp Business App on this number, contact support to plan a dedicated / Bring Your Own SIM cutover. This is a separate process that can require deleting the Business App account and backing up data before disconnecting the existing Kapso connection. Do not use it as a workaround for coexistence errors. ## Known limitations These are known behaviors customers should expect: * occasional disconnects * some WhatsApp Web sync issues * some contact-name sync issues * some first inbound messages may not reach Kapso immediately * calling through Kapso is unavailable — you can still make and receive calls in WhatsApp Business App * the display name is managed in WhatsApp Business App, not in Kapso If your use case needs stable automation, templates, webhooks, and production reliability, use the dedicated / Cloud API path instead. ## Restrictions still apply If you connect a number in coexistence mode, Meta restrictions are still enforced. Examples: * display name still needs approval when Meta requires it * business verification may still be required * template sending can still be blocked by payment issues Coexistence only changes the connection mode. It does not remove WABA-level restrictions. ## When to contact support Contact support if: * you already completed the cleanup steps above * the correct WABA still does not appear * `3441041` or `2655093` still persists after cleanup * you suspect an old partner still controls the account but cannot remove it To speed up support, include: * the phone number and intended connection type * `phone_number_id`, `waba_id`, and Business Portfolio ID if available * the previous provider and its confirmation of credit-line revocation, if applicable * screenshots of **Summary → Payment method**, the WABA association, and any failed removal * the full error text/code and a recording or screenshots of the Meta flow * when you removed the WABA, waited, and retried Missing IDs do not prevent you from contacting support. # Connect WhatsApp Source: https://docs.kapso.ai/docs/how-to/whatsapp/connect-whatsapp Connect your WhatsApp Business account to Kapso Connecting WhatsApp takes about 5 minutes using Meta's embedded signup flow. ## Before you connect Meta controls the login, business selection, and approval checks inside embedded signup. Before you start, make sure you have a Facebook account. During the flow you can create a Meta Business Portfolio and WhatsApp Business Account (WABA) if you need. If you already have these created, make sure you have: * access to the correct Meta Business Portfolio * access to the WhatsApp Business Account (WABA) * complete business information in Meta, including legal name, address, business phone number, and a public HTTPS website * no unresolved number-status or capacity issues in [WhatsApp phone numbers](https://business.facebook.com/latest/whatsapp_manager/phone_numbers) * no unresolved WABA status, business verification, billing, or payment prompts in [WhatsApp account settings](https://business.facebook.com/latest/settings/whatsapp_account) * capacity in the selected Business Portfolio for another WhatsApp phone number Connecting a number does not bypass Meta requirements. Display-name review, business verification, WABA review, payment eligibility, template review, and country or account restrictions can still block production sending after the number is connected. ## Connection options | Option | Best for | Requirements | | --------------------------------------------------- | ---------------------------------- | ---------------------------------------------------- | | [**Instant setup**](#instant-setup-digital-number) | Get started immediately | Facebook account | | [**WhatsApp Business App**](#whatsapp-business-app) | Keep using the app alongside Kapso | Active WhatsApp Business App number and phone access | | [**Bring your own SIM**](#bring-your-own-sim) | Use your own phone number | Dedicated SIM card | ## Instant setup (digital number) Get a pre-verified US phone number without SMS verification. Before using this option: * use a Meta Business Portfolio that can add another WhatsApp phone number * choose a Kapso-provided or BSP-provided number inside Meta * watch the pre-signup guidance in Kapso if it appears, because it shows which Meta phone-number option to choose If Meta does not show a BSP-provided number, asks for SMS verification, or blocks setup, see [Instant setup troubleshooting](/docs/how-to/whatsapp/instant-setup-troubleshooting). In Kapso: 1. Go to **Connected numbers** → **Connect new number** 2. Select **Instant setup** 3. Confirm the instruction that you should choose a Kapso-provided or BSP-provided number in Meta 4. Watch the pre-signup guidance if Kapso shows it 5. Click through to open Meta embedded signup In Meta embedded signup: 1. Log in with the Facebook account that has access to your business. On **Seamlessly connect your account to Kapso**, review what Kapso will be able to do and click **Continue**. Meta embedded signup asks you to continue connecting the account to Kapso. 2. On **Select the business assets to share with Kapso**, choose the **Business portfolio** for the business you want to connect. Meta asks you to select the business portfolio to share with Kapso. 3. Choose an existing **WhatsApp Business account**, or create one if Meta asks you to. Meta asks you to select the WhatsApp Business account to share with Kapso. 4. On **Add your WhatsApp phone number**, do not choose **Use a display name only**. Choose **Use a new or existing WhatsApp number**. Meta shows Use a new or existing WhatsApp number on the Add your WhatsApp phone number screen. 5. Open the **Phone number** dropdown, select a number under **BSP provided number**, and click **Next**. Meta shows the BSP provided number option in the phone number dropdown. 6. When Meta shows **Your account is connected to Kapso**, click **Finish**. This final step sends you back to Kapso so the connection can be completed. Meta confirms that the account is connected to Kapso. After Meta returns you to Kapso, Kapso finalizes the connection and shows the phone number as connected. Kapso shows the phone number as connected after Meta returns to the app. If the WABA is still under Meta review, the number may stay pending until Meta finishes that review. Kapso shows the saved WABA status in the UI and automatically retries registration when Meta sends the review update. While waiting, make sure the Meta Business Portfolio has complete business information, including legal name, address, business phone number, and a public HTTPS website. The small deposit goes directly to your project credits. Available on all plans. If you need local numbers, your own Twilio account, or project-owned reusable pools, see [Provide local numbers](/docs/platform/phone-numbers/provide-local-numbers). If the flow asks for SMS verification instead of instant setup, cancel and retry. If it persists, see [Instant setup troubleshooting](/docs/how-to/whatsapp/instant-setup-troubleshooting). ## WhatsApp Business App Keep using the WhatsApp Business app on your phone alongside Kapso. Before using this option: * make sure the number is active in the WhatsApp Business App, not the personal WhatsApp app * update the WhatsApp Business App and keep the phone nearby with a working camera * for a number linked to another provider, follow the [coexistence recovery steps](/docs/how-to/whatsapp/coexistence-troubleshooting#number-is-already-linked-to-another-account-or-provider), including credit-line verification * complete Business App pairing when prompted; if Meta asks for SMS or voice code verification instead, stop and send support screenshots of the full flow If the number does not connect, your WABA does not appear, or Meta sends you to the wrong onboarding path, see [Coexistence troubleshooting](/docs/how-to/whatsapp/coexistence-troubleshooting). In Kapso: 1. Go to **Connected numbers** → **Connect new number** 2. Select **WhatsApp Business App** 3. Click through to open Meta embedded signup Meta has two onboarding versions: one offers **Connect a WhatsApp Business App** first; the newer version asks for your number and detects the Business App connection. Number entry alone is normal. The screenshots below show the version with the explicit Business App option; your screen order may differ. In Meta embedded signup: 1. Log in with the Facebook account that has access to your business. On **Seamlessly connect your account to Kapso**, review what Kapso will be able to do and click **Continue**. Meta embedded signup asks you to continue connecting the account to Kapso. 2. On **Select the business assets to share with Kapso**, choose the **Business portfolio** for the business that owns this WhatsApp number. Meta asks you to select the business portfolio to share with Kapso. 3. In **WhatsApp Business account**, choose **Connect a WhatsApp Business App** and click **Next**. Meta shows the Connect a WhatsApp Business App option under WhatsApp Business account. 4. On **Enter your WhatsApp Business phone number**, select the country code, enter the phone number that is already active in your WhatsApp Business App, and click **Next**. Meta asks for the WhatsApp Business phone number already active in the app. 5. On **Connect your existing WhatsApp Business App**, review the sharing access and protection information, then click **Next**. Meta explains what will be shared when connecting the existing WhatsApp Business App. 6. When the QR code appears, keep the browser window open and open the WhatsApp Business App on your phone. The phone camera view shows the QR code on the Meta pairing screen. 7. On your phone, tap **Connect to the Business Platform** in the WhatsApp Business App. The WhatsApp Business App shows the Connect to the Business Platform screen. 8. Use the WhatsApp Business App camera to scan the QR code shown by Meta. The WhatsApp Business App asks you to scan the QR code from Meta embedded signup. 9. Choose whether to share chat history, then confirm your choice. The WhatsApp Business App asks whether to share chat history. 10. Back in Meta, on **Confirm or edit your WhatsApp Business account**, confirm the WhatsApp Business account name, select the account **Timezone**, and click **Next**. Meta asks you to confirm the WhatsApp Business account name and timezone. 11. On **Review what you'll share with Kapso**, review the requested access and click **Confirm**. Meta shows the permissions Kapso will receive for the selected business and WhatsApp account. 12. When Meta shows **Your account is connected to Kapso**, click **Finish**. This final step sends you back to Kapso so the connection can be completed. Meta confirms that the account is connected to Kapso. After Meta returns you to Kapso, Kapso finalizes the connection and starts syncing the coexistence number. Messages can then appear in both the WhatsApp Business App and Kapso. Kapso shows the WhatsApp Business App number as connected after Meta returns to the app. If Meta asks for SMS or voice code verification instead of Business App pairing, stop and send support screenshots or a recording of the full flow. Entering the number first is normal in the newer flow; it does not by itself mean you chose the wrong connection mode. ## Bring your own SIM Use a phone number you already control as a dedicated Cloud API number. This path is for a number that can move fully to Kapso's API connection. It is not for keeping the same number active in the WhatsApp Business App. Before using this option: * keep the SIM, phone line, or landline available during setup * use a number you can verify by SMS or voice call * make sure the number is not already connected to another Kapso project * remove the number from previous providers or WhatsApp Business App registrations before retrying In Kapso: 1. Go to **Connected numbers** → **Connect new number** 2. Select **Bring your own SIM** 3. Click through to open Meta embedded signup In Meta embedded signup: 1. Log in with the Facebook account that has access to your business. On **Seamlessly connect your account to Kapso**, review what Kapso will be able to do and click **Continue**. Meta embedded signup asks you to continue connecting the account to Kapso. 2. On **Select the business assets to share with Kapso**, choose the **Business portfolio** for the business that owns this phone number. Meta asks you to select the business portfolio to share with Kapso. 3. Choose an existing **WhatsApp Business account**, or create one if Meta asks you to. Meta asks you to select the WhatsApp Business account to share with Kapso. 4. On **Add your WhatsApp phone number**, do not choose **Use a display name only**. Choose **Add a new number**. Meta shows Add a new number on the Add your WhatsApp phone number screen. 5. Select the country code, enter the phone number you want Kapso to use, enter the **WhatsApp Business display name**, choose **Text message** or **Phone call**, and click **Next**. Meta asks for the SIM phone number, display name, and verification method. 6. On **Verify your phone number**, enter the 6-digit verification code from Meta and click **Next**. Meta asks for the 6-digit verification code sent by text message or phone call. 7. When Meta shows **Your account is connected to Kapso**, click **Finish**. This final step sends you back to Kapso so the connection can be completed. Meta confirms that the account is connected to Kapso. After Meta returns you to Kapso, Kapso registers the number, creates the production WhatsApp configuration, and shows the number as connected. Kapso shows the SIM phone number as connected after Meta returns to the app. If Meta accepts the flow but the WABA is still under review, the phone number can remain pending. Kapso saves the WABA status and shows it in the UI as the number's **Business Account Status**, such as **Review pending**, **Review deferred**, **Review rejected**, or **Restricted**. When Meta sends the review update, Kapso automatically retries phone registration. While waiting, make sure the Meta Business Portfolio has complete business information and check [WhatsApp phone numbers](https://business.facebook.com/latest/whatsapp_manager/phone_numbers) for the number status. If Meta points to account-level setup, use [WhatsApp account settings](https://business.facebook.com/latest/settings/whatsapp_account) to check WABA status, business verification, billing, and payment prompts. If SMS or voice verification keeps failing, stop retrying before Meta rate-limits the number. Confirm the number is not still attached to another WABA, provider, WhatsApp Business App account, or Kapso project before trying again. If Meta does not show the expected number, SMS or voice verification fails, or Meta says the number is already linked somewhere else, see [Bring your own SIM troubleshooting](/docs/how-to/whatsapp/bring-your-own-sim-troubleshooting). ## Manual setup and Meta apps If you manually add a WhatsApp phone number instead of using embedded signup, Kapso may need your Facebook App ID and App Secret to validate incoming Meta webhooks. You can find these in Meta under **App settings** → **Basic**. In Kapso: 1. Go to **Phone numbers** 2. Open **Meta apps** 3. Create a Meta app with your App ID and App Secret 4. Edit the manually added phone number 5. Select the Meta app 6. Save If a manually added number can send messages but does not receive inbound messages, check this first. This is not your WhatsApp access token; App Secrets are stored write-only in Kapso. ## If the popup doesn't load Disable ad blockers and privacy extensions. They block Meta's SDK. Try Chrome or Firefox if Safari isn't working. ### Why to avoid Display name only In all of the signup flows in Meta you might find a **Display name only** option. If you choose it, you may complete Meta signup but end up with a limited Meta-managed number instead of the intended production number. Consequences: * The connected number may not be the Kapso-provided number, BSP-provided number, project-pool number, or SIM number you meant to connect. * These limited Meta-managed numbers can stop working after about 5 messages and ask for display-name approval before you can keep using them. * Display-name approval can take a long time, and Meta may defer the decision. While the decision is pending or deferred, you may be left unable to use the number. * You may need to delete or disconnect that WhatsApp profile and restart onboarding through the correct Kapso flow. Need help? In Kapso, go to the sidebar, click 'Help' and select 'Support' # Instant setup troubleshooting Source: https://docs.kapso.ai/docs/how-to/whatsapp/instant-setup-troubleshooting Fix common issues when connecting a Kapso-provided WhatsApp number Instant setup uses Meta embedded signup to connect a Kapso-provided WhatsApp number. For an explanation of instant setup and a full setup walkthrough, see [Connect WhatsApp](/docs/how-to/whatsapp/connect-whatsapp#instant-setup-digital-number). Use this page if: * Meta does not show **BSP provided number** * you chose or are considering **Display name only** option in Meta's flow * Meta asks for SMS or voice verification * your Business Portfolio or WABA does not appear * Meta blocks setup with an account, business, or phone-number error * Meta says the WABA is unverified or under account review * the number stays pending after setup ## Before you retry Instant setup only works when you select a Kapso-provided number inside Meta. Before restarting setup, check: * you are logged into the correct Facebook account * you have admin or sufficient access to the right Meta Business Portfolio * the selected Business Portfolio has capacity for another WhatsApp phone number * [WhatsApp phone numbers](https://business.facebook.com/latest/whatsapp_manager/phone_numbers) does not show unresolved number-status or capacity issues * [WhatsApp account settings](https://business.facebook.com/latest/settings/whatsapp_account) does not show unresolved WABA status, business verification, billing, or payment prompts * your business profile has complete information, including a public HTTPS website if Meta asks for one ## Common issues ### Meta popup does not open Try this first: 1. Use Chrome or Firefox. 2. Disable ad blockers and privacy extensions for the setup session. 3. Allow popups and cookies. 4. Log into Facebook before restarting setup. 5. Restart instant setup from Kapso. ### Business Portfolio or WABA is missing This usually means the wrong Facebook account is logged in, the WABA belongs to another Business Portfolio, or your Meta user does not have enough permissions. What to do: 1. Confirm you are logged into the Facebook account that owns or manages the business. 2. Open Meta Business Settings and verify the WABA exists under that business. 3. Ask a Meta business admin to grant access if you cannot see the business or WABA. 4. Retry instant setup after access is corrected. ### Business profile step is stuck If Meta stops on a business profile step and **Next** is disabled, the business profile data may not pass Meta validation. What to do: 1. Use a real public HTTPS website. 2. Confirm the website loads in an incognito browser. 3. Make sure the legal name, address, website, and phone number match the business. 4. Restart the Meta popup after updating the information. ### Meta does not show BSP-provided numbers If Meta only shows **Add a new WhatsApp number** or asks you to type a phone number, do not continue in that flow. What to do: 1. Cancel the Meta popup. 2. Restart instant setup from Kapso. 3. Choose **Use a new or existing WhatsApp number** at the phone-number step. 4. Select a number under **BSP provided number**. 5. If the BSP section is still missing, check WhatsApp Manager for account limits or restrictions. Useful Meta pages: * [WhatsApp account settings](https://business.facebook.com/latest/settings/whatsapp_account) * [WhatsApp phone numbers](https://business.facebook.com/latest/whatsapp_manager/phone_numbers) ### You chose Display name only Do not use **Display name only** for instant setup. That Meta option can create a WhatsApp profile with a generic, limited Meta-managed number instead of connecting the Kapso-provided number selected by instant setup. These numbers can stop working after about 5 messages and require display-name approval before you can keep using them. Approval in these cases might also take significantly longer. What to do: 1. If you are still in the Meta popup, cancel it. 2. Restart instant setup from Kapso. 3. At the phone-number step, choose **Use a new or existing WhatsApp number**. 4. Select a number under **BSP provided number**. 5. If you already completed setup with **Display name only**, disconnect or delete that profile if needed before restarting instant setup. 6. If you cannot reach the BSP-provided number path, contact support with screenshots from the Meta popup. ### BSP-provided numbers are visible but fail Visible BSP-provided numbers do not always mean the Kapso number pool is the blocker. Meta can still reject setup because of WABA, Business Portfolio, compliance, or phone-number-capacity state. What to do: 1. Open [WhatsApp phone numbers](https://business.facebook.com/latest/whatsapp_manager/phone_numbers) for the same Business Portfolio used in setup. 2. Check number status, phone-number capacity, and any visible warnings on the selected WABA. 3. Resolve any Meta warnings, restrictions, or review requests. 4. Retry instant setup after Meta clears the issue. ### Meta asks for SMS or voice verification SMS or voice verification means Meta moved you into manual number registration, not instant setup. What to do: 1. Cancel the Meta popup. 2. Restart instant setup from Kapso. 3. At the phone-number step, select a number under **BSP provided number**. 4. Do not type a new phone number. ### Business Portfolio cannot add another phone number Meta enforces phone-number capacity at the Business Portfolio level. New portfolios usually start with capacity for **2 registered WhatsApp business phone numbers**. Meta can later raise this to **20** after business verification or after the portfolio reaches a 2,000 messaging limit. When the portfolio has no capacity left, Meta's embedded signup can fail without a clear inline error. The UI can show only a red indicator on the phone-number step, disable **Add phone number**, or show capacity text such as `1 of 2 added`. What to do: 1. Open [WhatsApp phone numbers](https://business.facebook.com/latest/whatsapp_manager/phone_numbers). 2. Review all phone numbers across every WABA under the selected Business Portfolio. 3. Remove unused, pending, or stale numbers if you have Meta admin access. 4. Complete Meta Business Verification if you need higher phone-number limits. 5. Retry after Meta shows capacity is available. ### Meta shows error `#2655121` This usually points to a Meta-side restriction on the selected WABA or Business Portfolio. What to do: 1. Open [WhatsApp phone numbers](https://business.facebook.com/latest/whatsapp_manager/phone_numbers). 2. Review WABA, phone-number, or Business Portfolio alerts. 3. If Meta points to account-level setup, open [WhatsApp account settings](https://business.facebook.com/latest/settings/whatsapp_account) and check WABA status, business verification, and billing or payment prompts. 4. Use Meta's **Request Review** option if available. 5. Retry instant setup only after Meta clears the restriction. ### Meta shows error `141000` This means Meta could not link the phone number to the selected WhatsApp account, or the number stayed pending or blocked after selection. What to do: 1. Check the selected WABA in WhatsApp Manager. 2. Resolve any WABA verification or account warnings. 3. Retry instant setup. 4. If the same Kapso-provided number remains pending, restart and choose a different BSP-provided number. ### WABA review is pending Every newly created WABA goes through a Meta review before it is fully onboarded. During this period, the number can stay pending even if the Meta Business Portfolio already shows as verified. In Kapso, an issue may appear on the connected number raising this issue. What to do: 1. Open [WhatsApp phone numbers](https://business.facebook.com/latest/whatsapp_manager/phone_numbers) and confirm the number status. 2. Make sure the Meta Business Portfolio has complete business information, including legal name, address, business phone number, and a public HTTPS website. 3. Complete any visible Meta-requested action if one appears. 4. If no action appears, wait for Meta to finish the WABA review. Kapso saves the WABA review status and shows it in the UI while the number is pending. When Meta sends the WABA review update, Kapso automatically retries phone registration. You do not need to keep restarting instant setup unless support asks you to. ### WABA is banned or restricted If Meta shows errors such as `141014`, `131031`, or language about a banned WABA, account lock, compliance review, or business review, the blocker is controlled by Meta. What to do: 1. Open [WhatsApp phone numbers](https://business.facebook.com/latest/whatsapp_manager/phone_numbers) and check for WABA or phone-number warnings. 2. If Meta points to account-level setup, open [WhatsApp account settings](https://business.facebook.com/latest/settings/whatsapp_account) and check WABA status, business verification, and billing or payment prompts. 3. Complete any requested business verification, review, or remediation steps. 4. Retry with a WABA that is not blocked. ## Still stuck? Contact support with: * your Kapso project URL * the Business Portfolio and WABA you selected * the exact Meta error text * screenshots from the Meta popup * screenshots from WhatsApp account settings or WhatsApp phone numbers if they show warnings # Use Kapso Sandbox Source: https://docs.kapso.ai/docs/how-to/whatsapp/use-sandbox-for-testing Test your WhatsApp agents safely without production credentials ## Create a test session 1. Go to **WhatsApp** → **Sandbox** in your project 2. Click **Add Test Number** 3. Enter your WhatsApp phone number (the one you'll test from) 4. Click **Create** ## Activate your session After creating a session, you'll see: * A 6-character activation code (e.g., "ABC123") * The Kapso sandbox WhatsApp number * A direct link to start the conversation To activate: 1. Click the **Open WhatsApp** button 2. Send your activation code as a message 3. You'll receive a confirmation message Activation codes expire 15 minutes after the session is created. Create a new session to get a fresh code. ## Move a number to another project A phone number that is already active in another sandbox session can be claimed. Create a session for it in the new project, then send the activation code from WhatsApp. Once activated: * the new session becomes `active` * the previous session becomes `superseded` and its open sandbox conversations are closed * new inbound messages route only to the active session The code must be sent from the phone number the session was created for. ## Route to an agent 1. Navigate to **WhatsApp** → **Configurations** in your project 2. Find the **Sandbox WhatsApp** configuration (automatically created) 3. Go to your agent settings 4. Select **Sandbox WhatsApp** as the WhatsApp configuration 5. Save the agent ## Route to a flow 1. Navigate to **WhatsApp** → **Configurations** in your project 2. Find the **Sandbox WhatsApp** configuration (automatically created) 3. Go to **WhatsApp** → **Flows** in your project 4. Create or edit a flow 5. Add an **Inbound Message Trigger** 6. Select **Sandbox WhatsApp** as the WhatsApp configuration 7. Deploy the flow ## Route to webhooks 1. Navigate to **WhatsApp** → **Configurations** in your project 2. Find the **Sandbox WhatsApp** configuration (automatically created) 3. Click on the **Sandbox WhatsApp** configuration 4. Go to **Manage Webhooks** 5. Add webhook URLs for the events you want to receive ## Manage test sessions Go to **WhatsApp** → **Sandbox** to view all sessions: * See which phone numbers are authorized * Check which agent/config each session uses * Delete sessions when done testing Each session has a status: `pending_activation` (waiting for the code), `active`, or `superseded` (replaced by a newer session for the same number). ## Limitations The sandbox is for testing message flows, not production features. | Feature | Sandbox | Production | | ------------------------- | ------- | ---------- | | Send text messages | ✅ | ✅ | | Send interactive messages | ✅ | ✅ | | Send templates | ❌ | ✅ | | Sync from WhatsApp | ❌ | ✅ | | Multiple recipients | ❌ | ✅ | ### Common errors **"Active sandbox session required to send messages"** You're sending to a number that isn't registered in your sandbox session. The `to` field must match the phone number you added to your session. **"Sync from WhatsApp" button disabled** Template sync is not available for sandbox numbers. Connect a production number to sync templates. **Can't activate sandbox session** * Verify you're sending the exact 6-character code (case-sensitive) * Send from the phone number you registered, not a different device * Check the sandbox number is correct (displayed in your session) * Codes expire after 15 minutes — create a new session to get a fresh one # Introduction Source: https://docs.kapso.ai/docs/introduction Kapso is WhatsApp for developers. To get started with Kapso, you'll need: * A WhatsApp number connected to Kapso. On the free plan we give you a pre-verified number at no cost, so you can start without buying one or waiting on SMS verification * A Kapso API key Then you'll be ready to build either way of using WhatsApp: * **For your team**: connect your own number, handle conversations in a shared inbox, send broadcasts, and automate your operations * **For your customers**: onboard them with their own numbers through setup links, and message from each of them ## Quickstart Send text, media, templates, and interactive messages via API. Give your AI coding agent the tools and context for Kapso. Inbox, broadcasts, and workflows for your operations. Let your customers connect their own WhatsApp to your product. Give your AI agent a WhatsApp number. ## Explore Type-safe client for sending messages, managing templates, and more. Manage numbers, messages, and webhooks from the terminal. Automate conversations with visual flows and AI steps. Receive real-time events for messages, connections, and delivery. Handle incoming WhatsApp messages and events. Create and send pre-approved message templates. Build WhatsApp agents with Chat SDK and Kapso webhooks. Connect OpenClaw agents to WhatsApp with Kapso. Connect Hermes Agent to WhatsApp with Kapso. # Agent API Source: https://docs.kapso.ai/docs/kapso-agent/api Trigger Kapso Agent runs, inspect sessions, handle approvals, and receive lifecycle webhooks from your own code. Trigger agent runs through the Platform API with your project API key. Runs execute asynchronously: create a run, then poll it or receive lifecycle events through [project webhooks](/docs/platform/webhooks/project-webhooks#kapso-agent-run-events). Base URL: `https://api.kapso.ai/platform/v1`. Authenticate with the `X-API-Key` header. Full endpoint reference: [Kapso Agent API](/api/platform/v1/kapso-agent/agent-runs/create-a-run). ## List modes ```bash theme={null} curl "https://api.kapso.ai/platform/v1/kapso-agent/modes" \ -H "X-API-Key: YOUR_API_KEY" ``` Returns every mode. Only modes whose `available_invocations` includes `api` accept runs: built-in modes with the API trigger enabled, and [custom modes](/docs/kapso-agent/modes) with the `api` surface. Get one mode with `GET /kapso-agent/modes/:mode`. ## List sessions List the sessions for a built-in or custom mode: ```bash theme={null} curl "https://api.kapso.ai/platform/v1/kapso-agent/modes/api/sessions?limit=20" \ -H "X-API-Key: YOUR_API_KEY" ``` Sessions are ordered by most recent activity. Results include only API runs created with the same API key. ```json theme={null} { "data": [ { "conversation_id": "880e8400-e29b-41d4-a716-446655440003", "title": "Webhook delivery investigation", "status": "active", "created_at": "2026-08-21T14:30:00Z", "run_count": 3, "latest_run": { "run_id": "990e8400-e29b-41d4-a716-446655440004", "status": "completed", "created_at": "2026-08-21T14:35:00Z" }, "last_activity_at": "2026-08-21T14:35:00Z" } ], "paging": { "next": "NEXT_CURSOR", "previous": null, "cursors": { "before": "PREVIOUS_CURSOR", "after": "NEXT_CURSOR" } } } ``` `limit` defaults to 20 and has a maximum of 100. Pass `paging.next` as `after` to get the next page. ## Get a session Get a session and its runs: ```bash theme={null} curl "https://api.kapso.ai/platform/v1/kapso-agent/sessions/CONVERSATION_ID?limit=20" \ -H "X-API-Key: YOUR_API_KEY" ``` The response contains the session in `data.session` and its newest API runs in `data.runs`. Pagination applies only to the runs. The session-level `run_count` and `latest_run` describe the full session, not only the current page. Session history omits the `result` for each run. Get a run by its `run_id` when you need the completed result. ## Create a run ```bash theme={null} curl -X POST "https://api.kapso.ai/platform/v1/kapso-agent/runs" \ -H "X-API-Key: YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "mode": "api", "input": { "prompt": "Inspect the failed webhook delivery from this morning" }, "metadata": { "ticket_id": "T-1234" } }' ``` Response (`202 Accepted`): ```json theme={null} { "data": { "run_id": "990e8400-e29b-41d4-a716-446655440004", "conversation_id": "880e8400-e29b-41d4-a716-446655440003", "status": "queued", "mode": "api", "kind": "ask", "prompt": "Inspect the failed webhook delivery from this morning", "provider_model": "gpt-5.5", "reasoning_effort": "medium", "error_message": null, "ai_cost_microdollars": 0, "result": null, "pending_approval": null, "created_at": "2026-08-21T14:30:00Z", "started_at": null, "paused_at": null, "finished_at": null, "status_url": "/platform/v1/kapso-agent/runs/990e8400-e29b-41d4-a716-446655440004" } } ``` * `mode` accepts `auto`, `api`, `workflows`, `findings`, `findings-investigator`, `meta-onboarding`, `whatsapp-templates`, and `whatsapp-flow`, plus any custom mode with the `api` surface. `inbox` and `meta-business-agent` are dashboard-only. * `input.prompt` is required for the generic trigger. `findings-investigator` uses an optional `input.finding_id` instead - without it, the run investigates the next eligible finding - and doesn't accept `conversation_id`. * Triggering a mode that doesn't accept API runs returns `422`. * Pass `conversation_id` to continue an existing session. It only works for sessions created with the same API key and the same mode. * `metadata` is stored on the run and echoed back in webhooks. * Requests are not idempotent - repeating one creates a new run. * There's no model parameter. Runs use the model configured for the mode, or the project default. ## Poll a run ```bash theme={null} curl "https://api.kapso.ai/platform/v1/kapso-agent/runs/RUN_ID" \ -H "X-API-Key: YOUR_API_KEY" ``` Statuses: `queued`, `running`, `paused`, `waiting_for_approval`, `completed`, `failed`, `cancelled`. A completed run includes a `result`: ```json theme={null} { "result": { "type": "message", "message_id": "770e8400-e29b-41d4-a716-446655440002", "content": "The delivery failed with a 401..." } } ``` Only runs created via the API with the same API key can be retrieved or controlled. Anything else returns `404`. ## Control a run ```bash theme={null} curl -X POST "https://api.kapso.ai/platform/v1/kapso-agent/runs/RUN_ID/cancel" \ -H "X-API-Key: YOUR_API_KEY" ``` `POST /kapso-agent/runs/:id/pause`, `/resume`, and `/cancel`. Invalid transitions (for example, cancelling a completed run) return `409`. ## Approvals When a run needs approval its status becomes `waiting_for_approval` and `pending_approval` is set: ```json theme={null} { "pending_approval": { "id": "660e8400-e29b-41d4-a716-446655440001", "status": "pending", "tool_name": "send_whatsapp_message", "parameters": { "to": "+15551234567" }, "created_at": "2026-08-21T14:31:00Z" } } ``` Approve or reject: ```bash theme={null} curl -X POST "https://api.kapso.ai/platform/v1/kapso-agent/runs/RUN_ID/approvals/APPROVAL_ID/approve" \ -H "X-API-Key: YOUR_API_KEY" ``` ## Webhooks Subscribe to [Kapso Agent run events](/docs/platform/webhooks/project-webhooks#kapso-agent-run-events) instead of polling: * `kapso_agent.run.approval_required` * `kapso_agent.run.completed` * `kapso_agent.run.failed` * `kapso_agent.run.cancelled` These fire only for runs triggered through the API. Dashboard and Slack runs don't emit them. ```json theme={null} { "id": "3c0f1a5b9d8e7f6a4b2c1d0e9f8a7b6c5d4e3f2a1b0c9d8e7f6a5b4c3d2e1f0a", "event": "kapso_agent.run.completed", "created_at": "2026-08-21T14:35:00Z", "data": { "run_id": "990e8400-e29b-41d4-a716-446655440004", "session_id": "880e8400-e29b-41d4-a716-446655440003", "status": "completed", "agent": { "type": "mode", "id": "api" }, "result": { "type": "message", "content": "..." }, "metadata": { "ticket_id": "T-1234" }, "created_at": "2026-08-21T14:30:00Z", "started_at": "2026-08-21T14:30:05Z", "finished_at": "2026-08-21T14:35:00Z", "status_url": "/platform/v1/kapso-agent/runs/990e8400-e29b-41d4-a716-446655440004" } } ``` `id` is a deterministic SHA-256 hash of the run, event, and approval, so redeliveries carry the same id. Failed and cancelled runs include `error` with `code` (`run_failed` or `run_cancelled`) and `message`. Deliveries are signed with HMAC-SHA256 like all project webhooks - see [webhook security](/docs/platform/webhooks/security). # Modes Source: https://docs.kapso.ai/docs/kapso-agent/modes Built-in modes for common tasks and custom modes with your own instructions, tools, and permissions. A mode defines what the agent focuses on: its instructions, model, capabilities, and where it can be invoked (dashboard, API, Slack). ## Built-in modes | Mode | Use it for | | ----------------------- | -------------------------------------------------------------- | | `auto` | General Kapso Agent assistance - the default | | `api` | Inspecting and debugging project APIs and WhatsApp behavior | | `workflows` | Building, inspecting, and debugging workflows | | `inbox` | Operating live inbox conversations | | `findings` | Reviewing recurring problems and their evidence | | `findings-investigator` | Background investigation of findings | | `meta-onboarding` | Setting up and recovering WhatsApp Business connections | | `meta-business-agent` | Configuring and testing Meta Business Agent connectors | | `whatsapp-templates` | Creating, managing, and troubleshooting WhatsApp templates | | `whatsapp-flow` | Building and debugging WhatsApp Flows and their data endpoints | The [Agent API](/docs/kapso-agent/api) can trigger `auto`, `api`, `workflows`, `findings`, `findings-investigator`, `meta-onboarding`, `whatsapp-templates`, and `whatsapp-flow`. `inbox` and `meta-business-agent` are dashboard-only. Check `available_invocations` in the modes list for the current set. Custom modes need the `api` surface. `findings-investigator` runs in the background with auto-approval and does not appear in the mode picker. ## Custom modes Create your own modes under **Kapso Agent → Modes**. Project owners and admins can create and edit them. A mode requires a name, instructions, and a model. Configurable per mode: * **Instructions** - the mode's system prompt. * **Model and reasoning effort** - fixed for every run in this mode. * **Surfaces** - where the mode can be invoked: `ui`, `api`, `slack`. At least one is required. * **Capabilities** - what the agent is allowed to do (see below). * **Resources** - MCP connections, GitHub repositories, and encrypted environment variables. See [Resources](/docs/kapso-agent/resources). * **Sandbox** - read-only or command execution, with approval, network, secret, and setup controls. See [Sandbox](/docs/kapso-agent/resources#sandbox). The mode's slug is generated on creation and can't be changed afterwards. ## Capabilities | Capability | What it allows | Approval | | ---------------------------- | ------------------------------------------------------- | --------------------------------------------- | | `read_messages` | Read WhatsApp conversations and their messages | - | | `read_findings` | Read findings and their evidence | - | | `send_messages` | Send WhatsApp messages | Always required | | `modify_workflows` | Create, edit, and debug workflows | Configurable | | `operate_workflows` | Inspect workflow executions, and start and resume them | Starting and resuming always require approval | | `read_agent_sessions` | List Kapso Agent sessions and read their history | - | | `collaborate_agent_sessions` | Start sessions in other modes and send messages to them | Configurable | | `read_project_files` | Read files from repositories assigned to the mode | - | | `run_sandbox_commands` | Run commands and edit files in the mode's sandbox | Configurable | | `search_logs` | Search project logs | - | New custom modes start with `read_messages` and `read_findings`. `read_project_files` can't be toggled - it's granted when you assign a repository to the mode. # Kapso Agent Source: https://docs.kapso.ai/docs/kapso-agent/overview A conversational agent that builds, inspects, and manages your project from the dashboard, the API, or Slack. Kapso Agent operates your project for you: it inspects APIs and logs, builds workflows, configures the inbox, manages templates and WhatsApp Flows, and investigates findings. > Kapso Agent is the project-level agent in the dashboard. It is not the [agent step](/docs/flows/step-types/agent-node) inside a workflow, which handles end-user conversations.
``` Real-time message updates work automatically via WebSocket. ## Scopes Control which conversations are visible: | Scope | Conversations shown | | -------------- | -------------------------------------------- | | `project` | All conversations in the project | | `phone_number` | Conversations for a specific WhatsApp number | | `customer` | Conversations for a specific customer | ### Filter by assignee Optionally set `assigned_user_id` on the token to only show conversations assigned to a specific team member. The token will only return conversations with an active assignment to that user. ## Security ### Allowed origins Whitelist domains that can embed the inbox. Supports wildcards (`*.example.com`). Leave empty to allow any origin. Allowed origins are enforced via both CORS validation on API requests and `Content-Security-Policy: frame-ancestors` on the iframe page. ### Token expiration Optionally set an expiration date on the token. Expired tokens return a 401 error. ## Query parameters Pre-set filters and theme by passing query parameters to the embed URL. These apply as initial state when the iframe loads. ### Filters | Parameter | Values | Default | Description | | -------------------- | ------------------------ | -------- | -------------------------------------------------------------------------------- | | `status` | `active`, `ended`, `all` | `active` | Conversation status filter | | `search` | string | `""` | Pre-fill the search box | | `whatsapp_config_id` | UUID or `all` | `all` | Filter by a specific WhatsApp number | | `unread` | `1`, `true`, or omit | `all` | Show only unread conversations | | `handoff` | `1`, `true`, or omit | `all` | Show only conversations waiting for human handoff | | `contact_properties` | URL-encoded JSON array | `[]` | Filter by [contact properties](/docs/platform/inbox/overview#contact-properties) | `contact_properties` takes up to 3 objects, each with a `key`, an `operator` (`eq`, `present`, or `missing`), and a `value` when the operator is `eq`. Invalid entries are dropped. ``` https://inbox.kapso.ai/embed/{token}?contact_properties=%5B%7B%22key%22%3A%22plan%22%2C%22operator%22%3A%22eq%22%2C%22value%22%3A%22pro%22%7D%5D ``` Example — open the inbox pre-filtered to unread handoff conversations: ``` https://inbox.kapso.ai/embed/{token}?status=active&unread=1&handoff=1 ``` ### Theme Set the default theme when creating the token: `system`, `light`, or `dark`. Override the theme via the `mode` parameter: ``` https://inbox.kapso.ai/embed/{token}?mode=dark ``` The user's choice is persisted in localStorage for subsequent visits. ### Language Set the default UI language when creating or updating the token: | Value | Language | | ----- | -------- | | `en` | English | | `es` | Spanish | Override the token default for a specific iframe URL with `language` or `lang`: ``` https://inbox.kapso.ai/embed/{token}?language=es ``` ## Feature differences ### Disabled in embedded inbox * Assignments * Starting new conversations * Browser notifications * Contact notes (hidden entirely) ### Available in embedded inbox * Status, WhatsApp number, contact property, and search filters * Send text, media, and interactive messages * Workflow handoff (Handoff button works) * Contact display name editing * Real-time WebSocket updates # Messaging Source: https://docs.kapso.ai/docs/platform/inbox/messaging Send and receive messages from the inbox ## When you can send messages You can send messages when: * The conversation is **active** (not ended) * No workflow is running, or the workflow is in **handoff** or **failed** state If a workflow is running, the input is disabled. Click **Handoff** to take control. See [Automation and handoff](/docs/platform/inbox/automation). ## Message types | Type | Max size | Notes | | ------------- | -------- | ---------------------------------------- | | Text | — | Enter to send, Shift+Enter for newlines | | Image | 5 MB | Via file picker, drag-and-drop, or paste | | Video | 16 MB | | | Audio | 16 MB | | | Document | 100 MB | | | Voice message | — | Record from browser mic | You can also reply to specific messages (quoted replies). Reactions from contacts are shown as emoji badges on the target message. ## Interactive messages Send interactive WhatsApp messages via the `+` attachment menu: | Type | Details | | ------------- | --------------------------------------------------------------------------------------------------------- | | Buttons | Body + up to 3 reply buttons (max 20 chars each). Optional header (text/image/video/document) and footer. | | List | Body + sections with up to 10 total rows. Each row has a title and optional description. | | CTA URL | Body + button with text and URL. Optional header and footer. | | WhatsApp Flow | Select a published Flow, set body, header, footer, and CTA button text. | ## Quick replies Saved message templates you insert into the composer with a `/` shortcut. Type `/` followed by the shortcut in the message input. The picker filters as you type — shortcut prefix matches rank first, then name prefix, then any substring. Select one and its body replaces the `/shortcut` token. Manage quick replies from **Quick replies** in the sidebar. Each one has: * **Name** — up to 120 characters * **Shortcut** — up to 40 characters, lowercase letters, numbers, hyphens, and underscores only * **Body** — up to 10,000 characters * **Visibility** — `personal` (only you) or `shared` (everyone in the project) Shortcuts are unique per project. ### Variables Bodies can include variables that resolve against the conversation's contact when inserted. Type `{{` in the body editor to pick one. | Variable | Resolves to | | -------------------------- | ---------------------------------------- | | `{{contact.first_name}}` | First word of the contact's display name | | `{{contact.name}}` | Contact's display name | | `{{contact.phone_number}}` | Contact's phone number | If the contact has no display name, `contact.first_name` and `contact.name` fall back to the phone number. Any other variable is rejected when saving. ### AI prompts Bodies can also include prompts that an AI fills in when the reply is inserted. Type `{{` in the body editor and pick **AI prompt**, or write the syntax directly: ```text theme={null} Your refund ID is {{prompt: "Find the refund ID in this conversation"}}. ``` The prompt text is a JSON string — use `\"` for a literal quote. When you insert the reply, each prompt section shows a generating placeholder in the composer and is replaced with the generated text. Sending and voice recording are blocked while generation is running. What the AI can use: * The recent messages of the current conversation * Read-only inbox lookups (listing conversations, reading conversation messages) * Public web search, for current or public facts only — never for customer-specific data such as orders, refunds, or account details Generated text is written to fit the surrounding sentence: it matches the capitalization, spacing, and punctuation around the slot, and the text outside the slot is never changed. If essential context is missing, the AI returns a short clarification request instead of inventing a value. Limits: * Up to 10 prompts per reply * Up to 2,000 characters per prompt * Prompt text cannot be empty * Malformed prompt syntax is rejected when saving Resolution consumes AI credits from the project. In the embedded inbox, resolution is limited to 10 requests per minute per inbox token. ### Permissions | Action | Who | | -------------------------------------- | ------------------------- | | Create, edit, archive a personal reply | Its author | | Create, edit, archive a shared reply | Project owners and admins | | Change visibility | Project owners and admins | | Duplicate any visible reply | Any project member | Duplicating creates a personal copy owned by you, named ` copy` with a `-copy` shortcut suffix. Archiving hides a reply from the picker without deleting it. Use the **Archived** filter on the management page to restore it. In the [embedded inbox](/docs/platform/inbox/embedded), the `/` picker works but quick replies cannot be managed. ## Starting a new conversation Click the compose button in the conversation list header to start a new conversation: 1. Select a WhatsApp number (production numbers only) 2. Enter the recipient's phone number with country code 3. Pick an approved template 4. Fill in template parameters 5. Send ## Delivery errors Failed messages show a red warning icon with the error from WhatsApp. Common causes: * 24-hour customer service window expired * Template not approved * Invalid phone number ## Assignments Assign conversations to team members from the Info tab in the detail sidebar. * One active assignment per conversation * Creating a new assignment automatically deactivates the previous one * Not available in the [embedded inbox](/docs/platform/inbox/embedded) ## Contact info The Info tab in the detail sidebar shows: * **Display name** — Editable inline * **Phone number** * **Metadata** — Read-only, set via API or workflows * **Notes** — Add and edit notes on a contact (session auth only, not available in embedded inbox) ## Conversation history The Info tab shows previous conversations with the same phone number: date range, message count, and a link to view the full thread. # Inbox overview Source: https://docs.kapso.ai/docs/platform/inbox/overview Manage WhatsApp conversations from a shared team inbox ## Inbox variants Kapso provides three ways to access the inbox: * **App inbox** — Built into the main dashboard at `app.kapso.ai`. Full access to all project features. * **Standalone inbox** — Separate app at `inbox.kapso.ai`. For team members who only need to handle conversations. * **Embedded inbox** — Iframe you can embed in your own app. See [Embedded inbox](/docs/platform/inbox/embedded). ## Layout Three-panel layout: * **Left** — Conversation list with filters and search * **Center** — Message thread with input * **Right** — Detail sidebar with tabs: Info, Workflow, Page ## Keyboard shortcuts When the conversation list is focused, use these shortcuts to move through conversations: | Key | Action | | ------- | ------------------------------- | | `J` | Focus the previous conversation | | `K` | Focus the next conversation | | `Enter` | Open the focused conversation | | `R` | Focus the message composer | | `C` | Start a new conversation | Shortcuts are disabled while typing or interacting with another control. The `C` shortcut is not available in the [embedded inbox](/docs/platform/inbox/embedded). ## Filters | Filter | Options | | ---------------- | -------------------------------------------------- | | Status | All, Active, Ended | | Assignee | All, Me, Unassigned, specific user | | WhatsApp number | All, specific number | | Contact property | Property key with `is`, `has value`, or `is empty` | | Search | Phone number | The conversation list loads 30 conversations per page with infinite scroll. ### Contact properties Filter conversations by the custom properties stored on a contact. Up to 3 property filters can be active at once, and each supports three operators: | Operator | Matches | | --------- | ------------------------------------------------------- | | `eq` | Contacts whose property equals the given value | | `present` | Contacts that have the property set to a non-null value | | `missing` | Contacts without the property, or with it set to null | Keys are limited to 100 characters and values to 500 characters. ## URL parameters Deep-link into the inbox by appending query parameters: | Parameter | Values | Description | | -------------------- | -------------------------------- | ------------------------------------- | | `conversation_id` | UUID | Pre-selects a conversation | | `assignee` | `me`, `unassigned`, or user UUID | Pre-sets the assignee filter | | `mode` | `light`, `dark`, `system` | Sets the theme (embedded inbox only) | | `contact_properties` | URL-encoded JSON array | Pre-sets the contact property filters | Example: `https://inbox.kapso.ai/projects/{id}?conversation_id={uuid}&assignee=me` ## Real-time updates The inbox uses WebSocket for real-time updates. A green/red dot in the message thread indicates connection status. Events pushed in real-time: * New messages * Message status changes (delivered, read, failed) * New conversations * Conversation updates (status, assignment changes) The connection retries automatically with exponential backoff. ## Notifications Toggle browser notifications with the bell icon in the conversation list header. You'll get notified for new inbound messages when: * The tab isn't focused * You're viewing a different conversation Preference is persisted in localStorage. Not available in the embedded inbox. ## Standalone inbox The standalone inbox at `inbox.kapso.ai` is a separate app designed for support agents. Invite team members to the standalone inbox without giving them access to project settings, workflows, or API keys. Useful for support teams who only need to handle conversations. Features: * Member management (owner/admin only) * Account settings * Invitation system — invite via email, new users get directed to registration * Team members with the `human_agent` role are auto-redirected from `app.kapso.ai` to `inbox.kapso.ai` # Manual phone number setup Source: https://docs.kapso.ai/docs/platform/manual-phone-number-setup Connect a WhatsApp number you configure directly in Meta Use manual setup when you manage the Meta Business Portfolio and want to connect a dedicated WhatsApp Cloud API number to Kapso. Manual setup does not require Facebook Login for Business and does not require an embedded signup config ID. ## Before you start You need: * a [Meta app](/docs/platform/create-meta-app) * admin access to the Meta Business Portfolio * access to the WhatsApp account or permission to create one * a phone number you can verify by SMS or voice call if it's not already added to the portfolio ## Add the phone number in Meta In case you haven't added the number in your Meta app: 1. Open **Use cases**. 2. Click **Customize** on **Connect with customers through WhatsApp**. 3. Open **API Setup**. 4. In **Send and receive messages**, open the **From** selector. 5. Click **Add phone number**. 6. Add your phone number. 7. Verify it by SMS or voice call. 8. Copy the **Phone number ID**. 9. Copy the **WhatsApp Business Account ID**. 10. Do not copy or generate a temporary access token for Kapso. Paste the Phone number ID and WhatsApp Business Account ID into Kapso when the manual setup wizard asks for them. ## Create a permanent system-user token In [Meta Business Settings](https://business.facebook.com/latest/settings/system_users): 1. Go to **Users > System users**. 2. Add a system user, such as `Kapso API Access`. 3. Open the system user and click **Assign assets**. 4. Under **Apps**, select the Meta app you saved in Kapso and grant full access. 5. Under **WhatsApp accounts**, select the WhatsApp Business Account that owns the phone number and grant all available permissions. 6. Save the app asset assignment. 7. Click **Generate token**. 8. Select the same Meta app. 9. Set token expiration to **Never**. 10. Select both `whatsapp_business_management` and `whatsapp_business_messaging`. 11. Generate the token and copy it. Meta only shows the token once. Paste it into Kapso before closing the Meta dialog. The system user needs both asset assignments. App access lets the token belong to your Meta app. WhatsApp account access lets Kapso manage the WABA assets behind that app, including phone number registration, templates, messaging, and webhook-related operations. ## Create the number in Kapso In Kapso: 1. Open **Connected numbers**. 2. Start the manual phone number setup flow. 3. Choose the Meta app you saved under **Meta apps**. 4. Enter the WhatsApp number name. 5. Paste the Phone number ID. 6. Paste the WhatsApp Business Account ID. 7. Paste the permanent system-user token. 8. Create the configuration. Kapso generates a webhook verify token for this number. ## Configure the Meta webhook After Kapso creates the manual configuration, Kapso shows the callback URL and verify token to use in Meta. In your Meta app: 1. Open **Use cases**. 2. Click **Customize** on **Connect with customers through WhatsApp**. 3. Open **Configuration**. 4. In **Webhooks**, click **Configure** or **Edit**. 5. Paste this callback URL: ```text theme={null} https://meta-webhooks.kapso.ai/whatsapp ``` 6. Paste the verify token shown in Kapso. 7. Subscribe to all webhook fields available in Meta. 8. Click **Verify and Save**. If you later change the webhook verify token in Kapso, update the verify token in Meta too. # Onboard customers with your own Meta app Source: https://docs.kapso.ai/docs/platform/onboard-customers-own-meta-app Use Kapso setup links with your own Meta app and embedded signup configuration Onboard customers through Kapso setup links under your own Meta app, without MPS. Kapso runs messaging; customers pay Meta fees directly. ## At a glance 1. [Configure Embedded Signup](#configure-facebook-login-for-business) and [save the configuration in Kapso](#add-the-config-id-in-kapso). 2. [Create a Kapso customer](#create-customers) for each business you serve. 3. [Generate a setup link](#generate-setup-links) and let the customer connect their WhatsApp account. 4. [Confirm the connection](#detect-completion) and [start messaging](#operate-the-customer-number). To have Kapso run messaging and pay Meta fees while customers authorize your app, create a [Multi-partner Solution](/docs/platform/tech-providers/multi-partner-solutions). ## Requirements You need: * a [Meta app](/docs/platform/create-meta-app) * Facebook Login for Business on that app * an embedded signup configuration ID * [Be a Meta Tech Provider](/docs/platform/become-tech-provider). ## Configure Facebook Login for Business In your Meta app: 1. Add **Facebook Login for Business** if it is not already present. 2. Open **Facebook Login for Business > Configurations**. 3. Create a configuration from the WhatsApp embedded signup template when available. 4. If you create a custom configuration, choose the **WhatsApp Embedded Signup** login variation. 5. Select only the assets and permissions your customers need. 6. Save the configuration. 7. Copy the generated **Configuration ID**. This Configuration ID is the **Embedded signup config ID** Kapso asks for. ## Add domains for embedded signup In **Facebook Login for Business > Settings**, configure the OAuth and JavaScript SDK settings required by Meta. Add the domains where customers launch setup links or embedded signup. For Kapso-hosted setup links, include the Kapso setup domain shown in the generated link, usually `app.kapso.ai`. If you launch setup from your own app, include your app domain too. Meta requires HTTPS domains for embedded signup. ## Add the config ID in Kapso 1. In Kapso, open **Connected numbers > Meta apps**. 2. Edit or add your Meta app. 3. Turn on **Default app for embedded signup**. 4. Paste the **Embedded signup config ID**. 5. Save. Kapso setup links and reconnect flows can now use this app for embedded signup. ## Create customers Create one Kapso customer per business you serve. ```bash theme={null} curl -X POST https://api.kapso.ai/platform/v1/customers \ -H "X-API-Key: YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "customer": { "name": "Acme Clinic", "external_customer_id": "acme-clinic" } }' ``` Use `external_customer_id` to map Kapso customers back to your own database. ## Generate setup links Create a setup link for each customer. ```bash theme={null} curl -X POST https://api.kapso.ai/platform/v1/customers/{customer_id}/setup_links \ -H "X-API-Key: YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "setup_link": { "success_redirect_url": "https://your-app.com/whatsapp/success", "failure_redirect_url": "https://your-app.com/whatsapp/failed", "meta_billing_mode": "customer_managed", "allowed_connection_types": ["dedicated"] } }' ``` Send the returned `url` to the customer or open it from your onboarding UI. To launch onboarding directly inside your own product through an active Multi-partner Solution, see [Embed WhatsApp onboarding](/docs/platform/tech-providers/embed-onboarding). Use `allowed_connection_types` to control the customer path: * `["dedicated"]` for API-only WhatsApp Cloud API numbers * `["coexistence"]` for customers who want to keep using the WhatsApp Business App alongside Kapso * `["coexistence", "dedicated"]` when you want the customer to choose If you want Kapso to provision a number during setup, add: ```json theme={null} { "setup_link": { "provision_phone_number": true, "phone_number_country_isos": ["US"] } } ``` ## Detect completion Use both redirect handling and webhooks. The success redirect can include setup details such as: * `status` * `phone_number_id` * `business_account_id` * `display_phone_number` * `setup_link_id` For server-side reliability, subscribe to Kapso project webhooks and listen for `whatsapp.phone_number.created`. See [Detecting WhatsApp connection](/docs/platform/setup-links/detect-connection) and [Webhooks](/docs/platform/webhooks/overview). ## Operate the customer number After setup completes, use the customer's `phone_number_id`. ```bash theme={null} curl -X POST https://api.kapso.ai/meta/whatsapp/v24.0/{phone_number_id}/messages \ -H "X-API-Key: YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "messaging_product": "whatsapp", "to": "15551234567", "type": "text", "text": { "body": "Your appointment is confirmed." } }' ``` # Instant setup Source: https://docs.kapso.ai/docs/platform/phone-numbers/instant-setup Use Kapso-managed pre-verified numbers for the default fast onboarding flow. ## What it is Instant setup is the default Kapso-managed phone number path for WhatsApp onboarding. Kapso provisions the number and, when available, uses a pre-verified number so the customer can finish onboarding without manual SMS or phone-call verification. ## What it enables * Fast onboarding with Kapso-managed numbers * No Twilio account required * No custom telephony setup required * Pre-verified number assignment when capacity is available ## Availability * Default country: `US` * Number ownership: Kapso-managed * Best for: standard onboarding when you do not need local numbers or your own Twilio account If you need local numbers, your own Twilio billing, or reusable project-owned inventory, use [Provide local numbers](/docs/platform/phone-numbers/provide-local-numbers). ## Free number for new users Free plan users get one Kapso-managed number at no cost — no wallet deposit required. Eligibility: * Free plan only * The user's default first project * One lifetime claim per user * Kapso instant setup path only If that first free number is later deleted or disconnected, the lifetime free claim does **not** reset. A replacement Kapso-managed number may require the standard deposit, which goes directly to project credits. If the number has no production messages within 30 days, it is automatically released. ## How pre-verified numbers work Kapso keeps a shared pool of pre-verified numbers for the default instant-setup path. When a ready number is available for the requested country, Kapso can pass that number into the onboarding flow and skip the normal OTP verification step. When no ready pre-verified number is available, onboarding still works. Kapso falls back to the standard provisioning path. ## Embedded signup behavior For the default instant-setup path, Kapso tries: 1. A ready pre-verified number from the shared pool 2. Standard provisioning if no ready pre-verified number is available This means instant setup is opportunistically pre-verified, not a hard requirement. ## Setup links behavior Setup links use the same default logic. If `provision_phone_number` is enabled and the request is for a supported default country, Kapso will try the shared pre-verified pool first. If no ready number exists, the setup link still works and falls back to standard provisioning. ## Limits Instant setup is not the right fit when you need: * non-US local numbers * your own Twilio account * project-owned reusable number pools * country-by-country number inventory control For those cases, use [Provide local numbers](/docs/platform/phone-numbers/provide-local-numbers). # Provide local numbers Source: https://docs.kapso.ai/docs/platform/phone-numbers/provide-local-numbers Provision local numbers in any country through your own Twilio account. ## What it unlocks Provision local numbers in the countries you support, using your own Twilio account. * Provision new phone numbers into your own Twilio account instead of Kapso's shared account * Support multiple countries in the same project * Keep reusable project-owned pools per country * Use project-owned pre-verified numbers for faster onboarding ## Plans * Enterprise: included * Pro, Team, Platform: available as a `$400/mo` add-on on the existing project subscription * Free and Legacy: visible in the UI, but cannot be enabled ## How custom Twilio works Configure this in **Project settings → Custom Twilio**. You add Twilio credentials at the project level. Once active, Kapso uses that Twilio account for number search, provisioning, and pool maintenance for that project. You can configure: * a default country * multiple allowed countries * per-country overrides for regulatory bundle and address data API keys are optional. If present, Kapso uses them for Twilio API calls. The auth token is still required. Custom Twilio changes phone ownership. These numbers belong to the project's Twilio account, not Kapso's shared pool. ## Other telephony providers **Project settings → Telephony** lets you store credentials per provider and pick which one the project uses for search, provisioning, and pool maintenance. Each provider keeps its own secrets, allowed countries, and per-country configuration, and one provider is selected as the project default. Configuring any provider here counts as custom telephony: setup links and embedded signup can then request non-US countries. Telnyx is enabled per project by Kapso. If it is not available on your project, ask support to turn it on. ## What to set ### Required * `Account SID`: the Twilio account Kapso should use for search and provisioning * `Auth token`: required for webhook validation and for Twilio client auth when API keys are not used These two fields are enough for the basic flow. ### Optional * `API key SID` * `API key secret` Use these if you want Kapso to authenticate Twilio API calls with an API key pair instead of the auth token. Kapso still requires the auth token because Twilio webhook validation uses it. ### Country configuration * `Allowed countries`: the ISO country codes this project is allowed to provision in * `Country overrides`: per-country regulatory values used when Twilio requires them Use `Allowed countries` to keep provisioning scoped to the countries you actually support. Use country overrides when a specific country needs different regulatory setup than your project default. In v1, the main fields are: * `Bundle SID` * `Address SID` If a country override exists, Kapso uses it for that country. Otherwise it uses the project-level values, and if none are set it relies on the values passed in the provisioning flow. ## Why these fields matter * `Account SID` decides which Twilio account owns the number * `Allowed countries` controls where setup links and embedded signup are allowed to provision * `Bundle SID` and `Address SID` are how you satisfy country-specific Twilio compliance requirements * pool settings decide whether onboarding can use a ready pre-verified number or has to fall back to live provisioning ## How project pools work Project pools are Twilio-only in v1. Each project can keep multiple pools, with one pool per country. A pool config includes: * `country` * `enabled` * `target pool size` * `post disconnect behavior` ### What each pool setting does * `Country`: which country this pool covers * `Enabled`: whether Kapso should actively maintain the pool * `Target pool size`: how many ready project-owned numbers Kapso should keep available for that country * `Post disconnect behavior`: what happens after a WhatsApp config is removed from a pool-managed number Kapso maintains each enabled pool by provisioning numbers into the project's Twilio account and pre-verifying them for onboarding. ## Pre-verified project pools When a pool has ready numbers for a country, Kapso can pass those pre-verified numbers into the onboarding flow and skip the normal OTP step. This only works when the project uses: * custom Twilio * Kapso-managed Meta credentials * an enabled pool with ready numbers for the requested country If the project uses custom Meta credentials, Kapso still uses the project's Twilio account for live provisioning, but project-owned pre-verified pools are not available in v1. ## Embedded signup behavior Kapso resolves the number path in this order: 1. Matching project pool with a ready pre-verified number 2. Live provisioning through the project's Twilio account 3. Standard Kapso behavior when custom Twilio is not enabled That means: * if a matching pool exists, onboarding can skip OTP * if no matching pool exists, onboarding still works * pooled and non-pooled countries can coexist in the same project ## Setup links behavior Setup links use the same logic as embedded signup. If a setup link requests a country that has a ready project pool, Kapso uses that project-owned pre-verified path. If the setup link requests a country without a pool, or the pool is empty, the setup link still works. Kapso falls back to live Twilio search and provisioning for that country. ## Disconnect behavior Each pool has a default disconnect policy: * `mark_unavailable`: keep the number on the project, but do not reuse it for WhatsApp again * `return_to_pool`: keep the number on the project and make it reusable by that same project's pool In both cases, the number stays on the project's Twilio account. Kapso does not return it to the shared global pool. Project-owned custom-Twilio numbers are not auto-released after 24h. ## Typical setup 1. Enable the add-on, or use Enterprise 2. Add custom Twilio credentials 3. Add allowed countries 4. Create one pool per country you want pre-verified capacity for 5. Set target size and disconnect behavior 6. Use embedded signup or setup links normally ## When to use it Use this when you need Kapso to provision numbers through your Twilio account for: * local non-US numbers * numbers billed directly to your Twilio account * project-owned reusable inventory * instant onboarding from your own pre-verified pool If you only need the default US instant-setup flow, use [Instant setup](/docs/platform/phone-numbers/instant-setup). # Create and configure Source: https://docs.kapso.ai/docs/platform/setup-links/create-and-configure Create a setup link and configure everything your customer will see Setup links let customers connect their WhatsApp Business accounts to your platform. Send a link, customer clicks, logs in with Facebook, and you're connected. ## Quick start Create a setup link: ```bash cURL theme={null} curl -X POST https://api.kapso.ai/platform/v1/customers/{customer_id}/setup_links \ -H "X-API-Key: YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "setup_link": { "success_redirect_url": "https://your-app.com/whatsapp/success", "failure_redirect_url": "https://your-app.com/whatsapp/failed", "allowed_origins": ["https://your-app.com"], "meta_billing_mode": "partner_managed" } }' ``` ```javascript JavaScript theme={null} const res = await fetch( `https://api.kapso.ai/platform/v1/customers/${customerId}/setup_links`, { method: 'POST', headers: { 'X-API-Key': 'YOUR_API_KEY', 'Content-Type': 'application/json' }, body: JSON.stringify({ setup_link: { success_redirect_url: 'https://your-app.com/whatsapp/success', failure_redirect_url: 'https://your-app.com/whatsapp/failed', allowed_origins: ['https://your-app.com'], meta_billing_mode: 'partner_managed' } }) } ); const { data } = await res.json(); ``` The response carries the `url` you send to your customer: ```json theme={null} { "data": { "id": "9f8e7d6c-5b4a-3210-fedc-ba9876543210", "status": "active", "url": "https://setup.kapso.ai/s/aB3dEf5g", "created_at": "2026-08-01T10:00:00Z", "expires_at": "2026-08-31T10:00:00Z", "success_redirect_url": "https://your-app.com/whatsapp/success", "failure_redirect_url": "https://your-app.com/whatsapp/failed", "allowed_origins": ["https://your-app.com"], "meta_billing_mode": "partner_managed", "allowed_connection_types": ["coexistence", "dedicated"], "language": null, "theme_config": null, "provision_phone_number": false, "phone_number_area_code": null, "phone_number_country_isos": [], "reconnect_phone_number": null, "whatsapp_setup_status": "pending", "whatsapp_setup_error": null, "provisioned_phone_number": null } } ``` See [Connection detection](/docs/platform/setup-links/detect-connection) for handling successful connections. ## Parameters Every field is optional. Send only what you want to change from the defaults. Where the customer lands after completing setup. Receives query parameters with setup details. Where the customer lands if setup fails. HTTPS browser origins allowed to use this setup link with `@kapso/sdk`. Include scheme and hostname, without a path. Not required for the Kapso-hosted setup page. Which [connection types](#connection-types) the customer can pick. Passing a single value auto-selects it and skips the choice. Who pays Meta's message fees: `customer_managed` or `partner_managed`. See [Meta billing policy](#meta-billing-policy). Cannot be changed after the link is created. Language for the hosted setup page: `en`, `es`, `pt`, `hi`, `id`, or `ar`. Defaults to the customer's browser language. Provision a phone number for the customer during setup. See [Phone number provisioning](#phone-number-provisioning). Non-US numbers require your own telephony credentials on the project. Countries the provisioned number can come from, as ISO country codes. Preferred area code for the provisioned number. US only. Brand colors for the hosted setup page. See [Theme customization](#theme-customization). Scope the link to refresh credentials for a number the customer already has. See [Reconnect existing numbers](/docs/platform/setup-links/manage#reconnect-existing-numbers). ## Connection types Customers can connect their WhatsApp in two ways: **Coexistence** - Keep using WhatsApp Business app alongside API * 5 messages/second * App stays active * Good for small businesses **Dedicated** - API-only access for automation * Up to 1000 messages/second * No app access * Built for scale By default the customer chooses. Pass a single value in `allowed_connection_types` to decide for them: ```json theme={null} { "setup_link": { "allowed_connection_types": ["dedicated"] } } ``` ## Meta billing policy Choose how Meta's message fees should be paid after the customer connects their WABA: * `customer_managed` - the customer pays Meta directly. This is the default. * `partner_managed` - Kapso pays Meta and deducts the cost from the project's Kapso credits. ```json theme={null} { "setup_link": { "meta_billing_mode": "partner_managed" } } ``` `partner_managed` works with Kapso's default Meta app. It also works with your own Tech Provider app when the WABA is connected through an active Kapso [Multi-partner Solution](/docs/platform/tech-providers/multi-partner-solutions). A standalone custom Meta app without that solution is not eligible. `meta_billing_mode` can be set only when creating the link. The response echoes the selected value. Create a new link to change the policy. The hosted setup page does not let the customer change this policy or add billing disclosure. Explain the billing arrangement to your customer before sharing the link. See [Meta message billing](/docs/whatsapp/meta-message-billing) for the customer-facing differences. Kapso checks WABA-level eligibility after Meta returns the connected account. If Kapso billing cannot be attached, number setup still completes and the project owner sees a retryable warning in Kapso. A completed setup link or `meta_billing_mode: "partner_managed"` does not confirm that Kapso billing was attached. ## Embed setup in your product Tech Providers using an active Kapso Multi-partner Solution can launch the same setup flow from their own product with `@kapso/sdk`. Create the link from your backend, set `allowed_origins`, and pass the response's `token` to the browser. See [Embed WhatsApp onboarding](/docs/platform/tech-providers/embed-onboarding). ## Phone number provisioning Automatically provision a phone number for customers: ```json theme={null} { "setup_link": { "provision_phone_number": true, "phone_number_country_isos": ["US", "CL"] } } ``` ### Country support * Default Kapso provisioning: `["US"]`. See [Instant setup](/docs/platform/phone-numbers/instant-setup) * For your own telephony account, multi-country support, and project-owned pre-verified pools, see [Provide local numbers](/docs/platform/phone-numbers/provide-local-numbers) If the requested country has a ready project pool, setup can use a pre-verified number. If that country has no pool, or the pool is empty, the setup link still works and falls back to live provisioning. When `provision_phone_number` is enabled, complete Meta's phone-number step with the Kapso-provided, BSP-provided, or project-pool number shown by the flow, not with [Display name only](/docs/how-to/whatsapp/connect-whatsapp#why-to-avoid-display-name-only). ## Theme customization Match your brand colors: ```json theme={null} { "setup_link": { "theme_config": { "primary_color": "#3b82f6", "background_color": "#ffffff", "text_color": "#1f2937", "muted_text_color": "#64748b", "card_color": "#f9fafb", "border_color": "#e5e7eb" } } } ``` All colors use hex format (#RRGGBB). ## Full example ```javascript theme={null} const setupLink = await fetch( `https://api.kapso.ai/platform/v1/customers/${customerId}/setup_links`, { method: 'POST', headers: { 'X-API-Key': 'YOUR_API_KEY', 'Content-Type': 'application/json' }, body: JSON.stringify({ setup_link: { success_redirect_url: 'https://app.example.com/onboarding/complete', failure_redirect_url: 'https://app.example.com/onboarding/error', allowed_origins: ['https://app.example.com'], meta_billing_mode: 'partner_managed', allowed_connection_types: ['dedicated'], provision_phone_number: true, phone_number_country_isos: ['US'], language: 'es', theme_config: { primary_color: '#10b981', background_color: '#ffffff', text_color: '#111827' } } }) } ); // Send link to customer await sendEmail(customer.email, { subject: 'Connect your WhatsApp', body: `Click here to connect: ${setupLink.data.url}` }); ``` # Connection detection Source: https://docs.kapso.ai/docs/platform/setup-links/detect-connection Know when customers complete WhatsApp onboarding You have three ways to detect when customers connect their WhatsApp account through setup links. ## 1. Project webhooks Configure a project webhook to receive the `whatsapp.phone_number.created` event. This is the recommended approach for server-to-server notifications. ### Setup 1. Open **Integrations → Webhooks** 2. Go to the **Platform webhooks** tab 3. Click **Add Webhook** 4. Enter your HTTPS endpoint URL 5. Copy the auto-generated secret key 6. Subscribe to `whatsapp.phone_number.created` event ### Webhook payload ```json theme={null} { "phone_number_id": "123456789012345", "project": { "id": "990e8400-e29b-41d4-a716-446655440004" }, "customer": { "id": "880e8400-e29b-41d4-a716-446655440003" } } ``` ### Handle the webhook ```javascript theme={null} app.post('/webhooks/project', async (req, res) => { const event = req.headers['x-webhook-event']; if (event === 'whatsapp.phone_number.created') { const { phone_number_id, customer } = req.body; // Update your database await db.customers.update(customer.id, { phone_number_id, whatsapp_connected: true, connected_at: new Date() }); // Trigger welcome flow await sendWelcomeMessage(phone_number_id, customer.id); } res.status(200).send('OK'); }); ``` See [webhooks documentation](/docs/platform/webhooks) for signature verification and best practices. ## 2. Success redirect URL When customers complete WhatsApp setup, they're redirected to your `success_redirect_url` with query parameters. ### Setup When creating a setup link, provide redirect URLs: ```javascript theme={null} const setupLink = await fetch('https://api.kapso.ai/platform/v1/customers/customer-123/setup_links', { method: 'POST', headers: { 'X-API-Key': 'YOUR_API_KEY', 'Content-Type': 'application/json' }, body: JSON.stringify({ setup_link: { success_redirect_url: 'https://your-app.com/whatsapp/success', failure_redirect_url: 'https://your-app.com/whatsapp/failed', meta_billing_mode: 'partner_managed' } }) }); ``` ### Query parameters After successful setup, customer is redirected to: ``` https://your-app.com/whatsapp/success?setup_link_id=...&status=completed&phone_number_id=123456789012345&business_account_id=...&whatsapp_config_id=...&provisioned_phone_number_id=...&display_phone_number=%2B15551234567 ``` **Parameters**: * `setup_link_id` - UUID of the setup link * `status` - Always `completed` for success * `phone_number_id` - WhatsApp phone number ID (primary identifier) * `business_account_id` - Meta WABA ID (if available) * `whatsapp_config_id` - Legacy identifier (provided for backward compatibility) * `provisioned_phone_number_id` - Kapso phone number ID (if provisioning was used) * `display_phone_number` - E.164 formatted phone number (URL encoded) ### Handle the redirect ```javascript theme={null} app.get('/whatsapp/success', async (req, res) => { const { setup_link_id, status, phone_number_id, business_account_id, whatsapp_config_id, // Legacy, use phone_number_id instead provisioned_phone_number_id, display_phone_number } = req.query; // Update your database await db.customers.update({ phone_number_id, business_account_id, display_phone_number: decodeURIComponent(display_phone_number), whatsapp_connected: true, connected_at: new Date() }); // Show success page to customer res.render('whatsapp-connected', { phoneNumber: decodeURIComponent(display_phone_number) }); }); ``` These parameters are convenience identifiers to avoid extra API fetches. Use `phone_number_id` as the primary identifier. ### Failure redirect If setup fails, customer is redirected to your `failure_redirect_url`: ``` https://your-app.com/whatsapp/failed?setup_link_id=...&error_code=facebook_auth_failed ``` **Error codes**: * `facebook_auth_failed` - Facebook login cancelled * `phone_verification_failed` - Phone verification failed * `waba_limit_reached` - Too many WhatsApp accounts * `token_exchange_failed` - OAuth failed * `link_expired` - Link expired (30 days) * `already_used` - Link already used ```javascript theme={null} app.get('/whatsapp/failed', (req, res) => { const { setup_link_id, error_code } = req.query; // Log failure for monitoring await logSetupFailure(setup_link_id, error_code); // Show user-friendly error message res.render('whatsapp-setup-failed', { errorMessage: getErrorMessage(error_code) }); }); ``` ## 3. Read the setup link Read a link to see how far the customer got, without waiting for a redirect or a webhook: ```bash theme={null} curl https://api.kapso.ai/platform/v1/customers/{customer_id}/setup_links \ -H "X-API-Key: YOUR_API_KEY" ``` Two fields track progress: Lifecycle of the link itself: `active`, `used`, `expired`, or `revoked`. How far the WhatsApp connection got: `pending`, `processing`, `completed`, or `failed`. When `whatsapp_setup_status` is `failed`, `whatsapp_setup_error` carries the reason. When a number was provisioned, `provisioned_phone_number` carries it: ```json theme={null} { "whatsapp_setup_status": "completed", "whatsapp_setup_error": null, "provisioned_phone_number": { "id": "7c6b5a49-3827-1605-4433-2211aabbccdd", "phone_number": "+14155551234", "display_number": "+1 415 555 1234", "status": "assigned", "area_code": "415", "country_iso": "US", "country_dial_code": "1" } } ``` Use [webhooks](/docs/platform/webhooks/project-webhooks) to react to connections as they happen. Read the link when you need to reconcile state or debug a setup that never reported back. ## Choosing the right method **Use project webhooks when**: * You need server-to-server notification * Customer doesn't need immediate visual feedback * You're building automated onboarding flows * You need to process the connection before showing UI **Use success redirect when**: * Customer needs immediate confirmation in your app * You want to show a custom success page * You're building a wizard-style onboarding flow * You need to collect additional information after connection **Read the setup link when**: * You need to reconcile state after a webhook never arrived * You're debugging a setup that failed and want the error * You're checking status on demand rather than reacting to an event Most integrations use the first two together: the webhook for backend processing (database updates, welcome messages), the redirect for the frontend experience (success page, next steps). Reading the link is the fallback when neither reported back. # Manage links Source: https://docs.kapso.ai/docs/platform/setup-links/manage List, update, revoke, and expire setup links, and reconnect a broken number ## List all links ```bash theme={null} curl https://api.kapso.ai/platform/v1/customers/{customer_id}/setup_links \ -H "X-API-Key: YOUR_API_KEY" ``` ## Update a link `PATCH` an existing link to change its redirects, allowed browser origins, theme, language, connection types, provisioning settings, or expiry: ```bash theme={null} curl -X PATCH https://api.kapso.ai/platform/v1/customers/{customer_id}/setup_links/{setup_link_id} \ -H "X-API-Key: YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "setup_link": { "language": "es", "expires_at": "2026-12-31T23:59:59Z", "allowed_origins": ["https://app.example.com"] } }' ``` `meta_billing_mode` is the one field you cannot change after creation. ## Revoke a link Set `status` to `revoked` to kill a link before it expires: ```bash theme={null} curl -X PATCH https://api.kapso.ai/platform/v1/customers/{customer_id}/setup_links/{setup_link_id} \ -H "X-API-Key: YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{"setup_link": {"status": "revoked"}}' ``` You cannot set `status` to `used`. Creating a new link also revokes the previous one, since only one link can be active per customer. ## Expiration Links expire 30 days after creation. Check `expires_at`, or set it with `PATCH` to expire a link sooner or later. ## Reconnect existing numbers When a customer's WhatsApp connection breaks (token revoked, Meta de-auth, password change), generate a setup link scoped to a specific phone number they already have on Kapso: ```json theme={null} { "setup_link": { "reconnect_phone_number": "+14155551234" } } ``` Kapso looks up the customer's existing WhatsApp config matching that number, then forces the setup flow to refresh credentials for the same WABA + phone number. Behavior: * The number must already exist on a production WhatsApp config for this customer. * `provision_phone_number` is forced to `false`. Passing `true` returns `422`. * `allowed_connection_types` is locked to `["dedicated"]` or `["coexistence"]` to match the existing config. Passing a different value returns `422`. * During Meta's embedded signup, the phone number selector is constrained to the matching number — if the customer connects a different account or number, setup fails. If no config matches, the API returns `422` with `reconnect_phone_number must match an existing WhatsApp config for this customer`. If multiple configs match the same display number, the API returns `422` and you'll need to resolve the duplicates first. # Kapso infrastructure Source: https://docs.kapso.ai/docs/platform/tech-providers/embed-onboarding Connect customers through setup links or embed WhatsApp onboarding in your product Use this path after [setting up MPS](/docs/platform/tech-providers/multi-partner-solutions) with **Messages processed by: Kapso**. Kapso runs messaging and completes onboarding for you. ## At a glance 1. **Choose the onboarding experience:** [send a hosted setup link](#send-a-setup-link) or [embed onboarding in your app](#embed-in-your-app) with `@kapso/sdk`. 2. [Create a setup link for each customer](/docs/platform/setup-links/create-and-configure), choosing the connection type and who pays Meta fees. 3. **Let the customer connect:** share the link's URL, or [pass its token to the SDK](#connect-from-the-browser). 4. [Confirm the connection and billing](#confirm-connection-and-billing) through a completion webhook and a funding check if you selected Kapso credits. Both options use a setup link. Hosting onboarding on your domain does not change who processes messages: Kapso still does. ## Send a setup link The customer completes onboarding on a page hosted by Kapso. No frontend SDK is needed. 1. Create a [customer setup link](/docs/platform/setup-links/create-and-configure). Choose `customer_managed` for direct Meta billing or `partner_managed` to use Kapso credits. 2. Send the returned `url` to the customer or open it from your product. 3. [Confirm the connection and billing](#confirm-connection-and-billing) after the customer finishes. ## Embed in your app Use `@kapso/sdk` to open onboarding directly from your product. Your backend creates the same setup link, but passes its token to the browser instead of sharing its URL. Never expose your Kapso API key. `@kapso/sdk` requires an active Kapso [Multi-partner Solution](/docs/platform/tech-providers/multi-partner-solutions). It is not a generic embedded signup SDK and does not work with standalone custom Meta apps. ### Requirements * an active Multi-partner Solution with **Messages processed by: Kapso** * the browser origin in the setup link's `allowed_origins` * the same HTTPS domain [added in Meta's Facebook Login for Business settings](/docs/platform/onboard-customers-own-meta-app#add-domains-for-embedded-signup) The setup link must allow exactly one connection type: `dedicated` or `coexistence`. Create separate setup links if your product offers both paths. ### Install the SDK ```bash npm theme={null} npm install @kapso/sdk ``` ```bash yarn theme={null} yarn add @kapso/sdk ``` ```bash pnpm theme={null} pnpm add @kapso/sdk ``` ### Create a setup link on your backend Keep your Kapso API key on the server. Create one setup link for the customer and specify every browser origin that may launch it. ```bash cURL theme={null} curl -X POST https://api.kapso.ai/platform/v1/customers/{customer_id}/setup_links \ -H "X-API-Key: YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "setup_link": { "allowed_origins": ["https://app.example.com"], "allowed_connection_types": ["dedicated"], "meta_billing_mode": "partner_managed" } }' ``` ```javascript JavaScript theme={null} const res = await fetch( `https://api.kapso.ai/platform/v1/customers/${customerId}/setup_links`, { method: 'POST', headers: { 'X-API-Key': 'YOUR_API_KEY', 'Content-Type': 'application/json' }, body: JSON.stringify({ setup_link: { allowed_origins: ['https://app.example.com'], allowed_connection_types: ['dedicated'], meta_billing_mode: 'partner_managed' } }) } ); const { data } = await res.json(); ``` Return only `data.token` to your frontend. Do not expose your Kapso API key. ### Connect from the browser ```typescript theme={null} import { createWhatsAppOnboarding } from '@kapso/sdk'; const whatsapp = await createWhatsAppOnboarding({ token: setupLinkToken }); connectButton.onclick = async () => { const connection = await whatsapp.connect(); onConnected(connection); }; ``` The resolved connection includes: ```typescript theme={null} type WhatsAppConnection = { whatsappConfigId: string; wabaId: string; phoneNumberId: string; }; ``` The SDK validates the token and browser origin, loads the Meta JavaScript SDK, runs embedded signup with your Multi-partner Solution, and resolves once Kapso confirms the connection. ### Configure the setup path The setup link controls the customer experience: * `allowed_connection_types: ["dedicated"]` — API-only WhatsApp number * `allowed_connection_types: ["coexistence"]` — keep using WhatsApp Business App * `provision_phone_number: true` — use a Kapso-provided or project-pool number * `meta_billing_mode: "partner_managed"` — use Kapso credits for Meta fees Set these on the server when you create the link. The browser receives only the setup token. ### Handle cancellation and errors Catch SDK initialization failures separately from connection failures. Keep `connect()` inside the button's click handler so the browser allows Meta's popup. ```typescript theme={null} import { createWhatsAppOnboarding, WhatsAppOnboardingError, } from '@kapso/sdk'; connectButton.disabled = true; try { const whatsapp = await createWhatsAppOnboarding({ token: setupLinkToken }); connectButton.disabled = false; connectButton.onclick = async () => { connectButton.disabled = true; try { onConnected(await whatsapp.connect()); } catch (error) { if (!(error instanceof WhatsAppOnboardingError && error.code === 'user_cancelled')) { showConnectionError(); } } finally { connectButton.disabled = false; } }; } catch { showConnectionError(); } ``` Call `whatsapp.destroy()` if your application removes the onboarding component before setup completes. ## Confirm connection and billing For either onboarding option, use the setup redirect or SDK result for immediate UI feedback. For server-side reliability, subscribe to Kapso project webhooks and listen for `whatsapp.phone_number.created`. If you selected `partner_managed`, also confirm that managed funding is verified before sending paid messages. A successful WhatsApp connection alone does not confirm funding. Resolve any billing attention state first. See [Detect connection](/docs/platform/setup-links/detect-connection) and [Webhooks](/docs/platform/webhooks/overview). ## You run messaging If your MPS uses **Messages processed by: Your infrastructure**, follow [Your infrastructure](/docs/platform/tech-providers/your-infrastructure). That path uses Meta's SDK, not `@kapso/sdk`. # Set up MPS Source: https://docs.kapso.ai/docs/platform/tech-providers/multi-partner-solutions Connect your Tech Provider app to Kapso infrastructure or managed billing A Multi-partner Solution links your Tech Provider app with Kapso. Your app owns the customer relationship. Kapso can run the WhatsApp infrastructure, provide managed billing, or both. ## At a glance 1. [Check the requirements](#requirements) for your Meta app, Kapso project, and prepaid balance. 2. [Choose your infrastructure](#choose-your-infrastructure): Kapso or your own integration. 3. [Create the solution](#create-the-solution) and [confirm it is active](#solution-status) after both partners are accepted. 4. Onboard customers with [Kapso infrastructure](#onboard-with-kapso-infrastructure) or [your infrastructure](#onboard-with-your-infrastructure). Multi-partner Solutions are in beta. Supported features, eligibility, pricing, and reporting intervals may change. ## Requirements Before creating a solution: * [create your Meta app](/docs/platform/create-meta-app) and [complete Meta business and access verification as a Tech Provider](/docs/platform/become-tech-provider) * request the Meta permissions required by your embedded signup flow * create a WhatsApp embedded signup configuration for your app * save your Meta app and configuration ID in Kapso if Kapso will run messaging * your Kapso project must be on the **Platform or Enterprise plan** * ask Kapso to enable Multi-partner Solutions for the project * maintain the minimum prepaid balance shown in the project ## Choose your infrastructure Choose who will process messages after onboarding. This is separate from where you host the onboarding UI. | Messages processed by | Signup flow | Billing and usage | | --------------------- | -------------------------------- | ----------------------------------------------------------------------------- | | Kapso | Kapso setup link or `@kapso/sdk` | Kapso meters messages directly and can provide managed billing | | Your infrastructure | Your Meta integration | Kapso detects connected WABAs and tracks usage through Meta Pricing Analytics | ## Create the solution In Kapso, open **Tenants > Multi-partner solution**. Two ways to create it: * **Create with Kapso** — Kapso opens Meta's embedded creation flow and stores the returned solution. * **Create manually in Meta** — create and submit the solution request in Meta, then enter the exact solution ID and your app ID in Kapso. Kapso reviews and accepts its side. The solution activates after both partners are accepted in Meta. Choose: * **Messages processed by:** Kapso or Your infrastructure * **App with messaging permission:** Your Meta app Kapso validates the apps and permissions before activating the solution. ## Solution status | Status | Meaning | | -------------- | -------------------------------------------------------------------- | | Pending review | The solution exists in Meta but is not yet accepted by both partners | | Active | New customers can onboard through the solution | | Deactivating | Meta is processing deactivation; new onboarding is disabled | | Deactivated | Kept for history; cannot onboard new accounts | Deactivating a solution keeps existing customer connections. Customers are not moved to another solution. ## Onboard with Kapso infrastructure Create a [setup link](/docs/platform/setup-links/create-and-configure) or [embed onboarding in your product](/docs/platform/tech-providers/embed-onboarding). Kapso locks the solution to each signup attempt when it starts, so changing the solution mid-flow does not affect onboarding in progress. The connected WABA stays tied to the solution it onboarded with, for billing and analytics. Set `meta_billing_mode` when creating the setup link: * `customer_managed` — the customer pays Meta directly. * `partner_managed` — Kapso attaches its credit line and deducts WhatsApp fees from your project's prepaid USD balance. ## Onboard with your infrastructure Follow [Your infrastructure](/docs/platform/tech-providers/your-infrastructure) to initialize Meta's SDK, pass your Solution ID, and complete onboarding on your backend. Meta reports each associated WABA to Kapso. For every eligible active WABA, Kapso checks eligibility and project balance, attaches its credit line automatically, and starts metering from that point — usage from before the WABA was funded is not charged. Kapso checks aggregate Meta usage about every five minutes. Accounts that cannot be funded show a **Needs attention** state. Kapso retries recoverable failures without creating duplicate funding or charges. External usage comes from Meta Pricing Analytics. New messages and charges can take several minutes to appear in Kapso. ## Monitor customer accounts Open **Tenants > Multi-partner solution** to see: * detected customer WABAs and their connection state * managed funding status and WABA currency * messages and external spend observed in the last 24 hours * external spend observed in the last 30 days * when Meta last reported each account The summary also shows an estimated balance buffer. It compares your available balance with the highest five-minute WhatsApp spend observed in the last 24 hours. It is an estimate, not a spending limit or guarantee. ## Managed billing When Kapso runs messaging, a successful WhatsApp connection does not by itself confirm that managed billing was attached. Check the billing result returned by the setup flow. When messages run through your infrastructure, Kapso cannot pause your sends. Keep enough prepaid balance to cover usage reported between Pricing Analytics checks. The balance buffer helps you monitor that exposure but does not block sends. Messages sent through your infrastructure include a 5% managed billing fee on top of Meta's published USD list rate. This fee is separate from the FX margin applied to non-USD WABAs. Marketing Lite is billed at Meta's published marketing rate. Max-price bidding above the published rate is not supported. ## WhatsApp Calling Managed billing for WhatsApp Calling is available for eligible USD WABAs. User-initiated calls are free. Meta charges business-initiated calls based on duration and destination, and Kapso deducts the aggregate cost from your prepaid balance. When calls run through Kapso, Kapso checks billing health and balance before starting a business-initiated call. When calls run through your infrastructure, Kapso checks Meta Call Analytics about every five minutes and cannot block calls made through your own integration. Calling fees are reported as aggregate usage. Kapso does not show an exact Meta fee for an individual call. See [Meta message billing](/docs/whatsapp/meta-message-billing) for pricing and supported currencies. # Tech Provider onboarding Source: https://docs.kapso.ai/docs/platform/tech-providers/overview Onboard customers with your Meta app, with or without a Multi-partner Solution Onboard customers with your own Meta app. Choose whether to connect it to Kapso through a Multi-partner Solution (MPS): Use Kapso infrastructure with your Meta app. Customers pay Meta directly. Start with customer setup links. Use Kapso or your own infrastructure, with managed billing available. Start by setting up your solution. ## Before you start Both paths use your own Meta app and Tech Provider verification: 1. [Create the app](/docs/platform/create-meta-app). 2. [Become a Tech Provider](/docs/platform/become-tech-provider). Already completed these? Choose a path above. MPS has additional [project and balance requirements](/docs/platform/tech-providers/multi-partner-solutions#requirements). MPS is in **beta**. Features, eligibility, pricing, and reporting intervals may change. ## Don't have a Meta app? [Use Kapso's Meta app](/docs/platform/setup-links/create-and-configure) to onboard customers without creating your own app or becoming a Tech Provider. # Your infrastructure Source: https://docs.kapso.ai/docs/platform/tech-providers/your-infrastructure Onboard customers with your Meta integration and use Kapso for managed billing Use this path after [setting up MPS](/docs/platform/tech-providers/multi-partner-solutions) with **Messages processed by: Your infrastructure**. You own registration, messaging, and message webhooks; Kapso provides managed billing. ## At a glance 1. [Prepare your Meta app](#prepare-your-meta-app) and copy your MPS Solution ID. 2. [Launch Embedded Signup](#launch-embedded-signup) so the customer can authorize your app. 3. [Complete onboarding on your backend](#complete-onboarding-on-your-backend), including registration and webhook setup. 4. [Confirm Kapso detected and funded the account](#confirm-managed-funding-in-kapso) before paid traffic. 5. [Start messaging and monitor usage](#monitor-usage) in Kapso. This path uses Meta's JavaScript SDK, not `@kapso/sdk`, a Kapso setup link, or Kapso's onboarding endpoints. ## Prepare your Meta app * Activate a Multi-partner Solution with **Messages processed by: Your infrastructure** and **App with messaging permission: Your Meta app**. * Copy the **Meta solution ID** from **Tenants > Multi-partner solution**. This is not the Meta App ID or a Kapso UUID. * Create a Facebook Login for Business configuration for your Tech Provider app, using WhatsApp Embedded Signup v4 and the products you need. * Add your HTTPS domain and OAuth redirect URIs in [Meta's login settings](/docs/platform/onboard-customers-own-meta-app#add-domains-for-embedded-signup). Kapso setup-link `allowed_origins` does not apply to this path. * Configure your webhook endpoint and subscribe your Meta app to `account_update` and `messages`. Use your own app's IDs below. Never include your app secret or access tokens in browser code. See [Meta's implementation guide](https://developers.facebook.com/documentation/business-messaging/whatsapp/embedded-signup/implementation) for the app configuration. ## Launch Embedded Signup If you already have Embedded Signup, add `extras.setup.solutionID` to your existing `FB.login` options. Keep the SDK initialization and flow options appropriate for your configuration. For a new v4 integration, this example initializes Meta once and enables the button only after `FB.init` completes: ```html theme={null}

``` `solutionID` is case-sensitive and belongs inside `extras.setup`, not at the top level. Keep it as a string. The Solution ID links the customer's authorization to both partners. See [Meta's Multi-partner Solution instructions](https://developers.facebook.com/documentation/business-messaging/whatsapp/solution-providers/multi-partner-solutions#step-5-configure-embedded-signup). If you offer multiple solutions, select the correct App ID, configuration ID, and Solution ID for the customer before enabling the button. Store that expected context with the onboarding attempt on your backend. ## Complete onboarding on your backend The example's `/api/whatsapp/onboarding/complete` is an endpoint in **your application**. Protect it with your normal authentication and CSRF controls; adapt the request headers to your framework. Bind the attempt to the signed-in customer. 1. Exchange `code` through Meta's `GET /oauth/access_token`, using your App ID and app secret. Store the returned business token securely on your server; do not log it or return it to the browser. 2. Resolve the authorized WABA and phone number using that token. If multiple assets are available, confirm the customer's selection rather than choosing the first result. You can also collect IDs through Meta's `WA_EMBEDDED_SIGNUP` browser event: validate the event origin and verify asset access server-side. Browser-supplied IDs are not proof of ownership or funding. 3. Subscribe **your app** to the customer's WABA with `POST /{WABA_ID}/subscribed_apps` so your backend receives message webhooks. 4. Register the phone number with `POST /{PHONE_NUMBER_ID}/register` when required by the selected flow. The example targets the standard Cloud API flow; follow Meta's separate requirements for coexistence, migrations, or WABA-only signup. 5. Persist the connection in your system. Return success only after the required backend setup completes. Use [Meta's Tech Provider onboarding guide](https://developers.facebook.com/documentation/business-messaging/whatsapp/embedded-signup/onboarding-customers-as-a-tech-provider) for the requests. For Kapso-managed billing, use the funding confirmation below instead of asking the customer to add a Meta payment method. ## Confirm managed funding in Kapso Meta notifies Kapso through `account_update` when the customer joins the solution. Kapso detects the WABA and attempts to fund it automatically, subject to eligibility and your project's prepaid balance. You do not need to send Kapso the customer's authorization code or business token. Open **Tenants > Multi-partner solution**, find the WABA, and confirm that managed funding is **verified** before sending paid messages. A successful signup callback or a detected WABA alone does not confirm funding. If the account shows **Needs attention**, resolve the displayed issue first. External onboarding does not create a Kapso WhatsApp configuration or emit the hosted path's `whatsapp.phone_number.created` event. Your own backend tracks connection completion; the Kapso account table tracks funding and aggregate usage. If the WABA does not appear, check that signup used the exact active Solution ID and the correct Meta app. If those match, contact Kapso with the Solution ID, WABA ID, and signup time. Do not send secrets or access tokens. ## Monitor usage Once funded, keep sending through your own Meta integration. View each WABA's funding and aggregate usage in **Tenants > Multi-partner solution**. Kapso checks Pricing Analytics about every five minutes; usage can arrive later. Kapso cannot block sends made through your infrastructure, so keep enough balance for usage between checks. See [managed billing and beta limitations](/docs/platform/tech-providers/multi-partner-solutions#managed-billing). # Delivery Source: https://docs.kapso.ai/docs/platform/webhooks/advanced Message buffering, ordering, retry policy, and automatic pausing ## Message buffering Message buffering allows you to receive multiple `whatsapp.message.received` events in a single batched webhook, reducing load during high-volume conversations. ### How it works 1. **Debounce pattern** - Messages are collected until the configured time window expires 2. **Automatic batching** - Multiple messages from the same conversation are grouped 3. **Immediate delivery** - Batches are sent when max size is reached or window expires 4. **Per-conversation** - Each conversation has its own independent buffer ### Configuration When creating or editing a webhook, enable message buffering for the `whatsapp.message.received` event: * **Buffer window**: Time to wait before sending (1-60 seconds, default: 5) * **Maximum batch size**: Max messages per batch (1-100, default: 50) ### Batched webhook format ```json theme={null} { "type": "whatsapp.message.received", "batch": true, "data": [ { "message": { "id": "wamid.111", "timestamp": "1730092801", "type": "text", "text": { "body": "First in batch" }, "kapso": { "direction": "inbound", "status": "received", "processing_status": "pending", "origin": "cloud_api", "phone_number": "+15551234567", "phone_number_id": "123456789012345" } }, "conversation": { "id": "conv_123", "phone_number": "+15551234567", "status": "active", "last_active_at": "2025-10-28T14:26:01Z", "created_at": "2025-10-28T13:40:00Z", "updated_at": "2025-10-28T14:26:01Z", "metadata": {}, "phone_number_id": "123456789012345" }, "is_new_conversation": false, "phone_number_id": "123456789012345" }, { "message": { "id": "wamid.112", "timestamp": "1730092802", "type": "text", "text": { "body": "Second in batch" }, "kapso": { "direction": "inbound", "status": "received", "processing_status": "pending", "origin": "cloud_api", "phone_number": "+15551234567", "phone_number_id": "123456789012345" } }, "conversation": { "id": "conv_123", "phone_number": "+15551234567", "status": "active", "last_active_at": "2025-10-28T14:26:02Z", "created_at": "2025-10-28T13:40:00Z", "updated_at": "2025-10-28T14:26:02Z", "metadata": {}, "phone_number_id": "123456789012345" }, "is_new_conversation": false, "phone_number_id": "123456789012345" } ], "batch_info": { "size": 2, "window_ms": 5000, "first_sequence": 101, "last_sequence": 102, "conversation_id": "conv_123" } } ``` With buffering on, every delivery uses batch format, even a single message. The `data` array contains one message if only one arrived during the buffer window. Always check the `batch` field or the `X-Webhook-Batch` header rather than assuming the shape. ### Handling batched webhooks ```javascript theme={null} app.post('/webhooks', (req, res) => { const isBatch = req.headers['x-webhook-batch'] === 'true'; if (isBatch) { const { data, batch_info } = req.body; console.log(`Processing ${batch_info.size} messages`); data.forEach(event => { processMessage(event.message, event.conversation); }); } else { // Single event const { message, conversation } = req.body; processMessage(message, conversation); } res.status(200).send('OK'); }); ``` ## Message ordering Kapso ensures messages are delivered in the correct order within each conversation. ### How it works * **Sequence-based ordering** - Each webhook delivery gets a sequence number * **Automatic queuing** - Messages are queued if earlier messages haven't been delivered * **Ordering timeout** - After 30 seconds, messages are delivered regardless to prevent delays * **Per conversation** - Ordering is maintained independently per conversation * **Applies to** - Message received and message sent events This ensures your endpoint receives messages in the same order they were sent/received. ## Message origin The `message.kapso.origin` field tells you how the message entered the system: * **cloud\_api** - Sent via Kapso API (outbound jobs, flow actions, API calls) * **business\_app** - Sent from WhatsApp Business App (manual messages your team sends using the Business App) * **history\_sync** - Backfilled during message history import (only present if your project ran a sync) Use this to filter out messages you don't want to process. For example, skip `business_app` messages to avoid processing manual messages sent by your team. ## Retry policy If Kapso doesn't receive a 200 response, webhooks are automatically retried. ### Retry schedule Each webhook is attempted based on this schedule: * **Immediately** (initial attempt) * **10 seconds** after first failure * **40 seconds** after second failure **Total time to failure**: about 50 seconds across 3 attempts. `max_attempts` defaults to 3 and counts the initial delivery, so a webhook is retried twice. ### What happens after retries fail? After all retries are exhausted: * The webhook is marked as failed * Batched messages fall back to individual delivery * You can check failed deliveries in the Kapso dashboard * If the failure rate is high enough, the webhook is automatically paused (see below) ## Automatic pausing Kapso automatically pauses a webhook when it detects a persistently failing endpoint. This protects your system from retry storms and prevents queue buildup. ### Pause thresholds A webhook is paused when **all** of the following are met within a 15-minute window: | Condition | Value | | ------------------------- | ----- | | Minimum total deliveries | 40 | | Minimum failed deliveries | 10 | | Minimum failure rate | 85% | ### What happens when a webhook is paused * The webhook's `active` field is set to `false` * Pending deliveries are marked as `failed` with reason `"Webhook inactive; delivery skipped"` * All project members receive an email with failure details and a link to settings * No new deliveries are attempted until you re-enable the webhook ### Re-enabling a paused webhook 1. Fix the issue with your endpoint 2. Open **Integrations → Webhooks** in the Kapso dashboard 3. Toggle the webhook back to active Webhooks that are re-enabled will resume delivery. Make sure your endpoint is healthy before re-enabling, or it may be paused again. ### Handling retries in your code Implement idempotency to handle retry attempts gracefully: ```javascript theme={null} app.post('/webhooks', async (req, res) => { const idempotencyKey = req.headers['x-idempotency-key']; // Check if already processed const existing = await db.webhookEvents.findOne({ idempotency_key: idempotencyKey }); if (existing) { // Already processed this webhook return res.status(200).send('Already processed'); } try { // Process webhook await processEvent(req.body); // Store idempotency key await db.webhookEvents.create({ idempotency_key: idempotencyKey, processed_at: new Date() }); res.status(200).send('OK'); } catch (error) { // Return 500 to trigger retry console.error('Webhook processing failed:', error); res.status(500).send('Processing failed'); } }); ``` ## Best practices ### Performance 1. **Respond quickly** - Return 200 within 10 seconds 2. **Process asynchronously** - Use background jobs for heavy processing 3. **Scale horizontally** - Use load balancers to handle high volume 4. **Enable buffering** - Reduce webhook volume during busy periods ### Reliability 1. **Implement idempotency** - Use `X-Idempotency-Key` to prevent duplicate processing 2. **Handle all event types** - Even if you don't need them now 3. **Log everything** - Track webhook deliveries and failures 4. **Set up monitoring** - Alert on high failure rates ### Example production setup ```javascript theme={null} const queue = require('bull'); // Background job processor const webhookQueue = new queue('webhooks'); app.post('/webhooks', async (req, res) => { const idempotencyKey = req.headers['x-idempotency-key']; const event = req.headers['x-webhook-event']; // Check if already processed if (await isProcessed(idempotencyKey)) { return res.status(200).send('Already processed'); } // Add to background queue await webhookQueue.add({ idempotency_key: idempotencyKey, event, data: req.body.batch === true ? req.body.data : req.body, headers: { signature: req.headers['x-webhook-signature'], batch: req.headers['x-webhook-batch'] } }); // Respond immediately res.status(200).send('OK'); }); // Process webhooks in background webhookQueue.process(async (job) => { const { idempotency_key, event, data, headers } = job.data; // Verify signature if (!verifySignature(data, headers.signature)) { throw new Error('Invalid signature'); } // Process event await processEvent(event, data); // Mark as processed await markProcessed(idempotency_key); }); ``` ## Troubleshooting * Verify your endpoint is publicly accessible via HTTPS * Check firewall rules allow incoming requests from Kapso * Ensure you're returning 200 status within 10 seconds * Check webhook is enabled in dashboard * Implement idempotency using `X-Idempotency-Key` header * Store processed keys in database or cache * Use timing-safe comparison when checking keys * Check sequence numbers in `batch_info` * Implement ordering logic in your application if needed * Note: 30-second timeout allows out-of-order delivery to prevent indefinite delays * Verify signature verification logic is correct * Check endpoint response time (must be under 10 seconds) * Review error logs for exceptions in your code * Ensure database/external services aren't timing out * If Kapso auto-paused your webhook, fix the endpoint and re-enable it from **Integrations → Webhooks** # Legacy v1 webhooks Source: https://docs.kapso.ai/docs/platform/webhooks/legacy Migration guide for v1 webhook payloads v1 webhooks are legacy. Use v2 for all new integrations. New webhooks default to v2. Existing v1 webhooks continue to work with no breaking changes. ## v1 vs v2 differences ### v1 (legacy) * Includes nested `whatsapp_config` object in all payloads * Uses `whatsapp_config_id` as the primary identifier * Event: `whatsapp.config.created` for connection lifecycle * Message structure: `message_type`, `content` fields * Message origin: `message.origin` at message level ### v2 (recommended) * Phone-number-first with `phone_number_id` at top level * No nested `whatsapp_config` object * Event: `whatsapp.phone_number.created` for connection lifecycle * Message structure: Meta-style with `kapso` extensions * Message origin: `message.kapso.origin` inside kapso object ## Migration to v2 ### 1. Check current version Look for the `X-Webhook-Payload-Version` header in incoming webhooks: ```javascript theme={null} app.post('/webhooks', (req, res) => { const version = req.headers['x-webhook-payload-version']; console.log('Webhook version:', version); // "v1" or "v2" }); ``` ### 2. Update webhook handler **Before (v1)**: ```javascript theme={null} const { whatsapp_config, conversation } = data; const phoneNumberId = whatsapp_config.phone_number_id; const configId = conversation.whatsapp_config_id; ``` **After (v2)**: ```javascript theme={null} const { phone_number_id, conversation } = data; const phoneNumberId = phone_number_id; const conversationPhoneNumberId = conversation.phone_number_id; ``` ### 3. Handle message structure **Before (v1)**: ```javascript theme={null} const { message } = data; const content = message.content; const type = message.message_type; const direction = message.direction; const origin = message.origin; ``` **After (v2)**: ```javascript theme={null} const { message } = data; const content = message.text?.body || message.image?.caption; const type = message.type; const kapsoData = message.kapso; const direction = kapsoData.direction; const origin = kapsoData.origin; ``` ## v1 event: whatsapp.config.created Lifecycle event fired when customer connects WhatsApp (v1 only). For v2, use `whatsapp.phone_number.created` instead. See [Connection detection](/docs/platform/setup-links/detect-connection). **Headers**: ``` X-Webhook-Event: whatsapp.config.created X-Webhook-Signature: X-Idempotency-Key: X-Webhook-Payload-Version: v1 ``` **Payload** (abbreviated): ```json theme={null} { "whatsapp_config": { "id": "770e8400-e29b-41d4-a716-446655440002", "phone_number_id": "123456789012345", "business_account_id": "987654321098765", "customer_id": "880e8400-e29b-41d4-a716-446655440003", "display_phone_number": "+1 (555) 123-4567" }, "project": { "id": "990e8400-e29b-41d4-a716-446655440004" }, "customer": { "id": "880e8400-e29b-41d4-a716-446655440003", "external_customer_id": "acme-corp-123" } } ``` ## v1 payload examples ### message.received ```json theme={null} { "message": { "id": "550e8400-e29b-41d4-a716-446655440000", "message_type": "text", "content": "Hello", "direction": "inbound", "status": "received", "origin": "cloud_api", "from": "+15551234567" }, "conversation": { "id": "770e8400-e29b-41d4-a716-446655440002", "phone_number": "+15551234567", "whatsapp_config_id": "880e8400-e29b-41d4-a716-446655440001" }, "is_new_conversation": true, "whatsapp_config": { "id": "880e8400-e29b-41d4-a716-446655440001", "phone_number_id": "123456789012345" } } ``` ### message.sent ```json theme={null} { "message": { "id": "550e8400-e29b-41d4-a716-446655440000", "message_type": "text", "content": "On my way", "direction": "outbound", "status": "sent", "origin": "cloud_api" }, "conversation": { "id": "770e8400-e29b-41d4-a716-446655440002", "whatsapp_config_id": "880e8400-e29b-41d4-a716-446655440001" }, "whatsapp_config": { "id": "880e8400-e29b-41d4-a716-446655440001", "phone_number_id": "123456789012345" } } ``` ### conversation.created ```json theme={null} { "conversation": { "id": "770e8400-e29b-41d4-a716-446655440002", "phone_number": "+15551234567", "whatsapp_config_id": "880e8400-e29b-41d4-a716-446655440001" }, "whatsapp_config": { "id": "880e8400-e29b-41d4-a716-446655440001", "phone_number_id": "123456789012345" } } ``` ## Handling both versions If you need to support both v1 and v2 webhooks during migration: ```javascript theme={null} app.post('/webhooks', (req, res) => { const version = req.headers['x-webhook-payload-version']; const event = req.headers['x-webhook-event']; const data = req.body; if (event === 'whatsapp.message.received') { let phoneNumberId, content, type, origin; if (version === 'v2') { // v2 format phoneNumberId = data.phone_number_id; content = data.message.text?.body; type = data.message.type; origin = data.message.kapso.origin; } else { // v1 format (legacy) phoneNumberId = data.whatsapp_config.phone_number_id; content = data.message.content; type = data.message.message_type; origin = data.message.origin; } // Process with normalized data await processMessage({ phoneNumberId, content, type, origin }); } res.status(200).send('OK'); }); ``` ## Backward compatibility v1 webhooks remain fully supported. You can migrate at your own pace: * Existing v1 webhooks continue to work unchanged * No breaking changes or deprecation timeline * Both versions use the same signature verification (HMAC SHA256) * Both versions support the same retry policy and ordering guarantees Migrate to v2 when ready to benefit from simpler payload structure and Meta-compatible message format. # Message events Source: https://docs.kapso.ai/docs/platform/webhooks/message-events Message and conversation events sent to phone-number webhooks, and their payloads All webhook payloads use v2 format with `phone_number_id` at the top level. ## Payload structure Webhook payloads separate message data from conversation data: * **message.kapso** - Message-scoped only: direction, status, processing\_status, statuses (raw status history), origin, has\_media, content (text representation), transcript (for audio), media helpers (media\_data, media\_url, message\_type\_data) * **conversation** - Top-level identifiers such as `id`, `contact_name`, `phone_number`, `phone_number_id`, and, when available, `business_scoped_user_id`, `parent_business_scoped_user_id`, and `username`. Optional `conversation.kapso` contains summary metrics only (counts, last-message metadata, timestamps) — it never contains `contact_name` * **phone\_number\_id** - Included at top level for routing Do not assume `phone_number`, `from`, `to`, or `wa_id` are always present. Timestamps use two formats: `conversation.kapso` fields are UTC with microseconds (`2025-10-28T17:25:01.000000Z`), every other ISO timestamp carries a UTC offset (`2025-10-28T14:25:01-03:00`). `message.timestamp` is a Unix epoch string. Parse all three. ## WhatsApp webhook events Sent only to phone-number webhooks (`/whatsapp/phone_numbers/{phone_number_id}/webhooks`). `whatsapp.message.received` Fired when a new WhatsApp message is received from a customer. Supports message buffering for batch delivery. `whatsapp.message.sent` Fired when a message is successfully sent to WhatsApp `whatsapp.message.delivered` Fired when a message is successfully delivered to the recipient's device `whatsapp.message.read` Fired when the recipient reads your message `whatsapp.message.failed` Fired when a message fails to deliver `whatsapp.conversation.created` Fired when a new WhatsApp conversation is initiated `whatsapp.conversation.ended` Fired when a WhatsApp conversation ends (agent action, manual closure, or 24-hour inactivity) `whatsapp.conversation.inactive` Fired when no messages (inbound/outbound) for configured minutes (1-1440, default 60) `whatsapp.contact.identity_changed` Fired when a contact receives a new business-scoped user ID `whatsapp.contact.marketing_preference_changed` Fired when a contact stops or resumes marketing messages from one of your numbers ## Payload structures Kapso sends the event name in the `X-Webhook-Event` header. The examples below show unbuffered request bodies. If buffering is enabled for `whatsapp.message.received`, the request body uses a batch envelope with `type`, `batch: true`, `data: [...]`, and `batch_info`; each `data` item has the same shape as the unbuffered `whatsapp.message.received` payload below. See [Batched webhook format](/docs/platform/webhooks/advanced#batched-webhook-format) for a full batched payload example. ### whatsapp.message.received ```json theme={null} { "message": { "id": "wamid.123", "timestamp": "1730092800", "type": "text", "from": "16315551181", "from_user_id": "US.13491208655302741918", "from_parent_user_id": "US.ENT.506847293015824", "username": "@testusername", "text": { "body": "Hello" }, "kapso": { "direction": "inbound", "status": "received", "processing_status": "pending", "origin": "cloud_api", "has_media": false, "content": "Hello" } }, "conversation": { "id": "conv_123", "contact_name": "John Doe", "phone_number": "16315551181", "business_scoped_user_id": "US.13491208655302741918", "parent_business_scoped_user_id": "US.ENT.506847293015824", "username": "@testusername", "status": "active", "last_active_at": "2025-10-28T14:25:01-03:00", "created_at": "2025-10-28T13:40:00-03:00", "updated_at": "2025-10-28T14:25:01-03:00", "metadata": {}, "phone_number_id": "123456789012345", "kapso": { "messages_count": 1, "last_message_id": "wamid.123", "last_message_type": "text", "last_message_timestamp": "2025-10-28T17:25:01.000000Z", "last_message_text": "Hello", "last_inbound_at": "2025-10-28T17:25:01.000000Z", "last_outbound_at": null } }, "is_new_conversation": true, "phone_number_id": "123456789012345" } ``` ### whatsapp.message.sent ```json theme={null} { "message": { "id": "wamid.456", "timestamp": "1730092860", "type": "text", "to": "15551234567", "text": { "body": "On my way" }, "kapso": { "direction": "outbound", "status": "sent", "processing_status": "completed", "origin": "cloud_api", "has_media": false, "statuses": [ { "id": "wamid.456", "status": "sent", "timestamp": "1730092860", "recipient_id": "15551234567" } ] } }, "conversation": { "id": "conv_123", "contact_name": "John Doe", "phone_number": "15551234567", "business_scoped_user_id": "US.13491208655302741918", "parent_business_scoped_user_id": "US.ENT.506847293015824", "username": "@testusername", "status": "active", "last_active_at": "2025-10-28T14:31:00-03:00", "created_at": "2025-10-28T13:40:00-03:00", "updated_at": "2025-10-28T14:31:00-03:00", "metadata": {}, "phone_number_id": "123456789012345", "kapso": { "messages_count": 2, "last_message_id": "wamid.456", "last_message_type": "text", "last_message_timestamp": "2025-10-28T17:31:00.000000Z", "last_message_text": "On my way", "last_inbound_at": "2025-10-28T17:25:01.000000Z", "last_outbound_at": "2025-10-28T17:31:00.000000Z" } }, "is_new_conversation": false, "phone_number_id": "123456789012345" } ``` ### whatsapp.message.delivered ```json theme={null} { "message": { "id": "wamid.456", "timestamp": "1730092888", "type": "text", "to": "15551234567", "text": { "body": "On my way" }, "kapso": { "direction": "outbound", "status": "delivered", "processing_status": "completed", "origin": "cloud_api", "has_media": false, "statuses": [ { "id": "wamid.456", "status": "sent", "timestamp": "1730092860", "recipient_id": "15551234567" }, { "id": "wamid.456", "status": "delivered", "timestamp": "1730092888", "recipient_id": "15551234567" } ] } }, "conversation": { "id": "conv_123", "contact_name": "John Doe", "phone_number": "15551234567", "business_scoped_user_id": "US.13491208655302741918", "parent_business_scoped_user_id": "US.ENT.506847293015824", "username": "@testusername", "status": "active", "last_active_at": "2025-10-28T14:31:28-03:00", "created_at": "2025-10-28T13:40:00-03:00", "updated_at": "2025-10-28T14:31:28-03:00", "metadata": {}, "phone_number_id": "123456789012345" }, "is_new_conversation": false, "phone_number_id": "123456789012345" } ``` ### whatsapp.message.failed ```json theme={null} { "message": { "id": "wamid.789", "timestamp": "1730093200", "type": "text", "to": "15551234567", "text": { "body": "This message failed" }, "kapso": { "direction": "outbound", "status": "failed", "processing_status": "completed", "origin": "cloud_api", "has_media": false, "statuses": [ { "id": "wamid.789", "status": "sent", "timestamp": "1730093100", "recipient_id": "15551234567" }, { "id": "wamid.789", "status": "failed", "timestamp": "1730093200", "recipient_id": "15551234567", "errors": [ { "code": 131047, "title": "Re-engagement message", "message": "More than 24 hours have passed since the recipient last replied" } ] } ] } }, "conversation": { "id": "conv_123", "contact_name": "John Doe", "phone_number": "15551234567", "business_scoped_user_id": "US.13491208655302741918", "parent_business_scoped_user_id": "US.ENT.506847293015824", "username": "@testusername", "status": "active", "last_active_at": "2025-10-28T15:00:00-03:00", "created_at": "2025-10-28T13:40:00-03:00", "updated_at": "2025-10-28T15:00:00-03:00", "metadata": {}, "phone_number_id": "123456789012345" }, "is_new_conversation": false, "phone_number_id": "123456789012345" } ``` ### whatsapp.conversation.created ```json theme={null} { "conversation": { "id": "conv_789", "contact_name": "John Doe", "phone_number": "15551234567", "business_scoped_user_id": "US.13491208655302741918", "parent_business_scoped_user_id": "US.ENT.506847293015824", "username": "@testusername", "status": "active", "last_active_at": "2025-10-28T14:00:00-03:00", "created_at": "2025-10-28T14:00:00-03:00", "updated_at": "2025-10-28T14:00:00-03:00", "metadata": {}, "phone_number_id": "123456789012345", "kapso": { "messages_count": 0, "last_message_id": null, "last_message_type": null, "last_message_timestamp": null, "last_message_text": null, "last_inbound_at": null, "last_outbound_at": null } }, "phone_number_id": "123456789012345" } ``` ### whatsapp.conversation.ended ```json theme={null} { "conversation": { "id": "conv_789", "contact_name": "John Doe", "phone_number": "15551234567", "business_scoped_user_id": "US.13491208655302741918", "parent_business_scoped_user_id": "US.ENT.506847293015824", "username": "@testusername", "status": "ended", "last_active_at": "2025-10-28T15:10:45-03:00", "created_at": "2025-10-28T14:00:00-03:00", "updated_at": "2025-10-28T15:10:45-03:00", "metadata": {}, "phone_number_id": "123456789012345", "kapso": { "messages_count": 15, "last_message_id": "wamid.999", "last_message_type": "text", "last_message_timestamp": "2025-10-28T18:10:45.000000Z", "last_message_text": "Thanks!", "last_inbound_at": "2025-10-28T18:10:45.000000Z", "last_outbound_at": "2025-10-28T18:10:30.000000Z" } }, "phone_number_id": "123456789012345" } ``` ### whatsapp.conversation.inactive ```json theme={null} { "conversation": { "id": "conv_789", "contact_name": "John Doe", "phone_number": "15551234567", "business_scoped_user_id": "US.13491208655302741918", "parent_business_scoped_user_id": "US.ENT.506847293015824", "username": "@testusername", "status": "active", "last_active_at": "2025-10-28T13:00:00-03:00", "created_at": "2025-10-28T12:00:00-03:00", "updated_at": "2025-10-28T13:00:00-03:00", "metadata": {}, "phone_number_id": "123456789012345" }, "since_message": { "id": "msg_anchor", "whatsapp_message_id": "wamid.ANCHOR", "direction": "inbound", "created_at": "2025-10-28T13:00:00-03:00" }, "inactivity": { "minutes": 60 }, "phone_number_id": "123456789012345" } ``` ### whatsapp.contact.identity\_changed ```json theme={null} { "contact": { "id": "contact_123", "customer_id": "customer_456", "wa_id": "15551234567", "profile_name": "John Doe", "display_name": "John Doe", "business_scoped_user_id": "US.13491208655302741918", "parent_business_scoped_user_id": "US.ENT.506847293015824", "username": "@testusername", "created_at": "2025-10-28T14:00:00-03:00", "updated_at": "2025-10-28T15:10:45-03:00", "sandbox": false, "metadata": {} }, "previous": { "business_scoped_user_id": "US.OLD.13491208655302741918", "parent_business_scoped_user_id": "US.OLD.ENT.506847293015824" }, "phone_number_id": "123456789012345" } ``` `contact` holds the identity after the change; `previous` holds the business-scoped user IDs Meta reported for the contact before it. Either `previous` value can be `null` when Meta does not send it. Use it to re-key stored identifiers in your own system. Fired for Meta's `user_changed_number` and `user_changed_user_id` system messages. Those system messages do not produce a `whatsapp.message.received` event. See [Business-scoped user IDs](/docs/whatsapp/business-scoped-user-ids). ### whatsapp.contact.marketing\_preference\_changed ```json theme={null} { "contact": { "id": "contact_123", "wa_id": "15551234567", "profile_name": "John Doe", "display_name": "John Doe", "created_at": "2025-10-28T14:00:00-03:00", "updated_at": "2025-10-28T15:10:45-03:00", "metadata": {} }, "marketing_preference": { "status": "stopped", "previous_status": "resumed", "detail": "User requested to stop marketing messages", "occurred_at": "2025-10-28T15:10:45-03:00", "sequence": 4821 }, "phone_number_id": "123456789012345" } ``` `status` is `stopped` or `resumed`. `previous_status` is the state before this change, and is `null` the first time Kapso hears about the contact's preference. `detail` is the free-text reason Meta sends, and can be `null`. `occurred_at` is the timestamp WhatsApp reported for the change. Kapso only records preference changes that carry one: an event Kapso cannot place in time cannot be ordered against what it already knows, so it is rejected at ingest and raised for investigation rather than applied. `sequence` increases with every preference change Kapso records, and is what you should compare to decide which of two deliveries is newer. Deliveries are at-least-once and are not guaranteed to arrive in order, so if you store the contact's preference, keep the `sequence` you last applied and only apply a delivery whose `sequence` is strictly greater — a redelivery of the same event carries the same value, and applying it again would repeat your side effects. Do not order on `occurred_at` alone: WhatsApp reports it to the second, so a rapid stop and resume can share a value. The preference is scoped to the phone number it arrived on: stopping marketing on one of your numbers does not stop it on the others. The event only fires when the state actually changes, so a repeated `stop` from Meta does not produce a second delivery. Once a contact is stopped, Kapso refuses marketing template sends to them on that number with error code `marketing_preference_stopped`. Utility and authentication templates and free-form session messages are unaffected. Broadcasts mark those recipients `suppressed` and do not charge for them. See [Marketing opt-outs](/docs/whatsapp/templates/marketing-opt-outs) and [Broadcasts](/docs/platform/broadcasts/overview). ### Multiple inactivity timeouts Create separate webhooks for different timeout thresholds: ```json theme={null} // First webhook: 5 minute warning { "events": ["whatsapp.conversation.inactive"], "inactive_after_minutes": 5 } // Second webhook: 30 minute escalation { "events": ["whatsapp.conversation.inactive"], "inactive_after_minutes": 30 } ``` Each webhook fires independently when its threshold is reached. ## Message origin The `message.kapso.origin` field indicates how the message entered the system: * **cloud\_api** - Sent via Kapso API (outbound jobs, flow actions, API calls) * **business\_app** - Echoed from WhatsApp Business App (when using the Business App) * **history\_sync** - Backfilled during message history imports (only if project ran sync) ## Status history The `message.kapso.statuses` array contains the complete history of raw Meta status events for a message, ordered chronologically. Each entry is the unmodified payload from Meta's webhook. ### Status object structure Each status object in the array follows Meta's webhook format: ```json theme={null} { "id": "", "status": "", "timestamp": "", "recipient_id": "", "pricing": { "billable": true, "pricing_model": "", "category": "" }, "errors": [ { "code": 131031, "title": "", "message": "", "error_data": { "details": "" }, "href": "" } ], ... } ``` | Field | Included when | | --------- | ----------------------------------- | | `pricing` | Sent status, plus delivered or read | | `errors` | Failed to send or deliver | See [Meta's status webhook reference](https://developers.facebook.com/docs/whatsapp/cloud-api/webhooks/components#statuses-object) for the complete schema. Use this field to track the full lifecycle of outbound messages and understand failure causes. The array only appears when status events have been recorded. ## Message types The `message.type` field can be one of: * `text` - Plain text message * `image` - Image attachment * `video` - Video attachment * `audio` - Audio/voice message * `document` - Document attachment * `location` - Location sharing * `template` - WhatsApp template message * `interactive` - Interactive message (buttons, lists) * `reaction` - Message reaction * `contacts` - Contact card sharing ## Message type-specific data ### Media messages (image/video/document) ```json theme={null} { "message": { "id": "wamid.789", "timestamp": "1730093000", "type": "image", "image": { "caption": "Photo description", "id": "media_id_123" }, "kapso": { "direction": "inbound", "status": "received", "processing_status": "pending", "origin": "cloud_api", "has_media": true, "content": "Photo description Image attached (photo.jpg) [Size: 200 KB | Type: image/jpeg] URL: https://api.kapso.ai/media/...", "media_url": "https://api.kapso.ai/media/...", "media_data": { "url": "https://api.kapso.ai/media/...", "filename": "photo.jpg", "content_type": "image/jpeg", "byte_size": 204800 }, "message_type_data": { "caption": "Photo description" } } } } ``` ### Audio messages ```json theme={null} { "message": { "id": "wamid.790", "timestamp": "1730093100", "type": "audio", "audio": { "id": "media_id_456" }, "kapso": { "direction": "inbound", "status": "received", "processing_status": "pending", "origin": "cloud_api", "has_media": true, "content": "[Audio attached] (voice.ogg) [Size: 50 KB | Type: audio/ogg] URL: https://api.kapso.ai/media/...\nTranscript: Hello, I need help with my order", "transcript": { "text": "Hello, I need help with my order" }, "media_url": "https://api.kapso.ai/media/...", "media_data": { "url": "https://api.kapso.ai/media/...", "filename": "voice.ogg", "content_type": "audio/ogg", "byte_size": 51200 } } } } ``` ### Location messages ```json theme={null} { "message": { "type": "location", "location": { "latitude": 37.7749, "longitude": -122.4194, "name": "San Francisco", "address": "San Francisco, CA, USA" } } } ``` ### Template messages ```json theme={null} { "message": { "type": "template", "template": { "name": "order_confirmation", "language": { "code": "en_US" }, "components": [...] } } } ``` ### Interactive messages ```json theme={null} { "message": { "type": "interactive", "interactive": { "type": "button_reply", "button_reply": { "id": "btn_1", "title": "Confirm" } } } } ``` ### Reaction messages ```json theme={null} { "message": { "type": "reaction", "reaction": { "message_id": "wamid.HBgNNTU0MTIzNDU2Nzg5MA", "emoji": "👍" } } } ``` ### Contact messages When you send a [request contact info](/docs/whatsapp/send-messages/request-contact-info) CTA and the user taps it, WhatsApp sends a `contacts` message. It includes the user's phone number and keeps BSUID identity when available. ```json theme={null} { "type": "contacts", "from_user_id": "US.13491208655302741918", "contacts": [ { "origin": "contact_request", "phones": [ { "phone": "+16505551234", "wa_id": "16505551234" } ] } ] } ``` If the user shares a contact directly from WhatsApp instead, `origin` is `other` and the payload can include a vCard. # Webhooks overview Source: https://docs.kapso.ai/docs/platform/webhooks/overview Get real-time notifications for WhatsApp events Webhooks are how you receive WhatsApp messages. Kapso pushes real-time notifications about your messages and conversations over HTTPS, delivering a JSON payload you can use in your application. Do not assume every payload has a phone number. See the [BSUID migration guide](/docs/whatsapp/business-scoped-user-ids). WhatsApp can now send identity without a phone number, so Kapso adds `business_scoped_user_id`, `parent_business_scoped_user_id`, and `username` to the relevant payloads. Update your parser before assuming `phone_number`, `wa_id`, `from`, or `to` are present. ## What are webhooks? Webhooks notify your application when events occur. You can use them to: * Send automated replies when customers message you * Update conversation status in your CRM * Track message delivery and read receipts * Trigger alerts when conversations go inactive * Store events in your database for analytics ## Steps to receive webhooks 1. Create an endpoint to receive requests 2. Register your webhook 3. Verify signatures 4. Test your endpoint ## 1. Create an endpoint Create a route in your application that accepts POST requests. ```javascript theme={null} app.post('/webhooks/whatsapp', async (req, res) => { const event = req.headers['x-webhook-event']; const isBatch = req.headers['x-webhook-batch'] === 'true' || req.body.batch === true; const payloads = isBatch ? req.body.data : [req.body]; console.log('Event:', event); console.log('Payload count:', payloads.length); for (const payload of payloads) { await processWebhookPayload(event, payload); } // Return 200 to acknowledge receipt res.status(200).send('OK'); }); ``` Kapso sends the event name in `X-Webhook-Event`. Unbuffered webhooks send the event payload directly as the request body. When buffering is enabled for `whatsapp.message.received`, the body uses a batch envelope with `type`, `batch: true`, `data: [...]`, and `batch_info`. Your endpoint must return `200 OK` within 10 seconds. ## 2. Register your webhook Kapso supports two types of webhooks: ### Project webhooks Project-wide events like WhatsApp connection lifecycle and workflow execution. No message or conversation events here. Use a WhatsApp webhook per phone number. **Setup:** 1. Open **Integrations → Webhooks** 2. Go to the **Platform webhooks** tab 3. Click **Add Webhook** 4. Enter your HTTPS endpoint URL 5. Copy the auto-generated secret key 6. Subscribe to events ### WhatsApp webhooks Message and conversation events for specific WhatsApp numbers. Two webhook kinds available: #### Kapso webhooks (default) Event-based webhooks with Kapso payload format. Subscribe to specific events, use buffering, and receive structured payloads. ```javascript theme={null} await fetch('https://api.kapso.ai/platform/v1/whatsapp/phone_numbers/{phone_number_id}/webhooks', { method: 'POST', headers: { 'X-API-Key': 'YOUR_API_KEY', 'Content-Type': 'application/json' }, body: JSON.stringify({ whatsapp_webhook: { kind: 'kapso', // optional, this is the default url: 'https://your-app.com/webhooks/whatsapp', events: ['whatsapp.message.received'], secret_key: 'your-secret-key' } }) }); ``` #### Meta webhooks Receive the exact payload Meta sends. No event filtering, no buffering - just raw Meta webhook forwarding with an idempotency key for deduplication. ```javascript theme={null} await fetch('https://api.kapso.ai/platform/v1/whatsapp/phone_numbers/{phone_number_id}/webhooks', { method: 'POST', headers: { 'X-API-Key': 'YOUR_API_KEY', 'Content-Type': 'application/json' }, body: JSON.stringify({ whatsapp_webhook: { kind: 'meta', url: 'https://your-app.com/webhooks/whatsapp-meta', secret_key: 'your-secret-key', active: true } }) }); ``` `secret_key` is required for meta webhooks too, and is not generated for you. `events` is not: Meta decides what it sends. Meta webhooks include an `X-Idempotency-Key` header (SHA256 hash of the payload) for deduplication. Only one meta webhook is allowed per phone number. You can also set this up from the dashboard: open your connected number, click **Edit**, and add your **Webhook destination URL**. Kapso forwards all Meta webhook events to that endpoint. Forwarded Meta payloads can include BSUID-only identity. For the Meta-side rollout, see [Meta: business-scoped user IDs](https://developers.facebook.com/documentation/business-messaging/whatsapp/business-scoped-user-ids/). To parse raw Meta payloads, use the TypeScript SDK: ```typescript theme={null} import express from 'express'; import { normalizeWebhook } from '@kapso/whatsapp-cloud-api/server'; const app = express(); app.post('/webhook', express.json(), (req, res) => { const events = normalizeWebhook(req.body); events.messages.forEach((message) => { console.log(message.type, message.kapso?.direction); }); events.raw.accountAlerts?.forEach((alert) => { console.log('Account alert', alert.alertInfo?.alertType); }); res.sendStatus(200); }); ``` `normalizeWebhook()` converts Meta's payload to the same structure as `messages.query()`, adds `kapso.direction`, and keeps all raw fields under `events.raw`. ## 3. Verify signatures Always verify webhook signatures to ensure requests come from Kapso. ```javascript theme={null} const crypto = require('crypto'); function verifyWebhook(rawBody, signature, secret) { if (typeof signature !== 'string') return false; const expected = crypto .createHmac('sha256', secret) .update(rawBody) .digest('hex'); const a = Buffer.from(expected, 'utf8'); const b = Buffer.from(signature, 'utf8'); // timingSafeEqual throws on length mismatch, so check length first return a.length === b.length && crypto.timingSafeEqual(a, b); } // express.raw gives you the bytes Kapso signed; express.json does not app.post( '/webhooks/whatsapp', express.raw({ type: 'application/json' }), (req, res) => { const signature = req.headers['x-webhook-signature']; if (!verifyWebhook(req.body, signature, process.env.WEBHOOK_SECRET)) { return res.status(401).send('Invalid signature'); } // Process webhook res.status(200).send('OK'); } ); ``` See [Security](/docs/platform/webhooks/security) for detailed verification guide. ## 4. Test your endpoint Use ngrok or Cloudflare tunnel for local testing: ```bash theme={null} # Option 1: ngrok ngrok http 3000 # Option 2: Cloudflare tunnel brew install cloudflared cloudflared tunnel --url http://localhost:3000 ``` Register the generated HTTPS URL in your webhook configuration. ## Webhook headers ### Kapso webhooks ``` X-Webhook-Event: whatsapp.message.received X-Webhook-Signature: X-Idempotency-Key: X-Webhook-Payload-Version: v2 Content-Type: application/json ``` Batched webhooks also include: ``` X-Webhook-Batch: true X-Batch-Size: 2 ``` ### Meta webhooks ``` Content-Type: application/json X-Idempotency-Key: ``` Meta webhooks forward the exact payload received from Meta, without modification. ## More webhook docs See available webhook events and payload examples Verify signatures and secure your endpoint Understand buffering, ordering, and retries Reference the older webhook format and migration path ## FAQ If Kapso doesn't receive a 200 response, webhooks are retried automatically: * 10 seconds * 40 seconds Total time: about 50 seconds. `max_attempts` defaults to 3 and counts the initial delivery, so there are two retries. After max retries, batched messages fall back to individual delivery. See [Advanced](/docs/platform/webhooks/advanced) for details. Use the `X-Idempotency-Key` header to track processed events: ```javascript theme={null} const processedKeys = new Set(); app.post('/webhooks', (req, res) => { const idempotencyKey = req.headers['x-idempotency-key']; if (processedKeys.has(idempotencyKey)) { return res.status(200).send('Already processed'); } // Process event processedKeys.add(idempotencyKey); res.status(200).send('OK'); }); ``` New webhooks default to v2. Existing v1 webhooks continue to work. See [Legacy webhooks](/docs/platform/webhooks/legacy) for migration guide. # Project webhooks Source: https://docs.kapso.ai/docs/platform/webhooks/project-webhooks Events sent to project-scoped webhooks - connections, workflow executions, and custom events Project webhooks are scoped to the whole project rather than a single phone number. They never receive message or conversation events - for those, see [Message events](/docs/platform/webhooks/message-events). Configure them in **Integrations → Webhooks → Platform webhooks**. ## Available events | Event | Fires when | | ------------------------------------ | ---------------------------------------------------------------------------- | | `whatsapp.phone_number.created` | A customer connects WhatsApp through a setup link | | `whatsapp.phone_number.deleted` | A phone number is removed from your project | | `whatsapp.phone_number.offboarded` | Meta offboards a phone number from the Cloud API | | `whatsapp.phone_number.disconnected` | Meta reports that your partner access was removed or the app was uninstalled | | `whatsapp.phone_number.reconnected` | Meta reports that the Cloud API connection was restored | | `whatsapp.account.disabled` | Meta disables the WhatsApp Business Account | | `whatsapp.account.restricted` | Meta restricts capabilities on the WhatsApp Business Account | | `whatsapp.account.reinstated` | Meta reinstates a previously disabled WhatsApp Business Account | | `whatsapp.account.violation` | Meta reports a policy violation on the WhatsApp Business Account | | `workflow.execution.handoff` | A workflow hands off to a human agent | | `workflow.execution.failed` | A workflow execution fails | | `project.event` | A custom project event is emitted | | `kapso_agent.run.approval_required` | An API-triggered agent run needs a tool approval | | `kapso_agent.run.completed` | An API-triggered agent run completes | | `kapso_agent.run.failed` | An API-triggered agent run fails | | `kapso_agent.run.cancelled` | An API-triggered agent run is cancelled | Sent only to project webhooks. These do not include message or conversation events. ## whatsapp.phone\_number.created Fires when a customer successfully connects their WhatsApp through a setup link. See [Connection detection](/docs/platform/setup-links/detect-connection) for implementation guide. **Payload**: ```json theme={null} { "phone_number_id": "123456789012345", "project": { "id": "990e8400-e29b-41d4-a716-446655440004" }, "customer": { "id": "880e8400-e29b-41d4-a716-446655440003", "external_id": "CUS-12345" } } ``` ## whatsapp.phone\_number.deleted Fires when a WhatsApp phone number is removed from your project. This event triggers at the start of the teardown process, before the number is fully deleted. **Payload**: ```json theme={null} { "phone_number_id": "123456789012345", "project": { "id": "990e8400-e29b-41d4-a716-446655440004" }, "customer": { "id": "880e8400-e29b-41d4-a716-446655440003", "external_id": "CUS-12345" } } ``` ## whatsapp.phone\_number.offboarded Fires when Meta offboards a phone number from the Cloud API. These three events (`offboarded`, `disconnected`, `reconnected`) are only available on project-scoped webhooks using payload version `v2`. Subscribing with `payload_version: "v1"` returns `422`. **Payload**: ```json theme={null} { "id": "wae_9f2c1d7b8a4e3f5069c1b2a3d4e5f60718293a4b5c6d7e8f90a1b2c3d4e5f607", "event": "whatsapp.phone_number.offboarded", "occurred_at": "2026-08-14T12:00:00.000000Z", "phone_number_id": "123456789012345", "project": { "id": "990e8400-e29b-41d4-a716-446655440004" }, "customer": { "id": "880e8400-e29b-41d4-a716-446655440003", "external_id": "CUS-12345" }, "connection_type": "dedicated", "source": { "provider": "meta", "event": "ACCOUNT_OFFBOARDED", "business_account_id": "102290129340398" } } ``` | Field | Description | | ----------------- | ------------------------------------------------------------------ | | `id` | Stable event id, unique per phone number and underlying Meta event | | `connection_type` | `dedicated` or `coexistence` | | `customer` | Present only when the number belongs to a customer | | `source.event` | The raw Meta account update event that produced this notification | ## whatsapp.phone\_number.disconnected Fires when Meta reports that your partner access to the number was removed (`PARTNER_REMOVED`) or the partner app was uninstalled (`PARTNER_APP_UNINSTALLED`). The number can no longer send or receive messages through Kapso until it is reconnected. Same payload as `whatsapp.phone_number.offboarded`, plus a `disconnection` object with the lowercased Meta values: ```json theme={null} { "id": "wae_9f2c1d7b8a4e3f5069c1b2a3d4e5f60718293a4b5c6d7e8f90a1b2c3d4e5f607", "event": "whatsapp.phone_number.disconnected", "occurred_at": "2026-08-14T12:00:00.000000Z", "phone_number_id": "123456789012345", "project": { "id": "990e8400-e29b-41d4-a716-446655440004" }, "customer": { "id": "880e8400-e29b-41d4-a716-446655440003", "external_id": "CUS-12345" }, "connection_type": "dedicated", "source": { "provider": "meta", "event": "PARTNER_REMOVED", "business_account_id": "102290129340398" }, "disconnection": { "reason": "primary_inactivity", "initiated_by": "system" } } ``` `disconnection.reason` and `disconnection.initiated_by` contain lowercased Meta values and are `null` when Meta does not provide them. ## whatsapp.phone\_number.reconnected Fires when Meta reports that the Cloud API connection was restored (`ACCOUNT_RECONNECTED`). Same payload as `whatsapp.phone_number.offboarded`, with `source.event` set to `ACCOUNT_RECONNECTED`. ## Account enforcement events `whatsapp.account.disabled`, `whatsapp.account.restricted`, `whatsapp.account.reinstated` and `whatsapp.account.violation` report Meta enforcement against the WhatsApp Business Account (WABA), not against a single number. Like the phone number lifecycle events, they are only available on project-scoped webhooks using payload version `v2`. Subscribing with `payload_version: "v1"` returns `422`. Because the underlying Meta payloads carry no phone number, you receive one delivery per project and WABA, with every affected number listed in `phone_numbers` - not one delivery per number. **Payload**: ```json theme={null} { "id": "wae_9f2c1d7b8a4e3f5069c1b2a3d4e5f60718293a4b5c6d7e8f90a1b2c3d4e5f607", "event": "whatsapp.account.restricted", "occurred_at": "2026-08-14T12:00:00.000000Z", "business_account_id": "102290129340398", "project": { "id": "990e8400-e29b-41d4-a716-446655440004" }, "phone_numbers": [ { "id": "123456789012345", "display_phone_number": "+1 555 010 1234" } ], "restrictions": [ { "type": "RESTRICTED_BIZ_INITIATED_MESSAGING", "expires_at": "2026-09-14T12:00:00Z" } ], "source": { "provider": "meta", "event": "ACCOUNT_RESTRICTION" } } ``` | Field | Description | | --------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `id` | Stable event id, unique per project, WABA and underlying Meta event | | `phone_numbers` | Every number in the project on that WABA | | `restrictions` | Present on `whatsapp.account.restricted`. `type` is the raw Meta restriction type; `expires_at` is omitted when the restriction has no expiry | | `violation` | Present on `whatsapp.account.violation`: `type` is the raw Meta violation type, with an optional `remediation` string | | `ban` | Present on `whatsapp.account.disabled` and `whatsapp.account.reinstated`: `state` is `DISABLE` or `REINSTATE`, with an optional `date_label` string that Meta sends already localized and formatted | | `source.event` | The raw Meta account update event that produced this notification (`ACCOUNT_RESTRICTION`, `ACCOUNT_VIOLATION` or `DISABLED_UPDATE`) | Keys with no value are omitted from the payload. `whatsapp.account.disabled` and `whatsapp.account.reinstated` both come from Meta's `DISABLED_UPDATE` event, split by ban state: ```json theme={null} { "id": "wae_9f2c1d7b8a4e3f5069c1b2a3d4e5f60718293a4b5c6d7e8f90a1b2c3d4e5f607", "event": "whatsapp.account.disabled", "occurred_at": "2026-08-14T12:00:00.000000Z", "business_account_id": "102290129340398", "project": { "id": "990e8400-e29b-41d4-a716-446655440004" }, "phone_numbers": [ { "id": "123456789012345", "display_phone_number": "+1 555 010 1234" } ], "ban": { "state": "DISABLE", "date_label": "August 7, 2026" }, "source": { "provider": "meta", "event": "DISABLED_UPDATE" } } ``` ## workflow\.execution.handoff Fires when a workflow execution is handed off to a human agent. **Payload**: ```json theme={null} { "event": "workflow.execution.handoff", "occurred_at": "2025-12-08T12:00:00Z", "project_id": "990e8400-e29b-41d4-a716-446655440004", "workflow_id": "880e8400-e29b-41d4-a716-446655440001", "workflow_execution_id": "770e8400-e29b-41d4-a716-446655440002", "status": "handoff", "tracking_id": "track-abc123", "channel": "whatsapp", "whatsapp_conversation_id": "conv_789", "handoff": { "reason": "User requested human assistance", "source": "agent_tool" } } ``` | Field | Description | | ---------------- | ---------------------------------------------------------------------- | | `handoff.reason` | Optional reason provided during handoff | | `handoff.source` | `agent_tool` (from agent step) or `action_step` (from workflow action) | ## workflow\.execution.failed Fires when a workflow execution fails due to an error. **Payload**: ```json theme={null} { "event": "workflow.execution.failed", "occurred_at": "2025-12-08T12:00:00Z", "project_id": "990e8400-e29b-41d4-a716-446655440004", "workflow_id": "880e8400-e29b-41d4-a716-446655440001", "workflow_execution_id": "770e8400-e29b-41d4-a716-446655440002", "status": "failed", "tracking_id": "track-abc123", "channel": "whatsapp", "whatsapp_conversation_id": "conv_789", "error": { "message": "Workflow execution timed out" } } ``` ## project.event Fires when a custom project event is emitted. This event is only available on project-scoped webhooks. Phone-number webhooks do not receive custom project events. Subscribing to this event requires project events to be available on your plan. **Payload**: ```json theme={null} { "id": "990e8400-e29b-41d4-a716-446655440004", "event": "project.event", "name": "lead.qualified", "occurred_at": "2026-06-27T14:30:00.000000Z", "project_id": "880e8400-e29b-41d4-a716-446655440003", "conversation_id": "770e8400-e29b-41d4-a716-446655440002", "properties": { "score": 92, "source": "pricing_page" } } ``` `conversation_id` is included only when the event is linked to a WhatsApp conversation. See [Events](/docs/platform/events) for emitting and triggering workflows from project events. ## Kapso Agent run events Fire when a Kapso Agent run triggered through the [Agent API](/docs/kapso-agent/api) needs approval, completes, fails, or is cancelled. Runs started from the dashboard or Slack don't emit these events. **Payload** (`kapso_agent.run.completed`): ```json theme={null} { "id": "3c0f1a5b9d8e7f6a4b2c1d0e9f8a7b6c5d4e3f2a1b0c9d8e7f6a5b4c3d2e1f0a", "event": "kapso_agent.run.completed", "created_at": "2026-08-21T14:35:00Z", "data": { "run_id": "990e8400-e29b-41d4-a716-446655440004", "session_id": "880e8400-e29b-41d4-a716-446655440003", "status": "completed", "agent": { "type": "mode", "id": "api" }, "result": { "type": "message", "content": "..." }, "metadata": {}, "created_at": "2026-08-21T14:30:00Z", "started_at": "2026-08-21T14:30:05Z", "finished_at": "2026-08-21T14:35:00Z", "status_url": "/platform/v1/kapso-agent/runs/990e8400-e29b-41d4-a716-446655440004" } } ``` `kapso_agent.run.approval_required` includes `pending_approval` with the tool name and parameters. Failed and cancelled runs include `error` with `code` (`run_failed` or `run_cancelled`) and `message`. Null fields are omitted. # Webhook security Source: https://docs.kapso.ai/docs/platform/webhooks/security Verify webhook signatures to prevent unauthorized requests Always verify webhook signatures to ensure requests come from Kapso. ## Signature verification Kapso signs all webhooks with HMAC SHA256 using your webhook secret key. The signature is included in the `X-Webhook-Signature` header. ### How it works 1. Kapso creates a signature by hashing the raw JSON payload with your secret key 2. The signature is sent in the `X-Webhook-Signature` header 3. Your endpoint recreates the signature using the same method 4. Compare signatures using a timing-safe comparison ### Node.js example ```javascript theme={null} const crypto = require('crypto'); const express = require('express'); const app = express(); function verifyWebhook(rawBody, signature, secret) { if (typeof signature !== 'string') return false; const expected = crypto .createHmac('sha256', secret) .update(rawBody) .digest('hex'); const a = Buffer.from(expected, 'utf8'); const b = Buffer.from(signature, 'utf8'); // timingSafeEqual throws on length mismatch, so check length first return a.length === b.length && crypto.timingSafeEqual(a, b); } // express.raw gives you the bytes Kapso signed; express.json does not app.post( '/webhooks/whatsapp', express.raw({ type: 'application/json' }), (req, res) => { const signature = req.headers['x-webhook-signature']; if (!verifyWebhook(req.body, signature, process.env.WEBHOOK_SECRET)) { return res.status(401).send('Invalid signature'); } const event = JSON.parse(req.body); console.log('Event:', event.event); res.status(200).send('OK'); } ); ``` ### Python example ```python theme={null} import hashlib import hmac import json import os from flask import Flask, request app = Flask(__name__) def verify_webhook(raw_body, signature, secret): if not signature: return False expected = hmac.new( secret.encode('utf-8'), raw_body, hashlib.sha256 ).hexdigest() return hmac.compare_digest(signature, expected) @app.route('/webhooks/whatsapp', methods=['POST']) def webhook(): # get_data() returns the bytes Kapso signed; request.json does not raw_body = request.get_data() signature = request.headers.get('X-Webhook-Signature') if not verify_webhook(raw_body, signature, os.environ['WEBHOOK_SECRET']): return 'Invalid signature', 401 event = json.loads(raw_body) print('Event:', event['event']) return 'OK', 200 ``` ### Ruby example ```ruby theme={null} require 'sinatra' require 'json' require 'openssl' def verify_webhook(raw_body, signature, secret) return false if signature.nil? expected = OpenSSL::HMAC.hexdigest('SHA256', secret, raw_body) Rack::Utils.secure_compare(signature, expected) end post '/webhooks/whatsapp' do request.body.rewind # the raw string is what Kapso signed; a re-serialized hash is not raw_body = request.body.read signature = request.env['HTTP_X_WEBHOOK_SIGNATURE'] halt 401, 'Invalid signature' unless verify_webhook(raw_body, signature, ENV['WEBHOOK_SECRET']) payload = JSON.parse(raw_body) puts "Event: #{payload['event']}" status 200 end ``` ## Important notes ### Use the raw payload Always verify against the raw JSON payload, not a parsed object: ```javascript theme={null} // ❌ Wrong - re-serializing a parsed object does not reproduce the bytes app.post('/webhooks', express.json(), (req, res) => { verifyWebhook(JSON.stringify(req.body), signature, secret); }); // ✅ Correct - keep the bytes as they arrived app.post('/webhooks', express.raw({ type: 'application/json' }), (req, res) => { verifyWebhook(req.body, signature, secret); }); ``` `JSON.stringify` on a parsed body is not a substitute. Key order, whitespace, and unicode escaping can all differ from what Kapso signed, and every difference is a failed verification. ### Use timing-safe comparison Never use `===` or `==` to compare signatures. Use timing-safe comparison to prevent timing attacks: ```javascript theme={null} // ❌ Wrong - vulnerable to timing attacks if (signature === expectedSignature) { ... } // ✅ Correct - timing-safe comparison const a = Buffer.from(expectedSignature, 'utf8'); const b = Buffer.from(signature, 'utf8'); a.length === b.length && crypto.timingSafeEqual(a, b) ``` `crypto.timingSafeEqual` throws a `RangeError` when the buffers are different lengths, and `Buffer.from` throws when the header is missing altogether. Guard on both, or a forged signature returns `500` instead of `401`. ### Store secrets securely * Never hardcode webhook secrets in your code * Use environment variables or secret management services * Rotate secrets periodically * Use different secrets for development and production ## Idempotency Webhooks may be delivered more than once. Use the `X-Idempotency-Key` header to track processed events. ### Simple in-memory tracking ```javascript theme={null} const processedKeys = new Set(); app.post('/webhooks', (req, res) => { const idempotencyKey = req.headers['x-idempotency-key']; if (processedKeys.has(idempotencyKey)) { return res.status(200).send('Already processed'); } // Process event processEvent(req.body); processedKeys.add(idempotencyKey); res.status(200).send('OK'); }); ``` ### Database-backed tracking ```javascript theme={null} app.post('/webhooks', async (req, res) => { const idempotencyKey = req.headers['x-idempotency-key']; // Check if already processed const existing = await db.webhookEvents.findOne({ idempotency_key: idempotencyKey }); if (existing) { return res.status(200).send('Already processed'); } // Process event await processEvent(req.body); // Store idempotency key await db.webhookEvents.create({ idempotency_key: idempotencyKey, event: req.body.event, processed_at: new Date() }); res.status(200).send('OK'); }); ``` ## Best practices 1. **Verify signatures first** - Before processing any webhook data 2. **Return 200 quickly** - Respond within 10 seconds to avoid retries 3. **Process asynchronously** - Use background jobs for heavy processing 4. **Handle duplicates** - Implement idempotency using `X-Idempotency-Key` 5. **Monitor failures** - Set up alerts for signature verification failures 6. **Use HTTPS only** - Never accept webhooks over HTTP 7. **Rotate secrets** - Change webhook secrets periodically 8. **Log everything** - Keep audit logs of webhook deliveries and failures # WhatsApp data Source: https://docs.kapso.ai/docs/platform/whatsapp-data What data Kapso stores and how to access it ## Overview | Data | Dashboard | Webhooks | WhatsApp API | Platform API | | ---------------- | --------- | -------- | ----------------------- | ----------------- | | Conversations | Yes | Yes | - | Yes (status only) | | Messages | Yes | Yes | Yes (delivery status) | - | | Media | Yes | Yes | Yes (retrieve/download) | Yes (upload) | | Contacts | Yes | - | - | - | | Calls | Yes | Yes | - | - | | Referrals (CTWA) | Yes | Yes | - | - | | Broadcasts | Yes | - | - | Yes (list/get) | ## Identity fields WhatsApp identity is no longer phone-only. Kapso can now store and expose: * `wa_id` or `phone_number` * `business_scoped_user_id` * `parent_business_scoped_user_id` * `username` Depending on the payload and rollout stage, phone-based fields can be `null` while BSUID fields are present. For migration details: * [Business-scoped user IDs](/docs/whatsapp/business-scoped-user-ids) ## Conversations **Fields**: phone\_number, business\_scoped\_user\_id, parent\_business\_scoped\_user\_id, username, status (active/ended), last\_active\_at, metadata, assignee **Access**: * Dashboard: WhatsApp > Data > Conversations * Webhooks: [`message.received`](/docs/platform/webhooks/message-events#whatsapp-message-received), [`message.status_updated`](/docs/platform/webhooks/message-events#status-history) * Platform API: [PATCH /conversations/:id](/api/platform/v1/conversations/update-conversation-status) (close/reopen) ## Messages **Fields**: content, message\_type, direction (inbound/outbound), status (pending/sent/delivered/read/failed), metadata, additive identity fields on the message payload (`business_scoped_user_id`, `parent_business_scoped_user_id`, `username` or Meta-style `from_user_id` / `to_user_id`) **Message types**: text, image, video, audio, document, location, interactive, template, reaction, contacts **Access**: * Dashboard: WhatsApp > Data > Messages * Webhooks: [`message.received`](/docs/platform/webhooks/message-events#whatsapp-message-received), [`message.status_updated`](/docs/platform/webhooks/message-events#status-history) * WhatsApp API: `GET /{phone_number_id}/message_history` (delivery status only, not content) ## Media **Stored**: File attachments on messages (images, videos, audio, documents) **Audio transcripts**: Automatic transcription with provider info, detected language, and duration **Access**: * Dashboard: WhatsApp > Data > Media * WhatsApp API: [GET /](/api/meta/whatsapp/media/get-media-url) (get download URL), then fetch the URL to download * Platform API: [POST /media](/api/platform/v1/media/upload-media) (upload files for sending) ## Contacts **Fields**: wa\_id, business\_scoped\_user\_id, parent\_business\_scoped\_user\_id, username, profile\_name, display\_name, metadata, notes **Access**: * Dashboard: WhatsApp > Data > Contacts ## Calls **Fields**: call\_id, direction (incoming/outgoing), status (ringing/accepted/missed/declined/ended), duration\_seconds, started\_at, ended\_at, user\_wa\_id, business\_scoped\_user\_id, parent\_business\_scoped\_user\_id, username **Access**: * Dashboard: WhatsApp > Data > Calls * Webhooks: Delivered via WhatsApp call events ## Referrals (CTWA) Click-to-WhatsApp ad data captured when users message from Meta ads. **Fields**: source\_type (ad/post/organic), source\_id, source\_url, ctwa\_clid, headline, body, media\_type **Access**: * Dashboard: WhatsApp > Data > Ads (CTWA) * Webhooks: Included in [`message.received`](/docs/platform/webhooks/message-events#whatsapp-message-received) payload when present ## Broadcasts **Fields**: name, template, status (draft/sending/completed/failed), recipient counts, delivery stats **Access**: * Dashboard: WhatsApp > Outbound > Broadcasts * Platform API: [GET /whatsapp/broadcasts](/api/platform/v1/broadcasts/list-broadcasts), [GET /whatsapp/broadcasts/](/api/platform/v1/broadcasts/get-broadcast), [GET /whatsapp/broadcasts//recipients](/api/platform/v1/broadcasts/list-recipients) ## Retention **Default**: Everything above is kept indefinitely. **Retention window**: Paid projects can set one in Project settings > Message retention — 30 days, 90 days, 6 months, 12 months, or a custom number of days. Free projects keep everything. **Deleted**: Ended conversations whose last activity is older than the window, along with their messages, media, audio transcripts, referrals, flow executions, webhook deliveries, broadcast recipient rows, and the project events recorded against them. Agent execution transcripts go once nothing else references them. Messages that never belonged to a conversation are deleted on the same window, measured from when they were created. **Kept**: Contacts, broadcasts and their delivery counts, and template statistics. Calls are unlinked from a deleted conversation but not deleted themselves. Project events that are not tied to a deleted conversation follow the retention window from your plan, not this one. **Message logs**: The searchable log of messages is kept for 30 days, or for your retention window if that window is shorter. Deleting a conversation removes its log entries at the same time. **Applying a change**: Shortening a window takes effect after 48 hours, and project owners are emailed when it is scheduled. Lengthening it, or going back to keeping everything, applies immediately. Deleted conversations cannot be recovered. # Business-scoped user IDs Source: https://docs.kapso.ai/docs/whatsapp/business-scoped-user-ids What changes when WhatsApp starts sending BSUIDs and how to adapt your integration Use the copy button to paste this guide into Codex, Claude Code, Cursor, or another coding agent and ask it to adapt your integration. ## TL;DR Meta is rolling out **business-scoped user IDs (BSUIDs)** as a primary identity in WhatsApp. Inbound payloads can already arrive with BSUIDs, and phone numbers can be omitted in some username-related cases. Your integration needs to: * Store `business_scoped_user_id`, `parent_business_scoped_user_id`, and `username` * Make `phone_number` and `wa_id` nullable * Match users by BSUID first, phone number second * Handle identity-change events if you consume raw Meta webhooks or keep your own identity store * Reply with `to` when you have a phone number, or `recipient` when you only have a BSUID ## What changed Meta is rolling out **business-scoped user IDs** for WhatsApp. They identify a user inside a business account and can appear together with a phone number or by themselves. Kapso now exposes these additive fields where identity is already exposed today: * `business_scoped_user_id` * `parent_business_scoped_user_id` * `username` Relevant official guide: * [Meta: business-scoped user IDs](https://developers.facebook.com/documentation/business-messaging/whatsapp/business-scoped-user-ids/) ## What each field means * `business_scoped_user_id`: the main WhatsApp identifier for a user inside your business context. When present, treat this as the primary identity key. * `parent_business_scoped_user_id`: a parent BSUID that Meta only sends for eligible managed businesses with linked business portfolios. Unlike `business_scoped_user_id`, it can work across the linked portfolio group. Store it when present, but treat `business_scoped_user_id` as the primary identity key inside a normal single-portfolio integration. * `username`: the user's WhatsApp username when available. Useful for display and some reconciliation flows, but not a stable primary identifier. ## Current status What is live now in Kapso: * inbound payloads can include both phone-based identity and BSUID-based identity * some webhook and API payloads can have `phone_number` or `wa_id` as `null` * conversations, contacts, messages, and flow context can now include the new identity fields * outbound sends can target phone numbers with `to` or BSUIDs with `recipient` ### Outbound BSUID sends Use `recipient` for a regular BSUID or parent BSUID: ```json theme={null} { "messaging_product": "whatsapp", "recipient": "US.13491208655302741918", "type": "template", "template": { "name": "order_update", "language": { "code": "en_US" } } } ``` Use `to` for phone numbers. If both `to` and `recipient` are present, Meta uses the phone number in `to`. When replying to an inbound message: ```js theme={null} const destination = message.from ? { to: message.from } : { recipient: message.from_user_id }; ``` Do not pass a BSUID in `to`. Use the full BSUID in `recipient`, including its country prefix and period. With the [TypeScript SDK](/docs/whatsapp/typescript-sdk/introduction), every message builder takes `recipient` alongside `to`: ```ts theme={null} await client.messages.sendText({ phoneNumberId: '123', recipient: 'US.13491208655302741918', body: 'On its way.' }); ``` Notes: * non-template messages still require an open 24-hour customer service window * template messages can start or reopen a conversation * sandbox numbers do not support BSUID recipients * authentication templates cannot be sent to BSUID recipients * BSUIDs are scoped to the business portfolio that owns the sending phone number ### Addressing contacts by BSUID The Platform contacts API accepts a BSUID or parent BSUID wherever it accepts a contact UUID or phone number: ```bash theme={null} curl https://api.kapso.ai/platform/v1/whatsapp/contacts/US.13491208655302741918 \ -H "X-API-Key: $KAPSO_API_KEY" ``` This works for `GET`, `PATCH`, and `DELETE` on `/platform/v1/whatsapp/contacts/{identifier}`. Pass the BSUID verbatim, including the country prefix and period. A parent BSUID (`US.ENT.…`) can be shared by several contacts in a project, and the same BSUID can exist under two portfolios. An ambiguous lookup returns `409 Conflict` with the candidate contact UUIDs instead of guessing; address the contact by its UUID in that case. To list instead of fetch a single contact, filter with `business_scoped_user_id`: ```bash theme={null} curl "https://api.kapso.ai/platform/v1/whatsapp/contacts?business_scoped_user_id=US.13491208655302741918" \ -H "X-API-Key: $KAPSO_API_KEY" ``` ### Starting workflows by BSUID `POST /platform/v1/workflows/{id}/executions` accepts `recipient` in place of `phone_number`. See [Start and resume via API](/docs/workflows/start-and-resume-via-api). ## Payload shapes Your parser should handle these inbound shapes: * phone identity **and** BSUID identity together * BSUID identity with **no phone number** * `username` present with `phone_number` missing * status payloads with recipient identity fields * identity-change system messages: `user_changed_number` and `user_changed_user_id` Kapso surfaces the new fields in these places: | Location | Fields | | ----------------------------- | -------------------------------------------------------------------------------------------------------------------------- | | Contact payloads | `wa_id`, `business_scoped_user_id`, `parent_business_scoped_user_id`, `username` | | Conversation payloads | `phone_number`, `business_scoped_user_id`, `parent_business_scoped_user_id`, `username` | | Message payloads (Kapso) | `business_scoped_user_id`, `parent_business_scoped_user_id`, `username` | | Message payloads (Meta-style) | `from_user_id`, `from_parent_user_id`, `to_user_id`, `to_parent_user_id`, `username` | | Workflow context | `context.whatsapp_business_scoped_user_id`, `context.whatsapp_parent_business_scoped_user_id`, `context.whatsapp_username` | ### Payload assumptions During the rollout, build for these cases: * `phone_number`, `wa_id`, `from`, or `to` can be omitted instead of set to `null` * `username` can change over time * identity can transition over time through system messages like `user_changed_number` and `user_changed_user_id` ### Example payloads Phone number and BSUID together: ```json theme={null} { "conversation": { "phone_number": "16315551181", "business_scoped_user_id": "US.13491208655302741918", "parent_business_scoped_user_id": "US.ENT.506847293015824", "username": "@testusername" } } ``` Kapso webhook with a BSUID-only inbound message: ```json theme={null} { "message": { "id": "wamid.123", "type": "text", "from_user_id": "US.13491208655302741918", "username": "@testusername", "text": { "body": "Hello" } }, "conversation": { "id": "conv_123", "phone_number": null, "business_scoped_user_id": "US.13491208655302741918", "parent_business_scoped_user_id": null, "username": "@testusername" } } ``` `message.from` is omitted and `conversation.phone_number` is `null` in this case. Do not reject the webhook because the phone identity is missing. Forwarded Meta webhooks follow the same rule: `entry[].changes[].value.messages[].from` can be absent while `from_user_id` is present. Forwarded Meta status identity without a phone number: ```json theme={null} { "contacts": [ { "user_id": "US.13491208655302741918", "parent_user_id": "US.ENT.506847293015824", "profile": { "username": "@testusername" } } ], "statuses": [ { "id": "wamid.456", "status": "delivered", "recipient_user_id": "US.13491208655302741918", "recipient_parent_user_id": "US.ENT.506847293015824" } ] } ``` The forwarded Meta payload uses `user_id` in `contacts` and `recipient_user_id` in `statuses`. Phone-based fields such as `wa_id` and `recipient_id` can be omitted. ## When phone numbers are included If a WhatsApp user enables a username, Meta omits their phone number unless at least one condition applies: * the receiving business phone number messaged or called the user's phone number in the last 30 days * the receiving business phone number received a message or call from the user's phone number in the last 30 days * the user is stored in the business portfolio's Meta contact book The 30-day checks apply per business phone number. An interaction through another number in the same portfolio does not satisfy those checks on its own. The contact book is portfolio-scoped. Once it records the identity mapping, any business phone number in that portfolio can receive the phone number in eligible webhooks. There is no universal date when Meta will stop sending phone numbers. Availability depends on username adoption and the conditions above. ## Matching and storage Treat WhatsApp identity as a compound shape, not just a phone number. Recommended matching order: 1. `business_scoped_user_id` when present 2. `wa_id` or `phone_number` when present 3. keep both when you have both Recommended storage rules: * store `business_scoped_user_id`, `parent_business_scoped_user_id`, and `username` * allow `wa_id` and `phone_number` to be nullable * keep BSUID identity and phone identity on the same logical user/contact when both refer to the same person * do not key your data model only by phone number anymore If phone identity and BSUID identity point to different local records, merge or relink them only when the same Meta payload or an identity-change event establishes the relationship. Otherwise, keep the records separate for manual reconciliation. ## Identity-change events If you only consume normal Kapso webhooks and read the current state from Kapso APIs, this is lower priority at the beginning because Kapso already reconciles these identity changes internally. If you keep your own identity store, mirror WhatsApp users into your own database, or consume forwarded Meta webhooks directly, these events matter and you should handle them. Meta announces a BSUID rotation on the regular `messages` field as a system message. `user_changed_number` is used when the new phone number can be shared, `user_changed_user_id` when only the BSUID transition can be shared. Both carry the current `user_id` (and `parent_user_id` when enabled) plus `previous_user_id` and `previous_parent_user_id`. There is no subscribable `user_id_update` field. ```json theme={null} { "type": "system", "system": { "type": "user_changed_number", "wa_id": "12195555358", "user_id": "US.99225512874400319256", "previous_user_id": "US.13491208655302741918" } } ``` Kapso reconciles the change onto the existing contact, conversation, and sandbox session, then emits [`whatsapp.contact.identity_changed`](/docs/platform/webhooks/message-events#whatsapp-contact-identity_changed). These system messages are not delivered as `whatsapp.message.received` and do not reach workflows or agents. See: * [Receive messages](/docs/platform/webhooks/overview) * [Meta: business-scoped user IDs](https://developers.facebook.com/documentation/business-messaging/whatsapp/business-scoped-user-ids/) If you consume raw Meta webhooks yourself: * treat them as identity reconciliation events, not normal user content * update the existing user/contact/conversation linkage instead of creating a new user blindly * keep previous phone identity if it is still the same logical person * use the event to move from phone-first matching to BSUID-first matching * expect coexistence windows where old and new identifiers can both appear ## Migration checklist * update your schema so `wa_id` and `phone_number` can be nullable where appropriate * store `business_scoped_user_id`, `parent_business_scoped_user_id`, and `username` * stop keying your users only by phone number * make webhook parsers accept phone-based and BSUID-based payloads * review validations, unique indexes, searches, and CRM mappings that require a phone number * make your matching logic tolerate transition periods where both old and new identifiers can appear * use `recipient` for BSUID or parent BSUID outbound sends * use [request contact info](/docs/whatsapp/send-messages/request-contact-info) when you need the customer's phone number * test at least these cases before rollout reaches your users: * phone + BSUID inbound payload * BSUID-only inbound payload * username + BSUID payload with no phone * status webhook with recipient identity * `user_changed_number` or `user_changed_user_id` identity change system message ## Timeline The rollout has started. If you already consume Kapso WhatsApp payloads, adapt now. Phone numbers can be omitted for username adopters, but many payloads still include them during the rollout - especially after a recent interaction, or when the user is in Meta's contact book. * **Early April 2026**: BSUIDs begin appearing in inbound webhooks * **Early April 2026**: Meta contact book rollout begins, which affects when phone numbers can still appear after prior interactions * **June 29, 2026**: businesses can reserve usernames through Meta tools or the Username API * **Early July 2026**: Meta begins enabling BSUID sends and phone number request CTAs * **2026**: broader usernames rollout continues gradually by region ## What's next Planned follow-up areas: * expanding examples and API references as later username and BSUID phases become generally available * updating this guide as Meta publishes more concrete rollout dates Until then, treat this page as the source of truth for the currently supported Kapso behavior. ## Related docs * [Receive messages](/docs/platform/webhooks/overview) * [Request contact info](/docs/whatsapp/send-messages/request-contact-info) * [Business usernames](/docs/whatsapp/business-usernames) * [Webhook event types](/docs/platform/webhooks/message-events) * [WhatsApp data](/docs/platform/whatsapp-data) * [Variables and context](/docs/flows/variables-and-context) ## Changelog * **2026-08-04**: Added addressing Platform API contacts by BSUID or parent BSUID. * **2026-07-30**: Added BSUID-only webhook examples, reply routing, phone number availability rules, and safer reconciliation guidance. * **2026-06-30**: Added BSUID outbound sends, request contact info, and username rollout notes. * **2026-04-20**: Clarified that phone numbers can be omitted only in some rollout cases, added identity-change event guidance, and expanded the timeline with Meta's published dates. # Business usernames Source: https://docs.kapso.ai/docs/whatsapp/business-usernames Reserve and manage WhatsApp business usernames through the Meta proxy API Use the Username API to reserve, read, change, or delete the username for a WhatsApp business phone number. Base URL: ```txt theme={null} https://api.kapso.ai/meta/whatsapp/v24.0 ``` ## Get current username ```bash theme={null} curl 'https://api.kapso.ai/meta/whatsapp/v24.0/647015955153740/username' \ -H 'X-API-Key: YOUR_API_KEY' ``` ```json theme={null} { "username": "kapso_support", "status": "reserved" } ``` `username` is omitted when the phone number has no username. ## Get reserved suggestions ```bash theme={null} curl 'https://api.kapso.ai/meta/whatsapp/v24.0/647015955153740/username_suggestions' \ -H 'X-API-Key: YOUR_API_KEY' ``` ```json theme={null} { "data": [ { "username_suggestions": ["kapso_support", "kapso_ai"] } ] } ``` ## Claim or change username ```bash theme={null} curl -X POST 'https://api.kapso.ai/meta/whatsapp/v24.0/647015955153740/username' \ -H 'X-API-Key: YOUR_API_KEY' \ -H 'Content-Type: application/json' \ -d '{ "username": "kapso_support", "transfer_action": "none" }' ``` `transfer_action` is optional: * `none`: default. Fail if the username belongs to another phone number in the same business portfolio. * `force_transfer`: move the username from another phone number in the same business portfolio. Success response: ```json theme={null} { "status": "reserved" } ``` ## Delete username ```bash theme={null} curl -X DELETE 'https://api.kapso.ai/meta/whatsapp/v24.0/647015955153740/username' \ -H 'X-API-Key: YOUR_API_KEY' ``` ```json theme={null} { "success": true } ``` ## Statuses * `reserved`: approved but not visible until usernames are available in the user's region. * `approved`: visible to WhatsApp users. * `deleted`: username was removed. Kapso records username state when requests go through the Meta proxy. Kapso also processes Meta `business_username_updates` webhooks and updates the visible username on the WhatsApp configuration. ## Format Usernames: * are 3-35 characters * use English letters, digits, periods, and underscores * contain at least one English letter * do not start or end with a period * do not contain consecutive periods * do not start with `www` * do not end with a domain suffix like `.com`, `.org`, `.net`, `.io`, or `.ai` Case is ignored for comparisons. Periods and underscores are distinct. ## Common errors | Code | Meaning | | -------- | ------------------------------------------------------------------- | | `10` | Token or business asset permissions are missing | | `33` | Phone number or WABA access is invalid | | `100` | Username format is invalid | | `147001` | Username is not available | | `147002` | Account is not eligible to request a username | | `147003` | Facebook account must be linked | | `147004` | Instagram account must be linked | | `147005` | Use `force_transfer` to move the username inside the same portfolio | | `133010` | Phone number is not registered for API use | ## Related docs * [Business-scoped user IDs](/docs/whatsapp/business-scoped-user-ids) * [API overview](/api/introduction) # Chat SDK Source: https://docs.kapso.ai/docs/whatsapp/chat-sdk Build WhatsApp agents with Chat SDK and Kapso. Use `@kapso/chat-adapter` when a [Chat SDK](https://chat-sdk.dev) agent needs to receive Kapso WhatsApp webhooks, reply in threads, send buttons or media, and read Kapso conversation history. Use [`@kapso/whatsapp-cloud-api`](/docs/whatsapp/typescript-sdk/introduction) directly when you need raw templates, flows, catalogs, or lower-level WhatsApp API calls. ## Installation ```bash theme={null} npm install chat @kapso/chat-adapter @chat-adapter/state-memory ``` Requires Node.js >= 20.19. `@chat-adapter/state-memory` is good for local development. For production, use durable Chat SDK state. ## Quickstart ```ts theme={null} import { Chat } from 'chat'; import { createMemoryState } from '@chat-adapter/state-memory'; import { createKapsoAdapter } from '@kapso/chat-adapter'; export const bot = new Chat({ userName: 'support', state: createMemoryState(), adapters: { kapso: createKapsoAdapter(), }, }); bot.onDirectMessage(async (thread, message) => { await thread.post(`You said: ${message.text}`); }); ``` ## Environment ```bash theme={null} KAPSO_API_KEY=your_project_api_key KAPSO_PHONE_NUMBER_ID=your_phone_number_id KAPSO_WEBHOOK_SECRET=your_webhook_secret ``` | Variable | Required | Use | | ----------------------- | ----------- | -------------------------------------------------------------------------------- | | `KAPSO_API_KEY` | Yes | Sends messages, reads history, contacts, conversations, and media through Kapso. | | `KAPSO_PHONE_NUMBER_ID` | Recommended | Default WhatsApp phone number ID. Required for `openDM()`. | | `KAPSO_WEBHOOK_SECRET` | Recommended | Verifies Kapso `X-Webhook-Signature` deliveries. | | `KAPSO_BASE_URL` | No | Kapso proxy URL. Defaults to `https://api.kapso.ai/meta/whatsapp`. | | `KAPSO_BOT_USERNAME` | No | Bot display name. Defaults to the Chat SDK `userName`. | You can also pass config directly: ```ts theme={null} createKapsoAdapter({ kapsoApiKey: process.env.KAPSO_API_KEY, phoneNumberId: process.env.KAPSO_PHONE_NUMBER_ID, webhookSecret: process.env.KAPSO_WEBHOOK_SECRET, }); ``` ## Webhook route Kapso sends platform webhooks as `POST` requests. Forward the raw `Request` to Chat SDK. ```ts theme={null} import { bot } from '@/lib/bot'; export async function POST(request: Request): Promise { return bot.webhooks.kapso(request); } ``` Configure the webhook in Kapso: | Setting | Value | | ------------ | ------------------------------------------------------------------------------ | | Endpoint URL | Your public `POST` route, for example `https://app.example.com/webhooks/kapso` | | Secret key | Same value as `KAPSO_WEBHOOK_SECRET` | | Events | `whatsapp.message.received` | Add `whatsapp.message.sent` only if your app needs sent-message echoes. ## Send replies Reply inside a direct message handler: ```ts theme={null} bot.onDirectMessage(async (thread, message) => { await thread.post({ markdown: `Received: **${message.text}**`, }); }); ``` Start an outbound WhatsApp conversation: ```ts theme={null} import type { KapsoAdapter } from '@kapso/chat-adapter'; const adapter = bot.getAdapter('kapso') as KapsoAdapter; const threadId = await adapter.openDM('15551234567'); const thread = bot.thread(threadId); await thread.post('Hello from Kapso.'); ``` ## Buttons Chat SDK cards with buttons become WhatsApp reply buttons. ```tsx theme={null} import { Actions, Button, Card } from 'chat'; await thread.post( Card({ title: 'Approve refund?', children: [ Actions([ Button({ id: 'approve', label: 'Approve', value: 'refund-123' }), Button({ id: 'reject', label: 'Reject', value: 'refund-123' }), ]), ], }), ); ``` Handle button clicks: ```ts theme={null} bot.onAction('approve', async (action) => { await action.thread?.post(`Approved ${action.value}`); }); ``` WhatsApp supports up to 3 reply buttons. Button labels must be 1-20 characters. ## Media Send files through Chat SDK: ```ts theme={null} await thread.post({ markdown: 'Here is the receipt.', files: [ { filename: 'receipt.pdf', mimeType: 'application/pdf', data: await fs.promises.readFile('receipt.pdf'), }, ], }); ``` Inbound media appears as Chat SDK attachments. When Kapso includes a mirrored media URL, the attachment has `url`. When a WhatsApp media ID is available, the attachment has lazy `fetchData()`. ## History With `KAPSO_API_KEY`, history reads from Kapso: ```ts theme={null} const page = await thread.adapter.fetchMessages(thread.id, { limit: 20 }); ``` `fetchThread()` enriches metadata with Kapso contact and conversation records when available. ## Source Source code, examples, and issue tracker for `@kapso/chat-adapter`. # CLI Source: https://docs.kapso.ai/docs/whatsapp/cli Manage WhatsApp numbers, messages, templates, and webhooks from the terminal The Kapso CLI gives you full control over your WhatsApp integration from the command line. Send messages, manage webhooks, inspect conversations, and automate workflows — all without leaving your terminal. Using an MCP-capable agent? Use [Project MCP](/docs/whatsapp/mcp) when the agent should manage Kapso without shell access. ## Installation ```bash theme={null} npm install -g @kapso/cli ``` Requires Node.js >= 20.19. ## Authentication **Browser login (interactive)** ```bash theme={null} kapso login ``` Opens your browser to authenticate. Your session is stored locally in `~/.kapso/cli/`. **API key (CI / non-interactive environments)** ```bash theme={null} export KAPSO_API_KEY=your_project_api_key ``` When set, all project-scoped commands use the API key directly — no browser needed. Use this for CI pipelines, Docker containers, scripts, or any headless environment. You can create your project API key in the [Kapso dashboard](https://app.kapso.ai). ## Project context After login, the CLI remembers your active project. Switch between projects with: ```bash theme={null} kapso projects list kapso projects use kapso projects current ``` Check your full setup status: ```bash theme={null} kapso status ``` ## Setup Connect a WhatsApp number to your project: ```bash theme={null} kapso setup ``` This resolves your project and customer, then generates a setup link to connect or provision a WhatsApp number. For automation: ```bash theme={null} kapso setup \ --customer \ --country US \ --connection-type dedicated ``` ## Numbers ```bash theme={null} # List all numbers kapso whatsapp numbers list # Get a specific number kapso whatsapp numbers get "+1234567890" kapso whatsapp numbers get --phone-number-id # Start WhatsApp number setup kapso whatsapp numbers new # Health check kapso whatsapp numbers health "+1234567890" # Resolve a number reference to its canonical ID kapso whatsapp numbers resolve "+1234567890" ``` Most WhatsApp commands accept `--phone-number` or `--phone-number-id` to specify which number to act on. You can also pass the number as a positional argument where supported. ## Messages ### Send a text message ```bash theme={null} kapso whatsapp messages send \ --phone-number-id \ --to "+1234567890" \ --text "Hello from Kapso CLI" ``` ### Send with a JSON payload For media, interactive, or any advanced message type, pass a JSON payload: ```bash theme={null} # From a file kapso whatsapp messages send --phone-number-id --input message.json # From stdin cat message.json | kapso whatsapp messages send --phone-number-id --stdin ``` ### List and get messages ```bash theme={null} # List messages for a number kapso whatsapp messages list --phone-number-id # Filter by direction, status, or time range kapso whatsapp messages list \ --phone-number-id \ --direction inbound \ --since "2025-01-01T00:00:00Z" \ --limit 50 # Get a specific message kapso whatsapp messages get ``` ## Conversations ```bash theme={null} # List conversations (most recent first) kapso whatsapp conversations list --phone-number-id # Filter by status or contact phone kapso whatsapp conversations list --phone-number-id --status active --phone "+1234567890" # Get a specific conversation kapso whatsapp conversations get ``` ## Templates ```bash theme={null} # List templates for a number kapso whatsapp templates list --phone-number-id # Filter by status or category kapso whatsapp templates list --phone-number-id --status APPROVED --category UTILITY # Get a specific template kapso whatsapp templates get --phone-number-id # Create a template from JSON kapso whatsapp templates new --phone-number-id --input template.json ``` ## Webhooks ### Create a webhook ```bash theme={null} kapso whatsapp webhooks new \ --phone-number-id \ --url "https://example.com/webhook" \ --event whatsapp.message.received \ --event whatsapp.message.delivered \ --active ``` Available events: `whatsapp.message.received`, `whatsapp.message.sent`, `whatsapp.message.delivered`, `whatsapp.message.read`, `whatsapp.message.failed`, `whatsapp.conversation.created`, `whatsapp.conversation.ended`, `whatsapp.conversation.inactive`, `whatsapp.contact.identity_changed`. ### Manage webhooks ```bash theme={null} # List webhooks kapso whatsapp webhooks list --phone-number-id # Update a webhook kapso whatsapp webhooks update --phone-number-id --inactive # Delete a webhook kapso whatsapp webhooks delete --phone-number-id ``` ### Message buffering Buffer multiple `whatsapp.message.received` events into a single delivery: ```bash theme={null} kapso whatsapp webhooks new \ --phone-number-id \ --url "https://example.com/webhook" \ --event whatsapp.message.received \ --buffer-enabled \ --buffer-window-seconds 5 \ --max-buffer-size 10 \ --active ``` ## Customers ```bash theme={null} # List customers kapso customers list # Get a customer kapso customers get # Create a customer kapso customers new --name "Acme Corp" --external-id "acme-123" ``` ## Local workflow development Use these commands when you want to keep Kapso workflows and functions in a local repository. ```bash theme={null} # Create a local workspace mkdir kapso-workflows cd kapso-workflows npm init -y # Optional: install the workflow code library npm install --save-dev @kapso/workflows # Link this directory to one Kapso project kapso login kapso link --project kapso pull ``` `kapso pull` writes local source files for workflows and functions. If you edit workflows with `@kapso/workflows`, build the generated JSON before pushing: ```bash theme={null} kapso build kapso push --dry-run kapso push workflow ``` You can also push one function or everything in the repo: ```bash theme={null} kapso push function kapso push ``` If a pull is blocked by local edits, inspect or replace the incoming changes: ```bash theme={null} kapso pull --diff kapso pull --overwrite ``` See [Build locally](/docs/workflows/build-locally) for the full workflow source-control guide. ## Output formats All commands support `--output json` or `--output human`. Most default to `json` — pipe into `jq` for scripting: ```bash theme={null} kapso whatsapp numbers list --output json | jq '.[0].id' ``` ## Help ```bash theme={null} # General help kapso help # Help for a specific command kapso help whatsapp messages send ``` # Display names Source: https://docs.kapso.ai/docs/whatsapp/display-names Change your WhatsApp Business display name Your display name appears in your WhatsApp Business profile and at the top of chat threads. ## Change display name 1. Go to **Connected numbers** → select your number 2. Click **Profile** tab 3. Edit **Display name** and submit ```bash theme={null} curl -X POST 'https://api.kapso.ai/platform/v1/whatsapp/{phone_number_id}/display_names' \ -H 'X-API-Key: YOUR_API_KEY' \ -H 'Content-Type: application/json' \ -d '{"display_name_request": {"new_display_name": "My Business Name"}}' ``` ## Numbers on WhatsApp Business App Numbers connected in coexistence mode, or otherwise still on WhatsApp Business App, manage their display name in the app. Kapso rejects the change with `422` and does not create a request: ```json theme={null} { "error": "Manage this number's display name in WhatsApp Business App. Display name changes through Kapso are not supported for coexistence numbers." } ``` ## Review statuses Meta reviews display name changes. Most complete within 24-48 hours. | Status | Meaning | | -------------------------- | --------------------------------------------------------------- | | `submitted` | Request sent to Meta | | `pending_review` | Meta is reviewing | | `approved` | Name approved and active | | `available_without_review` | Approved instantly (common names) | | `deferred` | Decision postponed—usually approved later without action needed | | `declined` | Name rejected—see rejection reason and edit | ### What does "Deferred" mean? Meta defers decisions when additional review is needed. This typically resolves to `approved` within a few days without action on your part. If it stays deferred for more than a week, contact [Meta Business Support](https://business.facebook.com/business/help). ## Rejection reasons | Reason | Fix | | --------------------------- | --------------------------------------------- | | `NAME_NOT_CONSISTENT` | Name must match your website/branding | | `NAME_FORMAT_UNACCEPTABLE` | Remove URLs, emails, or promotional text | | `NAME_EMPLOYEE_ISSUE` | Remove personal names unless tied to branding | | `NAME_ENDCLIENT_NOTRELATED` | Name must relate to your business | ## Guidelines Meta requires display names to: * Accurately represent your business * Match your external branding (website, social media) * Not include "Official", "Verified", or Meta brand names * Not be generic terms or locations * Not contain promotional language [Meta's display name guidelines](https://www.facebook.com/business/help/338047025165344) # Data endpoint Source: https://docs.kapso.ai/docs/whatsapp/flows/data-endpoint Serve dynamic data to WhatsApp Flows Dynamic WhatsApp Flows call your data endpoint to fetch screen content at runtime. ## How it works ```mermaid theme={null} sequenceDiagram participant User participant Meta participant Kapso participant Function as Your Function User->>Meta: Opens flow in WhatsApp Meta->>Kapso: Encrypted request Kapso->>Function: Decrypted payload Function->>Kapso: Screen data Kapso->>Meta: Encrypted response Meta->>User: Dynamic content ``` 1. User opens the flow in WhatsApp 2. Meta sends an encrypted request to Kapso 3. Kapso decrypts and forwards to your function 4. Your function returns screen data 5. Kapso encrypts and returns to Meta 6. User sees the dynamic content ## Setup requirements Before using data endpoints: 1. **WhatsApp phone number** - Set in the Info tab 2. **Flows encryption** - One-time setup per phone number (Kapso handles the key exchange) 3. **Function** - Your code that returns screen data ## Request format Kapso sends this payload to your function: ```json theme={null} { "source": "whatsapp_flow", "flow": { "id": "kapso_flow_id", "meta_flow_id": "meta_flow_id" }, "data_exchange": { "version": "3.0", "action": "data_exchange", "screen": "CURRENT_SCREEN", "data": {}, "flow_token": "unique_session_token" }, "signature_valid": true, "received_at": "2024-01-15T10:30:00Z" } ``` The `data_exchange` object contains Meta's original payload with the current screen and any user-submitted data. ## Response format Your function must return: ```json theme={null} { "version": "3.0", "screen": "NEXT_SCREEN_ID", "data": { "field_name": "value" } } ``` | Field | Description | | --------- | --------------------------------------------- | | `version` | Always `"3.0"` | | `screen` | Screen ID to display next | | `data` | Key-value pairs matching screen's data schema | ### Completing the flow To end the flow and close it: ```json theme={null} { "version": "3.0", "screen": "SUCCESS", "data": { "extension_message_response": { "params": { "flow_token": "from_request" } } } } ``` ## Limits | Limit | Value | | ---------------- | ---------------------------- | | Response timeout | 15 seconds | | Rate limit | 100 requests/minute per flow | ## Error handling If your function fails or times out, Kapso returns an error screen to the user. Check the **Invocations** in your function dashboard for debugging. # Examples Source: https://docs.kapso.ai/docs/whatsapp/flows/examples WhatsApp Flow examples and templates ## WhatsApp Flows demo