# 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**.
2. On **Select the business assets to share with Kapso**, choose the **Business portfolio** for the business you want to connect.
3. Choose an existing **WhatsApp Business account**, or create one if Meta asks you to.
4. On **Add your WhatsApp phone number**, do not choose **Use a display name only**. Choose **Use a new or existing WhatsApp number**.
5. Open the **Phone number** dropdown, select a number under **BSP provided number**, and click **Next**.
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.
After Meta returns you to Kapso, Kapso finalizes the connection and shows the phone number as connected.
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**.
2. On **Select the business assets to share with Kapso**, choose the **Business portfolio** for the business that owns this WhatsApp number.
3. In **WhatsApp Business account**, choose **Connect a WhatsApp Business App** and click **Next**.
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**.
5. On **Connect your existing WhatsApp Business App**, review the sharing access and protection information, then click **Next**.
6. When the QR code appears, keep the browser window open and open the WhatsApp Business App on your phone.
7. On your phone, tap **Connect to the Business Platform** in the WhatsApp Business App.
8. Use the WhatsApp Business App camera to scan the QR code shown by Meta.
9. Choose whether to share chat history, then confirm your choice.
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**.
11. On **Review what you'll share with Kapso**, review the requested access and click **Confirm**.
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.
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.
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**.
2. On **Select the business assets to share with Kapso**, choose the **Business portfolio** for the business that owns this phone number.
3. Choose an existing **WhatsApp Business account**, or create one if Meta asks you to.
4. On **Add your WhatsApp phone number**, do not choose **Use a display name only**. Choose **Add a new number**.
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**.
6. On **Verify your phone number**, enter the 6-digit verification code from Meta and click **Next**.
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.
After Meta returns you to Kapso, Kapso registers the number, creates the production WhatsApp configuration, and shows the number as connected.
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.
## Where it runs
* **Project home** - the home page opens on the Agent view with your recent sessions.
* **Agent panel** - a side panel available on every project page. Pages like Logs and Findings can pre-fill a prompt with the thing you're looking at.
* **Inbox** - the inbox has its own Agent sidebar.
* **Slack** - mention the agent in a connected channel or message it directly. See [Slack integration](/docs/kapso-agent/slack).
* **API** - trigger runs programmatically and receive lifecycle events through project webhooks. See [Agent API](/docs/kapso-agent/api).
## Sessions and runs
A session is a conversation with the agent. Each request inside a session is a run.
Run statuses: `queued`, `running`, `paused`, `waiting_for_approval`, `completed`, `failed`, `cancelled`.
One run executes at a time per session. Messages sent while a run is active wait in a queue - you can send a waiting message to the active run right away or remove it. Runs can be paused, resumed, or cancelled while active.
For multi-step work the agent creates a plan and keeps it updated as it progresses. Simple questions and single-tool operations run without one.
## Models
Every session starts on the mode's default model. Change the model and reasoning effort with the selector in the chat - reasoning effort is available for models that support it. [Custom modes](/docs/kapso-agent/modes) fix the model, so the selector is locked to the mode's configuration.
In Slack, the default mode, model, and reasoning effort come from **Integrations → Slack**. Runs triggered through the [API](/docs/kapso-agent/api) use the mode's configured model - there's no model parameter.
## Approvals
Tool calls with side effects ask for approval before executing. Which tools need it depends on the mode's [capabilities](/docs/kapso-agent/modes#capabilities). In Slack, the **Tool approvals** setting can turn approvals off for a project.
## Settings
Configure the agent under **Kapso Agent** in your project:
* **Resources** - MCP connections, GitHub repositories, and encrypted environment variables the agent can use. See [Resources](/docs/kapso-agent/resources).
* **Modes** - built-in and custom modes. See [Modes](/docs/kapso-agent/modes).
* **Sessions**, **Runs**, **API requests** - execution activity, live run traces, per-run AI cost, and a 30-day history of Platform API requests made by the agent.
## Billing
Agent runs consume your project's AI credits. Each run and session shows its AI cost. When credits run out, runs stop with "Not enough AI credits" until you add more.
## Next steps
Connect tools, repositories, secrets, and sandboxes.
Built-in modes and how to create custom ones.
Run the agent from your Slack workspace.
Trigger runs and receive lifecycle webhooks.
# Resources
Source: https://docs.kapso.ai/docs/kapso-agent/resources
Connect MCP servers, GitHub repositories, environment variables, and sandboxes to Kapso Agent.
Resources define which external tools, repositories, and secrets each mode can access. Create project resources once, then assign only the resources that each mode needs.
## Assign resources to a mode
Open **Kapso Agent → Modes**, select a mode, then open **Resources**. Only enabled resources assigned to that mode are available during a run.
Assigned MCP tools appear directly in the current mode. Conflicting tool names receive a `{connection}__{tool}` namespace so both tools remain available.
Assigning a GitHub repository gives the mode read-only file access and turns on its sandbox. Secret values never appear in the agent instructions.
## Connect an MCP server
Add the server URL and its required headers under **Kapso Agent → Resources**. Use `${ENV:NAME}` in a header to reference an encrypted project variable.
Test the connection, then assign it to each mode that needs its tools.
## Connect GitHub
Connect the GitHub App once, then select repositories for the project. You can also add a repository manually with its owner, name, branch, and personal access token.
A personal access token is optional for public repositories. Assign each repository to the modes that need its files.
## Environment variables
Create encrypted project variables under **Kapso Agent → Resources**. MCP connections can reference them in headers, and custom-mode sandboxes can receive selected variables.
Kapso masks stored values in the settings page. It injects values only into the MCP connections and sandboxes that use them.
## Sandbox
A sandbox gives a custom mode an isolated workspace. Open **Kapso Agent → Modes**, select a custom mode, then open **Sandbox**.
Built-in modes use sandbox settings managed by Kapso. Assigning a repository to a custom mode keeps its sandbox enabled.
| Setting | Behavior |
| --------------------- | --------------------------------------------------------------------------------------------------------- |
| Environment variables | Inject selected project variables into the sandbox. |
| Workspace access | Read-only access exposes file-reading tools. Execute access also exposes shell and file-editing tools. |
| Require approval | When enabled, shell commands and file changes ask for approval. |
| Network access | Allow all outbound hosts or only selected hosts. Kapso and assigned GitHub hosts are added automatically. |
| Setup command | Run once when a new sandbox starts, before the agent begins work. |
# Slack integration
Source: https://docs.kapso.ai/docs/kapso-agent/slack
Run Kapso Agent from Slack in channels or direct messages.
Add the Kapso Slack app, then mention `@Kapso` in a connected channel or message it directly. Each Slack thread is one agent session, so follow-up messages in the same thread continue the conversation.
If **Integrations → Slack** is not available in your project, contact support.
## Install
You need to be a project owner or admin.
1. Go to **Integrations → Slack** in your project and add the Kapso app to your workspace.
2. Invite `@Kapso` into the channel you want to use.
3. Mention `@Kapso` in that channel. The app replies with a connect button - pick the project if the workspace is connected to more than one.
A channel connects to exactly one project. To move it to another project, disconnect it first.
Once an owner or admin has connected the workspace to a project, any member of that workspace can connect a channel to it, with or without a Kapso account. The channel then runs with the authority of the owner or admin who connected the workspace, unless the person clicking has their own Kapso account that manages the project - then it runs as them. People from other workspaces in a Slack Connect channel cannot connect channels.
Adding the app from Kapso links your Slack identity to your Kapso account. Only linked users can approve tool calls, so each owner or admin who needs to do that should add the app from their own Kapso account.
## Settings
Under **Integrations → Slack** you can set the defaults for new Slack conversations:
* **Default mode** - the [mode](/docs/kapso-agent/modes) Slack sessions start in. Defaults to `api`.
* **Tool approvals** - `Strict` follows the approval settings of the mode. `Autonomous` runs every tool action without approval, including sending messages, MCP tools, and sandbox changes.
* **Default model** and **reasoning effort** - used by built-in modes. Custom modes use the model configured on the mode.
## Use it
Mention `@Kapso` with your request:
```text theme={null}
@Kapso why did the webhook delivery to acme.com fail this morning?
```
* The agent replies in the thread. Mention it again in the same thread to continue that session.
* If a run is already active in the thread, new mentions wait in the queue like in the dashboard.
* For multi-step work, the agent posts a live plan card and tool progress in the thread.
* The final reply includes who asked, the project, and a **View in Kapso** link to the full session.
* The app's **Messages** tab shows suggested prompts based on what needs attention in your project. Click one to start a session with that request.
* Attach files to your message and the agent reads them, up to 5 files per request.
To read files, the app needs the `files:read` permission. Workspaces installed before it was added show **Action needed** under **Integrations → Slack** with a button to update permissions. Channels and settings stay connected through the update.
Anyone in the channel can ask. The requester is shown on the response, but tools execute with the authority of the owner or admin who connected the channel. If that person loses project access, the channel stops working until someone reconnects it.
## Direct messages
Message `@Kapso` directly to work without a channel. Each new message starts a session; replies in its thread continue it.
DMs pick the project automatically: the workspace's connected project when there is only one, or the project you manage (or your default project) when there are several. If Kapso can't pick one, it asks you to link your Slack identity or have an owner or admin connect the workspace to a single project.
## Approvals
When the agent needs approval - sending a message, for example - it posts an approval card in the thread. A card can cover several tool calls; approve or reject them together. Only Slack users linked to a Kapso account with access to the project can approve or reject.
## Disconnect
Remove a channel or the whole project connection from **Integrations → Slack**. Disconnecting the last project removes the app installation from the workspace. Uninstalling the app from Slack disconnects everything too.
# Migrate from 360dialog
Source: https://docs.kapso.ai/docs/migrate/from-360dialog
Migrate your WhatsApp numbers from 360dialog to Kapso.
## Step 1: Connect your number
1. In [WhatsApp Manager](https://business.facebook.com/latest/whatsapp_manager/phone_numbers), check the WABA for other numbers, templates, or assets you still need.
2. Turn off two-step verification, under **Phone numbers → Settings → Two-step verification**.
3. Confirm you are an admin of the owning Business Portfolio, and that the number has not sent a paid message in the last 30 days.
4. Remove the phone number from the WABA.
5. Wait about five minutes.
6. In Kapso, start embedded signup. Share your existing WABA, or create one when the flow asks.
7. Recreate your templates if you created a new WABA (see [Step 5](#step-5-templates)). Wait for Meta review.
The number stops sending after step 4, and on a new WABA it can send templates again once Meta approves them. Plan the migration accordingly.
See [Connect WhatsApp](/docs/how-to/whatsapp/connect-whatsapp) for the signup flow.
If the reconnect fails, see [coexistence troubleshooting](/docs/how-to/whatsapp/coexistence-troubleshooting) and [bring your own SIM troubleshooting](/docs/how-to/whatsapp/bring-your-own-sim-troubleshooting).
You can also start on a fresh number. [Instant setup](/docs/platform/phone-numbers/instant-setup) gives you a pre-verified US number, with no SMS verification step.
**Testing first?** The [sandbox](/docs/how-to/whatsapp/use-sandbox-for-testing) replaces messaging `START` to 360dialog's sandbox number. Create a session for your test phone, then send the 6-character code from WhatsApp. Unlike 360dialog's sandbox, it uses the same paths as production, supports media, and is not capped at 200 messages.
## Step 2: Get your phone number IDs
```bash theme={null}
curl https://api.kapso.ai/platform/v1/whatsapp/phone_numbers \
-H "X-API-Key: YOUR_API_KEY"
```
```json theme={null}
{
"data": [
{
"id": "1234567890",
"phone_number_id": "1234567890",
"name": "Support Line",
"business_account_id": "98765432109",
"display_phone_number": "+1 555-123-4567",
"quality_rating": "GREEN",
"throughput_tier": "TIER_10K",
"status": "CONNECTED"
}
]
}
```
Store the mapping from each old channel key to its `phone_number_id`.
| 360dialog | Kapso | Scopes to |
| ------------------------------ | ----------------------------- | ---------------- |
| `D360-API-KEY`, one per number | `X-API-Key`, one per project | authentication |
| The key itself | `phone_number_id` in the path | one phone number |
| The WABA behind the key | `business_account_id` | templates |
If you monitor `GET /health_status`, its replacement is `GET /platform/v1/whatsapp/phone_numbers/{phone_number_id}/health`, which checks actual messaging availability rather than the stored record.
## Step 3: Update message sending
Both platforms pass the message body to Meta as-is, so it does not change. Two things do, in every call:
* The base URL: `https://waba-v2.360dialog.io` becomes `https://api.kapso.ai/meta/whatsapp/v24.0`.
* The sender: the API key no longer implies it, so it moves into the path, as in `POST /{phone_number_id}/messages`.
Free-form replies, inside the 24-hour window. Only the URL and the auth header change.
**360dialog:**
```bash theme={null}
curl -X POST https://waba-v2.360dialog.io/messages \
-H "D360-API-KEY: YOUR_CHANNEL_KEY" \
-H "Content-Type: application/json" \
-d '{
"messaging_product": "whatsapp",
"recipient_type": "individual",
"to": "15551234567",
"type": "text",
"text": { "body": "Your order shipped!" }
}'
```
**Kapso:**
```bash theme={null}
curl -X POST https://api.kapso.ai/meta/whatsapp/v24.0/1234567890/messages \
-H "X-API-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"messaging_product": "whatsapp",
"recipient_type": "individual",
"to": "15551234567",
"type": "text",
"text": { "body": "Your order shipped!" }
}'
```
Kapso forwards the body to Meta byte for byte, so optional fields like `recipient_type` pass through unchanged. See [Send text](/docs/whatsapp/send-messages/text).
Business-initiated sends, outside the 24-hour window.
```bash theme={null}
curl -X POST https://api.kapso.ai/meta/whatsapp/v24.0/1234567890/messages \
-H "X-API-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"messaging_product": "whatsapp",
"to": "15551234567",
"type": "template",
"template": {
"name": "order_update",
"language": { "code": "en_US" },
"components": [
{
"type": "body",
"parameters": [{ "type": "text", "text": "John" }]
}
]
}
}'
```
See [Simple text templates](/docs/whatsapp/templates/simple-text).
Images, video, audio, documents, and stickers.
Uploads move from `POST /media` to `POST /{phone_number_id}/media`, with the same multipart form:
```bash theme={null}
curl -X POST https://api.kapso.ai/meta/whatsapp/v24.0/1234567890/media \
-H "X-API-Key: YOUR_API_KEY" \
-F "messaging_product=whatsapp" \
-F "file=@receipt.png;type=image/png"
```
Downloading loses the hostname-rewrite step. On 360dialog you fetch the media URL, replace `lookaside.fbsbx.com` with `waba-v2.360dialog.io`, strip backslashes, and fetch within five minutes. On Kapso, `GET /{media_id}?phone_number_id=1234567890` returns Meta's fields plus a `download_url` you fetch directly, valid for 4 minutes, with the auth embedded in the URL.
For inbound media on [Kapso events](#kapso-events) you can skip the flow entirely: payloads carry a ready `message.kapso.media_url`. A meta webhook forwards Meta's raw payload, so keep fetching by media ID there. Kapso also ingests from a URL and hands back a Meta media ID via `POST /platform/v1/whatsapp/media`.
Buttons, lists, CTAs, and Flows use Meta's `interactive` object in both platforms, so these payloads move across as-is:
```bash theme={null}
curl -X POST https://api.kapso.ai/meta/whatsapp/v24.0/1234567890/messages \
-H "X-API-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"messaging_product": "whatsapp",
"to": "15551234567",
"type": "interactive",
"interactive": {
"type": "button",
"body": { "text": "Confirm your appointment?" },
"action": {
"buttons": [
{ "type": "reply", "reply": { "id": "btn_yes", "title": "Yes" } },
{ "type": "reply", "reply": { "id": "btn_no", "title": "No" } }
]
}
}
}'
```
See [Send buttons](/docs/whatsapp/send-messages/buttons), [Send lists](/docs/whatsapp/send-messages/lists), and [Sending Flows](/docs/whatsapp/flows/sending-flows).
Both platforms mark a message read on the send endpoint, with Meta's status body, so this moves unchanged:
```bash theme={null}
curl -X POST https://api.kapso.ai/meta/whatsapp/v24.0/1234567890/messages \
-H "X-API-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"messaging_product": "whatsapp",
"status": "read",
"message_id": "wamid.HBgNMTU1NTE0OTU5Nzg1...",
"typing_indicator": { "type": "text" }
}'
```
Keep `typing_indicator` to mark the message read and show the indicator. Drop it to only mark read. See [Mark as read](/docs/whatsapp/send-messages/mark-read).
Reactions are their own message type, `type: "reaction"`, in both. See [Send reaction](/docs/whatsapp/send-messages/reaction).
The [TypeScript SDK](/docs/whatsapp/typescript-sdk/introduction) wraps all of this, so you do not have to write the envelopes by hand.
## Step 4: Update webhooks
360dialog sends you Meta's webhook payload as-is. Kapso can forward the same payload, send its own event format, or both: the two webhook kinds register independently on the same number.
### Keep your Meta parser
Register a webhook with `"kind": "meta"` and Kapso forwards Meta's exact payload with no reshaping. Your `entry[].changes[].value` parser keeps working unchanged:
```bash theme={null}
curl -X POST https://api.kapso.ai/platform/v1/whatsapp/phone_numbers/1234567890/webhooks \
-H "X-API-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"whatsapp_webhook": {
"kind": "meta",
"url": "https://yourapp.com/webhooks/whatsapp",
"secret_key": "your-signing-secret"
}
}'
```
Kapso adds two headers on every delivery: `X-Webhook-Signature` (see [below](#signature-verification)) and `X-Idempotency-Key` for deduplication. If you authenticated 360dialog webhooks with custom headers, pass the same `headers` object here.
### Kapso events
Kapso's own webhook kind unwraps Meta's envelope into one event per occurrence, named in the `X-Webhook-Event` header, with the payload flattened to `message`, `conversation`, and `phone_number_id`:
```bash theme={null}
curl -X POST https://api.kapso.ai/platform/v1/whatsapp/phone_numbers/1234567890/webhooks \
-H "X-API-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"whatsapp_webhook": {
"url": "https://yourapp.com/webhooks/whatsapp",
"events": [
"whatsapp.message.received",
"whatsapp.message.sent",
"whatsapp.message.delivered",
"whatsapp.message.read",
"whatsapp.message.failed"
],
"secret_key": "your-signing-secret"
}
}'
```
What you parse out of Meta's envelope today maps to:
| Meta payload (what 360dialog forwards) | Kapso |
| ---------------------------------------------------- | ------------------------------------------------------------------------------------------------ |
| `value.messages[]`, field `messages` | `whatsapp.message.received`, one event per message |
| `value.statuses[].status: "sent"` | `whatsapp.message.sent` |
| `value.statuses[].status: "delivered"` | `whatsapp.message.delivered` |
| `value.statuses[].status: "read"` | `whatsapp.message.read` |
| `value.statuses[].status: "failed"`, with `errors[]` | `whatsapp.message.failed`, errors in `message.kapso.statuses[].errors[]` |
| `value.metadata.phone_number_id` | `phone_number_id`, at the top level |
| `entry[].id` (WABA ID) | Implicit in the registered number |
| `value.contacts[0].profile.name` | `conversation.contact_name` |
| `messages[].image.id` and friends | `message.kapso.media_url`, ready to fetch |
| `messages[].referral` | On the payload when present. See [Referrals (CTWA)](/docs/platform/whatsapp-data#referrals-ctwa) |
| None | `conversation.id`, `message.kapso.transcript` for voice notes |
Full payload shapes in [Message events](/docs/platform/webhooks/message-events).
Do not assume `message.from` is present. Some contacts have no visible phone number, and WhatsApp identifies them with a `business_scoped_user_id` instead. See [business-scoped user IDs](/docs/whatsapp/business-scoped-user-ids).
### Webhook configuration
| 360dialog | Kapso |
| -------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
| `POST /v1/configs/webhook` with `{url, headers}` | `POST /platform/v1/whatsapp/phone_numbers/{id}/webhooks` |
| `GET /v1/configs/webhook` | `GET /platform/v1/whatsapp/phone_numbers/{id}/webhooks` |
| `POST /waba_webhook`, WABA-level fallback | None. Register each number |
| `/multi_webhook`, up to 3 URLs, secondaries fire only after the main returns 200 | Multiple webhooks per number, each delivered independently. Only one can be `kind: "meta"`; fan out extra raw-Meta destinations yourself |
| Account events mixed into the same payload | [Project webhooks](/docs/platform/webhooks/project-webhooks), configured once per project |
### Signature verification
360dialog authenticates webhooks with static custom headers, if you set them. Kapso signs every delivery: HMAC-SHA256 over the raw body, hex digest in `X-Webhook-Signature`, keyed with the `secret_key` you chose at registration.
```javascript theme={null}
const crypto = require('crypto');
// express.raw, not express.json: Kapso signs the exact bytes it sent, and
// re-serializing a parsed body does not reproduce them
app.post('/webhooks/whatsapp', express.raw({ type: 'application/json' }), (req, res) => {
const expected = crypto
.createHmac('sha256', process.env.WEBHOOK_SECRET)
.update(req.body)
.digest('hex');
const a = Buffer.from(expected, 'utf8');
const b = Buffer.from(req.headers['x-webhook-signature'] ?? '', 'utf8');
// timingSafeEqual throws on length mismatch, so compare lengths first
if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) {
return res.status(401).send('Invalid signature');
}
const event = JSON.parse(req.body);
res.sendStatus(200);
});
```
If you verified the partner-level `x-360dialog-signature`, the scheme is the same HMAC-SHA256 over the raw body. Only the header name and the secret change. See [Security](/docs/platform/webhooks/security) for Python and Ruby.
### Delivery behavior
| | 360dialog | Kapso |
| -------- | ----------------------------------------------- | ----------------------------------------------------------------- |
| Retries | Exponential backoff, documented as lasting days | 3 attempts in about 50 seconds: the first, then 10s and 40s later |
| Timeout | 5s | 10s |
| Dedup | None. Their docs recommend you build it | `X-Idempotency-Key` header |
| Scope | Per number, WABA fallback, 3 URLs max | Per phone number, plus project webhooks |
| Batching | None | Optional buffering on `whatsapp.message.received` |
See [Advanced](/docs/platform/webhooks/advanced) for buffering and ordering.
## Step 5: Templates
The main difference is how you tell the API which WABA you mean: 360dialog reads it from your API key, Kapso takes it in the path.
**360dialog:**
```bash theme={null}
curl -X POST https://waba-v2.360dialog.io/message_templates \
-H "D360-API-KEY: YOUR_CHANNEL_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "order_update",
"language": "en_US",
"category": "UTILITY",
"components": [
{
"type": "BODY",
"text": "Hi {{1}}, your order is confirmed.",
"example": { "body_text": [["John"]] }
}
]
}'
```
**Kapso:**
```bash theme={null}
curl -X POST https://api.kapso.ai/meta/whatsapp/v24.0/98765432109/message_templates \
-H "X-API-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "order_update",
"category": "UTILITY",
"language": "en_US",
"components": [
{
"type": "BODY",
"text": "Hi {{1}}, your order is confirmed.",
"example": { "body_text": [["John"]] }
}
]
}'
```
The lifecycle maps directly:
| 360dialog | Kapso |
| ---------------------------------------------- | --------------------------------------------------------------- |
| `GET /message_templates` | `GET /{waba_id}/message_templates` |
| `POST /message_templates` | `POST /{waba_id}/message_templates` |
| `GET /message_templates/{template_id}` | `GET /{waba_id}/message_templates/{template_id}` |
| `POST /message_templates/{template_id}` (edit) | `POST /{waba_id}/message_templates?hsm_id=...` |
| `DELETE /message_templates` (by name or ID) | `DELETE /{waba_id}/message_templates?name=...` or `?hsm_id=...` |
Templates belong to the WABA. If the number stayed on a WABA you already owned, they are still there and there is no re-review. If it landed on a new one, recreate them and wait for review.
## Multi-tenant setups
If you run 360dialog's Partner API, clients and channels become customers and phone numbers. Instead of the connect button or a direct signup link, each customer connects through a setup link:
```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 Corporation", "external_customer_id": "CUS-12345"}}'
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",
"meta_billing_mode": "partner_managed"
}
}'
```
Send your customer the returned `url`. They log in with Facebook and connect in about five minutes. Where 360dialog redirects back with `client` and `channels` query parameters, Kapso fires `whatsapp.phone_number.created` on your project webhook, with the `customer.id` and `phone_number_id`. See [Onboard customers](/docs/platform/customer-guide) and [Connection detection](/docs/platform/setup-links/detect-connection).
You create customers yourself, so there is no `client_created` event to wait for, and no per-channel API key generation. One API key covers every customer.
Partner webhook events map to [project webhooks](/docs/platform/webhooks/project-webhooks):
| 360dialog partner event | Kapso |
| ---------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- |
| `channel_created` / `channel_submitted` / `channel_ready` / `channel_live` / `channel_running` | `whatsapp.phone_number.created`, then `status` on the number |
| `phone_number_migrated` | `whatsapp.phone_number.created` |
| `waba_disabled_update` | `whatsapp.account.disabled` or `.reinstated`, by ban state |
| `waba_account_restriction` | `whatsapp.account.restricted` |
| `waba_account_violation` | `whatsapp.account.violation` |
| `waba_account_bsp_removed` | `whatsapp.phone_number.disconnected` |
| `waba_phone_number_removed` | `whatsapp.phone_number.deleted` |
| `account_offboarded` | `whatsapp.phone_number.offboarded` |
| `user_preferences` | `whatsapp.contact.marketing_preference_changed`, on the number's webhook, not the project webhook |
| `phone_number_quality_changed` | None. Poll `GET /platform/v1/whatsapp/phone_numbers` |
| `waba_template_status_changed` / `.category_changed` / `.quality_score_changed` | None. Poll `GET /{waba_id}/message_templates` |
| `business_verification_status_update` | None. Dashboard, or a raw Meta webhook |
| `cancellation_requested` / `.revoked` / `.processed` | None. No cancellation flow to track |
Partner balance and usage endpoints have no equivalent. Credits live in project settings, and message history is queryable per number.
## Billing
360dialog charges a per-number monthly license and prepays Meta from your conversation funds. Kapso runs on project credits, or direct Meta billing:
| | 360dialog | Kapso |
| ------------------ | ------------------------------------------------- | ---------------------------------------------------------------------------------------------------- |
| Who pays Meta | 360dialog, from prepaid conversation funds | Kapso from project credits, or your business directly |
| Per-number license | 49 to 249 EUR/month by plan, renews on the 1st | Numbers included by plan, then $5 to $10 per extra number. See [pricing](/docs/whatsapp/pricing-faq) |
| Marketing sends | 7% surcharge over Meta rates via `POST /messages` | Meta rates, no surcharge |
| Out of funds | Sends fail | Paid sends pause until credits are added, or Meta decides |
Choose the billing mode when the WABA is connected. See [Meta message billing](/docs/whatsapp/meta-message-billing) and [pricing](/docs/whatsapp/pricing-faq).
## Feature map
| Feature | 360dialog | Kapso |
| ----------------------- | --------------------------------------- | --------------------------------------------------------------------------------------------------------------------- |
| WhatsApp messaging | `POST /messages`, key-scoped | [`POST /{phone_number_id}/messages`](/docs/whatsapp/send-messages/text) |
| Interactive messages | Meta `interactive` object | [Same object](/docs/whatsapp/send-messages/buttons) |
| Templates | `/message_templates` | [`/{waba_id}/message_templates`](/docs/whatsapp/templates/simple-text) |
| WhatsApp Flows | Full Flows API | [Full Flows API](/docs/whatsapp/flows/overview), including hosted data endpoints |
| Bulk messaging | Your own loop | [Broadcasts](/docs/platform/broadcasts/overview), with CSV upload and scheduling |
| Conversation automation | None. API only | [Workflows](/docs/workflows/introduction) with AI steps and human handoff |
| Team inbox | None. API only | [Included](/docs/platform/inbox/overview), plus an [embeddable iframe](/docs/platform/inbox/embedded) |
| Contacts | Block list only | [Contacts](/docs/platform/whatsapp-data#contacts) with `metadata` |
| Message history | None. Stateless forwarding | [`/whatsapp/messages`](/docs/platform/whatsapp-data), `/whatsapp/conversations` |
| CTWA attribution | `referral` in the Meta payload | [Same, plus an Ads view](/docs/platform/whatsapp-data#referrals-ctwa) |
| WhatsApp calling | Calling API | [Supported](/docs/whatsapp/typescript-sdk/calls), with call logs |
| Business profile | `/whatsapp_business_profile` | [Display names](/docs/whatsapp/display-names), [usernames](/docs/whatsapp/business-usernames) |
| Health check | `GET /health_status` | `GET /platform/v1/whatsapp/phone_numbers/{phone_number_id}/health` |
| Multi-tenant onboarding | Partner API, connect button | [Setup links](/docs/platform/setup-links/create-and-configure) your customer completes |
| Sandbox | `START` to a shared number, `/v1` paths | [6-character code](/docs/how-to/whatsapp/use-sandbox-for-testing), production paths |
| Rate limits | Undocumented | [100-2,000 req/min by plan](/api/rate-limits) |
| SDKs | None | [TypeScript](/docs/whatsapp/typescript-sdk/introduction), [MCP server](/docs/whatsapp/mcp), [CLI](/docs/whatsapp/cli) |
Things Kapso adds:
* Automatic voice-note transcription on inbound audio
* [Functions](/docs/functions/overview) on Cloudflare Workers, as workflow steps, agent tools, or plain endpoints
* [Findings](/docs/platform/findings), which reads ended conversations with AI and groups the recurring problems
* Conversation lifecycle webhooks, so you can act when a conversation goes quiet or ends
## What does not map
* **Groups API, Payments API, RCS.** WhatsApp one-to-one messaging only.
* **Template archiving.** `POST /message_templates/archive` is a 360dialog feature, not Meta's. Delete or keep.
* **Cascading multi-webhooks.** Kapso webhooks deliver independently; there is no primary that gates the secondaries.
## Cutover
Migrate one number at a time. A number's WhatsApp registration and webhook routing move as a unit, so there is no gradual split per number.
| Phase | Actions |
| ------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Prep | Create the project and API key, build against a [sandbox](/docs/how-to/whatsapp/use-sandbox-for-testing) number, download any media you still need from 360dialog |
| Pilot | Move one low-traffic number end to end. Budget for the time the number is down, and for Meta's template review on a new WABA |
| Rollout | Migrate remaining numbers in batches; for multi-tenant, one customer at a time |
| Cutoff | Cancel the channel subscription in the 360dialog Hub, then request the refund of unused conversation funds |
The license bills until you cancel, and it renews on the 1st, so time the cancellation after the number is confirmed sending on Kapso.
360dialog stores no message history to export, but uploaded media expires 30 days after last use and webhook media after 7 days. Download anything you still reference by media ID before the old keys die.
## Troubleshooting
| Symptom | 360dialog equivalent | Fix |
| -------------------------------------- | -------------------------- | -------------------------------------------------------------------------------------------------------------------------------------- |
| `401 Unauthorized` | `D360-API-KEY` rejected | Send `X-API-Key: YOUR_API_KEY` |
| `404` on send | `POST /messages` | The sender moved into the path: `POST /{phone_number_id}/messages` |
| Media download fails | Lookaside hostname rewrite | Fetch `download_url` from `GET /{media_id}?phone_number_id=...`, or use `message.kapso.media_url` from the webhook |
| Number cannot be connected | None | Two-step verification is still on, or the number is still on the old WABA. See [Step 1](#step-1-connect-your-number) |
| `(#131047)` re-engagement message | Same Meta error | Outside the 24-hour window. Send an approved template |
| Template not found | Same Meta error | Templates are addressed by `name` + `language`, and must exist and be approved on this WABA |
| Templates missing after the move | None | They belonged to the old WABA. Recreate them on the new one and wait for review |
| Webhook signature mismatch | No signature before | HMAC-SHA256 over the raw body, checked against `X-Webhook-Signature` |
| Webhook missed while endpoint was down | Days of redelivery | 3 attempts in about 50 seconds. Return `200` fast, process from a queue, backfill from [message history](/docs/platform/whatsapp-data) |
| Marketing template refused with `422` | None | The contact stopped marketing on that number. Check [marketing preferences](/docs/whatsapp/templates/marketing-opt-outs) |
| `429` | Undocumented | Back off on `Retry-After`; see [rate limits](/api/rate-limits) |
## Node.js example
The example keeps a raw Meta parser via a `kind: "meta"` webhook, which is the shortest path off 360dialog: the send function changes two lines, and the handler gains signature verification.
```javascript theme={null}
const crypto = require('crypto');
const express = require('express');
const app = express();
// Any durable queue works. 360dialog retried webhooks for days; Kapso
// stops after about 50 seconds, so the queue is what absorbs downtime.
const queue = require('./queue');
const KAPSO = 'https://api.kapso.ai/meta/whatsapp/v24.0';
const headers = {
'X-API-Key': process.env.KAPSO_API_KEY, // was: D360-API-KEY per number
'Content-Type': 'application/json',
};
// 360dialog: POST https://waba-v2.360dialog.io/messages, sender implied by key.
// Kapso: same body, sender in the path.
async function sendText({ phoneNumberId, to, text }) {
const res = await fetch(`${KAPSO}/${phoneNumberId}/messages`, {
method: 'POST',
headers,
body: JSON.stringify({
messaging_product: 'whatsapp',
to,
type: 'text',
text: { body: text },
}),
});
const body = await res.json();
if (!res.ok) {
throw new Error(`Send failed (${res.status}): ${JSON.stringify(body.error ?? body)}`);
}
return body.messages[0].id; // wamid, same as before
}
// kind: "meta" webhook. The payload is Meta's envelope, exactly what
// 360dialog forwarded, so the parsing below is your existing code.
app.post('/webhooks/whatsapp', express.raw({ type: 'application/json' }), async (req, res) => {
// New: 360dialog sent no signature. Kapso signs the raw body.
const expected = crypto
.createHmac('sha256', process.env.WEBHOOK_SECRET)
.update(req.body)
.digest('hex');
const a = Buffer.from(expected, 'utf8');
const b = Buffer.from(req.headers['x-webhook-signature'] ?? '', 'utf8');
if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) {
return res.status(401).send('Invalid signature');
}
const payload = JSON.parse(req.body);
const idempotencyKey = req.headers['x-idempotency-key'];
for (const entry of payload.entry ?? []) {
for (const change of entry.changes ?? []) {
const value = change.value ?? {};
for (const message of value.messages ?? []) {
await queue.add('inbound', {
idempotencyKey,
// `from` can be absent on a BSUID-only contact, so carry the
// scoped IDs too or the job cannot tell you who wrote in.
from: message.from ?? null,
fromUserId: message.from_user_id ?? null,
fromParentUserId: message.from_parent_user_id ?? null,
username: message.username ?? null,
text: message.text?.body,
phoneNumberId: value.metadata?.phone_number_id,
});
}
for (const status of value.statuses ?? []) {
await queue.add('status', {
idempotencyKey,
id: status.id,
status: status.status,
errors: status.errors ?? [],
});
}
}
}
res.sendStatus(200);
});
```
## Need help
* [Send messages](/docs/whatsapp/send-messages/text) and [webhooks](/docs/platform/webhooks/overview)
* [API reference](/api/introduction)
* [WhatsApp support](https://wa.me/16266694464?text=Hi!%20I%20need%20help%20migrating%20from%20360dialog)
# Migrate from another provider
Source: https://docs.kapso.ai/docs/migrate/from-another-provider
What to move, what to rebuild, and how to cut over
Move your WhatsApp number to Kapso from any Business Solution Provider. Are you on Twilio, YCloud, or 360dialog? Use the [Twilio guide](/docs/migrate/from-twilio), the [YCloud guide](/docs/migrate/from-ycloud), or the [360dialog guide](/docs/migrate/from-360dialog) instead.
## Step 1: Connect your number
1. In [WhatsApp Manager](https://business.facebook.com/latest/whatsapp_manager/phone_numbers), check the WABA for other numbers, templates, or assets you still need.
2. Remove the phone number from the WABA.
3. In [Meta Business Settings](https://business.facebook.com/latest/settings/whatsapp_account), open **Accounts → WhatsApp accounts**.
4. If the old WABA sits in your Business Portfolio, remove it.
5. Wait about five minutes.
6. In Kapso, start embedded signup. Create a new WABA when the flow asks.
7. Recreate your templates on the new WABA (see [Step 5](#step-5-templates)). Wait for Meta review.
The number stops sending after step 2, and it can send templates again once Meta approves them on the new WABA. Plan the migration accordingly.
See [Connect WhatsApp](/docs/how-to/whatsapp/connect-whatsapp) for the signup flow.
If Meta blocks the WABA removal over a pending balance, your old provider may still have a credit line attached to it. Meta Direct Support has to clear that. A different number on a new WABA unblocks you in the meantime.
If the reconnect fails, see [coexistence troubleshooting](/docs/how-to/whatsapp/coexistence-troubleshooting).
You can also start on a fresh number. [Instant setup](/docs/platform/phone-numbers/instant-setup) gives you a pre-verified US number, with no SMS verification step.
**Testing first?** Build against a [sandbox](/docs/how-to/whatsapp/use-sandbox-for-testing) number while your current provider still carries production traffic.
## Step 2: Get your phone number IDs
```bash theme={null}
curl https://api.kapso.ai/platform/v1/whatsapp/phone_numbers \
-H "X-API-Key: YOUR_API_KEY"
```
```json theme={null}
{
"data": [
{
"id": "1234567890",
"phone_number_id": "1234567890",
"name": "Support Line",
"business_account_id": "98765432109",
"display_phone_number": "+1 555-123-4567",
"quality_rating": "GREEN",
"throughput_tier": "TIER_10K",
"status": "CONNECTED"
}
]
}
```
Two IDs address different things:
| ID | Scopes to | Use it for |
| --------------------- | ------------------------------ | ------------------------------------------------------- |
| `phone_number_id` | one phone number | sending messages, uploading media, registering webhooks |
| `business_account_id` | the WABA that owns the numbers | templates |
Templates belong to the WABA, not to a number, so every number on that WABA can send them. That is why the template endpoints take the WABA ID in the path, as in `POST /{waba_id}/message_templates`.
## Step 3: Update message sending
For every message type, Kapso uses one endpoint: `POST /{phone_number_id}/messages`. The `type` field says which kind of message you are sending, and a field of that same name carries the content. A text message sets `type: "text"` and puts the body in `text`.
Some providers proxy Meta's Cloud API. If yours did, your payloads already have this shape, and you only change the base URL and the auth header. If your provider had its own message API, use the tabs below as the target shapes.
Free-form messages, inside the 24-hour window:
```bash theme={null}
curl -X POST https://api.kapso.ai/meta/whatsapp/v24.0/1234567890/messages \
-H "X-API-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"messaging_product": "whatsapp",
"to": "15551234567",
"type": "text",
"text": { "body": "Your order shipped!" }
}'
```
To reply to a specific message, add `"context": { "message_id": "wamid..." }`. To address a contact that has no phone number, use `recipient` with a [business-scoped user ID](/docs/whatsapp/business-scoped-user-ids) instead of `to`. See [Send text](/docs/whatsapp/send-messages/text).
Business-initiated messages, outside the 24-hour window. Anything that starts or reopens a conversation has to be an approved template:
```bash theme={null}
curl -X POST https://api.kapso.ai/meta/whatsapp/v24.0/1234567890/messages \
-H "X-API-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"messaging_product": "whatsapp",
"to": "15551234567",
"type": "template",
"template": {
"name": "order_update",
"language": { "code": "en_US" },
"components": [
{
"type": "body",
"parameters": [{ "type": "text", "text": "John" }]
}
]
}
}'
```
Parameters are positional and must match the placeholder order. Named parameters work too. See [Simple text templates](/docs/whatsapp/templates/simple-text).
Media is a message type, not a parameter on a text message:
```bash theme={null}
curl -X POST https://api.kapso.ai/meta/whatsapp/v24.0/1234567890/messages \
-H "X-API-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"messaging_product": "whatsapp",
"to": "15551234567",
"type": "image",
"image": {
"link": "https://example.com/receipt.png",
"caption": "Your receipt"
}
}'
```
`type` is `image`, `video`, `audio`, `document`, or `sticker`. Documents take a `filename`. Public URLs work directly. To upload bytes first, use `POST /{phone_number_id}/media` and pass the returned `id` instead of `link`. Kapso also ingests from a URL and hands back a Meta media ID via `POST /platform/v1/whatsapp/media`.
Buttons, lists, and CTAs all use Meta's `interactive` object:
```bash theme={null}
curl -X POST https://api.kapso.ai/meta/whatsapp/v24.0/1234567890/messages \
-H "X-API-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"messaging_product": "whatsapp",
"to": "15551234567",
"type": "interactive",
"interactive": {
"type": "button",
"body": { "text": "Confirm your appointment?" },
"action": {
"buttons": [
{ "type": "reply", "reply": { "id": "btn_yes", "title": "Yes" } },
{ "type": "reply", "reply": { "id": "btn_no", "title": "No" } }
]
}
}
}'
```
Taps arrive on `whatsapp.message.received` as an `interactive` message with `button_reply.id`. Swap `interactive.type` for `list` or `cta_url`. See [Send buttons](/docs/whatsapp/send-messages/buttons) and [Send lists](/docs/whatsapp/send-messages/lists).
Locations take numbers, not strings:
```json theme={null}
{
"messaging_product": "whatsapp",
"to": "15551234567",
"type": "location",
"location": {
"latitude": 37.7749,
"longitude": -122.4194,
"name": "San Francisco Office",
"address": "123 Market St"
}
}
```
Reactions point at the message they react to:
```json theme={null}
{
"messaging_product": "whatsapp",
"to": "15551234567",
"type": "reaction",
"reaction": { "message_id": "wamid...", "emoji": "👍" }
}
```
See [Send location](/docs/whatsapp/send-messages/location) and [Send reaction](/docs/whatsapp/send-messages/reaction).
Marking read and showing a typing indicator go to the same endpoint, without a `to`:
```bash theme={null}
curl -X POST https://api.kapso.ai/meta/whatsapp/v24.0/1234567890/messages \
-H "X-API-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"messaging_product": "whatsapp",
"status": "read",
"message_id": "wamid.HBgNMTU1NTE0OTU5Nzg1...",
"typing_indicator": { "type": "text" }
}'
```
The indicator clears when you send a message, or after about 25 seconds. See [Mark as read](/docs/whatsapp/send-messages/mark-read).
The [TypeScript SDK](/docs/whatsapp/typescript-sdk/introduction) wraps all of this, so you do not have to write the envelopes by hand.
## Step 4: Update webhooks
Register per phone number:
```bash theme={null}
curl -X POST https://api.kapso.ai/platform/v1/whatsapp/phone_numbers/1234567890/webhooks \
-H "X-API-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"whatsapp_webhook": {
"url": "https://yourapp.com/webhooks/whatsapp",
"events": [
"whatsapp.message.received",
"whatsapp.message.sent",
"whatsapp.message.delivered",
"whatsapp.message.read",
"whatsapp.message.failed"
],
"secret_key": "your-signing-secret"
}
}'
```
Payloads carry `message`, `conversation`, and `phone_number_id`, with Kapso's own fields under `message.kapso`. The event name arrives in the `X-Webhook-Event` header. Full shapes in [Message events](/docs/platform/webhooks/message-events).
`conversation` threads the messages for you, but it does not track the 24-hour window. A send can fail with `131047` while `conversation.status` is `active`. Read `conversation.kapso.last_inbound_at`, or handle the rejection and fall back to a template.
`from` is not always present. WhatsApp can identify a contact with `business_scoped_user_id` instead. See [business-scoped user IDs](/docs/whatsapp/business-scoped-user-ids).
For connection lifecycle events (a customer finishing a setup link, Meta disabling a WABA), use [project webhooks](/docs/platform/webhooks/project-webhooks) instead. You configure those once for the whole project.
Does your current provider forward raw Meta payloads? Register the webhook with `"kind": "meta"` and Kapso forwards Meta's exact payload with no reshaping. Your existing parser keeps working.
Kapso signs the raw request body with HMAC-SHA256 and sends the hex digest in `X-Webhook-Signature`:
```javascript theme={null}
const crypto = require('crypto');
// express.raw, not express.json: Kapso signs the exact bytes it sent, and
// re-serializing a parsed body does not reproduce them
app.post('/webhooks/whatsapp', express.raw({ type: 'application/json' }), (req, res) => {
const expected = crypto
.createHmac('sha256', process.env.WEBHOOK_SECRET)
.update(req.body)
.digest('hex');
const a = Buffer.from(expected, 'utf8');
const b = Buffer.from(req.headers['x-webhook-signature'] ?? '', 'utf8');
// timingSafeEqual throws on length mismatch, so compare lengths first
if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) {
return res.status(401).send('Invalid signature');
}
const event = JSON.parse(req.body);
res.sendStatus(200);
});
```
Delivery is at-least-once, with three attempts in about 50 seconds: the first, then retries at 10s and 40s. Dedupe on `X-Idempotency-Key`. See [Security](/docs/platform/webhooks/security) for Python and Ruby, and [Advanced](/docs/platform/webhooks/advanced) for buffering and ordering.
## Step 5: Templates
Templates belong to the WABA at Meta, not to your provider. If you build templates in WhatsApp Manager, pull them into Kapso from **WhatsApp → Templates → Sync from WhatsApp**.
The path takes the WABA ID, not the phone number ID:
```bash theme={null}
curl -X POST https://api.kapso.ai/meta/whatsapp/v24.0/98765432109/message_templates \
-H "X-API-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "order_update",
"category": "UTILITY",
"language": "en_US",
"components": [
{
"type": "BODY",
"text": "Hi {{1}}, your order is confirmed.",
"example": { "body_text": [["John"]] }
}
]
}'
```
Meta reviews them, usually within 24 hours. See [Template lifecycle](/docs/whatsapp/templates/lifecycle).
## Multi-tenant setups
If you message on behalf of your own customers, each one becomes a Kapso customer that connects its own number through a setup link:
```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 Corporation", "external_customer_id": "CUS-12345"}}'
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",
"meta_billing_mode": "partner_managed"
}
}'
```
Send your customer the returned `url`. They log in with Facebook and connect in about five minutes. You then get `whatsapp.phone_number.created` on your project webhook, with the `customer.id` and `phone_number_id`. See [Onboard customers](/docs/platform/customer-guide).
One API key covers every customer. Migrate one customer at a time.
## Cutover
Migrate one number at a time. A number's WhatsApp registration and webhook routing move as a unit, so there is no gradual split per number.
| Phase | Actions |
| ------- | -------------------------------------------------------------------------------------------------------------------------------------------------- |
| Prep | Create the project and API key, build against a [sandbox](/docs/how-to/whatsapp/use-sandbox-for-testing) number, export history from your provider |
| Pilot | Move one low-traffic number end to end. Budget for the time the number is down, and for Meta's template review |
| Rollout | Migrate remaining numbers in batches; for multi-tenant, one customer at a time |
| Cutoff | Stop sends on the old provider for each migrated number, then close the account |
Meta bills message charges against the WABA either way, per delivered template message. See [pricing](/docs/whatsapp/pricing-faq).
## Troubleshooting
| Symptom | Fix |
| ------------------------------------- | --------------------------------------------------------------------------------------------------------------------------- |
| Number cannot be connected | It is still on the provider's WABA. Migrate it in WhatsApp Manager, and ask the provider to release it if they hold the PIN |
| `401 Unauthorized` | Send `X-API-Key: YOUR_API_KEY` |
| `(#131047)` re-engagement message | Outside the 24-hour window. Send an approved template |
| Template not found | Templates are addressed by `name` + `language`, and must exist on this WABA. Sync or recreate them |
| Templates missing after the move | They belonged to the old WABA. Recreate them on the new one and wait for review |
| Webhook signature mismatch | HMAC-SHA256 over the raw body, checked against `X-Webhook-Signature` |
| Marketing template refused with `422` | The contact stopped marketing on that number. See [marketing opt-outs](/docs/whatsapp/templates/marketing-opt-outs) |
| `429` | Back off on `Retry-After`; see [rate limits](/api/rate-limits) |
## Need help
* [Send messages](/docs/whatsapp/send-messages/text) and [webhooks](/docs/platform/webhooks/overview)
* [API reference](/api/introduction)
* [WhatsApp support](https://wa.me/16266694464?text=Hi!%20I%20need%20help%20migrating%20to%20Kapso)
# Migrate from Twilio
Source: https://docs.kapso.ai/docs/migrate/from-twilio
Migrate your WhatsApp numbers from Twilio to Kapso.
## Step 1: Connect your number
1. In [WhatsApp Manager](https://business.facebook.com/latest/whatsapp_manager/phone_numbers), check the WABA for other numbers, templates, or assets you still need.
2. Remove the phone number from the WABA.
3. In [Meta Business Settings](https://business.facebook.com/latest/settings/whatsapp_account), open **Accounts → WhatsApp accounts**.
4. If the old WABA sits in your Business Portfolio, remove it.
5. Wait about five minutes.
6. In Kapso, start embedded signup. Create a new WABA when the flow asks.
7. Recreate your templates on the new WABA (see [Step 5](#step-5-templates)). Wait for Meta review.
The number stops sending after step 2, and it can send templates again once Meta approves them on the new WABA. Plan the migration accordingly.
See [Connect WhatsApp](/docs/how-to/whatsapp/connect-whatsapp) for the signup flow.
If Meta blocks the WABA removal over a pending balance, your old provider may still have a credit line attached to it. Meta Direct Support has to clear that. A different number on a new WABA unblocks you in the meantime.
If the reconnect fails, see [coexistence troubleshooting](/docs/how-to/whatsapp/coexistence-troubleshooting).
You can also start on a fresh number. [Instant setup](/docs/platform/phone-numbers/instant-setup) gives you a pre-verified US number, with no SMS verification step.
**Testing first?** The [sandbox](/docs/how-to/whatsapp/use-sandbox-for-testing) replaces Twilio's `join ` flow. Create a session for your test phone, then send the 6-character code from WhatsApp. You can then send and receive without a production number.
## Step 2: Get your phone number IDs
```bash theme={null}
curl https://api.kapso.ai/platform/v1/whatsapp/phone_numbers \
-H "X-API-Key: YOUR_API_KEY"
```
```json theme={null}
{
"data": [
{
"id": "1234567890",
"phone_number_id": "1234567890",
"name": "Support Line",
"business_account_id": "98765432109",
"display_phone_number": "+1 555-123-4567",
"quality_rating": "GREEN",
"throughput_tier": "TIER_10K",
"status": "CONNECTED"
}
]
}
```
Store the mapping from `whatsapp:+E164` to `phone_number_id`.
Two IDs address different things:
| ID | Scopes to | Use it for |
| --------------------- | ------------------------------ | ------------------------------------------------------- |
| `phone_number_id` | one phone number | sending messages, uploading media, registering webhooks |
| `business_account_id` | the WABA that owns the numbers | templates |
Templates belong to the WABA, not to a number, so every number on that WABA can send them. That is why the template endpoints take the WABA ID in the path, as in `POST /{waba_id}/message_templates`.
## Step 3: Update message sending
For every message type, Kapso uses one endpoint: `POST /{phone_number_id}/messages`. The `type` field says which kind of message you are sending, and a field of that same name carries the content. A text message sets `type: "text"` and puts the body in `text`.
Business-initiated sends, outside the 24-hour window. `ContentSid` becomes the template name, `ContentVariables` becomes Meta's `components` array.
**Twilio:**
```bash theme={null}
curl -X POST https://api.twilio.com/2010-04-01/Accounts/ACxxxx/Messages.json \
-u ACxxxx:auth_token \
-d "To=whatsapp:+15551234567" \
-d "From=whatsapp:+15557654321" \
-d "ContentSid=HXxxxxxxxxxxxx" \
-d 'ContentVariables={"1": "John", "2": "ORD-123"}'
```
**Kapso:**
```bash theme={null}
curl -X POST https://api.kapso.ai/meta/whatsapp/v24.0/1234567890/messages \
-H "X-API-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"messaging_product": "whatsapp",
"to": "15551234567",
"type": "template",
"template": {
"name": "order_update",
"language": { "code": "en_US" },
"components": [
{
"type": "body",
"parameters": [
{ "type": "text", "text": "John" },
{ "type": "text", "text": "ORD-123" }
]
}
]
}
}'
```
Parameters are positional and must match the placeholder order. Named parameters work too. See [Simple text templates](/docs/whatsapp/templates/simple-text).
Free-form replies, inside the 24-hour window.
**Twilio:**
```bash theme={null}
curl -X POST https://api.twilio.com/2010-04-01/Accounts/ACxxxx/Messages.json \
-u ACxxxx:auth_token \
-d "To=whatsapp:+15551234567" \
-d "From=whatsapp:+15557654321" \
-d "Body=Your order shipped!"
```
**Kapso:**
```bash theme={null}
curl -X POST https://api.kapso.ai/meta/whatsapp/v24.0/1234567890/messages \
-H "X-API-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"messaging_product": "whatsapp",
"to": "15551234567",
"type": "text",
"text": { "body": "Your order shipped!" }
}'
```
To reply to a specific message, add `"context": { "message_id": "wamid..." }`. To address a contact that has no phone number, use `recipient` with a [business-scoped user ID](/docs/whatsapp/business-scoped-user-ids) instead of `to`.
`MediaUrl` becomes a typed object. Media is a message type, not a parameter on a text message.
**Twilio:**
```bash theme={null}
curl -X POST https://api.twilio.com/2010-04-01/Accounts/ACxxxx/Messages.json \
-u ACxxxx:auth_token \
-d "To=whatsapp:+15551234567" \
-d "From=whatsapp:+15557654321" \
-d "Body=Here's your receipt" \
-d "MediaUrl=https://example.com/receipt.png"
```
**Kapso:**
```bash theme={null}
curl -X POST https://api.kapso.ai/meta/whatsapp/v24.0/1234567890/messages \
-H "X-API-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"messaging_product": "whatsapp",
"to": "15551234567",
"type": "image",
"image": {
"link": "https://example.com/receipt.png",
"caption": "Here'\''s your receipt"
}
}'
```
`type` is `image`, `video`, `audio`, `document`, or `sticker`. Documents take a `filename`. Public URLs work directly. To upload bytes first, use `POST /{phone_number_id}/media` and pass the returned `id` instead of `link`. Kapso also ingests from a URL and hands back a Meta media ID via `POST /platform/v1/whatsapp/media`.
Twilio expresses rich messages as Content API types. Kapso takes Meta's `interactive` object inline.
| Twilio content type | Kapso |
| ------------------------- | ------------------------------------------------------ |
| `twilio/text` | `type: "text"` |
| `twilio/media` | `type: "image"` / `video` / `document` |
| `twilio/quick-reply` | `interactive.type: "button"` |
| `twilio/list-picker` | `interactive.type: "list"` |
| `twilio/call-to-action` | `interactive.type: "cta_url"` |
| `twilio/card` | Media header + buttons, or a template |
| `twilio/carousel` | [Carousel template](/docs/whatsapp/templates/carousel) |
| `twilio/location` | `type: "location"` |
| `whatsapp/authentication` | `AUTHENTICATION` template |
| `whatsapp/flows` | `interactive.type: "flow"` |
Quick replies:
```bash theme={null}
curl -X POST https://api.kapso.ai/meta/whatsapp/v24.0/1234567890/messages \
-H "X-API-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"messaging_product": "whatsapp",
"to": "15551234567",
"type": "interactive",
"interactive": {
"type": "button",
"body": { "text": "Confirm your appointment?" },
"action": {
"buttons": [
{ "type": "reply", "reply": { "id": "btn_yes", "title": "Yes" } },
{ "type": "reply", "reply": { "id": "btn_no", "title": "No" } }
]
}
}
}'
```
Taps arrive on `whatsapp.message.received` as an `interactive` message with `button_reply.id`, Twilio's `ButtonPayload`. Full shapes in [Send buttons](/docs/whatsapp/send-messages/buttons) and [Send lists](/docs/whatsapp/send-messages/lists).
Twilio has no equivalent. Kapso passes Meta's through, on the same endpoint:
```bash theme={null}
curl -X POST https://api.kapso.ai/meta/whatsapp/v24.0/1234567890/messages \
-H "X-API-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"messaging_product": "whatsapp",
"status": "read",
"message_id": "wamid.HBgNMTU1NTE0OTU5Nzg1...",
"typing_indicator": { "type": "text" }
}'
```
Reactions are `type: "reaction"`. See [Mark as read](/docs/whatsapp/send-messages/mark-read) and [Send reaction](/docs/whatsapp/send-messages/reaction).
The [TypeScript SDK](/docs/whatsapp/typescript-sdk/introduction) wraps all of this, so you do not have to write the envelopes by hand.
## Step 4: Update webhooks
Kapso posts JSON and expects a `200`. There is no TwiML, so auto-replies move into your handler or into a [workflow](/docs/workflows/introduction).
Register per phone number:
```bash theme={null}
curl -X POST https://api.kapso.ai/platform/v1/whatsapp/phone_numbers/1234567890/webhooks \
-H "X-API-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"whatsapp_webhook": {
"url": "https://yourapp.com/webhooks/whatsapp",
"events": [
"whatsapp.message.received",
"whatsapp.message.sent",
"whatsapp.message.delivered",
"whatsapp.message.read",
"whatsapp.message.failed"
],
"secret_key": "your-signing-secret"
}
}'
```
For connection lifecycle events (a customer finishing a setup link, Meta disabling a WABA), use [project webhooks](/docs/platform/webhooks/project-webhooks) instead. You configure those once for the whole project.
### Field mapping
| Twilio param | Kapso (`whatsapp.message.received`) |
| ---------------------------------- | ---------------------------------------------------- |
| `MessageSid` | `message.id` |
| `From` (`whatsapp:+1...`) / `WaId` | `message.from` |
| `To` | `phone_number_id` |
| `ProfileName` | `conversation.contact_name` |
| `Body` | `message.text.body` |
| `NumMedia` / `MediaUrl0` | `message.kapso.has_media`, `message.kapso.media_url` |
| `ButtonText` / `ButtonPayload` | `message.interactive.button_reply.title` / `.id` |
| `Latitude` / `Longitude` | `message.location` |
| `MessageStatus` | The event name, plus `message.kapso.statuses` |
`conversation` threads the messages for you, but it does not track the 24-hour window. A send can fail with `131047` while `conversation.status` is `active`. Read `conversation.kapso.last_inbound_at`, or handle the rejection and fall back to a template.
`from` is not always present. WhatsApp can identify a contact with `business_scoped_user_id` instead. See [business-scoped user IDs](/docs/whatsapp/business-scoped-user-ids).
### Signature verification
Twilio signs the request URL plus every form field, so verification needs all of them. Twilio's helper assembles them for you. Kapso signs only the request body. Verification is one HMAC over the bytes you received.
**Twilio:**
```javascript theme={null}
const twilio = require('twilio');
app.post('/webhooks/whatsapp', express.urlencoded({ extended: false }), (req, res) => {
const valid = twilio.validateRequest(
process.env.TWILIO_AUTH_TOKEN,
req.headers['x-twilio-signature'],
'https://yourapp.com/webhooks/whatsapp', // must match exactly
req.body // every param, sorted for you
);
if (!valid) return res.status(403).send('Invalid signature');
res.type('text/xml').send('');
});
```
**Kapso:**
```javascript theme={null}
const crypto = require('crypto');
// express.raw, not express.json: Kapso signs the exact bytes it sent, and
// re-serializing a parsed body does not reproduce them
app.post('/webhooks/whatsapp', express.raw({ type: 'application/json' }), (req, res) => {
const expected = crypto
.createHmac('sha256', process.env.WEBHOOK_SECRET)
.update(req.body)
.digest('hex');
const a = Buffer.from(expected, 'utf8');
const b = Buffer.from(req.headers['x-webhook-signature'] ?? '', 'utf8');
// timingSafeEqual throws on length mismatch, so compare lengths first
if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) {
return res.status(401).send('Invalid signature');
}
const event = JSON.parse(req.body);
res.sendStatus(200);
});
```
No URL goes into the hash, so your endpoint can move or sit behind a proxy without breaking verification.
### Delivery behavior
| | Twilio | Kapso |
| -------- | --------------- | ----------------------------------------------------------------- |
| Retries | None by default | 3 attempts in about 50 seconds: the first, then 10s and 40s later |
| Timeout | 15s | 10s |
| Dedup | `MessageSid` | `X-Idempotency-Key` header |
| Batching | None | Optional buffering on `whatsapp.message.received` |
Delivery is at-least-once. Dedupe on `X-Idempotency-Key`. See [Advanced](/docs/platform/webhooks/advanced) for buffering and ordering, and [Security](/docs/platform/webhooks/security) for verification in Python and Ruby.
Do you parse raw Meta payloads elsewhere? Register the webhook with `"kind": "meta"` and Kapso forwards Meta's exact payload with no reshaping.
## Step 5: Templates
Twilio needs two calls: create the content, then request WhatsApp approval. Kapso submits on create.
**Twilio:**
```bash theme={null}
curl -X POST https://content.twilio.com/v1/Content \
-u ACxxxx:auth_token \
-H "Content-Type: application/json" \
-d '{
"friendly_name": "order_update",
"language": "en",
"variables": {"1": "John"},
"types": {"twilio/text": {"body": "Hi {{1}}, your order is confirmed."}}
}'
curl -X POST https://content.twilio.com/v1/Content/HXxxxx/ApprovalRequests/whatsapp \
-u ACxxxx:auth_token \
-d '{"name": "order_update", "category": "UTILITY"}'
```
**Kapso:**
```bash theme={null}
curl -X POST https://api.kapso.ai/meta/whatsapp/v24.0/98765432109/message_templates \
-H "X-API-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "order_update",
"category": "UTILITY",
"language": "en_US",
"components": [
{
"type": "BODY",
"text": "Hi {{1}}, your order is confirmed.",
"example": { "body_text": [["John"]] }
}
]
}'
```
The path takes the WABA ID, not the phone number ID.
If you build templates in WhatsApp Manager, pull them into Kapso from **WhatsApp → Templates → Sync from WhatsApp**. See [Template lifecycle](/docs/whatsapp/templates/lifecycle).
## Multi-tenant setups
Twilio subaccounts become customers, and each customer connects their own number through a setup link instead of you provisioning a sender per subaccount:
```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 Corporation", "external_customer_id": "CUS-12345"}}'
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",
"meta_billing_mode": "partner_managed"
}
}'
```
Send your customer the returned `url`. They log in with Facebook and connect in about five minutes. You then get `whatsapp.phone_number.created` on your project webhook, with the `customer.id` and `phone_number_id`. See [Onboard customers](/docs/platform/customer-guide) and [Connection detection](/docs/platform/setup-links/detect-connection).
One API key covers every customer, so there is no per-subaccount credential juggling.
## Bulk sends
If you loop over recipients yourself, or drive bulk sends from Studio, use [broadcasts](/docs/platform/broadcasts/api) instead. Create, add recipients with per-recipient template parameters, send, poll:
```bash theme={null}
POST /platform/v1/whatsapp/broadcasts
POST /platform/v1/whatsapp/broadcasts/{id}/recipients
POST /platform/v1/whatsapp/broadcasts/{id}/send
GET /platform/v1/whatsapp/broadcasts/{id}
```
Kapso paces broadcasts internally to stay inside Meta's throughput limits, so you do not throttle them yourself. `POST /{id}/schedule` sends later, `POST /{id}/cancel` stops one in flight.
## Keep your Twilio account for numbers
You do not have to leave Twilio to leave Twilio's WhatsApp API. Point Kapso at your Twilio credentials and it provisions new numbers into your Twilio account, billed to you. You choose the countries, and each one can keep a pool of reusable pre-verified numbers. This covers provisioning, not the numbers your Twilio WhatsApp senders already use. Those still follow [Step 1](#step-1-connect-your-number).
This is an Enterprise feature, or a paid add-on on other plans. See [Provide local numbers](/docs/platform/phone-numbers/provide-local-numbers).
## Feature map
| Feature | Twilio | Kapso |
| ----------------------- | -------------------------------- | --------------------------------------------------------------------------------------------------------------- |
| WhatsApp messaging | Messages + Content API | [Meta-shaped API](/docs/whatsapp/send-messages/text) |
| Interactive messages | Content types | [Buttons](/docs/whatsapp/send-messages/buttons), [lists](/docs/whatsapp/send-messages/lists), CTAs |
| Templates | Content API + approval requests | [`/{waba_id}/message_templates`](/docs/whatsapp/templates/simple-text) |
| WhatsApp Flows | `whatsapp/flows` content type | [Full Flows API](/docs/whatsapp/flows/overview), including hosted data endpoints |
| Bulk messaging | Your own loop | [Broadcasts](/docs/platform/broadcasts/overview), with CSV upload and scheduling |
| Conversation automation | Studio | [Workflows](/docs/workflows/introduction) with AI steps and human handoff |
| Team inbox | Flex, a separate contract | [Included](/docs/platform/inbox/overview), plus an [embeddable iframe](/docs/platform/inbox/embedded) |
| Multi-tenant onboarding | Subaccounts you provision | [Setup links](/docs/platform/setup-links/create-and-configure) your customer completes |
| Message history | Messages resource | [`/whatsapp/messages`](/docs/platform/whatsapp-data), `/whatsapp/conversations` |
| Marketing opt-outs | Advanced Opt-Out keywords | [Automatic](/docs/whatsapp/templates/marketing-opt-outs), from WhatsApp's native control |
| WhatsApp calling | Supported | [Supported](/docs/whatsapp/typescript-sdk/calls), with call logs |
| Business profile | API | [Display names](/docs/whatsapp/display-names), [usernames](/docs/whatsapp/business-usernames) |
| Sandbox | `join ` on a shared number | [6-character code](/docs/how-to/whatsapp/use-sandbox-for-testing) |
| Serverless code | Twilio Functions | [Functions](/docs/functions/overview) on Cloudflare Workers, as workflow steps, agent tools, or plain endpoints |
| Rate limits | Per-number throughput | [100-2,000 req/min by plan](/api/rate-limits) |
Things Kapso adds:
* An inbox for your team
* [CTWA attribution](/docs/platform/whatsapp-data) and [contact properties](/docs/platform/inbox/overview)
* Automatic voice-note transcription
* An [MCP server](/docs/whatsapp/mcp) and a [CLI](/docs/whatsapp/cli)
* [Findings](/docs/platform/findings), which reads ended conversations with AI and groups the recurring problems
## What does not map
* **Messaging Services.** No sender pools, sticky sender, or geomatch. You send from a specific `phone_number_id`.
* **TwiML.** Auto-replies move into your webhook handler or a [workflow](/docs/workflows/introduction).
* **Link shortening and click tracking.**
* **Conversation tags.** Organize in the inbox UI, not the API.
## Cutover
Migrate one number at a time. A number's WhatsApp registration and webhook routing move as a unit, so there is no gradual split per number.
| Phase | Actions |
| ------- | --------------------------------------------------------------------------------------------------------------------------------------------------- |
| Prep | Create the project and API key, build against a [sandbox](/docs/how-to/whatsapp/use-sandbox-for-testing) number, export message history from Twilio |
| Pilot | Move one low-traffic number end to end. Budget for the time the number is down, and for Meta's template review |
| Rollout | Migrate remaining numbers in batches; for multi-tenant, one customer at a time |
| Cutoff | Stop Twilio sends for each migrated number, then release the sender in Twilio |
One thing needs no migration. Meta bills message charges against the WABA on either platform, per delivered template message, at category and market rates. See [pricing](/docs/whatsapp/pricing-faq).
Message history does not transfer. Twilio keeps it in its Messages resource. Export what you need before you close the account.
## Troubleshooting
| Symptom | Twilio equivalent | Fix |
| ------------------------------------- | ------------------------------ | ------------------------------------------------------------------------------------------------------------------------ |
| `401 Unauthorized` | Basic auth credentials | Send `X-API-Key: YOUR_API_KEY` |
| Body rejected | Form-encoded params | Send JSON with `Content-Type: application/json` |
| `(#131047)` re-engagement message | Error 63016 | Outside the 24-hour window. Send an approved template |
| Number cannot be connected | None | It is still on another BSP's WABA; migrate it in WhatsApp Manager |
| Template variables ignored | `ContentVariables` JSON string | Use Meta `components` with a `parameters` array, in placeholder order |
| Template not found | `ContentSid` | Templates are addressed by `name` + `language`, and must exist on this WABA |
| Webhook signature mismatch | URL + sorted params, HMAC-SHA1 | HMAC-SHA256 over the raw body, checked against `X-Webhook-Signature` |
| Webhook keeps retrying | TwiML response expected | Return `200` within 10 seconds; the body is ignored |
| Marketing template refused with `422` | Advanced Opt-Out | The contact stopped marketing on that number. Check [marketing preferences](/docs/whatsapp/templates/marketing-opt-outs) |
| `429` | `429 Too Many Requests` | Back off on `Retry-After`; see [rate limits](/api/rate-limits) |
## Node.js example
```javascript theme={null}
const crypto = require('crypto');
const express = require('express');
const app = express();
// Any durable queue works. The point is that the job outlives the request.
const queue = require('./queue');
const KAPSO = 'https://api.kapso.ai/meta/whatsapp/v24.0';
const headers = {
'X-API-Key': process.env.KAPSO_API_KEY,
'Content-Type': 'application/json',
};
// Twilio: client.messages.create({ to, from, contentSid, contentVariables })
async function sendTemplate({ phoneNumberId, to, name, params = [] }) {
const res = await fetch(`${KAPSO}/${phoneNumberId}/messages`, {
method: 'POST',
headers,
body: JSON.stringify({
messaging_product: 'whatsapp',
to,
type: 'template',
template: {
name,
language: { code: 'en_US' },
components: [
{
type: 'body',
parameters: params.map((text) => ({ type: 'text', text })),
},
],
},
}),
});
const body = await res.json();
return body.messages[0].id; // wamid...
}
// Twilio: client.messages.create({ to, from, body })
async function sendText({ phoneNumberId, to, text }) {
const res = await fetch(`${KAPSO}/${phoneNumberId}/messages`, {
method: 'POST',
headers,
body: JSON.stringify({
messaging_product: 'whatsapp',
to,
type: 'text',
text: { body: text },
}),
});
const body = await res.json();
return body.messages[0].id;
}
// Twilio: form-encoded POST, reply with TwiML.
// express.raw, not express.json: the signature covers the bytes Kapso sent.
app.post('/webhooks/whatsapp', express.raw({ type: 'application/json' }), async (req, res) => {
const expected = crypto
.createHmac('sha256', process.env.WEBHOOK_SECRET)
.update(req.body)
.digest('hex');
const a = Buffer.from(expected, 'utf8');
const b = Buffer.from(req.headers['x-webhook-signature'] ?? '', 'utf8');
if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) {
return res.status(401).send('Invalid signature');
}
if (req.headers['x-webhook-event'] !== 'whatsapp.message.received') {
return res.sendStatus(200);
}
const { message, conversation, phone_number_id } = JSON.parse(req.body);
// Enqueue before acknowledging. A 200 tells Kapso the event is handled, so
// anything that throws after this point is lost rather than retried.
await queue.add('inbound', {
idempotencyKey: req.headers['x-idempotency-key'],
from: message.from,
text: message.text?.body,
conversationId: conversation.id,
phoneNumberId: phone_number_id,
});
res.sendStatus(200);
});
```
## Need help
* [Send messages](/docs/whatsapp/send-messages/text) and [webhooks](/docs/platform/webhooks/overview)
* [API reference](/api/introduction)
* [WhatsApp support](https://wa.me/16266694464?text=Hi!%20I%20need%20help%20migrating%20from%20Twilio)
# Migrate from YCloud
Source: https://docs.kapso.ai/docs/migrate/from-ycloud
Migrate your WhatsApp numbers from YCloud to Kapso.
## Step 1: Connect your number
1. In YCloud's [WhatsApp accounts](https://www.ycloud.com/console/#/app/dashboard/account), find which WABA owns the number, then confirm its owner in [Meta Business Settings](https://business.facebook.com/latest/settings/whatsapp_account). If YCloud or an agency owns it, only they can release the number.
2. In [WhatsApp Manager](https://business.facebook.com/latest/whatsapp_manager/phone_numbers), check the WABA for other numbers, templates, or assets you still need.
3. Confirm you are an admin of the owning Business Portfolio, which Meta requires for removal.
4. Under **Account tools → Insights**, confirm the number has not sent a paid message in the last 30 days.
5. Turn off two-step verification on the number, or re-registration will ask for a PIN.
6. Remove the phone number from the WABA.
7. Wait about five minutes.
8. In Kapso, start embedded signup. Share your existing WABA, or create one when the flow asks.
9. Recreate your templates if you created a new WABA (see [Step 5](#step-5-templates)). Wait for Meta review.
The number stops sending after step 6, and on a new WABA it can send templates again once Meta approves them. Plan the migration accordingly.
See [Connect WhatsApp](/docs/how-to/whatsapp/connect-whatsapp) for the signup flow.
Migrating does not touch the old WABA. If you retire it later and Meta blocks removal over a pending balance, YCloud may still have a credit line attached, which Meta Direct Support has to clear.
If the reconnect fails, see [coexistence troubleshooting](/docs/how-to/whatsapp/coexistence-troubleshooting) and [bring your own SIM troubleshooting](/docs/how-to/whatsapp/bring-your-own-sim-troubleshooting).
You can also start on a fresh number. [Instant setup](/docs/platform/phone-numbers/instant-setup) gives you a pre-verified US number, with no SMS verification step.
**Testing first?** Build against a [sandbox](/docs/how-to/whatsapp/use-sandbox-for-testing) number while YCloud still carries production traffic. Create a session for your test phone, then send the 6-character code from WhatsApp.
## Step 2: Get your phone number IDs
```bash theme={null}
curl https://api.kapso.ai/platform/v1/whatsapp/phone_numbers \
-H "X-API-Key: YOUR_API_KEY"
```
```json theme={null}
{
"data": [
{
"id": "1234567890",
"phone_number_id": "1234567890",
"name": "Support Line",
"business_account_id": "98765432109",
"display_phone_number": "+1 555-123-4567",
"quality_rating": "GREEN",
"throughput_tier": "TIER_10K",
"status": "CONNECTED"
}
]
}
```
Store the mapping from `+E164` to `phone_number_id`. It replaces every `"from": "+1555..."` in your send code.
| YCloud | Kapso | Scopes to |
| ------------------------ | ----------------------------- | ------------------------------ |
| `from` (E.164, with `+`) | `phone_number_id` in the path | one phone number |
| `wabaId` | `business_account_id` | the WABA that owns the numbers |
YCloud takes `wabaId` in the request body for templates and flows. Kapso takes it in the path, as in `POST /{waba_id}/message_templates`. Templates belong to the WABA in both, so every number on that WABA can send them.
## Step 3: Update message sending
YCloud has two send endpoints. `POST /v2/whatsapp/messages` queues the message and returns `accepted`, and `POST /v2/whatsapp/messages/sendDirectly` submits to Meta and waits.
Kapso has one: `POST /{phone_number_id}/messages`. It submits to Meta synchronously and returns the `wamid`, so it behaves like `sendDirectly`. If your code branches between the two YCloud endpoints, both branches collapse into this one.
Three things change in every call:
* `from` moves out of the body and into the path, as `phone_number_id`.
* `messaging_product: "whatsapp"` is required.
* `to` is written as bare digits by convention, but your existing E.164 values work unchanged. Kapso forwards the request body to Meta byte for byte, and Meta accepts the leading `+`.
The content object itself does not change. YCloud passes Meta's shapes through, and so does Kapso.
Business-initiated sends, outside the 24-hour window.
**YCloud:**
```bash theme={null}
curl -X POST https://api.ycloud.com/v2/whatsapp/messages \
-H "X-API-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"from": "+15557654321",
"to": "+15551234567",
"type": "template",
"externalId": "ORD-123",
"template": {
"name": "order_update",
"language": { "code": "en_US" },
"components": [
{
"type": "body",
"parameters": [{ "type": "text", "text": "John" }]
}
]
}
}'
```
**Kapso:**
```bash theme={null}
curl -X POST https://api.kapso.ai/meta/whatsapp/v24.0/1234567890/messages \
-H "X-API-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"messaging_product": "whatsapp",
"to": "15551234567",
"type": "template",
"biz_opaque_callback_data": "ORD-123",
"template": {
"name": "order_update",
"language": { "code": "en_US" },
"components": [
{
"type": "body",
"parameters": [{ "type": "text", "text": "John" }]
}
]
}
}'
```
The `template` object is identical. `externalId` becomes `biz_opaque_callback_data`, capped at 512 characters and echoed back on status webhooks. See [Simple text templates](/docs/whatsapp/templates/simple-text).
Free-form replies, inside the 24-hour window.
**YCloud:**
```bash theme={null}
curl -X POST https://api.ycloud.com/v2/whatsapp/messages/sendDirectly \
-H "X-API-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"from": "+15557654321",
"to": "+15551234567",
"type": "text",
"text": { "body": "Your order shipped!", "preview_url": false }
}'
```
**Kapso:**
```bash theme={null}
curl -X POST https://api.kapso.ai/meta/whatsapp/v24.0/1234567890/messages \
-H "X-API-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"messaging_product": "whatsapp",
"to": "15551234567",
"type": "text",
"text": { "body": "Your order shipped!", "preview_url": false }
}'
```
`context.message_id` works the same way, and takes a wamid in both. YCloud's `recipient` field for [business-scoped user IDs](/docs/whatsapp/business-scoped-user-ids) is also called `recipient` in Kapso, and takes the same `US.1234...` values. See [Send text](/docs/whatsapp/send-messages/text).
Images, video, audio, documents, and stickers. The object is identical in both.
**YCloud:**
```bash theme={null}
curl -X POST https://api.ycloud.com/v2/whatsapp/messages \
-H "X-API-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"from": "+15557654321",
"to": "+15551234567",
"type": "image",
"image": { "link": "https://example.com/receipt.png", "caption": "Your receipt" }
}'
```
**Kapso:**
```bash theme={null}
curl -X POST https://api.kapso.ai/meta/whatsapp/v24.0/1234567890/messages \
-H "X-API-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"messaging_product": "whatsapp",
"to": "15551234567",
"type": "image",
"image": { "link": "https://example.com/receipt.png", "caption": "Your receipt" }
}'
```
Uploads move too. `POST /v2/whatsapp/media/{phoneNumber}/upload` becomes `POST /{phone_number_id}/media`, addressed by ID instead of phone number, and returns a Meta media ID you pass as `id` instead of `link`. Kapso also ingests from a URL and hands back a media ID via `POST /platform/v1/whatsapp/media`.
Buttons, lists, CTAs, and Flows all use Meta's `interactive` object in both platforms, so these payloads move across as-is:
```bash theme={null}
curl -X POST https://api.kapso.ai/meta/whatsapp/v24.0/1234567890/messages \
-H "X-API-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"messaging_product": "whatsapp",
"to": "15551234567",
"type": "interactive",
"interactive": {
"type": "button",
"body": { "text": "Confirm your appointment?" },
"action": {
"buttons": [
{ "type": "reply", "reply": { "id": "btn_yes", "title": "Yes" } },
{ "type": "reply", "reply": { "id": "btn_no", "title": "No" } }
]
}
}
}'
```
Taps arrive on `whatsapp.message.received` as an `interactive` message with `button_reply.id`, the same shape YCloud delivers on `whatsapp.inbound_message.received`. See [Send buttons](/docs/whatsapp/send-messages/buttons), [Send lists](/docs/whatsapp/send-messages/lists), and [Sending Flows](/docs/whatsapp/flows/sending-flows).
YCloud marks a message read on a dedicated endpoint, keyed by the inbound message ID. Kapso uses the same send endpoint, with Meta's status body.
**YCloud:**
```bash theme={null}
curl -X POST https://api.ycloud.com/v2/whatsapp/inboundMessages/wamid.HBgNMTU1.../markAsRead \
-H "X-API-Key: YOUR_API_KEY"
```
**Kapso:**
```bash theme={null}
curl -X POST https://api.kapso.ai/meta/whatsapp/v24.0/1234567890/messages \
-H "X-API-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"messaging_product": "whatsapp",
"status": "read",
"message_id": "wamid.HBgNMTU1NTE0OTU5Nzg1...",
"typing_indicator": { "type": "text" }
}'
```
Keep `typing_indicator` to mark the message read and show the indicator. Drop it to only mark read. YCloud has a separate endpoint for each. See [Mark as read](/docs/whatsapp/send-messages/mark-read).
Reactions are their own message type, `type: "reaction"`. See [Send reaction](/docs/whatsapp/send-messages/reaction).
YCloud returns its own object ID on send, and the Meta `wamid` only later, on the `sent` status webhook. Kapso returns the wamid immediately:
```json theme={null}
{ "messaging_product": "whatsapp", "messages": [{ "id": "wamid.HBgNMTU1..." }] }
```
If your database keys messages on YCloud's `id`, switch that column to the wamid. Everything that took a wamid on YCloud (`context.message_id`, mark-as-read, reactions) keeps taking one.
The [TypeScript SDK](/docs/whatsapp/typescript-sdk/introduction) wraps all of this, so you do not have to write the envelopes by hand.
## Step 4: Update webhooks
YCloud webhooks are account-wide: one endpoint receives every event for every number, and you route in your own handler. Which field carries your number depends on the event:
| YCloud event family | Your number is in |
| ------------------------------------------ | ----------------------------------------------------------------- |
| Inbound messages | `whatsappInboundMessage.to` |
| Outbound status updates | `whatsappMessage.from`, with `.to` holding the customer |
| Account, template, and phone-number events | Neither. Switch on `wabaId` or the phone number in the event body |
Kapso registers webhooks per phone number, so that routing disappears: the endpoint you register only ever receives that number's events.
```bash theme={null}
curl -X POST https://api.kapso.ai/platform/v1/whatsapp/phone_numbers/1234567890/webhooks \
-H "X-API-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"whatsapp_webhook": {
"url": "https://yourapp.com/webhooks/whatsapp",
"events": [
"whatsapp.message.received",
"whatsapp.message.sent",
"whatsapp.message.delivered",
"whatsapp.message.read",
"whatsapp.message.failed"
],
"secret_key": "your-signing-secret"
}
}'
```
You choose the signing secret. YCloud generates one and returns it on create.
Account-level events (a WABA ban, a number offboarded, a customer finishing a setup link) arrive on [project webhooks](/docs/platform/webhooks/project-webhooks) instead. You configure those once for the whole project.
### Event mapping
| YCloud event | Kapso |
| ------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------- |
| `whatsapp.inbound_message.received` | `whatsapp.message.received` |
| `whatsapp.message.updated`, `status: sent` | `whatsapp.message.sent` |
| `whatsapp.message.updated`, `status: delivered` | `whatsapp.message.delivered` |
| `whatsapp.message.updated`, `status: read` | `whatsapp.message.read` |
| `whatsapp.message.updated`, `status: failed` | `whatsapp.message.failed` |
| `whatsapp.business_account.updated` | By `updateEvent`. See [below](#account-and-number-events) |
| `whatsapp.business_account.deleted` | `whatsapp.phone_number.disconnected` for partner removal. None for a real deletion |
| `whatsapp.phone_number.deleted` | `whatsapp.phone_number.deleted`, `.offboarded`, or `.disconnected`, by cause |
| `whatsapp.template.reviewed` / `.category_updated` / `.quality_updated` | None. Poll `GET /{waba_id}/message_templates` |
| `whatsapp.phone_number.quality_updated` / `.name_updated` | None |
| `whatsapp.user.preferences` | `whatsapp.contact.marketing_preference_changed` |
| `contact.unsubscribe.created` / `.deleted` | None. YCloud's own list, not Meta's. See [Contacts and opt-outs](#contacts-and-opt-outs) |
| `whatsapp.payment.updated` | None |
| `whatsapp.business_account.reviewed` | None. Dashboard only, or a [raw Meta webhook](#raw-meta-webhooks) |
| `contact.created` / `.deleted` | None. Contacts appear silently, on first message or via the API |
| `contact.attributes_changed` | None. Patching `metadata` fires nothing |
| `contact.note.created` / `.updated` / `.deleted` | None |
| `whatsapp.phone_number.business_username_updated` | None. Read `GET /{phone_number_id}/username` |
| `whatsapp.flow.status_change` | None. [Flows API](/docs/whatsapp/flows/overview), or a raw Meta webhook |
| `whatsapp.call.connect` / `.status.updated` / `.terminate` | None named. Meta events, so a [raw Meta webhook](#raw-meta-webhooks) carries them |
| `whatsapp.call.recording.updated` / `whatsapp.call.transcription.updated` | None, and no fallback. YCloud's own pipeline, never sent by Meta |
| `whatsapp.group.*`, `whatsapp.smb.*` | None |
| `sms.*`, `voice.*`, `email.*` | None. See [What does not map](#what-does-not-map) |
| None | `whatsapp.conversation.created`, `.ended`, `.inactive` |
| None | `whatsapp.contact.identity_changed` |
YCloud dispatches one event for delivery and asks you to switch on `whatsappMessage.status`. Kapso dispatches one per status, named in the `X-Webhook-Event` header. The switch moves from the body to the header.
### Account and number events
`whatsapp.business_account.updated` carries seven `updateEvent` values, on [project webhooks](/docs/platform/webhooks/project-webhooks):
| `updateEvent` | Kapso |
| --------------------------------------------------------------------------------- | ---------------------------------------------------------- |
| `DISABLED_UPDATE` | `whatsapp.account.disabled` or `.reinstated`, by ban state |
| `ACCOUNT_RESTRICTION` | `whatsapp.account.restricted` |
| `ACCOUNT_VIOLATION` | `whatsapp.account.violation` |
| `PARTNER_REMOVED` / `PARTNER_APP_UNINSTALLED` | `whatsapp.phone_number.disconnected` |
| `AUTH_INTL_PRICE_ELIGIBILITY_UPDATE` / `BUSINESS_PRIMARY_LOCATION_COUNTRY_UPDATE` | None |
Since 2026-06-10 YCloud sends partner removal as `business_account.deleted` instead, so handle both. Its `removedReason`, `removedInitiatedBy`, and `removedTime` are optional, so their absence does not prove a real deletion.
### Raw Meta webhooks
Kapso can also forward every Meta webhook for a number, unreshaped. Register a second webhook with `"kind": "meta"`:
```bash theme={null}
curl -X POST https://api.kapso.ai/platform/v1/whatsapp/phone_numbers/1234567890/webhooks \
-H "X-API-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"whatsapp_webhook": {
"kind": "meta",
"url": "https://yourapp.com/webhooks/meta",
"secret_key": "your-signing-secret"
}
}'
```
Payloads are Meta's, so this needs its own handler. See [Webhooks overview](/docs/platform/webhooks/overview).
### Field mapping
Inbound, from `whatsappInboundMessage` to Kapso's `whatsapp.message.received`:
| YCloud | Kapso |
| ----------------------------------------- | ------------------------------------------------------------------------------------------------ |
| `wamid` | `message.id` |
| `id` (YCloud's own ID) | None. Kapso keys on the wamid |
| `from` (`+E164`) | `message.from` (no `+`) |
| `fromUserId` / `fromParentUserId` | `message.from_user_id` / `message.from_parent_user_id` |
| `customerProfile.name` | `conversation.contact_name` |
| `customerProfile.username` | `conversation.username` |
| `to` (`+E164`) | `phone_number_id`, at the top level |
| `sendTime` (RFC 3339) | `message.timestamp` (Unix epoch string) |
| `type`, `text`, `interactive`, `location` | Same names, same shapes |
| `image.link` | `message.kapso.media_url`, plus `media_data` |
| `referral` | On the payload when present. See [Referrals (CTWA)](/docs/platform/whatsapp-data#referrals-ctwa) |
| None | `conversation.id`, `message.kapso.transcript` for voice notes |
Outbound, from `whatsappMessage` to the status events:
| YCloud | Kapso |
| ------------------------------------------------- | -------------------------------------------------------------------------------- |
| `wamid` | `message.id` |
| `status` | The event name |
| `errorCode` / `errorMessage` / `whatsappApiError` | `message.kapso.statuses[].errors[]` |
| `recipientUserId` / `parentRecipientUserId` | `message.to_user_id` / `message.to_parent_user_id` |
| `externalId` | `biz_opaque_callback_data` |
| `conversation.id` / `conversation.expireTime` | None. Kapso's `conversation.id` is its own, not Meta's pricing conversation |
| `totalPrice` / `currency` | None. See [Meta message billing](/docs/whatsapp/meta-message-billing) |
| `pricingCategory` | `message.kapso.statuses[].pricing.category`, with `billable` and `pricing_model` |
`from` is not always present. WhatsApp can identify a contact with `business_scoped_user_id` instead. See [business-scoped user IDs](/docs/whatsapp/business-scoped-user-ids).
### Signature verification
YCloud signs the timestamp and the body together, and sends both in one header. Kapso signs only the body.
**YCloud:**
```javascript theme={null}
// YCloud-Signature: t=1654084800,s=8eb70f2a...
const [t, received] = signatureHeader.split(',').map((part) => part.split('=')[1]);
const expected = crypto
.createHmac('sha256', secret)
.update(`${t}.${rawBody}`) // timestamp, a dot, then the body
.digest('hex');
```
**Kapso:**
```javascript theme={null}
const crypto = require('crypto');
// express.raw, not express.json: Kapso signs the exact bytes it sent, and
// re-serializing a parsed body does not reproduce them
app.post('/webhooks/whatsapp', express.raw({ type: 'application/json' }), (req, res) => {
const expected = crypto
.createHmac('sha256', process.env.WEBHOOK_SECRET)
.update(req.body)
.digest('hex');
const a = Buffer.from(expected, 'utf8');
const b = Buffer.from(req.headers['x-webhook-signature'] ?? '', 'utf8');
// timingSafeEqual throws on length mismatch, so compare lengths first
if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) {
return res.status(401).send('Invalid signature');
}
const event = JSON.parse(req.body);
res.sendStatus(200);
});
```
Both need the raw bytes, so whatever you did to capture `rawBody` still applies. The freshness check does not carry over: Kapso signs no timestamp, so the tolerance you tuned against YCloud's `t` has nothing to bind to. See [Security](/docs/platform/webhooks/security#signature-verification) for Python and Ruby.
### Delivery behavior
| | YCloud | Kapso |
| ------------------- | ----------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------- |
| Retries | 7, at 10s / 30s / 5m / 30m / 1h / 2h / 2h | 3 attempts in about 50 seconds: the first, then 10s and 40s later |
| Timeout | 6s recommended, 10s deprioritized | 10s in the documented contract; the sender aborts at 15s |
| Dedup | `event.id` | `X-Idempotency-Key` header |
| Scope | Account-wide, max 20 endpoints | Per phone number, plus project webhooks |
| Batching | None | Optional buffering on `whatsapp.message.received` |
| Endpoint suspension | 3 minutes, then automatic resume | Paused once a 15-minute window holds 40+ deliveries, 10+ failures, and an 85% failure rate. Re-enabled by hand in the dashboard |
See [Advanced](/docs/platform/webhooks/advanced) for buffering and ordering, and [Security](/docs/platform/webhooks/security) for verification in Python and Ruby.
## Step 5: Templates
Same components, different addressing. YCloud takes `wabaId` in the body, Kapso takes it in the path.
**YCloud:**
```bash theme={null}
curl -X POST https://api.ycloud.com/v2/whatsapp/templates \
-H "X-API-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"wabaId": "98765432109",
"name": "order_update",
"language": "en_US",
"category": "UTILITY",
"components": [
{
"type": "BODY",
"text": "Hi {{1}}, your order is confirmed.",
"example": { "body_text": [["John"]] }
}
]
}'
```
**Kapso:**
```bash theme={null}
curl -X POST https://api.kapso.ai/meta/whatsapp/v24.0/98765432109/message_templates \
-H "X-API-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "order_update",
"category": "UTILITY",
"language": "en_US",
"components": [
{
"type": "BODY",
"text": "Hi {{1}}, your order is confirmed.",
"example": { "body_text": [["John"]] }
}
]
}'
```
The rest of the lifecycle maps directly:
| YCloud | Kapso |
| --------------------------------------------------------- | ------------------------------------------------ |
| `POST /v2/whatsapp/templates` | `POST /{waba_id}/message_templates` |
| `GET /v2/whatsapp/templates` | `GET /{waba_id}/message_templates` |
| `PATCH /v2/whatsapp/templates/{wabaId}/{name}/{language}` | `POST /{waba_id}/message_templates?hsm_id=...` |
| `DELETE /v2/whatsapp/templates/{wabaId}/{name}` | `DELETE /{waba_id}/message_templates?name=...` |
| `GET /v2/whatsapp/templates/{wabaId}/{name}/{language}` | `GET /{waba_id}/message_templates/{template_id}` |
Create and edit are one endpoint in Kapso. `hsm_id` is a query parameter, not a body field: add `?hsm_id=...` to update instead of create. Delete also identifies the template by query string, taking either `?name=...` or `?hsm_id=...`.
Templates belong to the WABA. If the number stayed on a WABA you already owned, they are still there. If it landed on a new one, recreate them and wait for review.
Rebuilt them in WhatsApp Manager? Pull them in from **WhatsApp → Templates → Sync from WhatsApp**. See [Template lifecycle](/docs/whatsapp/templates/lifecycle).
## Contacts and opt-outs
YCloud stores contacts in its own CRM, with tags, custom attributes, owners, and source attribution. Kapso stores a smaller contact record and a free-form `metadata` object:
```bash theme={null}
curl -X POST https://api.kapso.ai/platform/v1/whatsapp/contacts \
-H "X-API-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"contact": {
"wa_id": "+15551234567",
"profile_name": "John Doe",
"metadata": { "plan": "pro", "crm_id": "CUS-12345" }
}
}'
```
| YCloud | Kapso |
| --------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `phoneNumber` | `wa_id` |
| `nickname` on a read (WhatsApp's own, read-only) | `profile_name` |
| `remarkName` | `display_name` |
| `nickname` on a write | `display_name`. YCloud deprecated it as an alias for `remarkName`, so anything you set through it is a business label, not WhatsApp's nickname |
| `metaUsername` | `username`, read-only |
| `customAttributes` | `metadata`, an untyped JSON object |
| `tags`, `ownerEmail`, `sourceType`, `email` | None. Put them in `metadata` |
| `GET /v2/contact/contacts` | `GET /platform/v1/whatsapp/contacts`, with offset (`page`, `per_page`) or cursor (`limit`, `after`, `before`) pagination |
| Contact attribute definitions | None. `metadata` is schemaless |
| Contact notes API, and `notes` on contact create | No Platform API route. The developer API serves them at `/api/v1/whatsapp_contacts/{identifier}/notes` on the same key, outside the [API reference](/api/introduction) |
| `POST /v2/event/events` and event definitions | [Project events](/docs/platform/events) |
| `POST /v2/unsubscribers`, `filterUnsubscribed`, `filterBlocked` | None. Kapso reads WhatsApp's own opt-out and refuses marketing sends with `422`. Only the contact can change it |
Export your YCloud unsubscriber list before you close the account and keep suppressing those contacts on your side. See [Marketing opt-outs](/docs/whatsapp/templates/marketing-opt-outs).
## Multi-tenant setups
If you message on behalf of your own customers, each one becomes a Kapso customer that connects its own number through a setup link, instead of you onboarding WABAs on their behalf:
```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 Corporation", "external_customer_id": "CUS-12345"}}'
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",
"meta_billing_mode": "partner_managed"
}
}'
```
Send your customer the returned `url`. They log in with Facebook and connect in about five minutes. You then get `whatsapp.phone_number.created` on your project webhook, with the `customer.id` and `phone_number_id`. See [Onboard customers](/docs/platform/customer-guide) and [Connection detection](/docs/platform/setup-links/detect-connection).
One API key covers every customer. Migrate one customer at a time.
## Bulk sends
YCloud's queueing endpoint absorbs bursts, so bulk sends there are often just a fast loop over `POST /v2/whatsapp/messages`, whatever the message type. Kapso's send endpoint is synchronous, and what replaces the loop depends on what you were sending.
Approved-template campaigns become [broadcasts](/docs/platform/broadcasts/api). `whatsapp_template_id` is required on create, so a broadcast always sends one approved template, of any category. The one restriction is that authentication templates cannot go to [BSUID](/docs/whatsapp/business-scoped-user-ids) recipients:
```bash theme={null}
POST /platform/v1/whatsapp/broadcasts
POST /platform/v1/whatsapp/broadcasts/{id}/recipients
POST /platform/v1/whatsapp/broadcasts/{id}/send
GET /platform/v1/whatsapp/broadcasts/{id}
```
Kapso paces broadcasts internally to stay inside Meta's throughput limits, so you do not throttle them yourself. On a marketing broadcast, recipients who stopped marketing are marked `suppressed` and skipped, which covers what `filterUnsubscribed` did per send. The guard checks the template category first, so utility and authentication broadcasts are not subject to marketing opt-outs at all. They can still fail per recipient for other reasons, including the BSUID restriction above.
Bulk text, media, and interactive sends have no broadcast equivalent, because those are session messages rather than templates. Keep your own durable queue in front of `POST /{phone_number_id}/messages` for them, and pace it yourself against your [rate limit](/api/rate-limits).
`POST /{id}/schedule` sends later. Stopping depends on what state the broadcast is in: `POST /{id}/cancel` returns a *scheduled* broadcast to draft, and it does nothing for one that is already sending. To stop a send in flight, patch the status:
```bash theme={null}
curl -X PATCH https://api.kapso.ai/platform/v1/whatsapp/broadcasts/BROADCAST_ID \
-H "X-API-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"whatsapp_broadcast": {"status": "stopped"}}'
```
Pending recipients stay pending and no new sends start.
Campaigns built in the YCloud console move to [broadcasts](/docs/platform/broadcasts/overview), with [CSV upload](/docs/platform/broadcasts/csv-upload) for the recipient list.
## Billing
YCloud runs on a prepaid balance: you top up, YCloud pays Meta, and `BALANCE_INSUFFICIENT` stops sends. Kapso offers the same shape, or direct Meta billing:
| | YCloud | Kapso |
| ------------------------ | --------------------------------------------------- | --------------------------------------------------------- |
| Who pays Meta | YCloud, from your balance | Kapso from project credits, or your business directly |
| Where you see the charge | `GET /v2/balance`, `totalPrice` on message webhooks | Project credits, or Meta Billing Hub |
| Out of funds | `BALANCE_INSUFFICIENT`, `403` | Paid sends pause until credits are added, or Meta decides |
Choose the mode when the WABA is connected. See [Meta message billing](/docs/whatsapp/meta-message-billing) and [pricing](/docs/whatsapp/pricing-faq).
## Feature map
| Feature | YCloud | Kapso |
| ----------------------- | ---------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- |
| WhatsApp messaging | `/v2/whatsapp/messages` and `sendDirectly` | [`POST /{phone_number_id}/messages`](/docs/whatsapp/send-messages/text) |
| Interactive messages | Meta `interactive` object | [Same object](/docs/whatsapp/send-messages/buttons) |
| Templates | `/v2/whatsapp/templates`, `wabaId` in body | [`/{waba_id}/message_templates`](/docs/whatsapp/templates/simple-text) |
| WhatsApp Flows | Full Flows API | [Full Flows API](/docs/whatsapp/flows/overview), including hosted data endpoints |
| Bulk messaging | Campaigns, or your own loop, any message type | [Broadcasts](/docs/platform/broadcasts/overview) for approved templates, with CSV upload and scheduling. Session messages stay your own loop |
| Conversation automation | Chatbot Flow Builder, AI Agent | [Workflows](/docs/workflows/introduction) with AI steps and human handoff |
| Team inbox | Shared Team Inbox | [Included](/docs/platform/inbox/overview), plus an [embeddable iframe](/docs/platform/inbox/embedded) |
| Contacts | Contacts with tags and custom attributes | [Contacts](/docs/platform/whatsapp-data#contacts) with `metadata` |
| Message history | `GET /v2/whatsapp/messages/{id}` | [`/whatsapp/messages`](/docs/platform/whatsapp-data), `/whatsapp/conversations` |
| CTWA attribution | `referral` on inbound messages | [Same, plus an Ads view](/docs/platform/whatsapp-data#referrals-ctwa) |
| WhatsApp calling | Connect, accept, terminate, plus stored recordings and transcriptions | [Supported](/docs/whatsapp/typescript-sdk/calls), with call logs. No recordings or transcriptions |
| Business profile | Profile and commerce settings | [Display names](/docs/whatsapp/display-names), [usernames](/docs/whatsapp/business-usernames) |
| Opt-outs | `/v2/unsubscribers`, plus per-send filters | [Automatic](/docs/whatsapp/templates/marketing-opt-outs), from WhatsApp's native control |
| Multi-tenant onboarding | WABAs you onboard per client | [Setup links](/docs/platform/setup-links/create-and-configure) your customer completes |
| Rate limits | 200 rps per sender on `/messages`, 80 rps per sender on `sendDirectly` | [100-2,000 req/min by plan](/api/rate-limits) |
| SDKs | Go, Java, Node.js, PHP | [TypeScript](/docs/whatsapp/typescript-sdk/introduction), [MCP server](/docs/whatsapp/mcp), [CLI](/docs/whatsapp/cli) |
Things Kapso adds:
* Automatic voice-note transcription on inbound audio
* [Functions](/docs/functions/overview) on Cloudflare Workers, as workflow steps, agent tools, or plain endpoints
* [Findings](/docs/platform/findings), which reads ended conversations with AI and groups the recurring problems
* Conversation lifecycle webhooks, so you can act when a conversation goes quiet or ends
## What does not map
* **SMS, email, voice, and Verify.** YCloud's `/v2/sms`, `/v2/emails`, `/v2/voices`, and `/v2/verify/verifications` have no Kapso equivalent. Kapso is WhatsApp only. Keep a YCloud account or another provider for those channels.
* **Contact attribute definitions, tags, and owners.** Contact `metadata` is schemaless.
* **Balance API.** No `GET /v2/balance`. Credits live in project settings.
## Cutover
Migrate one number at a time. A number's WhatsApp registration and webhook routing move as a unit, so there is no gradual split per number.
| Phase | Actions |
| ------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Prep | Create the project and API key, build against a [sandbox](/docs/how-to/whatsapp/use-sandbox-for-testing) number, export contacts, unsubscribers, history, and call recordings from YCloud |
| Pilot | Move one low-traffic number end to end. Budget for the time the number is down, and for Meta's template review |
| Rollout | Migrate remaining numbers in batches; for multi-tenant, one customer at a time |
| Cutoff | Stop YCloud sends for each migrated number, then drain the remaining balance before closing the account |
Message history does not transfer, and neither do contacts or unsubscribers. YCloud keeps them behind its own APIs. Export what you need before you close the account.
## Troubleshooting
| Symptom | YCloud equivalent | Fix |
| ------------------------------------------------ | --------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
| `401 Unauthorized` | `UNAUTHORIZED` | Same header, different key. Send Kapso's `X-API-Key` |
| `404` on send | Right key, wrong path | The sender is `phone_number_id` in the path, not `from` in the body |
| Webhook reports a different number than you sent | E.164 stored verbatim | Meta received yours unchanged. Kapso keyed its own records on the normalized form, with Argentina `549` as `54` and Mexico `521` as `52` |
| `(#131047)` re-engagement message | "More than 24 hours have passed" | Outside the 24-hour window. Send an approved template |
| Template not found | `WHATSAPP_TEMPLATE_UNAVAILABLE` | Templates are addressed by `name` + `language`, and must exist and be approved on this WABA |
| Templates missing after the move | None | They belonged to the old WABA. Recreate them on the new one and wait for review |
| Number cannot be connected | None | Still on the old WABA, a PIN is set, or it sent a paid message in the last 30 days. See [Step 1](#step-1-connect-your-number) |
| Webhook signature mismatch | HMAC over `{timestamp}.{body}` | HMAC-SHA256 over the raw body alone, checked against `X-Webhook-Signature` |
| Webhook fires once and gives up | 7 retries over hours | 3 attempts in about 50 seconds. Return `200` fast and process from a queue |
| Status handler never runs | Switched on `whatsappMessage.status` | Status is the event name now. Read `X-Webhook-Event` |
| Marketing template refused with `422` | `RECIPIENT_UNSUBSCRIBED` | The contact stopped marketing on that number. Check [marketing preferences](/docs/whatsapp/templates/marketing-opt-outs) |
| `429` | `ACCOUNT_RATE_LIMITED`, `SENDER_RATE_LIMITED` | Back off on `Retry-After`; see [rate limits](/api/rate-limits) |
## Node.js example
The example below is scoped to what changes when you move off YCloud. It is not a hardened receiver: deduplication, retry handling, and buffering are the same on Kapso whatever you migrated from, so they live in [Advanced](/docs/platform/webhooks/advanced) and [Security](/docs/platform/webhooks/security#idempotency). Read those before this handler takes production traffic.
```javascript theme={null}
const crypto = require('crypto');
const express = require('express');
const app = express();
// Any durable queue works. YCloud queued sends for you; Kapso does not.
const queue = require('./queue');
const KAPSO = 'https://api.kapso.ai/meta/whatsapp/v24.0';
const headers = {
'X-API-Key': process.env.KAPSO_API_KEY,
'Content-Type': 'application/json',
};
// YCloud: POST /v2/whatsapp/messages with { from, to, type, template, externalId }
async function sendTemplate({ phoneNumberId, to, name, params = [], externalId }) {
const res = await fetch(`${KAPSO}/${phoneNumberId}/messages`, {
method: 'POST',
headers,
body: JSON.stringify({
messaging_product: 'whatsapp',
to, // forwarded to Meta as-is; '+1555...' is fine
type: 'template',
biz_opaque_callback_data: externalId, // YCloud: externalId
template: {
name,
language: { code: 'en_US' },
components: [
{
type: 'body',
parameters: params.map((text) => ({ type: 'text', text })),
},
],
},
}),
});
const body = await res.json();
// A 4xx or 5xx returns an error object, not `messages`. Reading
// messages[0] there throws a TypeError that hides the real cause.
if (!res.ok) {
throw new Error(`Send failed (${res.status}): ${JSON.stringify(body.error ?? body)}`);
}
return body.messages[0].id; // wamid, not a YCloud object ID
}
// YCloud: one endpoint, switch on event.type and whatsappMessage.status.
// Kapso: event name in the header, raw body signed without a timestamp.
app.post('/webhooks/whatsapp', express.raw({ type: 'application/json' }), async (req, res) => {
const expected = crypto
.createHmac('sha256', process.env.WEBHOOK_SECRET)
.update(req.body)
.digest('hex');
const a = Buffer.from(expected, 'utf8');
const b = Buffer.from(req.headers['x-webhook-signature'] ?? '', 'utf8');
if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) {
return res.status(401).send('Invalid signature');
}
const event = req.headers['x-webhook-event'];
const payload = JSON.parse(req.body);
// YCloud: event.id. Delivery is at-least-once, so every job carries the
// key and the worker has to be idempotent on it.
const idempotencyKey = req.headers['x-idempotency-key'];
if (event === 'whatsapp.message.failed') {
// YCloud: whatsappMessage.errorCode / errorMessage
const errors = (payload.message.kapso.statuses ?? []).flatMap((s) => s.errors ?? []);
await queue.add('send-failed', {
idempotencyKey,
id: payload.message.id,
phoneNumberId: payload.phone_number_id,
errors,
});
return res.sendStatus(200);
}
if (event !== 'whatsapp.message.received') {
return res.sendStatus(200);
}
const { message, conversation, phone_number_id } = payload;
await queue.add('inbound', {
idempotencyKey,
// `from` can be absent on a BSUID-only contact, so carry the scoped
// IDs too or the job cannot tell you who wrote in.
from: message.from ?? conversation.phone_number ?? null,
fromUserId: message.from_user_id ?? conversation.business_scoped_user_id ?? null,
fromParentUserId:
message.from_parent_user_id ?? conversation.parent_business_scoped_user_id ?? null,
username: conversation.username ?? null,
text: message.text?.body,
mediaUrl: message.kapso.media_url,
conversationId: conversation.id,
phoneNumberId: phone_number_id,
});
res.sendStatus(200);
});
```
## Need help
* [Send messages](/docs/whatsapp/send-messages/text) and [webhooks](/docs/platform/webhooks/overview)
* [API reference](/api/introduction)
* [WhatsApp support](https://wa.me/16266694464?text=Hi!%20I%20need%20help%20migrating%20from%20YCloud)
# Become a Meta Tech Provider
Source: https://docs.kapso.ai/docs/platform/become-tech-provider
Get your Meta app ready to onboard customers through embedded signup
Become a Meta Tech Provider when you want to offer WhatsApp Business Platform services to your own customers under your own Meta app.
## At a glance
1. [Prepare and test Embedded Signup](#configure-embedded-signup-first), then record a demo of onboarding and messaging.
2. [Complete business verification](#verification) and the app settings Meta requires.
3. [Submit your app for review](#submit-meta-app-review) with permission explanations, data handling answers, and reviewer instructions.
## Configure embedded signup first
Before applying, configure your app and Kapso setup links:
1. Create a [Meta app](/docs/platform/create-meta-app).
2. Configure [customer setup links with your own Meta app](/docs/platform/onboard-customers-own-meta-app).
3. Test setup with a customer or test business.
4. Send a message through the connected number.
5. Record a short demo of the setup and messaging flow.
## Submit Meta app review
In the Meta app dashboard, submit for advanced access.
Meta shows the review as a checklist. Complete each section before you submit.
### Verification
Start business verification from the App Review checklist or from **Business Settings > Security Center**.
### App settings
Complete the app settings Meta requires for review:
* app icon
* privacy policy URL
* app category
* primary contact
The privacy policy URL must be public and accessible to Meta's crawlers.
### Allowed usage
Meta asks how the app uses each requested permission or feature.
For `whatsapp_business_management`, explain that your product onboards business customers through embedded signup and manages their WhatsApp assets, such as phone numbers, templates, WABA setup, and webhook subscriptions.
For `whatsapp_business_messaging`, explain how your product sends WhatsApp messages for onboarded customers. Include a video showing:
1. a customer or test customer connected to your product
2. a WhatsApp message sent from your product
3. the message received in WhatsApp
You can use Kapso for the demo: connect a test number, send a message through Kapso or the API, and show the result in WhatsApp.
For `public_profile`, explain that it is used as part of Facebook Login for Business during embedded signup.
Meta can also ask you to confirm required API test calls and agree to the allowed usage terms for each permission.
### Data handling
Answer the data handling questions for the entity responsible for the app.
Meta may ask whether you have provided personal data to public authorities in response to national security requests, and which policies or processes you have in place for those requests.
These answers should match your company's actual legal, security, and data handling practices. If you are unsure, ask the person responsible for privacy or security at your company before submitting.
If Meta asks for service providers, include **Kapso**. Describe Kapso as the service provider used for hosted setup links, embedded signup completion, WhatsApp API operations, webhooks, message sending, and operational logs.
### Reviewer instructions and service providers
Use reviewer instructions to make the review easy to reproduce. Include:
* a test account or test customer
* the exact setup link or product path the reviewer should use
* steps to complete embedded signup
* steps to send or trigger a WhatsApp message
* where the reviewer should see the connected `phone_number_id` or resulting message
* a screencast that shows the end-to-end flow
# Broadcasts API
Source: https://docs.kapso.ai/docs/platform/broadcasts/api
Send template messages to multiple recipients programmatically
Send WhatsApp template messages to hundreds or thousands of recipients with full tracking and statistics.
**Example repo:** [github.com/gokapso/whatsapp-broadcasts-example](https://github.com/gokapso/whatsapp-broadcasts-example)
## Complete workflow
```typescript theme={null}
// Step 1: Create broadcast
const createResponse = await fetch('https://api.kapso.ai/platform/v1/whatsapp/broadcasts', {
method: 'POST',
headers: {
'X-API-Key': 'your-api-key',
'Content-Type': 'application/json'
},
body: JSON.stringify({
whatsapp_broadcast: {
name: 'July promo',
phone_number_id: '1234567890',
whatsapp_template_id: '7a8b9c0d-1e2f-3a4b-5c6d-7e8f9a0b1c2d'
}
})
});
const { data: broadcast } = await createResponse.json();
const broadcastId = broadcast.id;
// Step 2: Add recipients (repeat for batches > 1000)
await fetch(`https://api.kapso.ai/platform/v1/whatsapp/broadcasts/${broadcastId}/recipients`, {
method: 'POST',
headers: {
'X-API-Key': 'your-api-key',
'Content-Type': 'application/json'
},
body: JSON.stringify({
whatsapp_broadcast: {
recipients: [
{
phone_number: '+14155550123',
components: [
{
type: 'body',
parameters: [
{ type: 'text', parameter_name: 'first_name', text: 'John' },
{ type: 'text', parameter_name: 'discount', text: '50%' }
]
}
]
}
]
}
})
});
// Step 3: Send broadcast
await fetch(`https://api.kapso.ai/platform/v1/whatsapp/broadcasts/${broadcastId}/send`, {
method: 'POST',
headers: { 'X-API-Key': 'your-api-key' }
});
// Step 4: Poll for progress
async function waitForCompletion(broadcastId) {
while (true) {
const statusResponse = await fetch(
`https://api.kapso.ai/platform/v1/whatsapp/broadcasts/${broadcastId}`,
{ headers: { 'X-API-Key': 'your-api-key' } }
);
const { data } = await statusResponse.json();
if (data.completed_at || data.stopped_at) {
console.log(`Broadcast complete: ${data.sent_count}/${data.total_recipients} sent`);
break;
}
console.log(`Progress: ${data.sent_count}/${data.total_recipients}`);
await new Promise(resolve => setTimeout(resolve, 5000)); // Wait 5s
}
}
await waitForCompletion(broadcastId);
```
***
## Step 1: Create broadcast
```typescript theme={null}
const response = await fetch('https://api.kapso.ai/platform/v1/whatsapp/broadcasts', {
method: 'POST',
headers: {
'X-API-Key': 'your-api-key',
'Content-Type': 'application/json'
},
body: JSON.stringify({
whatsapp_broadcast: {
name: 'July promo',
phone_number_id: '1234567890',
whatsapp_template_id: '7a8b9c0d-1e2f-3a4b-5c6d-7e8f9a0b1c2d'
}
})
});
const { data } = await response.json();
// data.id = broadcast ID for next steps
// data.status = 'draft'
```
Requirements:
* Phone number must be production type (not sandbox)
* Template must be approved
## Step 2: Add recipients
Add up to 1,000 recipients per request. Repeat for batches over 1,000.
Recipients use Meta's component syntax to populate template variables.
### Body parameters
For templates with body text variables:
```typescript theme={null}
await fetch(`https://api.kapso.ai/platform/v1/whatsapp/broadcasts/${broadcastId}/recipients`, {
method: 'POST',
headers: {
'X-API-Key': 'your-api-key',
'Content-Type': 'application/json'
},
body: JSON.stringify({
whatsapp_broadcast: {
recipients: [
{
phone_number: '+14155550123',
components: [
{
type: 'body',
parameters: [
{ type: 'text', parameter_name: 'first_name', text: 'John' },
{ type: 'text', parameter_name: 'discount', text: '50%' }
]
}
]
},
{
phone_number: '+14155550124',
components: [
{
type: 'body',
parameters: [
{ type: 'text', parameter_name: 'first_name', text: 'Jane' },
{ type: 'text', parameter_name: 'discount', text: '40%' }
]
}
]
}
]
}
})
});
```
### Button parameters
For templates with dynamic button URLs:
```typescript theme={null}
await fetch(`https://api.kapso.ai/platform/v1/whatsapp/broadcasts/${broadcastId}/recipients`, {
method: 'POST',
headers: {
'X-API-Key': 'your-api-key',
'Content-Type': 'application/json'
},
body: JSON.stringify({
whatsapp_broadcast: {
recipients: [
{
phone_number: '+14155550123',
components: [
{
type: 'body',
parameters: [
{ type: 'text', parameter_name: 'first_name', text: 'Alicia' },
{ type: 'text', parameter_name: 'discount_code', text: 'JULY25' }
]
},
{
type: 'button',
sub_type: 'url',
index: 0,
parameters: [
{ type: 'text', text: 'promo-code-12345' }
]
}
]
}
]
}
})
});
```
### Media headers
For templates with image/video/document headers:
```typescript theme={null}
await fetch(`https://api.kapso.ai/platform/v1/whatsapp/broadcasts/${broadcastId}/recipients`, {
method: 'POST',
headers: {
'X-API-Key': 'your-api-key',
'Content-Type': 'application/json'
},
body: JSON.stringify({
whatsapp_broadcast: {
recipients: [
{
phone_number: '+14155550123',
components: [
{
type: 'header',
parameters: [
{
type: 'image',
image: {
link: 'https://example.com/promo.jpg'
}
}
]
},
{
type: 'body',
parameters: [
{ type: 'text', parameter_name: 'first_name', text: 'Alicia' },
{ type: 'text', parameter_name: 'discount_code', text: 'JULY25' }
]
}
]
}
]
}
})
});
```
### Response format
```json theme={null}
{
"data": {
"added": 495,
"duplicates": 5,
"errors": [
"Recipient 3: template parameters invalid - Template requires 2 parameters but 1 provided",
"Recipient 7: invalid phone number format"
]
}
}
```
## Step 3: Send broadcast
Trigger async send job:
```typescript theme={null}
const response = await fetch(
`https://api.kapso.ai/platform/v1/whatsapp/broadcasts/${broadcastId}/send`,
{
method: 'POST',
headers: {
'X-API-Key': 'your-api-key'
}
}
);
const { data } = await response.json();
// data.status = 'sending'
// data.started_at = timestamp
```
The broadcast processes asynchronously in the background. Poll for progress.
### Schedule instead of sending now
Send at a future time with `POST /schedule`:
```typescript theme={null}
await fetch(
`https://api.kapso.ai/platform/v1/whatsapp/broadcasts/${broadcastId}/schedule`,
{
method: 'POST',
headers: {
'X-API-Key': 'your-api-key',
'Content-Type': 'application/json'
},
body: JSON.stringify({ scheduled_at: '2025-08-01T09:00:00Z' })
}
);
// data.status = 'scheduled'
```
`scheduled_at` is required. Kapso starts the send automatically at that time.
### Cancel a scheduled broadcast
```typescript theme={null}
await fetch(
`https://api.kapso.ai/platform/v1/whatsapp/broadcasts/${broadcastId}/cancel`,
{
method: 'POST',
headers: {
'X-API-Key': 'your-api-key'
}
}
);
// data.status = 'draft'
```
Cancelling returns the broadcast to `draft` and clears `scheduled_at`. It only works while the broadcast is `scheduled` — to halt one that is already sending, use [Stop a broadcast](#stop-a-broadcast).
## Step 4: Track progress
```typescript theme={null}
const response = await fetch(
`https://api.kapso.ai/platform/v1/whatsapp/broadcasts/${broadcastId}`,
{
headers: {
'X-API-Key': 'your-api-key'
}
}
);
const { data } = await response.json();
```
### Response metrics
```json theme={null}
{
"data": {
"id": "5f6a7b8c-9d0e-1f2a-3b4c-5d6e7f8a9b0c",
"name": "July promo",
"status": "sending",
"total_recipients": 1000,
"sent_count": 650,
"failed_count": 50,
"delivered_count": 600,
"read_count": 320,
"responded_count": 45,
"pending_count": 300,
"response_rate": 6.9,
"started_at": "2025-07-15T10:00:00Z",
"stopped_at": null,
"completed_at": null
}
}
```
### Status transitions
* `draft` → `sending` → `completed`, `failed`, or `stopped`
* `draft` → `scheduled` → `sending`
* `copying` → `draft`, for a broadcast duplicated in the dashboard while its recipients are copied, or → `failed` if the copy could not be completed
* Poll every 5-10 seconds during send
* Check `status`, `completed_at`, and `stopped_at` for terminal states
### Stop a broadcast
Stop a broadcast that is currently sending:
```typescript theme={null}
const response = await fetch(
`https://api.kapso.ai/platform/v1/whatsapp/broadcasts/${broadcastId}`,
{
method: 'PATCH',
headers: {
'X-API-Key': 'your-api-key',
'Content-Type': 'application/json'
},
body: JSON.stringify({
whatsapp_broadcast: {
status: 'stopped'
}
})
}
);
const { data } = await response.json();
// data.status = 'stopped'
// data.stopped_at = timestamp
```
Pending recipients stay pending. Kapso does not start new sends after the broadcast is stopped.
***
## Additional features
### Clear recipients
Remove all recipients from a draft or scheduled broadcast:
```typescript theme={null}
await fetch(`https://api.kapso.ai/platform/v1/whatsapp/broadcasts/${broadcastId}/recipients`, {
method: 'DELETE',
headers: {
'X-API-Key': 'your-api-key'
}
});
```
If the broadcast is scheduled, Kapso returns it to `draft` and clears `scheduled_at`.
### List individual recipients
Get detailed per-recipient status:
```typescript theme={null}
const response = await fetch(
`https://api.kapso.ai/platform/v1/whatsapp/broadcasts/${broadcastId}/recipients?page=1&per_page=20`,
{
headers: {
'X-API-Key': 'your-api-key'
}
}
);
const { data, meta } = await response.json();
```
### Recipient fields
```json theme={null}
{
"id": "8c9d0e1f-2a3b-4c5d-6e7f-8a9b0c1d2e3f",
"phone_number": "14155550123",
"status": "sent",
"sent_at": "2025-07-15T10:05:23Z",
"delivered_at": "2025-07-15T10:05:30Z",
"read_at": "2025-07-15T10:12:45Z",
"responded_at": "2025-07-15T10:15:00Z",
"error_message": null,
"template_components": [
{
"type": "body",
"parameters": [
{ "type": "text", "parameter_name": "first_name", "text": "John" },
{ "type": "text", "parameter_name": "discount", "text": "50%" }
]
}
]
}
```
### Batch processing (large campaigns)
For campaigns with 1,000+ recipients:
```typescript theme={null}
// Split recipients into chunks of 1000
const chunkSize = 1000;
for (let i = 0; i < allRecipients.length; i += chunkSize) {
const chunk = allRecipients.slice(i, i + chunkSize);
await fetch(`https://api.kapso.ai/platform/v1/whatsapp/broadcasts/${broadcastId}/recipients`, {
method: 'POST',
headers: {
'X-API-Key': 'your-api-key',
'Content-Type': 'application/json'
},
body: JSON.stringify({ whatsapp_broadcast: { recipients: chunk } })
});
// Optional: small delay between batches
await new Promise(resolve => setTimeout(resolve, 100));
}
```
# CSV upload
Source: https://docs.kapso.ai/docs/platform/broadcasts/csv-upload
Add broadcast recipients and template parameters from a spreadsheet
Upload a CSV in the dashboard to add recipients to a `draft` or `scheduled` broadcast. One row per recipient.
## Targeting columns
Each row needs one of these:
| Column | Use |
| -------------------------------- | ----------------------------------------------------------------------------------------------- |
| `phone_number` | The recipient's number. Normalized automatically |
| `recipient` | Alternative to `phone_number` |
| `business_scoped_user_id` | Target a [business-scoped user ID](/docs/whatsapp/business-scoped-user-ids) instead of a number |
| `parent_business_scoped_user_id` | Target a parent BSUID |
Rows without a valid target are skipped and reported back with the row that failed. The rest of the file still imports.
## Template parameters
**Every other column becomes a template parameter**, keyed by its header name.
```csv theme={null}
phone_number,first_name,discount
14155550123,John,50%
14155550124,Ana,30%
```
That file fills `{{first_name}}` and `{{discount}}` per recipient. Header names are matched after trimming whitespace, so `first_name` and `first_name` are the same parameter.
## File format
* Separators are detected automatically: comma, semicolon, tab, or pipe
* The file must be UTF-8. Other encodings are rejected with an encoding error
* The first row is the header row
## Limits
Recipients can only be added while the broadcast is `draft` or `scheduled`. To replace a list, clear the existing recipients first — clearing a `scheduled` broadcast returns it to `draft`.
# Overview
Source: https://docs.kapso.ai/docs/platform/broadcasts/overview
Send an approved template to many recipients with per-recipient tracking
A broadcast sends one approved WhatsApp template to a list of recipients, and tracks delivery, reads, and replies for each one.
Broadcasts are template-only. Meta requires an approved template to open a conversation, so free-form text cannot be broadcast.
## Lifecycle
| Status | Meaning |
| ----------- | --------------------------------------------------------------------------------------------------------- |
| `draft` | Being built. Recipients can be added or cleared |
| `scheduled` | Will start automatically at `scheduled_at` |
| `copying` | Duplicated from another broadcast, recipients still being copied. Shown as **Preparing** in the dashboard |
| `sending` | Currently delivering |
| `completed` | All recipients processed |
| `stopped` | Manually stopped mid-send |
| `failed` | Could not complete |
Two paths through it:
* `draft` → `sending` → `completed`, `failed`, or `stopped`
* `draft` → `scheduled` → `sending` → …
The transitions are enforced:
* Scheduling requires a `scheduled_at` timestamp
* Recipients can only be added or cleared while `draft` or `scheduled`
* Cancelling only works while `scheduled`, and returns the broadcast to `draft`
* Stopping only works while `sending`. Pending recipients stay pending and are not sent
* A `draft` or `scheduled` broadcast with a CSV upload or contact import still running rejects those changes with `422` until the import finishes
* A `copying` broadcast rejects them for the same reason, until its recipients have been copied
## Add recipients
Two ways, and they produce the same result:
Upload a spreadsheet in the dashboard. No code.
Create and send programmatically.
The API accepts up to 1000 recipients per request. Send more by repeating the call.
## Duplicate a broadcast
Duplicate an existing broadcast from the broadcast list or its details page. The copy keeps the
template, template parameters, agent, and every recipient with their personalized values, and
starts as a new `draft` with delivery state reset — no counts, timestamps, or per-recipient results
are carried over.
Large recipient lists are copied in the background. The copy stays in `copying` until every
recipient has been written, and cannot be edited, sent, or scheduled until then. If the copy
cannot be completed it becomes `failed` rather than `draft`.
While a copy is being prepared, the source broadcast is also locked: it cannot be edited, sent,
scheduled, or deleted until the copy finishes.
## Metrics
Every broadcast exposes running counts:
| Field | Meaning |
| ------------------ | ------------------------------------------------------------ |
| `total_recipients` | Recipients on the list |
| `pending_count` | Not yet sent |
| `sent_count` | Accepted by WhatsApp |
| `delivered_count` | Reached the device |
| `read_count` | Opened by the recipient |
| `failed_count` | Rejected or undeliverable |
| `suppressed_count` | Skipped because the contact stopped marketing on this number |
| `responded_count` | Recipients who replied |
| `response_rate` | `responded_count` as a percentage of `sent_count` |
Per-recipient status, timestamps, and error messages are available through [List individual recipients](/docs/platform/broadcasts/api#list-individual-recipients).
## Marketing opt-outs
A contact can tell WhatsApp to stop receiving marketing messages from one of your numbers. Kapso
records that per number and enforces it on marketing template sends.
Recipients of a marketing broadcast who have stopped get the `suppressed` status. Nothing is sent
to them, and they are not charged. They stay out of the `sent_count` and `failed_count`
denominators, so delivery and failure rates describe only the messages you actually attempted.
The opt-out applies to marketing templates only. Utility and authentication templates, and
free-form replies inside an open session, are unaffected. It is also scoped to the number the
contact stopped: a broadcast from a different number reaches them normally.
Subscribe to [`whatsapp.contact.marketing_preference_changed`](/docs/platform/webhooks/message-events#whatsapp-contact-marketing_preference_changed)
to mirror these opt-outs into your own system, or check a contact before sending with the
[marketing preferences API](/docs/whatsapp/templates/marketing-opt-outs#check-a-contacts-preference).
See [Marketing opt-outs](/docs/whatsapp/templates/marketing-opt-outs) for the full behavior.
## Delivery pacing
Kapso batches sends automatically and paces them to stay within WhatsApp's throughput limits. Batch size scales with the size of the list, so you do not need to throttle your own requests or split large lists.
# Create a Meta app
Source: https://docs.kapso.ai/docs/platform/create-meta-app
Create the Meta app used by Kapso for WhatsApp setup
Create a Meta app when you want Kapso to use your own Meta app credentials.
## At a glance
1. [Create the app](#create-the-app) and copy its App ID and App Secret.
2. [Add the credentials in Kapso](#add-it-in-kapso) if Kapso will run messaging.
3. [Continue with your setup path](#what-comes-next): connect your own numbers or onboard customers.
This app can be used for two different flows:
| Flow | What you need from this guide |
| --------------------------------------------------------------------------------------- | ---------------------------------------------------------- |
| [Manual phone number setup](/docs/platform/manual-phone-number-setup) | App ID and App Secret |
| [Customer setup links with your own app](/docs/platform/onboard-customers-own-meta-app) | App ID, App Secret, and later an embedded signup config ID |
## Create the app
1. Open [Meta for Developers](https://developers.facebook.com/apps/).
2. Create a new app.
3. Choose the **Connect with Customers through WhatsApp** use case.
4. Select the Business Portfolio that owns the number, or the portfolio where you will add the number.
5. Finish the app setup.
6. Open **App settings > Basic**.
7. Copy the **App ID**.
8. Reveal and copy the **App Secret**.
## Add it in Kapso
Complete this step if Kapso will run messaging. If you will use [your own infrastructure with MPS](/docs/platform/tech-providers/your-infrastructure), keep the credentials on your backend instead.
1. In Kapso, open your project.
2. Go to **Connected numbers > Meta apps**.
3. Click **Add Meta app**.
4. Paste the **App ID** from **App settings > Basic** in Meta.
5. Paste the **App Secret**.
6. Save
## What comes next
For connecting numbers in a Business Portfolio you manage, continue with [Manual phone number setup](/docs/platform/manual-phone-number-setup).
For customer onboarding through setup links, continue with [Onboard customers with your own Meta app](/docs/platform/onboard-customers-own-meta-app).
# Onboard customers
Source: https://docs.kapso.ai/docs/platform/customer-guide
Enable WhatsApp for your customers
Kapso Platform lets your customers connect their own WhatsApp Business accounts without sharing credentials. Each customer uses their own number while you handle the automation.
## Quick example
```javascript theme={null}
// 1. Create customer
const customer = await createCustomer({ name: 'Acme Corp' });
// 2. Setup link
const setupLink = await generateSetupLink(customer.id);
console.log(`Setup link: ${setupLink.url}`);
// 3. Send message
await sendWhatsAppMessage({
customer_id: customer.id,
phone_number: '+1234567890',
content: 'Order confirmed!'
});
```
## Use cases
Perfect when you need your customers to use their own WhatsApp:
* **CRM platforms** - Let each client connect their WhatsApp for customer communication
* **Appointment booking** - Clinics and salons send reminders from their own number
* **E-commerce tools** - Stores send order updates using their WhatsApp Business
* **Marketing platforms** - Agencies manage multiple client WhatsApp accounts
* **Support software** - Each company provides support through their WhatsApp
## Step 1: Create a customer
```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 Corporation",
"external_customer_id": "CUS-12345"
}
}'
```
Response:
```json theme={null}
{
"data": {
"id": "customer-abc123",
"name": "Acme Corporation",
"external_customer_id": "CUS-12345"
}
}
```
## Step 2: Generate setup link
```bash theme={null}
curl -X POST https://api.kapso.ai/platform/v1/customers/customer-abc123/setup_links \
-H "X-API-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"setup_link":{"meta_billing_mode":"partner_managed"}}'
```
Response:
```json theme={null}
{
"data": {
"id": "link-xyz789",
"url": "https://app.kapso.ai/whatsapp/setup/aBcD123...",
"expires_at": "2024-03-15T10:00:00Z"
}
}
```
Share the `url` with your customer. They'll click it, log in with Facebook, and connect their WhatsApp in \~5 minutes.
## Step 3: Send messages
After setup completes, use the customer's `phone_number_id` to send messages:
```bash theme={null}
curl -X POST https://api.kapso.ai/meta/whatsapp/v24.0/110987654321/messages \
-H "X-API-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"messaging_product": "whatsapp",
"to": "15551234567",
"type": "text",
"text": {
"body": "Your order has been shipped!"
}
}'
```
## JavaScript example
```javascript theme={null}
const KAPSO_API_KEY = 'YOUR_API_KEY';
const PLATFORM_API_URL = 'https://api.kapso.ai/platform/v1';
const WHATSAPP_API_URL = 'https://api.kapso.ai/meta/whatsapp';
async function onboardCustomer(customerData) {
// 1. Create customer
const customer = await fetch(`${PLATFORM_API_URL}/customers`, {
method: 'POST',
headers: {
'X-API-Key': KAPSO_API_KEY,
'Content-Type': 'application/json'
},
body: JSON.stringify({ customer: customerData })
}).then(r => r.json());
// 2. Generate setup link
const setupLink = await fetch(
`${PLATFORM_API_URL}/customers/${customer.data.id}/setup_links`,
{
method: 'POST',
headers: {
'X-API-Key': KAPSO_API_KEY,
'Content-Type': 'application/json'
},
body: JSON.stringify({
setup_link: { meta_billing_mode: 'partner_managed' }
})
}
).then(r => r.json());
return setupLink.data.url;
}
async function sendMessage(phoneNumberId, recipientPhone, message) {
return fetch(`${WHATSAPP_API_URL}/v24.0/${phoneNumberId}/messages`, {
method: 'POST',
headers: {
'X-API-Key': KAPSO_API_KEY,
'Content-Type': 'application/json'
},
body: JSON.stringify({
messaging_product: 'whatsapp',
to: recipientPhone,
type: 'text',
text: { body: message }
})
});
}
```
## What customers see
1. Click your setup link
2. Log in with Facebook
3. Connect their WhatsApp Business
4. Verify phone number
5. Done - you can now send messages
The entire process takes \~5 minutes.
## Coding with AI agents
To build your platform integration faster, add Kapso skills to your AI coding assistant:
```bash theme={null}
npx skills add gokapso/agent-skills
```
For documentation context, pass [docs.kapso.ai/llms.txt](https://docs.kapso.ai/llms.txt) or connect via MCP:
```json Cursor theme={null}
{ "mcpServers": { "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
```
```bash Codex theme={null}
codex mcp add kapso-docs --url https://docs.kapso.ai/mcp
```
```json VS Code theme={null}
{ "servers": { "kapso-docs": { "type": "http", "url": "https://docs.kapso.ai/mcp" } } }
```
## Next steps
* [Setup links](/docs/platform/setup-links/create-and-configure) - Customize redirect URLs, branding, connection types
* [Connection detection](/docs/platform/setup-links/detect-connection) - Know when customers connect
* [Webhooks](/docs/platform/webhooks/overview) - Handle real-time message events
# Project events
Source: https://docs.kapso.ai/docs/platform/events
Store timestamped data points and trigger workflows from them
Project events let you store timestamped data points in Kapso, then inspect them later or use them to trigger workflows.
> Project events are durable custom records, not [workflow execution events](/docs/flows/events). Workflow execution events are the step-by-step logs used to debug a workflow run.
Use events for product moments and derived data you want to keep over time:
* `lead.qualified`
* `checkout.abandoned`
* `conversation.csat_scored`
* `support.intent_detected`
* `handoff.quality_reviewed`
Events are append-only records during normal operation. Deleting an event definition also permanently deletes its recorded events. They work well for facts, scores, labels, outcomes, and workflow outputs that should be queryable later.
Examples:
* Store an LLM-generated CSAT score after a conversation ends
* Save the detected intent, language, or urgency for a support conversation
* Record whether a handoff was successful
* Keep lead scoring outputs from a qualification workflow
* Store a quality review result for future analysis
## Emit an event
```bash theme={null}
curl -X POST "https://api.kapso.ai/platform/v1/events" \
-H "X-API-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "conversation.csat_scored",
"occurred_at": "2026-06-27T14:30:00Z",
"conversation_id": "770e8400-e29b-41d4-a716-446655440002",
"properties": {
"score": 4,
"reason": "Issue resolved in one reply",
"model": "gpt-4.1"
}
}'
```
Response:
```json theme={null}
{
"data": {
"id": "990e8400-e29b-41d4-a716-446655440004",
"name": "conversation.csat_scored",
"occurred_at": "2026-06-27T14:30:00Z",
"conversation_id": "770e8400-e29b-41d4-a716-446655440002",
"properties": {
"score": 4,
"reason": "Issue resolved in one reply",
"model": "gpt-4.1"
}
}
}
```
`conversation_id` is optional. Include it when the event belongs to a WhatsApp conversation, so workflows and webhooks can keep that context.
## Event format
Event names must be lowercase and can be up to 128 characters:
```text theme={null}
lead.qualified
checkout.abandoned
conversation.csat_scored
```
Properties must be a flat JSON object. Values can be strings, numbers, booleans, or `null`.
Limits:
* Event name: 128 characters
* Properties: 25 keys
* Property key: 64 characters
* String property value: 1 KB
* Total properties payload: 8 KB
* `occurred_at`: no more than 5 minutes in the future
Events older than your project's event retention window are rejected.
## Manage event definitions
Event definitions describe an event name, its meaning, and its expected property schema. Use the Platform API to manage them:
* `GET /platform/v1/event-definitions` lists active definitions, ordered by name. Add `?include_archived=true` to include archived definitions.
* `POST /platform/v1/event-definitions` creates a definition or updates the editable metadata of an existing definition with the same name. Upserting an archived definition restores it.
* `GET /platform/v1/event-definitions/:id` retrieves one definition.
* `PATCH /platform/v1/event-definitions/:id` updates metadata. Set `archived` to `true` to archive a definition or `false` to restore it.
* `DELETE /platform/v1/event-definitions/:id` queues permanent deletion of the definition and all events recorded with it. The endpoint returns `202 Accepted`; deletion happens asynchronously and cannot be undone. While deletion is pending, new events with the same name are rejected.
### Archive and restore
Archive a definition without deleting its recorded events:
```bash theme={null}
curl -X PATCH "https://api.kapso.ai/platform/v1/event-definitions/EVENT_DEFINITION_ID" \
-H "X-API-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"archived": true}'
```
Archived definitions retain their historical events and expose `archived_at`, but are hidden from default definition lists, event tools, and workflow selectors. Restore one by setting `archived` to `false`, or by creating a definition with the same name. Archiving does not prevent the Events API from recording another event with the same name.
### Permanently delete
Delete a definition and all of its recorded events:
```bash theme={null}
curl -X DELETE "https://api.kapso.ai/platform/v1/event-definitions/EVENT_DEFINITION_ID" \
-H "X-API-Key: YOUR_API_KEY"
```
Deletion is asynchronous. A successful request returns `202 Accepted`, permanently removes the definition and its recorded events, and cannot be undone.
## Query events
```bash theme={null}
curl "https://api.kapso.ai/platform/v1/events?name=conversation.csat_scored&occurred_after=2026-06-01T00:00:00Z&limit=20" \
-H "X-API-Key: YOUR_API_KEY"
```
Available filters:
* `name`
* `conversation_id`
* `occurred_after`
* `occurred_before`
Results are ordered newest first. Include `limit` to use cursor pagination with `after` and `before`.
## Use in workflows
Workflows can start from project events and emit them as part of their execution. See [Use project events in workflows](/docs/flows/project-events) for trigger configuration, workflow context, limits, and the available emission methods.
## Webhooks
Project-scoped webhooks can subscribe to `project.event` to receive emitted events.
`project.event` is only available on project webhooks. Phone-number webhooks do not receive custom project events.
Subscribing a webhook to `project.event` requires project events to be available on your plan.
## Plans and usage
Project events are plan-gated and counted against your monthly project event allowance. When project events are not available on your plan, creating or updating event definitions and `project.event` webhook subscriptions returns `402 Payment Required`. When the allowance is exceeded, event emission returns `402 Payment Required` unless your plan allows metered overage.
# Findings
Source: https://docs.kapso.ai/docs/platform/findings
Detect recurring problems in your conversations and investigate them with AI
Findings reviews completed WhatsApp conversations with AI, records what it sees as structured events, and groups the recurring ones into findings you can investigate.
## How it works
1. A managed workflow evaluates each conversation when it ends and emits [project events](/docs/platform/events) such as `conversation.user_frustrated`.
2. Kapso compares the last 7 days against the previous 28 and opens a finding when a signal rises, falls, or keeps recurring.
3. The Kapso Agent Investigator reads the evidence behind the finding, inspects your workflows, executions, logs, and connected repositories, and reports the cause with suggested fixes.
4. You apply a fix and mark the finding as addressed. Kapso then watches new conversations to see whether the problem comes back.
## Turn it on
Open **Findings** in the sidebar and complete the setup. It creates two components in your project:
* **Default event evaluator** — a workflow triggered by `whatsapp.conversation.ended` that reads the transcript and emits events. It never replies to the customer and takes no outbound action.
* **Kapso Agent Investigator** — a Kapso Agent mode that investigates a finding when you start an investigation.
During setup you can also review up to 50 conversations from the last 7 days, so you get findings without waiting for new traffic.
Both components use AI credits when they run. Findings needs project events on your plan, and only evaluates conversations on production numbers.
## Default events
The evaluator emits these events. Each one carries a short `reason` property with the evidence for it.
| Event | Emitted when |
| ------------------------------------------------ | ------------------------------------------------------------------------------------------ |
| `conversation.user_frustrated` | The user clearly expressed frustration, dissatisfaction, or loss of confidence |
| `conversation.positive_outcome` | The user said the problem was solved, or expressed gratitude |
| `conversation.agent_response_wrong` | The agent made a specific wrong claim and the user corrected it |
| `conversation.unresolved` | The conversation ended without answering the user's request or giving a concrete next step |
| `conversation.security.prompt_injection_attempt` | The user tried to override agent instructions or manipulate the runtime |
A conversation can emit zero, one, or several of them. The evaluator optimizes for precision: with ambiguous evidence it emits nothing.
The evaluator treats the transcript as untrusted data and never follows instructions found inside it.
## How findings are detected
Kapso refreshes findings once a day. It compares a **current window** of the last 7 days against a **baseline** of the 28 days before it, counting distinct conversations.
| Direction | Meaning |
| ----------- | ------------------------------------------------------------------------------- |
| `rising` | The signal happens in a larger share of conversations than the baseline |
| `falling` | A positive signal happens in a smaller share of conversations than the baseline |
| `recurring` | The signal keeps appearing, with no baseline to compare against |
A rising finding needs all of:
* At least 30 evaluated conversations in both the current and the baseline window
* At least 5 affected conversations
* An affected rate of at least 10%
* A rate at least 5 points above the baseline
* Evidence on at least 2 different days, with something in the last 2 days
A falling finding needs a rate at least 5 points below the baseline on at least 2 days, and a shortfall of at least 5 conversations.
When there isn't enough history for a baseline, a signal seen in at least 5 conversations over the last 2 days opens a recurring finding instead. Prompt injection attempts are always tracked as recurring.
Recurring findings have no baseline comparison. What matters is that the problem keeps appearing.
A finding that stops qualifying goes quiet and leaves the list: after 7 days without new qualifying evidence, or 14 days once it's under investigation or monitoring.
## Investigate with AI
Open a finding to see the evidence behind it: the affected conversations, the events that qualified it, and the workflow they run through.
Click **Investigate with AI** to run the Kapso Agent Investigator. It reads the finding evidence, compares affected conversations against successful ones, follows them into workflow executions and execution events, searches your logs, and inspects any repositories connected to the project.
The investigation is read-only. It never messages customers, changes configuration, edits repositories, or starts workflows.
When it finishes, the finding shows:
* **Summary** — what is broken and why
* **Causes** — each with an explanation, a confidence of low, medium, or high, and evidence references
* **Suggested fixes** — at most three, each with the target, the change, and how to verify it
* **Coverage limitations** — what the investigator could not verify, so you can weigh it before changing anything
Evidence references link back to the source: the conversation, the workflow, the execution, the project events, or the log entry.
An investigation that fails or times out can be retried, up to 3 attempts for the same evidence. A new investigation also becomes available when new evidence arrives after the last one ran.
A retry triggered by new evidence only reads what arrived after the previous investigation. It starts from that investigation's conclusion and reports how it changed, as one of `unchanged`, `narrowed`, `revised`, `replaced`, or `insufficient_evidence`, with a short explanation.
## Act on a finding
**Mark as addressed** after you apply a fix. Kapso records the current metrics as a baseline and starts monitoring:
* If the signal doesn't qualify again, the finding resolves.
* If it qualifies again, the finding is marked as not improved and comes back.
You can only mark a finding as addressed once an investigation has completed and covers the current evidence.
**Dismiss** a finding you don't want to see. A note is required. If monitoring later detects the problem again, the finding can reopen.
## Custom events
Findings works on your own [project events](/docs/platform/events), not just the default ones. Any active event definition with recent data is included in the daily refresh.
Events linked to a conversation are measured as a rate over conversations. Events without a conversation are measured by volume, and qualify when the current window exceeds the expected count by at least 50% and 5 events.
## From the Kapso Agent and MCP
Findings are also available to agents through the `findings` tool on the [Kapso MCP server](/docs/whatsapp/mcp), with the actions `list`, `get`, `read_evidence`, `start_investigation`, `dismiss`, and `mark_addressed`.
## From the API
The [Platform API](/api/platform/v1/findings/list-findings) exposes the same operations with a project-scoped `X-API-Key`. The project comes from the key, so there is no project ID in the path.
```bash theme={null}
curl 'https://api.kapso.ai/platform/v1/findings?limit=20' \
-H 'X-API-Key: '
```
| Endpoint | Purpose |
| ----------------------------------------- | -------------------------------------------------- |
| `GET /findings` | List open and candidate findings, cursor paginated |
| `GET /findings/{id}` | Read one finding with its investigation |
| `GET /findings/{id}/evidence` | Read the evidence behind a finding |
| `POST /findings/{id}/start_investigation` | Queue an investigation |
| `POST /findings/{id}/dismiss` | Dismiss a finding |
| `POST /findings/{id}/mark_addressed` | Start monitoring after a fix |
`GET /findings` returns findings with status `candidate` or `open`. Quiet and dismissed findings are left out, as are findings from sources that Kapso does not read evidence from yet. `GET /findings/{id}` is not filtered — it reads any finding in the project, including quiet and dismissed ones, and caps `related_findings` at 24 entries.
`limit` must be between 1 and 25 and defaults to 20. Anything else returns `400`. Grouped findings stay on the same page, so a page can hold slightly more than `limit` items.
`paging.next` and `paging.previous` are opaque cursors, not URLs. Pass one back as `after` or `before`, never both.
```bash theme={null}
curl 'https://api.kapso.ai/platform/v1/findings?limit=20&after=' \
-H 'X-API-Key: '
```
Starting an investigation returns `202` and runs asynchronously — poll the finding to read the result.
When a result comes from a retry over new evidence, `investigation.result` carries `change_since_previous` with `status` and `explanation`. `GET /findings/{id}/evidence` then also returns `new_evidence` (the items recorded after the previous investigation, each with `id`, `observed_at`, `source_type`, and `new_since_previous`), an `evidence_summary` with the `cutoff` and `new_item_count`, and `cutoff` and `freshness` on `coverage`.
## API errors
Some errors carry a `code` alongside the message. It is how you tell apart the two reasons an endpoint returns `404`:
| Code | Status | Meaning |
| --------------------------- | ------ | ------------------------------------------------------------- |
| `findings_not_enabled` | 404 | Findings is not enabled for the project |
| `finding_not_found` | 404 | No finding with that ID in the project |
| `investigation_unavailable` | 422 | The finding is already being investigated, or is not eligible |
```json theme={null}
{
"error": "Findings is not enabled for this project.",
"code": "findings_not_enabled"
}
```
Validation and pagination errors return `error` only, with no `code`.
# For your team
Source: https://docs.kapso.ai/docs/platform/for-your-team
Use Kapso for your own WhatsApp operations
Use Kapso when your own team needs to run support, sales, operations, or outbound messaging on WhatsApp. The common patterns below cover most internal use cases.
* [Use Kapso as your WhatsApp API](#use-kapso-as-your-whatsapp-api)
* [Use workflows to automate WhatsApp](#use-workflows-to-automate-whatsapp)
* [Connect your team WhatsApp Business apps](#connect-your-team-whatsapp-business-apps)
* [Use Broadcasts for mass messaging](#use-broadcasts-for-mass-messaging)
* [Use the shared team inbox](#use-the-shared-team-inbox)
## Use Kapso as your WhatsApp API
Connect your company WhatsApp numbers to Kapso and use it as your operational WhatsApp layer.
This setup works when you want to:
* send notifications and outbound messages with templates
* receive WhatsApp messages through webhooks
* let AI agents or backend systems operate on conversations
* monitor activity from the inbox, webhook deliveries, and execution logs
This is the best fit when WhatsApp is part of your product or operations stack and you want one API surface for sending, receiving, and observing activity.
## Use workflows to automate WhatsApp
Kapso can also sit on top of your number as the automation layer. Use workflows and functions when you want more than simple message sending.
Typical uses:
* build AI agents, such as a support or qualification agent
* build deterministic workflows for reminders, routing, handoff, and follow-up
* call your own systems through APIs
* combine human and automated handling in the same conversation flow
This is the right setup when you want WhatsApp to behave like an operational system, not just a messaging endpoint.
## Connect your team WhatsApp Business apps
You can also connect the WhatsApp Business apps used by your own team. A common example is a sales team that wants to keep using the WhatsApp Business app while centralizing automation and visibility in Kapso.
With this setup, you can:
* sync your CRM automatically through APIs and webhooks
* analyze conversations with LLMs
* manage multiple numbers from one project
* keep a centralized record of activity instead of relying on each phone separately
This works well when the team still needs the app experience, but you also want shared visibility and programmatic access.
## Use Broadcasts for mass messaging
Use Broadcasts when you need mass outbound messaging with approved templates. This is a better fit than manual inbox sending when the same campaign or notification needs to reach many recipients with tracking and delivery stats.
Typical uses include reminders, promotions, re-engagement campaigns, and operational notifications.
## Use the shared team inbox
Use `inbox.kapso.ai` when your support or operations team needs a dedicated workspace for live conversations. This is especially useful when you want a mix of AI and human support:
* AI can automate parts of the conversation through workflows
* humans can take over through the inbox when needed
* the team can work in one shared interface instead of separate phones
This is often the best setup for support teams that want structured operations without giving every operator full dashboard access.
## Invite unlimited team members
Kapso does not charge per seat. Invite as many people to a project as you need, on any plan.
Each member gets a role:
* `owner`: full access, including billing and adding other owners
* `admin`: manage the project and its members
* `member`: use the dashboard
* `human_agent`: inbox only, no dashboard access
Use `human_agent` for support staff who only handle conversations. Invite from **Project settings → Members**.
## Next steps
Connect your first production number.
Start handling conversations from the shared inbox.
Send approved template campaigns at scale.
Add automation, handoff, and AI-driven flows.
# Automation and handoff
Source: https://docs.kapso.ai/docs/platform/inbox/automation
How the inbox interacts with running workflows
## Conversation modes
| Mode | Indicator | Input |
| ---------------- | ------------------- | --------------------------------------- |
| Normal | No banner | Send freely |
| Workflow running | Workflow tab active | Disabled — click "Handoff" to intervene |
| Handoff | Yellow banner | Send freely |
| Failed | Red banner | Send freely |
When a workflow is running, the message input is disabled. You must handoff the workflow before you can reply manually.
## Handoff
Click **Handoff** when a workflow is running to take control. The workflow pauses immediately and you can reply freely.
Handoff can also be triggered by:
* A **HandoffNode** in the workflow
* The **HandoffTool** called by the AI within a workflow step
### Resume from handoff
From the Workflow tab, click **Resume from Handoff** to set the execution back to `waiting` state. The workflow continues from where it left off.
## Jump to node
Move a workflow execution to a different node without changing its status or variables.
Open the workflow actions dropdown from the Workflow tab and select **Jump to node**. A dialog shows the current node and lets you pick a target.
Requirements:
* Execution must be `running`, `waiting`, or `handoff`
* You must be an **owner** or **admin** on the project
Jump events are recorded in the execution log with actor details.
## Workflow tab
The Workflow tab in the detail sidebar appears when a conversation has workflow executions. It shows:
* **Events log** — Timeline of flow events (step transitions, messages, errors)
* **Observer chat** — Chat view of the workflow execution
* **Actions** — Handoff, Resume from Handoff, Jump to Node, Send message to workflow
* **Execution selector** — Switch between multiple executions if the conversation has more than one
# Embedded inbox
Source: https://docs.kapso.ai/docs/platform/inbox/embedded
Embed the inbox into your own application via iframe
Embed the inbox into your own application via iframe. Useful for CRM integrations, customer portals, or internal tools.
## Setup
### API
Create an embed:
```bash theme={null}
curl -X POST https://api.kapso.ai/platform/v1/inbox_embeds \
-H "X-API-Key: $KAPSO_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"inbox_embed": {
"name": "Support inbox",
"scope_type": "phone_number",
"scope_id": "1234567890",
"allowed_origins": ["https://app.example.com"],
"default_mode": "system",
"language": "es"
}
}'
```
The create response returns `token` and `embed_url` once. Store the `embed_url`; list, get, and update responses do not return it.
Scope types:
| Scope | `scope_id` |
| -------------- | -------------------------- |
| `project` | Leave blank |
| `customer` | Customer UUID |
| `phone_number` | WhatsApp `phone_number_id` |
Manage embeds:
```bash theme={null}
curl https://api.kapso.ai/platform/v1/inbox_embeds \
-H "X-API-Key: $KAPSO_API_KEY"
curl -X PATCH https://api.kapso.ai/platform/v1/inbox_embeds/{id} \
-H "X-API-Key: $KAPSO_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"inbox_embed": {
"allowed_origins": ["https://app.example.com"],
"default_mode": "dark",
"language": "es"
}
}'
curl -X DELETE https://api.kapso.ai/platform/v1/inbox_embeds/{id} \
-H "X-API-Key: $KAPSO_API_KEY"
```
### Dashboard
1. Go to **Project → Inbox Embeds**
2. Create an access token with a name and scope
3. Choose the default UI language for the embedded inbox
4. Copy the embed URL or iframe snippet
## Embed code
```html theme={null}
```
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
## Login with Kapso Flows
# Flow JSON
Source: https://docs.kapso.ai/docs/whatsapp/flows/flow-json
Structure of WhatsApp Flow JSON
Flow JSON defines the screens, components, and navigation of your WhatsApp Flow.
## Structure
```json theme={null}
{
"version": "7.0",
"data_api_version": "3.0",
"routing_model": {
"SCREEN_ONE": ["SCREEN_TWO"],
"SCREEN_TWO": []
},
"screens": [
{
"id": "SCREEN_ONE",
"title": "Welcome",
"layout": {
"type": "SingleColumnLayout",
"children": []
}
}
]
}
```
## Key fields
| Field | Description |
| ------------------ | ------------------------------------------------ |
| `version` | Flow JSON schema version (e.g., `7.0`) |
| `data_api_version` | Data API version for dynamic flows (e.g., `3.0`) |
| `routing_model` | Defines allowed screen transitions |
| `screens` | Array of screen definitions |
## Screen components
Screens contain layouts with children components. Common ones include:
* **TextHeading** - Section headers
* **TextBody** - Paragraph text
* **TextInput** - Single-line input
* **TextArea** - Multi-line input
* **Dropdown** - Select from options
* **RadioButtonsGroup** - Single selection
* **CheckboxGroup** - Multiple selection
* **DatePicker** - Date selection
* **Footer** - Action buttons
Additional components like `Image`, `EmbeddedLink`, `OptIn`, and more are available. See [Meta's components reference](https://developers.facebook.com/docs/whatsapp/flows/reference/components) for the full list.
## Meta documentation
For the complete Flow JSON reference, component properties, and validation rules:
Official documentation from Meta
# Flows in Kapso
Source: https://docs.kapso.ai/docs/whatsapp/flows/kapso-integration
What Kapso handles and what you configure
## What Kapso handles
When you create and manage WhatsApp Flows through Kapso, we handle:
| Feature | Description |
| -------------------------- | ------------------------------------------------ |
| **Flow registration** | Creates and syncs flows with Meta's API |
| **Version tracking** | Immutable history of all Flow JSON changes |
| **Encryption** | Encrypts/decrypts payloads for dynamic flows |
| **Data endpoint URLs** | Generates secure webhook URLs for your functions |
| **Secret management** | Creates and rotates data endpoint secrets |
| **Preview URLs** | Generates preview links to test in browser |
| **Publishing** | Handles the publish flow to Meta |
| **Signature verification** | Validates Meta's webhook signatures |
| **Invocation logging** | Tracks all data endpoint calls and responses |
## What you configure
| Task | Description |
| -------------------------- | ------------------------------------------ |
| **Flow JSON** | Define screens, components, and navigation |
| **WhatsApp phone number** | Required to publish and test flows |
| **Data endpoint function** | Your logic for dynamic flows (optional) |
## Dashboard workflow
1. Go to **WhatsApp** > **Flows** in your project
2. Click **Sync flows** to import existing flows from Meta
3. Open a flow to edit JSON, configure settings, or view responses
4. Use the **Editor** tab to modify Flow JSON
5. Set a phone number in the **Info** tab
6. Click **Publish** to deploy to Meta
# Overview
Source: https://docs.kapso.ai/docs/whatsapp/flows/overview
What are WhatsApp Flows and how to use them with Kapso
WhatsApp Flows are interactive forms that run natively inside WhatsApp. Users tap through screens, fill out fields, and submit data - all without leaving the chat.
WhatsApp Flows require a **Verified Meta Business Portfolio**. [Learn more about verification](https://business.facebook.com/latest/settings/security_center).
## What you can build
* Lead capture forms
* Appointment booking
* Surveys and feedback collection
* Order forms
* Customer support intake
* Registration flows
## How it works
1. You define screens and components in a JSON structure (Flow JSON)
2. A user taps a button in WhatsApp to open your flow
3. They navigate through screens and submit data
4. You receive the response via webhook or store it in Kapso
## Next steps
What Kapso handles vs what you configure
Choose the right mode for your use case
Structure of WhatsApp Flow JSON
Create and manage WhatsApp Flows programmatically
# Sending & receiving
Source: https://docs.kapso.ai/docs/whatsapp/flows/sending-flows
Send WhatsApp Flows and receive responses via webhooks
## As interactive message
Send a flow in a user-initiated conversation (within the 24-hour window).
```typescript TypeScript SDK icon="square-js" theme={null}
import { WhatsAppClient } from '@kapso/whatsapp-cloud-api';
const client = new WhatsAppClient({
baseUrl: 'https://api.kapso.ai/meta/whatsapp',
kapsoApiKey: process.env.KAPSO_API_KEY!
});
await client.messages.sendInteractiveFlow({
phoneNumberId: '647015955153740',
to: '15551234567',
bodyText: 'Book your appointment',
parameters: {
flowId: '1197715005513101',
flowCta: 'Book now',
flowAction: 'navigate',
flowActionPayload: {
screen: 'BOOKING',
data: { available_dates: ['2024-01-15', '2024-01-16'] }
}
}
});
```
```bash REST API icon="code" theme={null}
curl -X POST 'https://api.kapso.ai/meta/whatsapp/v24.0/647015955153740/messages' \
-H 'X-API-Key: YOUR_API_KEY' \
-H 'Content-Type: application/json' \
-d '{
"messaging_product": "whatsapp",
"to": "15551234567",
"type": "interactive",
"interactive": {
"type": "flow",
"body": {
"text": "Book your appointment"
},
"action": {
"name": "flow",
"parameters": {
"flow_message_version": "3",
"flow_id": "1197715005513101",
"flow_cta": "Book now",
"flow_action": "navigate",
"flow_action_payload": {
"screen": "BOOKING",
"data": {
"available_dates": ["2024-01-15", "2024-01-16"]
}
}
}
}
}
}'
```
## As template message
Send a flow outside the 24-hour window using a message template with a FLOW button.
### Create template
```typescript TypeScript SDK icon="square-js" theme={null}
await client.templates.create({
businessAccountId: '123456789',
name: 'booking_flow',
category: 'UTILITY',
language: 'en_US',
components: [
{
type: 'BODY',
text: 'Ready to book your appointment?'
},
{
type: 'BUTTONS',
buttons: [
{
type: 'FLOW',
text: 'Book now',
flowId: '1197715005513101',
flowAction: 'navigate',
navigateScreen: 'BOOKING'
}
]
}
]
});
```
```bash REST API icon="code" theme={null}
curl -X POST 'https://api.kapso.ai/meta/whatsapp/v24.0/123456789/message_templates' \
-H 'X-API-Key: YOUR_API_KEY' \
-H 'Content-Type: application/json' \
-d '{
"name": "booking_flow",
"category": "UTILITY",
"language": "en_US",
"components": [
{
"type": "BODY",
"text": "Ready to book your appointment?"
},
{
"type": "BUTTONS",
"buttons": [
{
"type": "FLOW",
"text": "Book now",
"flow_id": "1197715005513101",
"flow_action": "navigate",
"navigate_screen": "BOOKING"
}
]
}
]
}'
```
### Send template
```typescript TypeScript SDK icon="square-js" theme={null}
await client.messages.sendTemplate({
phoneNumberId: '647015955153740',
to: '15551234567',
template: {
name: 'booking_flow',
language: { code: 'en_US' },
components: [
{
type: 'button',
subType: 'flow',
index: '0',
parameters: [
{
type: 'action',
action: {
flowToken: 'user_123_booking',
flowActionData: {
available_dates: ['2024-01-15', '2024-01-16']
}
}
}
]
}
]
}
});
```
```bash REST API icon="code" theme={null}
curl -X POST 'https://api.kapso.ai/meta/whatsapp/v24.0/647015955153740/messages' \
-H 'X-API-Key: YOUR_API_KEY' \
-H 'Content-Type: application/json' \
-d '{
"messaging_product": "whatsapp",
"to": "15551234567",
"type": "template",
"template": {
"name": "booking_flow",
"language": { "code": "en_US" },
"components": [
{
"type": "button",
"sub_type": "flow",
"index": "0",
"parameters": [
{
"type": "action",
"action": {
"flow_token": "user_123_booking",
"flow_action_data": {
"available_dates": ["2024-01-15", "2024-01-16"]
}
}
}
]
}
]
}
}'
```
## Parameters
| Parameter | Description |
| ------------------- | -------------------------------------------- |
| `flowId` | The unique ID of your published flow |
| `flowCta` | Button text (1-20 characters) |
| `flowAction` | `navigate` (default) or `data_exchange` |
| `flowActionPayload` | Initial screen and data to pass to the flow |
| `flowToken` | Identifier for this flow session - see below |
## Flow token
The `flowToken` is a correlation ID. Meta sends it back with every data endpoint request and when the user completes the flow.
**Default behavior:** If not provided, Kapso uses the `flowId` as the token.
Custom tokens are safe to use. Kapso links a flow response to its flow through the outbound message the reply replies to, not through the token value, so response collection keeps working with any `flowToken`. This applies to flows sent as interactive messages and to flows sent through a template's `FLOW` button.
Responses are stored even when the flow can't be identified — for example when the reply carries no reference to the originating message, or when the project has more than one flow matching the same name.
**When to customize:**
* You're using a data endpoint and need to pass context (e.g., user ID, order ID)
* You're handling response tracking yourself
* The token is available in every data endpoint request via `data_exchange.flow_token`
## Receiving responses
When a user completes a flow, you receive a `whatsapp.message.received` webhook. The message has `type: interactive` with `interactive.type: nfm_reply`.
```json theme={null}
{
"message": {
"id": "wamid.ABC123...",
"from": "15551234567",
"timestamp": "1704067200",
"type": "interactive",
"interactive": {
"type": "nfm_reply",
"nfm_reply": {
"name": "flow",
"body": "Sent",
"response_json": "{\"flow_token\":\"1197715005513101\",\"appointment_date\":\"2024-01-15\",\"appointment_time\":\"10:00\"}"
}
},
"kapso": {
"flow_response": {
"flow_token": "1197715005513101",
"appointment_date": "2024-01-15",
"appointment_time": "10:00"
},
"flow_token": "1197715005513101",
"flow_name": "flow"
}
}
}
```
The `interactive.nfm_reply.response_json` contains the raw JSON string from the flow's final screen. Kapso parses this and adds:
| Field | Description |
| --------------------- | -------------------------------------------- |
| `kapso.flow_response` | Parsed response data from the flow |
| `kapso.flow_token` | The flow token (matches `flowId` by default) |
| `kapso.flow_name` | Always `"flow"` for WhatsApp Flows |
To receive flow responses, subscribe to the `whatsapp.message.received` webhook event and filter for `interactive.type === "nfm_reply"`.
# Static vs dynamic
Source: https://docs.kapso.ai/docs/whatsapp/flows/static-vs-dynamic
Choose between static and dynamic WhatsApp Flows
WhatsApp Flows can operate in two modes: static and dynamic.
## Static flows
All content is defined in the Flow JSON. No server calls during user interaction.
**Use when:**
* Content doesn't change between users
* No real-time data needed
* Simple forms with fixed options
**Example:** A feedback survey with predefined questions and answer choices.
## Dynamic flows
Meta calls your data endpoint to fetch content at runtime. You provide a function that returns screen data.
**Use when:**
* Content varies per user (personalization)
* Real-time data needed (available slots, inventory)
* Options depend on previous answers
* External API integration required
**Example:** An appointment booking flow that shows available time slots from your calendar API.
## How Kapso determines the mode
A flow is dynamic when it has a **data endpoint function** attached. In the dashboard, you'll see a badge:
* **Static** - No data endpoint, Flow JSON only
* **Dynamic** - Data endpoint function configured
## Setting up dynamic flows
1. Open your flow in the dashboard
2. Go to the **Data Endpoint** tab in the editor
3. Follow the setup wizard:
* Enable flows encryption (one-time per phone number)
* Create or select a function
* Deploy and test
Request format, response structure, and timeout limits
# Hermes Agent
Source: https://docs.kapso.ai/docs/whatsapp/hermes-agent
Connect Hermes Agent to WhatsApp with Kapso.
Use `gokapso/hermes-agent-plugin` when a Hermes Agent gateway needs to receive Kapso webhooks, turn inbound WhatsApp messages into Hermes `MessageEvent`s, and send replies through Kapso.
Use [`@kapso/whatsapp-cloud-api`](/docs/whatsapp/typescript-sdk/introduction) directly when you are building your own app and do not need Hermes platform routing.
## Installation
Requires:
| Requirement | Use |
| ------------------------------- | ------------------------------------------------------ |
| Hermes Agent gateway | Runs the Kapso platform adapter. |
| Kapso API key | Sends WhatsApp replies through Kapso. |
| Connected Kapso WhatsApp number | Sender number for replies and cron delivery. |
| Public HTTPS URL | Receives Kapso webhooks. Tailscale Funnel works well. |
| Node.js/npm | Needed only when setup installs or uses the Kapso CLI. |
```bash Public repo theme={null}
hermes plugins install gokapso/hermes-agent-plugin --enable
```
```bash SSH theme={null}
hermes plugins install git@github.com:gokapso/hermes-agent-plugin.git --enable
```
The installer prompts only for `KAPSO_API_KEY`. Webhook secret, phone number, allowlist, and webhook creation happen in the setup command.
Hermes clones the plugin into `~/.hermes/plugins/kapso` and loads it on the next session or gateway restart.
If your Hermes environment does not already include `aiohttp`:
```bash theme={null}
pip install aiohttp
```
## Setup
Recommended setup:
```bash theme={null}
hermes kapso setup --install-cli --funnel-url https://
```
The setup command can install the Kapso CLI, list connected WhatsApp numbers, ask which number Hermes should use, ask for your WhatsApp user ID, generate `KAPSO_WEBHOOK_SECRET`, and create the phone-number webhook in Kapso.
The created webhook points at:
```text theme={null}
https:///kapso/webhook
```
Non-interactive setup:
```bash theme={null}
hermes kapso setup \
--api-key "$KAPSO_API_KEY" \
--phone-number-id "1041695002363992" \
--home-channel "15551234567" \
--allowed-users "15551234567" \
--funnel-url "https://" \
--configure-webhook \
--install-cli \
--no-prompt
```
Restart the gateway after enabling the plugin or changing env vars:
```bash theme={null}
hermes gateway restart
hermes gateway status
```
Check setup:
```bash theme={null}
hermes kapso status
curl http://127.0.0.1:8648/health
curl https:///health
```
## Environment
The setup command writes these values to `~/.hermes/.env`. Manual env vars also work:
```bash theme={null}
KAPSO_API_KEY=kapso_...
KAPSO_WEBHOOK_SECRET=shared_webhook_secret
KAPSO_PHONE_NUMBER_ID=1041695002363992
KAPSO_HOME_CHANNEL=15551234567
KAPSO_ALLOWED_USERS=15551234567
KAPSO_WEBHOOK_URL=https:///kapso/webhook
```
| Variable | Required | Use |
| ----------------------- | ----------------------- | ---------------------------------------------------------------------- |
| `KAPSO_API_KEY` | Yes | Sends outbound WhatsApp messages. |
| `KAPSO_WEBHOOK_SECRET` | Yes for signed webhooks | Verifies signed Kapso webhooks. Generated by setup if missing. |
| `KAPSO_PHONE_NUMBER_ID` | Recommended | Default sender number ID for replies, manual sends, and cron delivery. |
| `KAPSO_HOME_CHANNEL` | No | Default WhatsApp recipient for `deliver=kapso` cron jobs. |
| `KAPSO_ALLOWED_USERS` | No | Comma-separated WhatsApp user IDs allowed to talk to the bot. |
| `KAPSO_ALLOW_ALL_USERS` | No | Set `true` for development-only open access. |
| `KAPSO_WEBHOOK_URL` | No | Public webhook URL saved by setup for status and debugging. |
| `KAPSO_BASE_URL` | No | Defaults to `https://api.kapso.ai/meta/whatsapp`. |
| `KAPSO_GRAPH_VERSION` | No | Defaults to `v24.0`. |
| `KAPSO_HOST` | No | Webhook bind host. Defaults to `0.0.0.0`. |
| `KAPSO_PORT` | No | Webhook port. Defaults to `8648`. |
| `KAPSO_WEBHOOK_PATH` | No | Webhook path. Defaults to `/kapso/webhook`. |
For unsigned local fixtures only:
```bash theme={null}
KAPSO_VERIFY_WEBHOOK_SIGNATURES=false
```
Allow a WhatsApp user later:
```bash theme={null}
hermes kapso setup --allowed-users 15551234567 --no-prompt
hermes gateway restart
```
For local testing only:
```bash theme={null}
hermes kapso setup --allow-all-users --no-prompt
hermes gateway restart
```
## Kapso webhook
The adapter listens on `0.0.0.0:8648` and accepts `POST /kapso/webhook` by default.
With Tailscale Funnel:
```bash theme={null}
tailscale funnel reset
tailscale funnel --bg http://127.0.0.1:8648
tailscale funnel status
```
The setup command can create the webhook for you. If webhook creation fails, create a phone-number scoped Kapso webhook manually:
| Setting | Value |
| --------------- | ------------------------------------------ |
| Scope | WhatsApp phone number |
| Endpoint URL | `https:///kapso/webhook` |
| Events | `whatsapp.message.received` |
| Kind | `kapso` |
| Payload version | `v2` |
| Secret | Same value as `KAPSO_WEBHOOK_SECRET` |
Use a public HTTPS URL. `localhost` and private network URLs cannot receive Kapso Cloud webhooks.
Health check:
```bash theme={null}
curl http://localhost:8648/health
```
## Send and receive
Inbound Hermes sessions use encoded chat IDs:
```text theme={null}
kapso::[:]
```
For manual sends or cron delivery, use a plain WhatsApp recipient when `KAPSO_PHONE_NUMBER_ID` is set:
```text theme={null}
15551234567
```
Or include the sender phone number ID explicitly:
```text theme={null}
:15551234567
```
Text replies are split at WhatsApp's 4096-character limit. Markdown links become `label (url)`, and `**bold**` becomes WhatsApp `*bold*`.
## Media
Inbound images are downloaded through Kapso, cached locally, and attached to Hermes `MessageEvent.media_urls` for native vision processing.
Inbound audio and voice notes are downloaded through Kapso, cached locally as audio media, and attached to `MessageEvent.media_urls` for Hermes STT. WhatsApp voice notes are cached as `.ogg`.
For local, no-key transcription:
```bash theme={null}
~/.hermes/hermes-agent/venv/bin/python -m pip install -U faster-whisper
hermes config set stt.provider local
hermes config set stt.local.model base
hermes gateway restart
```
For OpenAI transcription:
```bash theme={null}
printf '\nVOICE_TOOLS_OPENAI_KEY=sk-...\n' >> ~/.hermes/.env
hermes config set stt.provider openai
hermes config set stt.openai.model gpt-4o-mini-transcribe
hermes gateway restart
```
Other non-image media currently lands as captions or descriptive placeholders.
## YAML config
Environment variables are the easiest path. YAML config also works:
```yaml theme={null}
gateway:
platforms:
kapso:
enabled: true
extra:
api_key: "kapso_..."
webhook_secret: "shared webhook secret"
phone_number_id: "1234567890"
```
## Troubleshooting
| Issue | Check |
| ------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------- |
| Plugin installed but no webhooks arrive | Run `hermes gateway restart`, check `hermes gateway status`, and confirm the public URL points to `/kapso/webhook`. |
| Webhook returns `401` | `KAPSO_WEBHOOK_SECRET` does not match the Kapso webhook secret, or the signature header is missing. |
| Outbound sends fail with missing phone number ID | Set `KAPSO_PHONE_NUMBER_ID` or send to `:15551234567`. |
| Bot should not answer everyone | Set `KAPSO_ALLOWED_USERS` and do not set `KAPSO_ALLOW_ALL_USERS=true` outside local development. |
| Images do not reach the agent | Tail gateway logs for `cached inbound image`; confirm the webhook payload includes `kapso.mediaUrl`, `kapso.media_url`, or an image media `id`. |
| Voice notes do not transcribe | Confirm audio was cached under `~/.hermes/audio_cache` or `~/.hermes/cache/audio`, then check the Hermes STT provider config. |
| Kapso CLI is missing | Run `hermes kapso install-cli` or `npm install -g @kapso/cli`. |
## Source
Source code, plugin metadata, and issue tracker for the Kapso Hermes Agent plugin.
# MCP server
Source: https://docs.kapso.ai/docs/whatsapp/mcp
Let AI agents operate your WhatsApp numbers through MCP.
Project MCP gives AI agents live tools to send messages, read conversations, manage templates, and configure webhooks on your WhatsApp numbers.
Use it when an MCP-capable agent should operate a WhatsApp number without shell access — read inbound messages, send replies, manage templates, configure webhooks, and provision new numbers.
Endpoint:
```text theme={null}
https://api.kapso.ai/mcp
```
## Authentication
Choose the auth option your MCP client supports:
* Browser sign-in: configure the MCP URL, sign in to Kapso, and choose one project.
* API key: create a project API key in Kapso and pass it as a header.
Both options give the agent access to the same Project MCP tools.
Accepted API key headers:
```http theme={null}
Authorization: Bearer YOUR_PROJECT_API_KEY
```
```http theme={null}
X-API-Key: YOUR_PROJECT_API_KEY
```
Use `Authorization: Bearer` for bearer-token clients. Use `X-API-Key` when your MCP client lets you configure custom headers.
## Connect
### Browser sign-in
For clients that support browser sign-in, configure only the MCP URL.
```bash Codex theme={null}
codex mcp add kapso --url https://api.kapso.ai/mcp
codex mcp login kapso
```
```bash Claude Code theme={null}
claude mcp add --transport http kapso https://api.kapso.ai/mcp
# Then run inside Claude Code:
/mcp
```
```json Cursor theme={null}
{
"mcpServers": {
"kapso": {
"url": "https://api.kapso.ai/mcp"
}
}
}
```
When Kapso opens in the browser, sign in and select the project the client should use.
### API key
Set your project API key:
```bash theme={null}
export KAPSO_API_KEY="your_project_api_key"
```
```bash Codex theme={null}
codex mcp add kapso --url https://api.kapso.ai/mcp --bearer-token-env-var KAPSO_API_KEY
```
```bash Claude Code theme={null}
claude mcp add --transport http kapso https://api.kapso.ai/mcp \
--header "Authorization: Bearer $KAPSO_API_KEY"
```
```json Cursor theme={null}
{
"mcpServers": {
"kapso": {
"url": "https://api.kapso.ai/mcp",
"headers": {
"Authorization": "Bearer ${env:KAPSO_API_KEY}"
}
}
}
}
```
### Use X-API-Key instead
Codex CLI has no `--header` flag, so X-API-Key goes in `~/.codex/config.toml`. For Cursor and Claude Code, just swap the header.
```toml Codex theme={null}
[mcp_servers.kapso]
url = "https://api.kapso.ai/mcp"
env_http_headers = { "X-API-Key" = "KAPSO_API_KEY" }
```
```bash Claude Code theme={null}
claude mcp add --transport http kapso https://api.kapso.ai/mcp \
--header "X-API-Key: $KAPSO_API_KEY"
```
```json Cursor theme={null}
{
"mcpServers": {
"kapso": {
"url": "https://api.kapso.ai/mcp",
"headers": {
"X-API-Key": "${env:KAPSO_API_KEY}"
}
}
}
}
```
## Tools
Project MCP exposes grouped tools. Most tools accept an `action` and `params`.
| Tool | Actions |
| ------------------------ | ------------------------------------------------------------------------------------------ |
| `status` | Shows auth, project status, customer count, WhatsApp number count, and next steps |
| `search_docs` | Searches Kapso and WhatsApp docs |
| `customers` | `help`, `list`, `get`, `create`, `update`, `delete` |
| `setup_links` | `help`, `list`, `create`, `update`, `revoke` |
| `whatsapp_numbers` | `help`, `list`, `get`, `resolve`, `health`, `start_setup`, `create`, `update`, `delete` |
| `whatsapp_conversations` | `help`, `list`, `get`, `set_status` |
| `whatsapp_messages` | `help`, `list`, `get`, `send`, `mark_read` |
| `whatsapp_templates` | `help`, `list`, `get`, `create` |
| `whatsapp_webhooks` | `help`, `list`, `get`, `create`, `update`, `delete` |
| `findings` | `help`, `list`, `get`, `read_evidence`, `start_investigation`, `dismiss`, `mark_addressed` |
Use `action: "help"` on grouped tools to get required params, optional params, and examples.
## Examples
Ask your agent to check setup:
```text theme={null}
Use the kapso MCP server to check my project status and tell me the next action.
```
Ask your agent to create a setup link:
```text theme={null}
Use kapso setup_links with action=create for the default customer, then return the setup URL.
```
Ask your agent to read and send WhatsApp messages:
```text theme={null}
Use kapso whatsapp_conversations to list recent active conversations, then use whatsapp_messages to read the latest inbound messages.
```
```text theme={null}
Use kapso whatsapp_messages with action=send to send "Hello from Kapso" to +15551234567 from my connected WhatsApp number.
```
## Project MCP vs Docs MCP
| Server | Endpoint | Use it for |
| ----------- | --------------------------- | ------------------------------------------------------------- |
| Project MCP | `https://api.kapso.ai/mcp` | Operating WhatsApp numbers with browser sign-in or an API key |
| Docs MCP | `https://docs.kapso.ai/mcp` | Documentation lookup only |
Use the [CLI](/docs/whatsapp/cli) when your agent has terminal access. Use Project MCP when your agent supports MCP and should operate Kapso directly.
# Meta message billing
Source: https://docs.kapso.ai/docs/whatsapp/meta-message-billing
Choose how Meta's WhatsApp message fees are paid
| | Kapso credits | Pay Meta directly |
| --------------------- | ------------------------------------------------------------------------------ | -------------------------------------------------- |
| Who pays Meta | Kapso | Your business |
| Where you are charged | Kapso project credits | Meta payment method |
| Price | Meta's published USD price, plus applicable FX and external MPS fees | Meta's published price |
| Scope | Entire WhatsApp Business Account | Entire WhatsApp Business Account |
| Payment setup | Managed by Kapso | Managed in Meta Billing Hub |
| Insufficient balance | Sends through Kapso pause; sends from your own infrastructure cannot be paused | Controlled by Meta |
| Changing later | Requires support | Managed through Meta, subject to Meta restrictions |
## Separate charges
Your Kapso plan includes a message allowance for using the platform. Meta separately charges for certain delivered WhatsApp messages. A message can count toward your Kapso allowance and also have a Meta fee.
Meta's price depends on the message category and the recipient's market. See [WhatsApp pricing](/docs/whatsapp/pricing-faq) for details.
## Kapso credits
Kapso pays Meta for billable messages and deducts the customer charge from your project credits. For USD WABAs using Kapso messaging infrastructure, the customer charge is Meta's published USD price. Enabled non-USD WABAs also include the FX margin shown in Kapso.
This billing mode applies to the entire WhatsApp Business Account (WABA), including every number in it. All of those numbers must remain in the same Kapso project.
You can connect Kapso billing with a zero balance and add credits later. Potentially paid sends pause when the project does not have enough credits.
For a Multi-partner Solution using the Tech Provider's infrastructure, Kapso does not control the send path. Usage is deducted after Meta reports it through Pricing Analytics, so charges can appear several minutes after messages are sent. Keep enough prepaid balance for usage between checks.
Messages sent through the Tech Provider's infrastructure include a 5% managed billing fee on top of Meta's published USD list rate. This fee is separate from any FX margin.
### Account currency
Meta assigns the billing currency of a WABA, and it cannot be changed in Kapso. Project credits, charges, and invoices stay in USD regardless of that currency.
For USD accounts, no FX margin applies. For non-USD accounts, the charge includes an FX margin (5% at launch). The exact margin for your account is shown on the WhatsApp billing card in your project settings. A separate managed billing fee applies when messages run through the Tech Provider's infrastructure.
Kapso enables non-USD accounts one currency at a time. If your account's currency is not enabled yet, connecting Kapso credits is blocked with `unsupported_currency` and the WABA stays on direct Meta billing.
If Meta changes the account's currency after Kapso billing is connected, potentially paid sends pause until the change is reviewed. Free messages are unaffected.
### Availability
Kapso billing is available for eligible WABAs connected with Kapso's default Meta app. A Tech Provider's own Meta app is also eligible when the WABA belongs to an active [Multi-partner Solution](/docs/platform/tech-providers/multi-partner-solutions) with Kapso. A standalone custom Meta app is not eligible.
After billing is connected, moving the WABA to another project or changing its payment source requires support.
If the WABA already has its own Meta payment method, Meta rejects the Kapso credit line and the WABA stays on direct Meta billing. Setup completes normally and sends are not paused. The setup link's billing mode is updated to `customer_managed`.
## Pay Meta directly
Meta charges the payment method configured for the WABA. Your business manages payment methods, billing details, tax information, and payment failures in Meta Billing Hub.
This mode is also WABA-wide. Meta controls whether its payment source can be changed.
If a message is blocked because Meta cannot collect payment, see [Payment issues](/docs/whatsapp/payment-method-troubleshooting).
# n8n
Source: https://docs.kapso.ai/docs/whatsapp/n8n
Use the official Kapso node in n8n.
Use the official Kapso node when an n8n workflow needs to receive WhatsApp events or send WhatsApp messages through Kapso.
Kapso is [verified by n8n](https://n8n.io/integrations/kapso/) and available from the n8n node panel. Start there. You should not need to install an npm package manually unless your n8n instance does not show verified community nodes.
## Add Kapso in n8n
In n8n:
1. Open a workflow
2. Open the nodes panel
3. Search for `Kapso`
4. Select **Kapso** or **Kapso Trigger**
5. Install it for the instance if n8n asks
Instance owners and admins can install verified community nodes. After that, every user in the n8n instance can use them.
If Kapso does not appear:
* Update your n8n instance to the latest stable version
* Make sure verified community nodes are enabled
* On self-hosted n8n, keep `N8N_COMMUNITY_PACKAGES_ENABLED=true` and `N8N_VERIFIED_PACKAGES_ENABLED=true`
## Manual install fallback
Use this only when Kapso is not available from the n8n node panel.
In **Settings → Community Nodes**, install:
```text theme={null}
@kapso/n8n-nodes-kapso
```
The current latest version is `0.1.3`. You only need to pin it if you want that exact version:
```text theme={null}
@kapso/n8n-nodes-kapso@0.1.3
```
For self-hosted manual installs:
```bash theme={null}
mkdir -p ~/.n8n/nodes
cd ~/.n8n/nodes
npm install @kapso/n8n-nodes-kapso
```
Restart n8n after installing manually.
## Credentials
Create a credential named **Kapso API**.
| Field | Value |
| ------------------ | ---------------------- |
| Base URL | `https://api.kapso.ai` |
| API Key | Kapso project API key |
| Meta Graph Version | `v24.0` |
The node sends the API key as `X-API-Key`.
## Send a message
Add a **Kapso** node.
| Field | Value |
| ---------------------- | ------------------------------------------- |
| Resource | `WhatsApp Message` |
| Operation | `Send Text` |
| Phone Number ID | Your Kapso-connected Meta `phone_number_id` |
| Recipient Phone Number | Destination phone number without `+` |
| Message | Message body |
Use **Send Template** for approved WhatsApp templates. Use **Send Raw Payload** when you need to send a WhatsApp Cloud API payload that the node does not model yet.
## Receive webhooks
Add a **Kapso Trigger** node as the first node in the workflow.
| Field | Value |
| ----------------- | ------------------------------------------------------------- |
| Scope | `Phone Number` |
| Phone Number ID | Meta `phone_number_id` for the sender |
| Events | `whatsapp.message.received` or the events your workflow needs |
| Registration Mode | `Automatic` |
| Verify Signature | Enabled in production |
When the workflow is activated, the node registers the webhook in Kapso automatically.
For local testing, n8n must have a public HTTPS webhook URL. If the generated webhook URL uses `localhost`, restart n8n with `WEBHOOK_URL` set to your public tunnel URL.
Use **Manual** registration only if you already created the webhook in Kapso yourself.
## What you can automate
* Auto-reply when `whatsapp.message.received` fires
* Send a text reply from any workflow
* Send an approved WhatsApp template
* Send a raw WhatsApp Cloud API payload through Kapso
* Mark a message as read, optionally with typing indicator
* List connected WhatsApp phone numbers
* List WhatsApp messages stored by Kapso
## Related docs
* [Kapso on n8n](https://n8n.io/integrations/kapso/)
* [Install verified nodes in n8n](https://docs.n8n.io/integrations/community-nodes/installation/verified-install/)
* [Send messages](/docs/whatsapp/send-messages/text)
* [Templates](/docs/whatsapp/templates/simple-text)
* [Webhooks](/docs/platform/webhooks/overview)
# OpenClaw
Source: https://docs.kapso.ai/docs/whatsapp/openclaw
Connect OpenClaw agents to WhatsApp with Kapso.
Use `@kapso/openclaw-whatsapp` when an OpenClaw agent needs to send and receive WhatsApp messages through Kapso.
Use [`@kapso/whatsapp-cloud-api`](/docs/whatsapp/typescript-sdk/introduction) directly when you are building your own app and do not need OpenClaw channel routing.
## Installation
Requires OpenClaw >= 2026.5.27.
```bash theme={null}
openclaw plugins install clawhub:@kapso/openclaw-whatsapp
openclaw config set 'plugins.allow' '["codex","kapso-whatsapp"]' --strict-json
openclaw config set 'channels["kapso-whatsapp"].enabled' true --strict-json
```
If `plugins.allow` already has entries, keep them and add `kapso-whatsapp`.
OpenClaw resolves the plugin from ClawHub and installs runtime dependencies, including `@kapso/whatsapp-cloud-api` and `@kapso/cli`. No separate `npm install` is needed.
The Kapso CLI is bundled with the plugin:
```bash theme={null}
openclaw kapso-whatsapp cli login
openclaw kapso-whatsapp cli status
```
## Setup
Requires:
| Requirement | Use |
| ------------------------ | --------------------------------------------------------- |
| Kapso API key | Sends messages, resolves numbers, and registers webhooks. |
| WhatsApp sender number | The Kapso-connected number your agent sends from. |
| Public HTTPS gateway URL | Receives Kapso webhooks. |
| Webhook secret | Verifies inbound webhook signatures. |
After your OpenClaw gateway has a public URL, let the plugin resolve the number, register the webhook, and write config:
```bash theme={null}
openclaw kapso-whatsapp setup \
--api-key "kapso_..." \
--phone-number "+15551234567" \
--webhook-url "https://your-machine.your-tailnet.ts.net/kapso/webhook" \
--webhook-secret "$(openssl rand -hex 32)" \
--default-to "+15551234567" \
--register-webhook \
--write-config
```
If you already know the Kapso/Meta `phone_number_id`, use `--phone-number-id`:
```bash theme={null}
openclaw kapso-whatsapp setup \
--api-key "kapso_..." \
--phone-number-id "1234567890" \
--webhook-url "https://your-openclaw-host.example.com/kapso/webhook" \
--webhook-secret "$(openssl rand -hex 32)" \
--register-webhook \
--write-config
```
Useful setup flags:
| Flag | Use |
| --------------- | ------------------------------------------------------------- |
| `--default-to` | Default outbound WhatsApp recipient. |
| `--allow-from` | Comma-separated inbound allowlist. |
| `--dm-security` | `allowlist`, `open`, or `disabled`. |
| `--dry-run` | Print actions without writing config or registering webhooks. |
| `--json` | Print machine-readable setup output. |
Omit `--write-config` to print the `openclaw config set` commands without applying them.
## Agent setup
The plugin ships an OpenClaw skill named `kapso-whatsapp-setup`.
Check that OpenClaw sees it:
```bash theme={null}
openclaw skills list | grep kapso
openclaw skills info kapso-whatsapp-setup
```
Before asking the agent to finish setup, do the interactive and secret-bearing steps yourself:
```bash theme={null}
openclaw kapso-whatsapp cli login
openclaw kapso-whatsapp cli status
openclaw config set 'channels["kapso-whatsapp"].apiKey' '"kapso_..."' --strict-json
```
Use the browser dashboard for setup visibility:
```bash theme={null}
openclaw dashboard
```
If you prefer the terminal UI, start `openclaw tui` and send:
```text theme={null}
/verbose full
/trace on
/tools verbose
```
Then paste something like this into OpenClaw:
```text theme={null}
Use the kapso-whatsapp-setup skill to finish my Kapso WhatsApp OpenClaw setup.
I already configured my Kapso API key on this server.
I already ran openclaw kapso-whatsapp cli login successfully.
Default outbound recipient: +15551234567
Public gateway: discover it if possible. I am using Tailscale Funnel on this machine.
Kapso WhatsApp sender number: discover it with the Kapso CLI. If there is more than one available number, ask me which one to use.
Please verify the Kapso CLI and @kapso/openclaw-whatsapp plugin are installed, but do not run interactive login. Discover the public webhook URL, resolve the phone_number_id, generate a webhook secret, register the Kapso phone-number webhook for whatsapp.message.received, write the OpenClaw channel config, set the default outbound recipient, run diagnostics, and tell me exactly what remains manual.
```
If you already know the public webhook URL, replace the gateway line with:
```text theme={null}
Public gateway webhook URL: https://your-openclaw-host.example.com/kapso/webhook
```
If you use another public tunnel or reverse proxy, name it:
```text theme={null}
Public gateway: discover it if possible. I am using ngrok on this machine.
```
If you already know the Kapso/Meta number ID, replace the sender-number line with:
```text theme={null}
Kapso/Meta phone_number_id: 1234567890
```
If the Kapso CLI is not logged in, the agent should stop and ask you to run `openclaw kapso-whatsapp cli login` in your terminal. It should not run interactive login for you.
## Public gateway
Kapso Cloud must reach your OpenClaw webhook over the public internet. A private tailnet URL is not enough.
For Tailscale, use Funnel:
```bash theme={null}
mkdir -p ~/.openclaw
openssl rand -base64 32 > ~/.openclaw/gateway-password
chmod 600 ~/.openclaw/gateway-password
openclaw gateway run \
--bind loopback \
--auth password \
--password-file ~/.openclaw/gateway-password \
--tailscale funnel \
--force
```
Use `--password-file` instead of passing a password directly in process arguments.
The Kapso webhook URL should use your Funnel hostname plus `/kapso/webhook`:
```text theme={null}
https://your-machine.your-tailnet.ts.net/kapso/webhook
```
Discover an existing Funnel URL from the machine running OpenClaw:
```bash theme={null}
tailscale funnel status --json
```
## Configure manually
```bash theme={null}
openclaw config set 'channels["kapso-whatsapp"].apiKey' '"kapso_..."' --strict-json
openclaw config set 'channels["kapso-whatsapp"].phoneNumberId' '"1234567890"' --strict-json
openclaw config set 'channels["kapso-whatsapp"].webhookSecret' '"shared webhook secret"' --strict-json
```
Use JSON strings for IDs and secrets, especially digit-only values. Current plugin versions coerce numeric `phoneNumberId` and `defaultTo` values, but JSON strings avoid precision loss for large IDs.
Optional settings:
```bash theme={null}
openclaw config set 'channels["kapso-whatsapp"].baseUrl' '"https://api.kapso.ai/meta/whatsapp"' --strict-json
openclaw config set 'channels["kapso-whatsapp"].webhookPath' '"/kapso/webhook"' --strict-json
openclaw config set 'channels["kapso-whatsapp"].defaultTo' '"+15551234567"' --strict-json
openclaw config set 'channels["kapso-whatsapp"].dmSecurity' '"allowlist"' --strict-json
openclaw config set 'channels["kapso-whatsapp"].allowFrom' '["+15551234567","15551234567"]' --strict-json
```
Kapso webhook sender IDs often arrive as digits-only values, while users usually type E.164 numbers with `+`. Current plugin versions compare both forms as the same sender. Including both is harmless for older installs or raw config edits.
You can also use environment variables:
```bash theme={null}
export KAPSO_API_KEY="kapso_..."
export KAPSO_PHONE_NUMBER_ID="1234567890"
export KAPSO_WEBHOOK_SECRET="shared webhook secret"
export KAPSO_BASE_URL="https://api.kapso.ai/meta/whatsapp"
export KAPSO_WEBHOOK_PATH="/kapso/webhook"
export KAPSO_BOT_USERNAME="support"
export KAPSO_DEFAULT_TO="+15551234567"
```
## Kapso webhook
Create a phone-number scoped Kapso webhook, not a project webhook.
| Setting | Value |
| --------------- | ---------------------------------------------------------------------------------- |
| Method | `POST` |
| URL | `https://your-openclaw-host.example.com/kapso/webhook` |
| Events | `whatsapp.message.received` |
| Secret | Same value as `channels["kapso-whatsapp"].webhookSecret` or `KAPSO_WEBHOOK_SECRET` |
| Payload version | `v2` recommended |
The setup command can register this webhook with `--register-webhook`.
You can also run the bundled Kapso CLI through OpenClaw:
```bash theme={null}
openclaw kapso-whatsapp cli login
openclaw kapso-whatsapp cli status
openclaw kapso-whatsapp cli whatsapp numbers list --output json
openclaw kapso-whatsapp cli whatsapp numbers resolve "+15551234567" --output json
openclaw kapso-whatsapp cli whatsapp webhooks new \
--phone-number-id "1234567890" \
--url "https://your-openclaw-host.example.com/kapso/webhook" \
--event whatsapp.message.received \
--active \
--output json
```
The setup command uses the bundled CLI internally for number resolution, project access checks, and webhook registration.
## Channel targets
The plugin accepts WhatsApp phone-number targets with these prefixes:
```text theme={null}
kapso:+15551234567
kapso-whatsapp:+15551234567
whatsapp:+15551234567
wa:+15551234567
```
Plain E.164 or digits-only phone numbers also work.
Set a default recipient when you want OpenClaw to send through this channel without an explicit target:
```bash theme={null}
openclaw config set 'channels["kapso-whatsapp"].defaultTo' '"+15551234567"' --strict-json
```
## Diagnostics
Check config and webhook registration:
```bash theme={null}
openclaw kapso-whatsapp setup --help
openclaw kapso-whatsapp doctor
openclaw kapso-whatsapp doctor --json
openclaw config get 'channels["kapso-whatsapp"]'
openclaw channels status --channel kapso-whatsapp --probe
```
Tail OpenClaw logs:
```bash theme={null}
openclaw logs --follow --plain --limit 500
openclaw channels logs --lines 500 | grep -Ei 'kapso|whatsapp|media|image|audio|document|webhook|signature'
```
If the gateway prints a log path, tail it directly:
```bash theme={null}
tail -f /tmp/openclaw/openclaw-YYYY-MM-DD.log
```
## Media
Images, videos, and documents can be forwarded when Kapso includes a downloadable media URL, such as `mediaUrl`, `media_data.url`, `media_data.downloadUrl`, or a direct media `link` or `url`.
The selected OpenClaw model/runtime must support that media type.
If Kapso sends only a media ID and no URL, the plugin records that media exists but cannot pass the file bytes to the model. Logs warn about `id but no URL`.
Voice notes and audio messages need a transcription provider. Without one, the agent may only receive audio metadata. Hosted options include OpenAI `gpt-4o-transcribe`, `gpt-4o-mini-transcribe`, or `whisper-1`. Self-hosted ASR can work if your OpenClaw deployment exposes a compatible transcription provider.
## Troubleshooting
### Config values rejected as "must be string"
Use strict JSON strings:
```bash theme={null}
openclaw config set 'channels["kapso-whatsapp"].phoneNumberId' '"1234567890"' --strict-json
openclaw config set 'channels["kapso-whatsapp"].defaultTo' '"+15551234567"' --strict-json
```
### Webhook returns 401
The Kapso webhook secret and `channels["kapso-whatsapp"].webhookSecret` do not match, or the request is missing the Kapso signature header.
### Webhook is reachable but no messages arrive
Confirm the webhook is phone-number scoped and subscribed to `whatsapp.message.received`. Project webhooks do not receive WhatsApp message events.
### Webhook dispatches but no agent session or reply
Check `allowFrom`. Kapso commonly sends inbound sender IDs without a leading `+`, even if your allowlist was configured with E.164 values.
Current plugin versions compare those forms as the same sender. On older installs, include both variants and restart the gateway:
```bash theme={null}
openclaw config set 'channels["kapso-whatsapp"].allowFrom' '["+56975746426","56975746426"]' --strict-json
```
### Images sometimes work and sometimes do not
Check logs for `media=image/url=yes`. If logs say `url=no` or warn that the event had an ID but no URL, the model did not receive the actual image bytes.
## Source
Source code, setup skill, and issue tracker for `@kapso/openclaw-whatsapp`.
# Payment issues
Source: https://docs.kapso.ai/docs/whatsapp/payment-method-troubleshooting
Fix template send errors caused by payment method and business setup issues
WhatsApp templates can fail even when the template itself is approved. If Meta cannot bill the WhatsApp Business Account (WABA), it blocks outbound template messages until the payment and business setup is complete.
The fix depends on how the WABA's [Meta message billing](/docs/whatsapp/meta-message-billing) is configured.
## Check the billing mode
In Kapso, open **Phone numbers**, expand the number, and find the **Meta message fees** card.
### Kapso credits
* If the project has insufficient credits, add credits and retry the message.
* If the card says **Needs attention**, use its retry action.
* If the warning remains, contact Kapso support. You do not need to add a card to this WABA in Meta.
### Paid directly to Meta
Follow the Meta Billing Hub steps below.
## Common error messages
You may see one of these messages in Kapso health checks, broadcasts, workflows, or API responses:
* `There is an issue with the payment method`
* `Meta blocked this template because your WhatsApp Business account is not payment eligible`
* `Meta blocked this template because your WhatsApp Business account is not payment elegible`
* `Payment method required`
* `WhatsApp Business Account is not eligible to send template messages`
## Why this happens
Template messages are billed by Meta. If billing setup is incomplete, business info is missing, tax information is required, or the account is not approved, Meta can reject template sends.
For WABAs paid directly through Meta, this is usually caused by one of these:
* No active payment method on the WABA account
* A payment method that failed verification
* Missing business details in Billing Hub
* Missing tax information when Meta requires it
* WhatsApp account status still in review or restricted
## How to fix
### 1. Add or confirm a payment method
* Open [Billing Hub account details](https://business.facebook.com/latest/billing_hub/accounts/details)
* Select the account connected to Kapso
* Add or confirm an active payment method
* Resolve any failed-payment, verification, or billing prompts shown by Meta
### 2. Complete business information
* Open Business info in Meta Business settings
* Complete the legal business name, address, phone number, website, currency, and any required tax information
* Save changes and return to Billing Hub to confirm there are no remaining prompts
### 3. Confirm account status is approved
* Open [WhatsApp Account settings](https://business.facebook.com/latest/settings/whatsapp_account)
* Confirm the WABA status is **Approved**
* The account should not be in review, restricted, disabled, or waiting on a Meta-requested action
### 4. Wait and retry
* Wait 5-10 minutes
* Retry sending the same template
### 5. If nothing works
Contact Kapso support.
# Personal AI agents
Source: https://docs.kapso.ai/docs/whatsapp/personal-agent
Give your AI agent a WhatsApp number.
Use Kapso when you want a personal agent to send and receive WhatsApp messages from a real number.
## What you need
* A Kapso project
* A connected WhatsApp number
* A project API key
* An agent runtime connected through OpenClaw, Hermes Agent, Chat SDK, MCP, or your own webhook
If your number is already connected, skip to [choose an agent runtime](#choose-an-agent-runtime).
## Connect a number
```bash theme={null}
# install the CLI
curl -fsSL https://kapso.ai/install.sh | bash
# connect a number
kapso setup
# check your numbers
kapso whatsapp numbers list
```
For other connection paths, see [Connect WhatsApp](/docs/how-to/whatsapp/connect-whatsapp).
## Choose an agent runtime
Use a native integration when your agent already runs on one of these runtimes:
Install `@kapso/openclaw-whatsapp` to add WhatsApp as an OpenClaw channel.
Install `gokapso/hermes-agent-plugin` to receive Kapso webhooks in Hermes.
Use `@kapso/chat-adapter` for Chat SDK agents, replies, media, and history.
Let an MCP-capable agent operate WhatsApp without shell access.
### OpenClaw
```bash theme={null}
openclaw plugins install clawhub:@kapso/openclaw-whatsapp
openclaw config set 'plugins.allow' '["codex","kapso-whatsapp"]' --strict-json
openclaw config set 'channels["kapso-whatsapp"].enabled' true --strict-json
```
Then run the setup command from the [OpenClaw guide](/docs/whatsapp/openclaw) to resolve the sender number, register the webhook, and write channel config.
### Hermes Agent
```bash theme={null}
hermes plugins install gokapso/hermes-agent-plugin --enable
hermes kapso setup --install-cli --funnel-url https://
hermes gateway restart
```
The setup command can install the Kapso CLI, pick the WhatsApp number, generate the webhook secret, and create the Kapso webhook. See the [Hermes Agent guide](/docs/whatsapp/hermes-agent).
## Custom agent
Create a project API key in **Integrations → API keys**.
Get the phone number ID for the number you want to use:
```bash theme={null}
kapso whatsapp numbers list --output json
```
Copy the `id` for the right number, then export both values:
```bash theme={null}
export KAPSO_API_KEY="your_project_api_key"
export KAPSO_PHONE_NUMBER_ID="your_phone_number_id"
```
With these values, your agent can call the CLI, the TypeScript SDK, the WhatsApp API proxy, or Project MCP.
### Send messages
```bash theme={null}
# send a text message
kapso whatsapp messages send \
--phone-number-id $KAPSO_PHONE_NUMBER_ID \
--to "+1234567890" \
--text "Your order has shipped"
# send a JSON payload (media, interactive, templates)
kapso whatsapp messages send \
--phone-number-id $KAPSO_PHONE_NUMBER_ID \
--input message.json
```
### Read conversations
```bash theme={null}
# list recent conversations
kapso whatsapp conversations list --phone-number-id $KAPSO_PHONE_NUMBER_ID
# list inbound messages
kapso whatsapp messages list \
--phone-number-id $KAPSO_PHONE_NUMBER_ID \
--direction inbound \
--limit 20
```
### Set up webhooks
```bash theme={null}
# receive messages via webhook
kapso whatsapp webhooks new \
--phone-number-id $KAPSO_PHONE_NUMBER_ID \
--url "https://your-agent.example.com/webhook" \
--event whatsapp.message.received \
--active
```
All commands support `--output json` for structured parsing. See the [CLI reference](/docs/whatsapp/cli) for the full command list.
For SDK code, use [`@kapso/whatsapp-cloud-api`](/docs/whatsapp/typescript-sdk/introduction). For live agent tools, use [Project MCP](/docs/whatsapp/mcp).
Agent skills, llms.txt, and MCP setup for AI coding agents.
Compare instant setup, WhatsApp Business app, and bring your own SIM.
# Pricing
Source: https://docs.kapso.ai/docs/whatsapp/pricing-faq
Understanding Kapso and Meta billing
## What does Kapso charge for?
Kapso charges for platform access:
* Message processing and storage
* Inbox and analytics
* Flows, AI agents, and functions
* Support
## What does Meta charge for?
As of July 1, 2025, Meta charges **per delivered template message**:
| Message type | Cost |
| ------------------------ | ------------------------------------------------------------------ |
| Template messages | Charged (rate varies by category + country) |
| Non-template messages | Free within the 24h customer service window, until October 1, 2026 |
| Utility templates in CSW | Free |
Rates vary by template category (marketing, utility, authentication) and recipient's country code.
**Non-template messages stop being free on October 1, 2026.** The free-form replies you send inside the 24-hour customer service window will be billed per message. Meta has said they will bill at each market's utility rate, and the rate-card update expected on September 1 may still move those rates. Templates are unaffected, and replies inside an ad's 72-hour entry-point window stay free.
The [WhatsApp Pricing guide](https://kapso.com/guides/whatsapp-pricing/october-1-2026/service-message-pricing) covers what changes and how to estimate the impact on your own bill.
[Meta's pricing](https://developers.facebook.com/docs/whatsapp/pricing) · [Rate cards](https://developers.facebook.com/docs/whatsapp/pricing#rates)
## How is Meta billed?
You can pay Meta message fees with Kapso credits or use the payment method configured in your Meta account. The selected mode applies to the entire WhatsApp Business Account.
See [Meta message billing](/docs/whatsapp/meta-message-billing) for the differences.
## Do Kapso credits cover Meta costs?
Yes, when the WABA uses Kapso credits for Meta message billing. Project credits and charges remain in USD. USD WABAs using Kapso messaging infrastructure use Meta's published USD price; enabled non-USD WABAs also include the FX margin shown in Kapso.
For a Multi-partner Solution using the Tech Provider's messaging infrastructure, messages also include a 5% managed billing fee. This fee is separate from any FX margin.
When the WABA pays Meta directly, Meta charges its configured payment method instead. Kapso plan message allowances and Meta message fees remain separate in both modes.
## What's included in Kapso plans?
All plans include unlimited API calls, AI agents, workflows, serverless function calls, and a Kapso sandbox number. Plans differ in message volume, connected numbers, media storage, and integration calls.
| | Free | Pro | Platform |
| ----------------------- | ----- | ------------------- | ------------------- |
| Messages/month | 2,000 | 100,000 | 1,000,000 |
| Connected numbers | 1 | 3 (then \$10/extra) | 50 (then \$5/extra) |
| Media storage | 1 GB | 100 GB | 1 TB |
| Integration calls/month | — | 1,000 | 10,000 |
Enterprise plans have custom limits and pricing.
Prices and limits are always up to date at [kapso.ai/pricing](https://kapso.ai/pricing).
## What counts toward my message limit?
Kapso counts **all messages** (inbound and outbound) toward your plan limit:
* Text messages you send
* Media messages (image, video, audio, document)
* Template messages
* Interactive messages (buttons, lists)
* Messages you receive from customers
* Reactions
**Not counted:**
* Read receipts
## Can I add a second number on the Free plan?
No. The Free plan includes 1 WhatsApp number. Upgrade to Pro for up to 3 numbers.
## Sandbox vs pre-verified numbers
**Sandbox number** — Free, shared with other users (but isolated). Use it for testing and development.
**Pre-verified US number** — A dedicated production number. Requires a small deposit that goes directly to your project credits. Available on all plans.
# Send audio
Source: https://docs.kapso.ai/docs/whatsapp/send-messages/audio
Send audio and voice messages via SDK or API
## Voice message (voice note)
Voice messages display with a voice icon, automatic download, and optional transcription. Must be .ogg files with OPUS codec.
```typescript TypeScript SDK icon="square-js" theme={null}
import { WhatsAppClient } from '@kapso/whatsapp-cloud-api';
const client = new WhatsAppClient({
baseUrl: 'https://api.kapso.ai/meta/whatsapp',
kapsoApiKey: process.env.KAPSO_API_KEY!
});
await client.messages.sendAudio({
phoneNumberId: '647015955153740',
to: '15551234567',
audio: {
id: '1013859600285441',
voice: true
}
});
```
```bash REST API icon="code" theme={null}
curl -X POST 'https://api.kapso.ai/meta/whatsapp/v24.0/647015955153740/messages' \
-H 'X-API-Key: YOUR_API_KEY' \
-H 'Content-Type: application/json' \
-d '{
"messaging_product": "whatsapp",
"to": "15551234567",
"type": "audio",
"audio": {
"id": "1013859600285441",
"voice": true
}
}'
```
## Basic audio message
Basic audio displays with a download icon and music icon. Supports AAC, AMR, MP3, MP4, and OGG formats (max 16MB).
```typescript TypeScript SDK icon="square-js" theme={null}
import { WhatsAppClient } from '@kapso/whatsapp-cloud-api';
const client = new WhatsAppClient({
baseUrl: 'https://api.kapso.ai/meta/whatsapp',
kapsoApiKey: process.env.KAPSO_API_KEY!
});
await client.messages.sendAudio({
phoneNumberId: '647015955153740',
to: '15551234567',
audio: {
link: 'https://example.com/ringtone.mp3'
}
});
```
```bash REST API icon="code" theme={null}
curl -X POST 'https://api.kapso.ai/meta/whatsapp/v24.0/647015955153740/messages' \
-H 'X-API-Key: YOUR_API_KEY' \
-H 'Content-Type: application/json' \
-d '{
"messaging_product": "whatsapp",
"to": "15551234567",
"type": "audio",
"audio": {
"link": "https://example.com/ringtone.mp3"
}
}'
```
# Send buttons
Source: https://docs.kapso.ai/docs/whatsapp/send-messages/buttons
Send interactive button messages via SDK or API
```typescript TypeScript SDK icon="square-js" theme={null}
import { WhatsAppClient } from '@kapso/whatsapp-cloud-api';
const client = new WhatsAppClient({
baseUrl: 'https://api.kapso.ai/meta/whatsapp',
kapsoApiKey: process.env.KAPSO_API_KEY!
});
await client.messages.sendInteractiveButtons({
phoneNumberId: '647015955153740',
to: '15551234567',
bodyText: 'Would you like to proceed with your order?',
buttons: [
{ id: 'btn_yes', title: 'Yes' },
{ id: 'btn_no', title: 'No' }
]
});
```
```bash REST API icon="code" theme={null}
curl -X POST 'https://api.kapso.ai/meta/whatsapp/v24.0/647015955153740/messages' \
-H 'X-API-Key: YOUR_API_KEY' \
-H 'Content-Type: application/json' \
-d '{
"messaging_product": "whatsapp",
"to": "15551234567",
"type": "interactive",
"interactive": {
"type": "button",
"body": {
"text": "Would you like to proceed with your order?"
},
"action": {
"buttons": [
{
"type": "reply",
"reply": {
"id": "btn_yes",
"title": "Yes"
}
},
{
"type": "reply",
"reply": {
"id": "btn_no",
"title": "No"
}
}
]
}
}
}'
```
# Send contact
Source: https://docs.kapso.ai/docs/whatsapp/send-messages/contact
Send contact card messages via SDK or API
```typescript TypeScript SDK icon="square-js" theme={null}
import { WhatsAppClient } from '@kapso/whatsapp-cloud-api';
const client = new WhatsAppClient({
baseUrl: 'https://api.kapso.ai/meta/whatsapp',
kapsoApiKey: process.env.KAPSO_API_KEY!
});
await client.messages.sendContacts({
phoneNumberId: '647015955153740',
to: '15551234567',
contacts: [
{
name: {
formatted_name: 'John Doe',
first_name: 'John',
last_name: 'Doe'
},
phones: [
{
phone: '+15559876543',
type: 'MOBILE'
}
],
emails: [
{
email: 'john.doe@example.com',
type: 'WORK'
}
]
}
]
});
```
```bash REST API icon="code" theme={null}
curl -X POST 'https://api.kapso.ai/meta/whatsapp/v24.0/647015955153740/messages' \
-H 'X-API-Key: YOUR_API_KEY' \
-H 'Content-Type: application/json' \
-d '{
"messaging_product": "whatsapp",
"to": "15551234567",
"type": "contacts",
"contacts": [
{
"name": {
"formatted_name": "John Doe",
"first_name": "John",
"last_name": "Doe"
},
"phones": [
{
"phone": "+15559876543",
"type": "MOBILE"
}
],
"emails": [
{
"email": "john.doe@example.com",
"type": "WORK"
}
]
}
]
}'
```
# Send document
Source: https://docs.kapso.ai/docs/whatsapp/send-messages/document
Send document messages via SDK or API
```typescript TypeScript SDK icon="square-js" theme={null}
import { WhatsAppClient } from '@kapso/whatsapp-cloud-api';
const client = new WhatsAppClient({
baseUrl: 'https://api.kapso.ai/meta/whatsapp',
kapsoApiKey: process.env.KAPSO_API_KEY!
});
await client.messages.sendDocument({
phoneNumberId: '647015955153740',
to: '15551234567',
document: {
link: 'https://example.com/invoice.pdf',
caption: 'Your invoice for order #12345',
filename: 'invoice-12345.pdf'
}
});
```
```bash REST API icon="code" theme={null}
curl -X POST 'https://api.kapso.ai/meta/whatsapp/v24.0/647015955153740/messages' \
-H 'X-API-Key: YOUR_API_KEY' \
-H 'Content-Type: application/json' \
-d '{
"messaging_product": "whatsapp",
"to": "15551234567",
"type": "document",
"document": {
"link": "https://example.com/invoice.pdf",
"caption": "Your invoice for order #12345",
"filename": "invoice-12345.pdf"
}
}'
```
# Send image
Source: https://docs.kapso.ai/docs/whatsapp/send-messages/image
Send image messages via SDK or API
```typescript TypeScript SDK icon="square-js" theme={null}
import { WhatsAppClient } from '@kapso/whatsapp-cloud-api';
const client = new WhatsAppClient({
baseUrl: 'https://api.kapso.ai/meta/whatsapp',
kapsoApiKey: process.env.KAPSO_API_KEY!
});
await client.messages.sendImage({
phoneNumberId: '647015955153740',
to: '15551234567',
image: {
link: 'https://example.com/product.jpg',
caption: 'Check out our new product!'
}
});
```
```bash REST API icon="code" theme={null}
curl -X POST 'https://api.kapso.ai/meta/whatsapp/v24.0/647015955153740/messages' \
-H 'X-API-Key: YOUR_API_KEY' \
-H 'Content-Type: application/json' \
-d '{
"messaging_product": "whatsapp",
"to": "15551234567",
"type": "image",
"image": {
"link": "https://example.com/product.jpg",
"caption": "Check out our new product!"
}
}'
```
# Send lists
Source: https://docs.kapso.ai/docs/whatsapp/send-messages/lists
Send interactive list messages via SDK or API
```typescript TypeScript SDK icon="square-js" theme={null}
import { WhatsAppClient } from '@kapso/whatsapp-cloud-api';
const client = new WhatsAppClient({
baseUrl: 'https://api.kapso.ai/meta/whatsapp',
kapsoApiKey: process.env.KAPSO_API_KEY!
});
await client.messages.sendInteractiveList({
phoneNumberId: '647015955153740',
to: '15551234567',
bodyText: 'Choose your preferred delivery option',
buttonText: 'View options',
sections: [
{
title: 'Delivery',
rows: [
{ id: 'standard', title: 'Standard', description: '3-5 business days' },
{ id: 'express', title: 'Express', description: '1-2 business days' },
{ id: 'overnight', title: 'Overnight', description: 'Next day delivery' }
]
}
]
});
```
```bash REST API icon="code" theme={null}
curl -X POST 'https://api.kapso.ai/meta/whatsapp/v24.0/647015955153740/messages' \
-H 'X-API-Key: YOUR_API_KEY' \
-H 'Content-Type: application/json' \
-d '{
"messaging_product": "whatsapp",
"to": "15551234567",
"type": "interactive",
"interactive": {
"type": "list",
"body": {
"text": "Choose your preferred delivery option"
},
"action": {
"button": "View options",
"sections": [
{
"title": "Delivery",
"rows": [
{
"id": "standard",
"title": "Standard",
"description": "3-5 business days"
},
{
"id": "express",
"title": "Express",
"description": "1-2 business days"
},
{
"id": "overnight",
"title": "Overnight",
"description": "Next day delivery"
}
]
}
]
}
}
}'
```
# Send location
Source: https://docs.kapso.ai/docs/whatsapp/send-messages/location
Send location messages via SDK or API
```typescript TypeScript SDK icon="square-js" theme={null}
import { WhatsAppClient } from '@kapso/whatsapp-cloud-api';
const client = new WhatsAppClient({
baseUrl: 'https://api.kapso.ai/meta/whatsapp',
kapsoApiKey: process.env.KAPSO_API_KEY!
});
await client.messages.sendLocation({
phoneNumberId: '647015955153740',
to: '15551234567',
location: {
latitude: 37.7749,
longitude: -122.4194,
name: 'Warehouse Location',
address: '123 Main St, San Francisco, CA'
}
});
```
```bash REST API icon="code" theme={null}
curl -X POST 'https://api.kapso.ai/meta/whatsapp/v24.0/647015955153740/messages' \
-H 'X-API-Key: YOUR_API_KEY' \
-H 'Content-Type: application/json' \
-d '{
"messaging_product": "whatsapp",
"to": "15551234567",
"type": "location",
"location": {
"latitude": 37.7749,
"longitude": -122.4194,
"name": "Warehouse Location",
"address": "123 Main St, San Francisco, CA"
}
}'
```
# Request user location
Source: https://docs.kapso.ai/docs/whatsapp/send-messages/location-request
Ask users to share their current location via interactive message
Request users to share their current location. Displays a "Send Location" button in WhatsApp.
```typescript TypeScript SDK icon="square-js" theme={null}
import { WhatsAppClient } from '@kapso/whatsapp-cloud-api';
const client = new WhatsAppClient({
baseUrl: 'https://api.kapso.ai/meta/whatsapp',
kapsoApiKey: process.env.KAPSO_API_KEY!
});
await client.messages.sendInteractiveLocationRequest({
phoneNumberId: '647015955153740',
to: '15551234567',
bodyText: 'Please share your location so we can provide accurate delivery estimates.',
parameters: {
requestMessage: 'Please share your location so we can provide accurate delivery estimates.'
}
});
```
```bash REST API icon="code" theme={null}
curl -X POST 'https://api.kapso.ai/meta/whatsapp/v24.0/647015955153740/messages' \
-H 'X-API-Key: YOUR_API_KEY' \
-H 'Content-Type: application/json' \
-d '{
"messaging_product": "whatsapp",
"to": "15551234567",
"type": "interactive",
"interactive": {
"type": "location_request_message",
"body": {
"text": "Please share your location so we can provide accurate delivery estimates."
},
"action": {
"name": "send_location"
}
}
}'
```
## Response handling
When the user shares their location, you'll receive a webhook with:
```json theme={null}
{
"type": "location",
"location": {
"latitude": 37.7749,
"longitude": -122.4194,
"name": "Optional location name",
"address": "Optional address"
}
}
```
## Use cases
* Delivery services requesting drop-off locations
* Store locators finding nearest branches
* Field service appointments
* Event check-ins
# Mark messages as read
Source: https://docs.kapso.ai/docs/whatsapp/send-messages/mark-read
Mark messages as read with optional typing indicator
Mark a message as read. Optionally show a typing indicator while preparing your reply.
## Mark as read
```typescript TypeScript SDK icon="square-js" theme={null}
import { WhatsAppClient } from '@kapso/whatsapp-cloud-api';
const client = new WhatsAppClient({
baseUrl: 'https://api.kapso.ai/meta/whatsapp',
kapsoApiKey: process.env.KAPSO_API_KEY!
});
await client.messages.markRead({
phoneNumberId: '647015955153740',
messageId: 'wamid.HBgNMTU1NTE0OTU5Nzg1FQIAERgSMDhGRjdBMDEyOTcxQzFFQkFBAA=='
});
```
```bash REST API icon="code" theme={null}
curl -X POST 'https://api.kapso.ai/meta/whatsapp/v24.0/647015955153740/messages' \
-H 'X-API-Key: YOUR_API_KEY' \
-H 'Content-Type: application/json' \
-d '{
"messaging_product": "whatsapp",
"status": "read",
"message_id": "wamid.HBgNMTU1NTE0OTU5Nzg1FQIAERgSMDhGRjdBMDEyOTcxQzFFQkFBAA=="
}'
```
## Mark as read with typing indicator
Show a typing indicator while you prepare a reply. The indicator is dismissed when you send a message or after \~25 seconds.
```typescript TypeScript SDK icon="square-js" theme={null}
import { WhatsAppClient } from '@kapso/whatsapp-cloud-api';
const client = new WhatsAppClient({
baseUrl: 'https://api.kapso.ai/meta/whatsapp',
kapsoApiKey: process.env.KAPSO_API_KEY!
});
await client.messages.markRead({
phoneNumberId: '647015955153740',
messageId: 'wamid.HBgNMTU1NTE0OTU5Nzg1FQIAERgSMDhGRjdBMDEyOTcxQzFFQkFBAA==',
typingIndicator: { type: 'text' }
});
```
```bash REST API icon="code" theme={null}
curl -X POST 'https://api.kapso.ai/meta/whatsapp/v24.0/647015955153740/messages' \
-H 'X-API-Key: YOUR_API_KEY' \
-H 'Content-Type: application/json' \
-d '{
"messaging_product": "whatsapp",
"status": "read",
"message_id": "wamid.HBgNMTU1NTE0OTU5Nzg1FQIAERgSMDhGRjdBMDEyOTcxQzFFQkFBAA==",
"typing_indicator": {
"type": "text"
}
}'
```
# Pix order messages
Source: https://docs.kapso.ai/docs/whatsapp/send-messages/pix-orders
Send order details and order status messages for Brazil Pix payments
Send interactive `order_details` messages so users can review and pay an order with Pix, then send `order_status` messages to update the order once the payment settles.
Kapso forwards these payloads to Meta unchanged. Meta validates account eligibility, payment details, and Pix key ownership.
## Order details
Use `type: "order_details"` with the `review_and_pay` action.
```bash REST API icon="code" theme={null}
curl -X POST 'https://api.kapso.ai/meta/whatsapp/v24.0/647015955153740/messages' \
-H 'X-API-Key: YOUR_API_KEY' \
-H 'Content-Type: application/json' \
-d '{
"messaging_product": "whatsapp",
"to": "5511999999999",
"type": "interactive",
"interactive": {
"type": "order_details",
"body": { "text": "Order details" },
"action": {
"name": "review_and_pay",
"parameters": {
"reference_id": "pix-order-123",
"type": "digital-goods",
"payment_type": "br",
"payment_settings": [
{
"type": "pix_dynamic_code",
"pix_dynamic_code": {
"code": "00020101021226820014br.gov.bcb.pix",
"merchant_name": "Kapso Merchant",
"key": "12345678000199",
"key_type": "CNPJ"
}
}
],
"currency": "BRL",
"total_amount": { "value": 1000, "offset": 100 },
"order": {
"status": "pending",
"tax": { "value": 0, "offset": 100 },
"items": [
{
"retailer_id": "item-1",
"name": "Subscription",
"amount": { "value": 1000, "offset": 100 },
"quantity": 1
}
],
"subtotal": { "value": 1000, "offset": 100 }
}
}
}
}
}'
```
Amounts use `value` and `offset`: `value: 1000` with `offset: 100` is R\$ 10,00.
## Order status
Use `type: "order_status"` with the `review_order` action and the same `reference_id`.
```bash REST API icon="code" theme={null}
curl -X POST 'https://api.kapso.ai/meta/whatsapp/v24.0/647015955153740/messages' \
-H 'X-API-Key: YOUR_API_KEY' \
-H 'Content-Type: application/json' \
-d '{
"messaging_product": "whatsapp",
"to": "5511999999999",
"type": "interactive",
"interactive": {
"type": "order_status",
"body": { "text": "Payment confirmed" },
"action": {
"name": "review_order",
"parameters": {
"reference_id": "pix-order-123",
"order": { "status": "processing" },
"payment": { "status": "captured" }
}
}
}
}'
```
## Requirements
* `action.name` must be `review_and_pay` for `order_details` and `review_order` for `order_status`. Other action names are rejected with HTTP 422.
* `action.parameters` must be an object.
* These messages are subject to the 24-hour customer service window.
* The WhatsApp Business Account must be enabled for the Meta Brazil Payments API.
# Send reaction
Source: https://docs.kapso.ai/docs/whatsapp/send-messages/reaction
React to messages with emoji via SDK or API
Send an emoji reaction to a previous message.
**Webhook limitation:** Reaction messages only trigger a 'sent' status webhook. Delivered and read webhooks are NOT triggered for reactions.
```typescript TypeScript SDK icon="square-js" theme={null}
import { WhatsAppClient } from '@kapso/whatsapp-cloud-api';
const client = new WhatsAppClient({
baseUrl: 'https://api.kapso.ai/meta/whatsapp',
kapsoApiKey: process.env.KAPSO_API_KEY!
});
await client.messages.sendReaction({
phoneNumberId: '647015955153740',
to: '15551234567',
messageId: 'wamid.HBgNMTU1NTE0OTU5Nzg1FQIAERgSMDhGRjdBMDEyOTcxQzFFQkFBAA==',
emoji: '👍'
});
```
```bash REST API icon="code" theme={null}
curl -X POST 'https://api.kapso.ai/meta/whatsapp/v24.0/647015955153740/messages' \
-H 'X-API-Key: YOUR_API_KEY' \
-H 'Content-Type: application/json' \
-d '{
"messaging_product": "whatsapp",
"to": "15551234567",
"type": "reaction",
"reaction": {
"message_id": "wamid.HBgNMTU1NTE0OTU5Nzg1FQIAERgSMDhGRjdBMDEyOTcxQzFFQkFBAA==",
"emoji": "👍"
}
}'
```
# Request contact info
Source: https://docs.kapso.ai/docs/whatsapp/send-messages/request-contact-info
Ask a WhatsApp user to share their phone number
Send a `request_contact_info` interactive message when you know a customer's BSUID but need their phone number.
The user must tap the button to share their phone number. Sending this message does not reveal the number automatically.
```bash REST API icon="code" theme={null}
curl -X POST 'https://api.kapso.ai/meta/whatsapp/v24.0/647015955153740/messages' \
-H 'X-API-Key: YOUR_API_KEY' \
-H 'Content-Type: application/json' \
-d '{
"messaging_product": "whatsapp",
"recipient_type": "individual",
"recipient": "US.13491208655302741918",
"type": "interactive",
"interactive": {
"type": "request_contact_info",
"body": {
"text": "Please share your phone number so we can finish your request."
},
"action": {
"name": "request_contact_info"
}
}
}'
```
Use `to` instead of `recipient` when you already have the phone number.
## What you receive
When the user taps the button, WhatsApp sends a `contacts` message. Kapso stores the phone identity and keeps the BSUID identity on the same contact/conversation when available.
```json theme={null}
{
"type": "contacts",
"from_user_id": "US.13491208655302741918",
"contacts": [
{
"origin": "contact_request",
"phones": [
{
"phone": "+16505551234",
"wa_id": "16505551234",
"type": "CELL"
}
]
}
]
}
```
`origin` can be:
* `contact_request`: the user tapped your request contact info button
* `other`: the user shared a contact directly in WhatsApp
## Template button
You can also add a `REQUEST_CONTACT_INFO` button to utility or marketing templates.
The button text is fixed to `Share Contact Info`, and you do not send runtime button parameters for it.
See [template buttons](/docs/whatsapp/templates/buttons).
# Send sticker
Source: https://docs.kapso.ai/docs/whatsapp/send-messages/sticker
Send stickers via SDK or API
Send static or animated stickers in WEBP format. Static stickers: 100KB max. Animated stickers: 500KB max.
```typescript TypeScript SDK icon="square-js" theme={null}
import { WhatsAppClient } from '@kapso/whatsapp-cloud-api';
const client = new WhatsAppClient({
baseUrl: 'https://api.kapso.ai/meta/whatsapp',
kapsoApiKey: process.env.KAPSO_API_KEY!
});
await client.messages.sendSticker({
phoneNumberId: '647015955153740',
to: '15551234567',
sticker: {
id: '798882015472548'
}
});
```
```bash REST API icon="code" theme={null}
curl -X POST 'https://api.kapso.ai/meta/whatsapp/v24.0/647015955153740/messages' \
-H 'X-API-Key: YOUR_API_KEY' \
-H 'Content-Type: application/json' \
-d '{
"messaging_product": "whatsapp",
"to": "15551234567",
"type": "sticker",
"sticker": {
"id": "798882015472548"
}
}'
```
## Using a link
```typescript TypeScript SDK icon="square-js" theme={null}
await client.messages.sendSticker({
phoneNumberId: '647015955153740',
to: '15551234567',
sticker: {
link: 'https://example.com/sticker.webp'
}
});
```
```bash REST API icon="code" theme={null}
curl -X POST 'https://api.kapso.ai/meta/whatsapp/v24.0/647015955153740/messages' \
-H 'X-API-Key: YOUR_API_KEY' \
-H 'Content-Type: application/json' \
-d '{
"messaging_product": "whatsapp",
"to": "15551234567",
"type": "sticker",
"sticker": {
"link": "https://example.com/sticker.webp"
}
}'
```
# Send text
Source: https://docs.kapso.ai/docs/whatsapp/send-messages/text
Send text messages via SDK or API
## Prerequisites
To send messages, you'll need to:
* Create a project API key in the Kapso dashboard under **Integrations → API keys**
* Connect a WhatsApp number under **WhatsApp → Phone numbers**
## Send a text message
```typescript TypeScript SDK icon="square-js" theme={null}
import { WhatsAppClient } from '@kapso/whatsapp-cloud-api';
const client = new WhatsAppClient({
baseUrl: 'https://api.kapso.ai/meta/whatsapp',
kapsoApiKey: process.env.KAPSO_API_KEY!
});
await client.messages.sendText({
phoneNumberId: '647015955153740',
to: '15551234567',
body: 'Hello! Your order #12345 has been shipped.'
});
```
```bash REST API icon="code" theme={null}
curl -X POST 'https://api.kapso.ai/meta/whatsapp/v24.0/647015955153740/messages' \
-H 'X-API-Key: YOUR_API_KEY' \
-H 'Content-Type: application/json' \
-d '{
"messaging_product": "whatsapp",
"to": "15551234567",
"type": "text",
"text": {
"body": "Hello! Your order #12345 has been shipped."
}
}'
```
## Send to a BSUID
Use `recipient` when you only have a BSUID or parent BSUID.
```bash REST API icon="code" theme={null}
curl -X POST 'https://api.kapso.ai/meta/whatsapp/v24.0/647015955153740/messages' \
-H 'X-API-Key: YOUR_API_KEY' \
-H 'Content-Type: application/json' \
-d '{
"messaging_product": "whatsapp",
"recipient_type": "individual",
"recipient": "US.13491208655302741918",
"type": "text",
"text": {
"body": "Hello! Your order #12345 has been shipped."
}
}'
```
Non-template messages still require an open 24-hour customer service window. Use an approved template to start or reopen a conversation.
See [business-scoped user IDs](/docs/whatsapp/business-scoped-user-ids).
# Send video
Source: https://docs.kapso.ai/docs/whatsapp/send-messages/video
Send video messages via SDK or API
```typescript TypeScript SDK icon="square-js" theme={null}
import { WhatsAppClient } from '@kapso/whatsapp-cloud-api';
const client = new WhatsAppClient({
baseUrl: 'https://api.kapso.ai/meta/whatsapp',
kapsoApiKey: process.env.KAPSO_API_KEY!
});
await client.messages.sendVideo({
phoneNumberId: '647015955153740',
to: '15551234567',
video: {
link: 'https://example.com/demo.mp4',
caption: 'Product demo video'
}
});
```
```bash REST API icon="code" theme={null}
curl -X POST 'https://api.kapso.ai/meta/whatsapp/v24.0/647015955153740/messages' \
-H 'X-API-Key: YOUR_API_KEY' \
-H 'Content-Type: application/json' \
-d '{
"messaging_product": "whatsapp",
"to": "15551234567",
"type": "video",
"video": {
"link": "https://example.com/demo.mp4",
"caption": "Product demo video"
}
}'
```
# Advanced
Source: https://docs.kapso.ai/docs/whatsapp/templates/advanced
Create and send templates with catalog, MPM, and Flow buttons
## Create
```typescript Create template - TypeScript SDK icon="square-js" theme={null}
import { WhatsAppClient } from '@kapso/whatsapp-cloud-api';
const client = new WhatsAppClient({
baseUrl: 'https://api.kapso.ai/meta/whatsapp',
kapsoApiKey: process.env.KAPSO_API_KEY!
});
await client.templates.create({
businessAccountId: '123456789',
name: 'appointment_booking',
category: 'UTILITY',
language: 'en_US',
parameterFormat: 'NAMED',
components: [
{
type: 'BODY',
text: 'Hi {{customer_name}}, book your appointment using the button below.',
example: {
body_text_named_params: [
{ param_name: 'customer_name', example: 'Michael' }
]
}
},
{
type: 'BUTTONS',
buttons: [
{
type: 'FLOW',
text: 'Book Now',
flow_id: '123456789012345',
flow_action: 'navigate'
}
]
}
]
});
```
```bash Create template - REST API icon="code" theme={null}
curl -X POST 'https://api.kapso.ai/meta/whatsapp/v24.0/123456789/message_templates' \
-H 'X-API-Key: YOUR_API_KEY' \
-H 'Content-Type: application/json' \
-d '{
"name": "appointment_booking",
"category": "UTILITY",
"language": "en_US",
"parameter_format": "NAMED",
"components": [
{
"type": "BODY",
"text": "Hi {{customer_name}}, book your appointment using the button below.",
"example": {
"body_text_named_params": [
{ "param_name": "customer_name", "example": "Michael" }
]
}
},
{
"type": "BUTTONS",
"buttons": [
{
"type": "FLOW",
"text": "Book Now",
"flow_id": "123456789012345",
"flow_action": "navigate"
}
]
}
]
}'
```
## Send
```typescript Send template - TypeScript SDK icon="square-js" theme={null}
import { WhatsAppClient } from '@kapso/whatsapp-cloud-api';
const client = new WhatsAppClient({
baseUrl: 'https://api.kapso.ai/meta/whatsapp',
kapsoApiKey: process.env.KAPSO_API_KEY!
});
await client.messages.sendTemplate({
phoneNumberId: '647015955153740',
to: '15551234567',
template: {
name: 'appointment_booking',
language: { code: 'en_US' },
components: [
{
type: 'body',
parameters: [
{ type: 'text', parameterName: 'customer_name', text: 'Michael' }
]
},
{
type: 'button',
sub_type: 'flow',
index: '0',
parameters: [
{
type: 'action',
action: {
flow_token: 'user_session_abc123',
flow_action_data: {
customer_id: 'cust_12345',
preferred_date: '2024-02-15'
}
}
}
]
}
]
}
});
```
```bash Send template - REST API icon="code" theme={null}
curl -X POST 'https://api.kapso.ai/meta/whatsapp/v24.0/647015955153740/messages' \
-H 'X-API-Key: YOUR_API_KEY' \
-H 'Content-Type: application/json' \
-d '{
"messaging_product": "whatsapp",
"to": "15551234567",
"type": "template",
"template": {
"name": "appointment_booking",
"language": {
"code": "en_US"
},
"components": [
{
"type": "body",
"parameters": [
{ "type": "text", "parameter_name": "customer_name", "text": "Michael" }
]
},
{
"type": "button",
"sub_type": "flow",
"index": "0",
"parameters": [
{
"type": "action",
"action": {
"flow_token": "user_session_abc123",
"flow_action_data": {
"customer_id": "cust_12345",
"preferred_date": "2024-02-15"
}
}
}
]
}
]
}
}'
```
# Authentication
Source: https://docs.kapso.ai/docs/whatsapp/templates/authentication
Create and send OTP authentication templates
Authentication templates require your WhatsApp Business Account to meet both conditions:
* **Business verification** — your Meta Business Portfolio must be verified
* **Messaging limit of 2,000 or higher** — your WABA's [messaging limit](https://developers.facebook.com/docs/whatsapp/messaging-limits/) must be Tier 1 (2K) or above
Authentication templates have a fixed body text generated by Meta: `*{{1}}* is your verification code.`
You can't customize the body text, but you can:
* Add a security recommendation: "For your security, do not share this code."
* Add code expiration: "This code expires in X minutes."
## OTP types
| Type | Button | Use case |
| ----------- | ----------- | -------------------------------------------- |
| `COPY_CODE` | "Copy code" | Universal - works on all devices |
| `ONE_TAP` | "Autofill" | Android only - auto-fills code in your app |
| `ZERO_TAP` | None | Silent auto-fill, no user interaction needed |
## Create copy code template
The most common type. Shows a "Copy code" button.
```typescript TypeScript SDK icon=square-js theme={null}
await client.templates.create({
businessAccountId: '123456789',
name: 'auth_copy_code',
category: 'AUTHENTICATION',
language: 'en_US',
components: [
{
type: 'BODY',
add_security_recommendation: true
},
{
type: 'FOOTER',
code_expiration_minutes: 10
},
{
type: 'BUTTONS',
buttons: [
{ type: 'OTP', otp_type: 'COPY_CODE' }
]
}
]
});
```
```bash REST API icon=code theme={null}
curl -X POST 'https://api.kapso.ai/meta/whatsapp/v24.0/123456789/message_templates' \
-H 'X-API-Key: YOUR_API_KEY' \
-H 'Content-Type: application/json' \
-d '{
"name": "auth_copy_code",
"category": "AUTHENTICATION",
"language": "en_US",
"components": [
{
"type": "BODY",
"add_security_recommendation": true
},
{
"type": "FOOTER",
"code_expiration_minutes": 10
},
{
"type": "BUTTONS",
"buttons": [
{ "type": "OTP", "otp_type": "COPY_CODE" }
]
}
]
}'
```
## Create one-tap template (Android)
Auto-fills the code in your Android app. Requires your app's package name and signature hash.
```typescript TypeScript SDK icon=square-js theme={null}
await client.templates.create({
businessAccountId: '123456789',
name: 'auth_one_tap',
category: 'AUTHENTICATION',
language: 'en_US',
components: [
{
type: 'BODY',
add_security_recommendation: true
},
{
type: 'BUTTONS',
buttons: [
{
type: 'OTP',
otp_type: 'ONE_TAP',
supported_apps: [
{
package_name: 'com.example.myapp',
signature_hash: 'K8a/AINcD...'
}
]
}
]
}
]
});
```
```bash REST API icon=code theme={null}
curl -X POST 'https://api.kapso.ai/meta/whatsapp/v24.0/123456789/message_templates' \
-H 'X-API-Key: YOUR_API_KEY' \
-H 'Content-Type: application/json' \
-d '{
"name": "auth_one_tap",
"category": "AUTHENTICATION",
"language": "en_US",
"components": [
{
"type": "BODY",
"add_security_recommendation": true
},
{
"type": "BUTTONS",
"buttons": [
{
"type": "OTP",
"otp_type": "ONE_TAP",
"supported_apps": [
{
"package_name": "com.example.myapp",
"signature_hash": "K8a/AINcD..."
}
]
}
]
}
]
}'
```
Get your Android app's signature hash:
```bash theme={null}
keytool -exportcert -alias YOUR_ALIAS -keystore YOUR_KEYSTORE | xxd -p | tr -d "[:space:]" | openssl sha256
```
## Create zero-tap template
No visible button. The code is broadcast to your app via Android's SMS Retriever API.
```typescript TypeScript SDK icon=square-js theme={null}
await client.templates.create({
businessAccountId: '123456789',
name: 'auth_zero_tap',
category: 'AUTHENTICATION',
language: 'en_US',
components: [
{
type: 'BODY',
add_security_recommendation: true
},
{
type: 'BUTTONS',
buttons: [
{
type: 'OTP',
otp_type: 'ZERO_TAP',
supported_apps: [
{
package_name: 'com.example.myapp',
signature_hash: 'K8a/AINcD...'
}
]
}
]
}
]
});
```
```bash REST API icon=code theme={null}
curl -X POST 'https://api.kapso.ai/meta/whatsapp/v24.0/123456789/message_templates' \
-H 'X-API-Key: YOUR_API_KEY' \
-H 'Content-Type: application/json' \
-d '{
"name": "auth_zero_tap",
"category": "AUTHENTICATION",
"language": "en_US",
"components": [
{
"type": "BODY",
"add_security_recommendation": true
},
{
"type": "BUTTONS",
"buttons": [
{
"type": "OTP",
"otp_type": "ZERO_TAP",
"supported_apps": [
{
"package_name": "com.example.myapp",
"signature_hash": "K8a/AINcD..."
}
]
}
]
}
]
}'
```
## Send
When sending, pass the OTP code in both the body and button parameters.
```typescript TypeScript SDK icon=square-js theme={null}
await client.messages.sendTemplate({
phoneNumberId: '647015955153740',
to: '15551234567',
template: {
name: 'auth_copy_code',
language: { code: 'en_US' },
components: [
{
type: 'body',
parameters: [
{ type: 'text', text: '123456' }
]
},
{
type: 'button',
sub_type: 'otp',
index: '0',
parameters: [
{ type: 'text', text: '123456' }
]
}
]
}
});
```
```bash REST API icon=code theme={null}
curl -X POST 'https://api.kapso.ai/meta/whatsapp/v24.0/647015955153740/messages' \
-H 'X-API-Key: YOUR_API_KEY' \
-H 'Content-Type: application/json' \
-d '{
"messaging_product": "whatsapp",
"to": "15551234567",
"type": "template",
"template": {
"name": "auth_copy_code",
"language": { "code": "en_US" },
"components": [
{
"type": "body",
"parameters": [
{ "type": "text", "text": "123456" }
]
},
{
"type": "button",
"sub_type": "otp",
"index": "0",
"parameters": [
{ "type": "text", "text": "123456" }
]
}
]
}
}'
```
## Component reference
### Body
| Field | Type | Description |
| ----------------------------- | ------- | ------------------------------------------------- |
| `add_security_recommendation` | boolean | Adds "For your security, do not share this code." |
### Footer
| Field | Type | Description |
| ------------------------- | ------ | -------------------------------------------- |
| `code_expiration_minutes` | number | 1-90. Adds "This code expires in X minutes." |
### OTP button
| Field | Type | Description |
| ---------------- | ------ | ----------------------------------------------------------------------------- |
| `otp_type` | string | `COPY_CODE`, `ONE_TAP`, or `ZERO_TAP` |
| `supported_apps` | array | Required for ONE\_TAP and ZERO\_TAP. List of `{package_name, signature_hash}` |
## Restrictions
* **No headers** - AUTHENTICATION templates cannot include HEADER components
* **Fixed body text** - Body text is generated by Meta, not customizable
* **No custom parameters** - Only the OTP code can be passed as a variable
# Buttons
Source: https://docs.kapso.ai/docs/whatsapp/templates/buttons
Create and send templates with URL, quick reply, and contact request buttons
## Create
```typescript Create template - TypeScript SDK icon="square-js" theme={null}
import { WhatsAppClient } from '@kapso/whatsapp-cloud-api';
const client = new WhatsAppClient({
baseUrl: 'https://api.kapso.ai/meta/whatsapp',
kapsoApiKey: process.env.KAPSO_API_KEY!
});
await client.templates.create({
businessAccountId: '123456789',
name: 'customer_feedback',
category: 'UTILITY',
language: 'en_US',
parameterFormat: 'NAMED',
components: [
{
type: 'BODY',
text: 'Hi {{customer_name}}, how was your experience with us?',
example: {
body_text_named_params: [
{ param_name: 'customer_name', example: 'Sarah' }
]
}
},
{
type: 'BUTTONS',
buttons: [
{
type: 'QUICK_REPLY',
text: 'Great!'
},
{
type: 'QUICK_REPLY',
text: 'Not good'
}
]
}
]
});
```
```bash Create template - REST API icon="code" theme={null}
curl -X POST 'https://api.kapso.ai/meta/whatsapp/v24.0/123456789/message_templates' \
-H 'X-API-Key: YOUR_API_KEY' \
-H 'Content-Type: application/json' \
-d '{
"name": "customer_feedback",
"category": "UTILITY",
"language": "en_US",
"parameter_format": "NAMED",
"components": [
{
"type": "BODY",
"text": "Hi {{customer_name}}, how was your experience with us?",
"example": {
"body_text_named_params": [
{ "param_name": "customer_name", "example": "Sarah" }
]
}
},
{
"type": "BUTTONS",
"buttons": [
{
"type": "QUICK_REPLY",
"text": "Great!"
},
{
"type": "QUICK_REPLY",
"text": "Not good"
}
]
}
]
}'
```
## Request contact info
Use `REQUEST_CONTACT_INFO` when you want a template to ask the user for their phone number.
The button text is fixed to `Share Contact Info`.
```bash Create template - REST API icon="code" theme={null}
curl -X POST 'https://api.kapso.ai/meta/whatsapp/v24.0/123456789/message_templates' \
-H 'X-API-Key: YOUR_API_KEY' \
-H 'Content-Type: application/json' \
-d '{
"name": "request_phone_number",
"category": "UTILITY",
"language": "en_US",
"components": [
{
"type": "BODY",
"text": "Please share your phone number so we can finish your request."
},
{
"type": "BUTTONS",
"buttons": [
{
"type": "REQUEST_CONTACT_INFO"
}
]
}
]
}'
```
When sending the approved template, do not include a button component for `REQUEST_CONTACT_INFO`.
```bash Send template - REST API icon="code" theme={null}
curl -X POST 'https://api.kapso.ai/meta/whatsapp/v24.0/647015955153740/messages' \
-H 'X-API-Key: YOUR_API_KEY' \
-H 'Content-Type: application/json' \
-d '{
"messaging_product": "whatsapp",
"recipient": "US.13491208655302741918",
"type": "template",
"template": {
"name": "request_phone_number",
"language": {
"code": "en_US"
}
}
}'
```
When the user taps the button, you receive a `contacts` message with `origin: "contact_request"`.
## Send
```typescript Send template - TypeScript SDK icon="square-js" theme={null}
import { WhatsAppClient } from '@kapso/whatsapp-cloud-api';
const client = new WhatsAppClient({
baseUrl: 'https://api.kapso.ai/meta/whatsapp',
kapsoApiKey: process.env.KAPSO_API_KEY!
});
await client.messages.sendTemplate({
phoneNumberId: '647015955153740',
to: '15551234567',
template: {
name: 'customer_feedback',
language: { code: 'en_US' },
components: [
{
type: 'body',
parameters: [
{ type: 'text', parameterName: 'customer_name', text: 'Sarah' }
]
},
{
type: 'button',
sub_type: 'quick_reply',
index: '0',
parameters: [
{ type: 'payload', payload: 'positive_feedback' }
]
},
{
type: 'button',
sub_type: 'quick_reply',
index: '1',
parameters: [
{ type: 'payload', payload: 'negative_feedback' }
]
}
]
}
});
```
```bash Send template - REST API icon="code" theme={null}
curl -X POST 'https://api.kapso.ai/meta/whatsapp/v24.0/647015955153740/messages' \
-H 'X-API-Key: YOUR_API_KEY' \
-H 'Content-Type: application/json' \
-d '{
"messaging_product": "whatsapp",
"to": "15551234567",
"type": "template",
"template": {
"name": "customer_feedback",
"language": {
"code": "en_US"
},
"components": [
{
"type": "body",
"parameters": [
{ "type": "text", "parameter_name": "customer_name", "text": "Sarah" }
]
},
{
"type": "button",
"sub_type": "quick_reply",
"index": "0",
"parameters": [
{ "type": "payload", "payload": "positive_feedback" }
]
},
{
"type": "button",
"sub_type": "quick_reply",
"index": "1",
"parameters": [
{ "type": "payload", "payload": "negative_feedback" }
]
}
]
}
}'
```
## Static buttons
`PHONE_NUMBER` and `REQUEST_CONTACT_INFO` buttons are fully defined by the approved template. Do not send a `button` component for them.
Every other button type takes send-time values and needs its own component: `quick_reply` payloads, `url` suffixes, `copy_code` coupon codes, `flow` tokens and action data (see [Flow templates](/docs/whatsapp/templates/advanced)), and `otp` codes (see [Authentication templates](/docs/whatsapp/templates/authentication)).
The `index` of each `button` component still refers to the button's position in the template's `BUTTONS` component, so a static button occupying position `0` does not shift the indexes of the buttons after it.
# Carousel
Source: https://docs.kapso.ai/docs/whatsapp/templates/carousel
Send WhatsApp carousel template messages with card headers, bodies, and buttons
Carousel templates use Meta's template send payload directly through Kapso's Meta
proxy. The proxy forwards the `carousel.cards` structure to Meta; it does not
translate per-card body, header, or button components.
This page covers approved template carousel messages. For free-form interactive
carousel messages sent inside the customer service window, use
`client.messages.sendInteractiveCarousel(...)` in the
[interactive message SDK guide](/docs/whatsapp/typescript-sdk/interactive).
Send exactly as many cards as the template was approved with.
If a template was approved with 10 cards, sending 2 or 3 returns Meta error `(#132012)` with the misleading detail `header component parameter should not be empty`.
For variable result counts, create one approved template per supported count, such as `flight_option_single`, `flight_options_carousel_v1_2`, and `flight_options_carousel_v1_3`.
## Send
```typescript TypeScript SDK icon="square-js" theme={null}
import { WhatsAppClient, buildTemplateSendPayload } from '@kapso/whatsapp-cloud-api';
const client = new WhatsAppClient({
baseUrl: 'https://api.kapso.ai/meta/whatsapp',
kapsoApiKey: process.env.KAPSO_API_KEY!
});
const template = buildTemplateSendPayload({
name: 'flight_options_carousel_v1_2',
language: 'pt_BR',
cards: [
{
cardIndex: 0,
components: [
{
type: 'header',
parameters: [
{ type: 'image', image: { link: 'https://example.com/flight-a.jpg' } }
]
},
{
type: 'body',
parameters: [
{ type: 'text', text: '12.000,00' }
]
},
{
type: 'button',
subType: 'quick_reply',
index: 0,
parameters: [
{ type: 'payload', payload: 'OPT_1' }
]
}
]
},
{
cardIndex: 1,
components: [
{
type: 'header',
parameters: [
{ type: 'image', image: { link: 'https://example.com/flight-b.jpg' } }
]
},
{
type: 'body',
parameters: [
{ type: 'text', text: '13.500,00' }
]
},
{
type: 'button',
subType: 'quick_reply',
index: 0,
parameters: [
{ type: 'payload', payload: 'OPT_2' }
]
}
]
}
]
});
await client.messages.sendTemplate({
phoneNumberId: '647015955153740',
to: '5514998062062',
template
});
```
```bash REST API icon="code" theme={null}
curl -X POST 'https://api.kapso.ai/meta/whatsapp/v24.0/647015955153740/messages' \
-H 'X-API-Key: YOUR_API_KEY' \
-H 'Content-Type: application/json' \
-d '{
"messaging_product": "whatsapp",
"to": "5514998062062",
"type": "template",
"template": {
"name": "flight_options_carousel_v1_2",
"language": {
"code": "pt_BR"
},
"components": [
{
"type": "carousel",
"cards": [
{
"card_index": 0,
"components": [
{
"type": "header",
"parameters": [
{ "type": "image", "image": { "link": "https://example.com/flight-a.jpg" } }
]
},
{
"type": "body",
"parameters": [
{ "type": "text", "text": "12.000,00" }
]
},
{
"type": "button",
"sub_type": "quick_reply",
"index": 0,
"parameters": [
{ "type": "payload", "payload": "OPT_1" }
]
}
]
},
{
"card_index": 1,
"components": [
{
"type": "header",
"parameters": [
{ "type": "image", "image": { "link": "https://example.com/flight-b.jpg" } }
]
},
{
"type": "body",
"parameters": [
{ "type": "text", "text": "13.500,00" }
]
},
{
"type": "button",
"sub_type": "quick_reply",
"index": 0,
"parameters": [
{ "type": "payload", "payload": "OPT_2" }
]
}
]
}
]
}
]
}
}'
```
## Format rules
* Send-time component and parameter types are lowercase: `carousel`, `header`, `body`, `button`, `image`, `text`, and `payload`.
* `card_index` is numeric and zero-based. In the TypeScript SDK helper, use `cardIndex`; it is converted to `card_index` on send.
* `button.index` is numeric and zero-based.
* Media card headers can use `image: { "link": "https://..." }` or `image: { "id": "" }` from a media upload. Do not use `image.url`.
* The runtime `cards` array must have exactly the same length as the approved carousel template.
* At template creation time, Meta component definitions use uppercase types such as `CAROUSEL`, `HEADER`, `BODY`, and `BUTTONS`. The lowercase rules above are for sending messages.
See Meta's [media card carousel template docs](https://developers.facebook.com/docs/whatsapp/business-management-api/message-templates/media-card-carousel-templates/) and [template message send reference](https://developers.facebook.com/docs/whatsapp/cloud-api/reference/messages#template-messages) for the underlying Cloud API behavior.
# Template lifecycle
Source: https://docs.kapso.ai/docs/whatsapp/templates/lifecycle
Understanding template statuses, Meta review, and syncing
## Create templates
Create templates directly in Kapso:
1. Go to **WhatsApp** > **Templates** in your project
2. Click **Create template**
3. Choose template type and configure
4. Submit for Meta review
Alternatively, create templates in [WhatsApp Manager](https://business.facebook.com/latest/whatsapp_manager/message_templates) and sync them to Kapso.
## Template statuses
| Status | Meaning |
| -------- | ----------------------------------------- |
| PENDING | Submitted to Meta, awaiting review |
| APPROVED | Ready to use |
| REJECTED | Failed Meta review (see rejection reason) |
| DISABLED | Temporarily disabled by Meta |
In Kapso, "Submitted" means the template was sent to Meta and is waiting for review.
## Meta review times
* Reviews typically take up to 24 hours
* First approvals on new accounts may take longer
* Kapso does not add delays — all reviews are performed by Meta
Creating a template in Meta Business Manager vs Kapso makes no difference in review time.
## Sync templates from Meta
If you created templates in Meta Business Manager:
1. Go to **WhatsApp** > **Templates** in your project
2. Click **Sync from WhatsApp**
3. All templates will be imported and available in Kapso
Sync automatically retrieves all templates from your WhatsApp Business Account, regardless of how many you have. You can also sync via API:
```bash theme={null}
POST /whatsapp_templates/sync
```
Sync is disabled for sandbox numbers. Connect a production number to sync templates.
## Common rejection reasons
* Missing business verification
* Policy violations in content
* Incorrect category selection (utility vs marketing)
* Low-quality media in headers
* Misleading or spammy content
# Location header
Source: https://docs.kapso.ai/docs/whatsapp/templates/location-header
Create and send templates with location headers
## Create
```typescript Create template - TypeScript SDK icon="square-js" theme={null}
import { WhatsAppClient } from '@kapso/whatsapp-cloud-api';
const client = new WhatsAppClient({
baseUrl: 'https://api.kapso.ai/meta/whatsapp',
kapsoApiKey: process.env.KAPSO_API_KEY!
});
await client.templates.create({
businessAccountId: '123456789',
name: 'order_delivery_update',
category: 'UTILITY',
language: 'en_US',
parameterFormat: 'NAMED',
components: [
{
type: 'HEADER',
format: 'LOCATION'
},
{
type: 'BODY',
text: 'Your order {{order_number}} is on the way! Track your delivery at the location above.',
example: {
body_text_named_params: [
{ param_name: 'order_number', example: 'ORD-12345' }
]
}
},
{
type: 'FOOTER',
text: 'Estimated delivery: 30 minutes'
}
]
});
```
```bash Create template - REST API icon="code" theme={null}
curl -X POST 'https://api.kapso.ai/meta/whatsapp/v24.0/123456789/message_templates' \
-H 'X-API-Key: YOUR_API_KEY' \
-H 'Content-Type: application/json' \
-d '{
"name": "order_delivery_update",
"category": "UTILITY",
"language": "en_US",
"parameter_format": "NAMED",
"components": [
{
"type": "HEADER",
"format": "LOCATION"
},
{
"type": "BODY",
"text": "Your order {{order_number}} is on the way! Track your delivery at the location above.",
"example": {
"body_text_named_params": [
{ "param_name": "order_number", "example": "ORD-12345" }
]
}
},
{
"type": "FOOTER",
"text": "Estimated delivery: 30 minutes"
}
]
}'
```
## Send
```typescript Send template - TypeScript SDK icon="square-js" theme={null}
import { WhatsAppClient } from '@kapso/whatsapp-cloud-api';
const client = new WhatsAppClient({
baseUrl: 'https://api.kapso.ai/meta/whatsapp',
kapsoApiKey: process.env.KAPSO_API_KEY!
});
await client.messages.sendTemplate({
phoneNumberId: '647015955153740',
to: '15551234567',
template: {
name: 'order_delivery_update',
language: { code: 'en_US' },
components: [
{
type: 'header',
parameters: [
{
type: 'location',
location: {
latitude: 37.7749,
longitude: -122.4194,
name: 'Delivery Location',
address: '123 Main St, San Francisco, CA'
}
}
]
},
{
type: 'body',
parameters: [
{ type: 'text', parameterName: 'order_number', text: 'ORD-12345' }
]
}
]
}
});
```
```bash Send template - REST API icon="code" theme={null}
curl -X POST 'https://api.kapso.ai/meta/whatsapp/v24.0/647015955153740/messages' \
-H 'X-API-Key: YOUR_API_KEY' \
-H 'Content-Type: application/json' \
-d '{
"messaging_product": "whatsapp",
"to": "15551234567",
"type": "template",
"template": {
"name": "order_delivery_update",
"language": {
"code": "en_US"
},
"components": [
{
"type": "header",
"parameters": [
{
"type": "location",
"location": {
"latitude": 37.7749,
"longitude": -122.4194,
"name": "Delivery Location",
"address": "123 Main St, San Francisco, CA"
}
}
]
},
{
"type": "body",
"parameters": [
{ "type": "text", "parameter_name": "order_number", "text": "ORD-12345" }
]
}
]
}
}'
```
# Marketing opt-outs
Source: https://docs.kapso.ai/docs/whatsapp/templates/marketing-opt-outs
What happens when a contact stops marketing messages, and how to check and mirror their preference
WhatsApp lets users stop marketing messages from a business number without blocking it. When a contact does, Meta notifies Kapso, Kapso records the preference, and marketing template sends to that contact on that number are refused.
The scope is narrow and per number:
* Only MARKETING-category templates are blocked. Utility and authentication templates, and free-form messages inside an open session, always go through.
* The preference is scoped to the number the contact stopped. Sends from your other numbers are unaffected.
* A contact with no recorded preference is subscribed.
The preference can only be set by the contact, inside WhatsApp. There is no API or dashboard control to opt a contact out or back in — to resume, the contact taps *Resume promotions* (or messages you and re-enables marketing) in WhatsApp.
## Blocked sends
Sending a marketing template to a stopped contact returns `422`:
```json theme={null}
{
"error": "Contact stopped marketing messages on this WhatsApp number",
"code": "marketing_preference_stopped"
}
```
Do not retry the same send — it will keep failing until the contact resumes. Switch to a utility or authentication template, or send from a different number.
The check runs before billing, so refused sends are never charged.
Other send surfaces handle it without erroring:
* **Broadcasts** mark the recipient `suppressed` and skip them. See [Marketing opt-outs in broadcasts](/docs/platform/broadcasts/overview#marketing-opt-outs).
* **Workflow send-template steps** record the send as skipped and the workflow continues.
## Check a contact's preference
```bash theme={null}
curl 'https://api.kapso.ai/platform/v1/whatsapp/contacts/15551234567/marketing_preferences' \
-H 'X-API-Key: YOUR_API_KEY'
```
The identifier is a contact UUID or phone number. The response has one entry per number the contact has a recorded preference on:
```json theme={null}
{
"data": [
{
"id": "123456789012345",
"phone_number_id": "123456789012345",
"status": "stopped",
"detail": "User requested to stop marketing messages",
"source": "meta_webhook",
"marketing_allowed": false,
"config_display_name": "Main line",
"last_event_at": "2026-08-18T15:44:16Z",
"created_at": "2026-08-18T15:44:17Z",
"updated_at": "2026-08-18T15:44:17Z"
}
]
}
```
`status` is `stopped` or `resumed`. `marketing_allowed` is what you should branch on before a marketing send. An empty `data` array means the contact never changed their preference — subscribed everywhere.
Fetch a single number with `GET /whatsapp/contacts/{identifier}/marketing_preferences/{phone_number_id}`. 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"`.
## Mirror opt-outs into your system
Subscribe to [`whatsapp.contact.marketing_preference_changed`](/docs/platform/webhooks/message-events#whatsapp-contact-marketing_preference_changed). It fires on every real transition — stop and resume — with a monotonic `sequence` to order deliveries by.
## In the dashboard
The contacts list shows a **Marketing** column and a subscribed/unsubscribed filter. The contact panel lists the preference per number. Starting a conversation with a marketing template warns and disables send when the contact has stopped that number, and broadcast details show how many recipients will be skipped before you send.
# Media header
Source: https://docs.kapso.ai/docs/whatsapp/templates/media-header
Create and send templates with image, video, or document headers
## Create
```typescript Create template - TypeScript SDK icon="square-js" theme={null}
import { WhatsAppClient } from '@kapso/whatsapp-cloud-api';
const client = new WhatsAppClient({
baseUrl: 'https://api.kapso.ai/meta/whatsapp',
kapsoApiKey: process.env.KAPSO_API_KEY!
});
await client.templates.create({
businessAccountId: '123456789',
name: 'limited_time_offer',
category: 'MARKETING',
language: 'en_US',
parameterFormat: 'NAMED',
components: [
{
type: 'HEADER',
format: 'IMAGE',
example: {
header_handle: ['4::aW1hZ2UvanBlZw==:ARa1ZDhwbLZM3EENeeg']
}
},
{
type: 'BODY',
text: 'Exclusive offer for {{customer_name}}! Get {{discount_amount}} off with code {{discount_code}}.',
example: {
body_text_named_params: [
{ param_name: 'customer_name', example: 'Mark' },
{ param_name: 'discount_amount', example: '25%' },
{ param_name: 'discount_code', example: 'SUMMER25' }
]
}
}
]
});
```
```bash Create template - REST API icon="code" theme={null}
curl -X POST 'https://api.kapso.ai/meta/whatsapp/v24.0/123456789/message_templates' \
-H 'X-API-Key: YOUR_API_KEY' \
-H 'Content-Type: application/json' \
-d '{
"name": "limited_time_offer",
"category": "MARKETING",
"language": "en_US",
"parameter_format": "NAMED",
"components": [
{
"type": "HEADER",
"format": "IMAGE",
"example": {
"header_handle": ["4::aW1hZ2UvanBlZw==:ARa1ZDhwbLZM3EENeeg"]
}
},
{
"type": "BODY",
"text": "Exclusive offer for {{customer_name}}! Get {{discount_amount}} off with code {{discount_code}}.",
"example": {
"body_text_named_params": [
{ "param_name": "customer_name", "example": "Mark" },
{ "param_name": "discount_amount", "example": "25%" },
{ "param_name": "discount_code", "example": "SUMMER25" }
]
}
}
]
}'
```
## Send
```typescript Send template - TypeScript SDK icon="square-js" theme={null}
import { WhatsAppClient } from '@kapso/whatsapp-cloud-api';
const client = new WhatsAppClient({
baseUrl: 'https://api.kapso.ai/meta/whatsapp',
kapsoApiKey: process.env.KAPSO_API_KEY!
});
await client.messages.sendTemplate({
phoneNumberId: '647015955153740',
to: '15551234567',
template: {
name: 'limited_time_offer',
language: { code: 'en_US' },
components: [
{
type: 'header',
parameters: [
{
type: 'image',
image: {
link: 'https://example.com/summer-sale.jpg'
}
}
]
},
{
type: 'body',
parameters: [
{ type: 'text', parameterName: 'customer_name', text: 'Mark' },
{ type: 'text', parameterName: 'discount_amount', text: '25%' },
{ type: 'text', parameterName: 'discount_code', text: 'SUMMER25' }
]
}
]
}
});
```
```bash Send template - REST API icon="code" theme={null}
curl -X POST 'https://api.kapso.ai/meta/whatsapp/v24.0/647015955153740/messages' \
-H 'X-API-Key: YOUR_API_KEY' \
-H 'Content-Type: application/json' \
-d '{
"messaging_product": "whatsapp",
"to": "15551234567",
"type": "template",
"template": {
"name": "limited_time_offer",
"language": {
"code": "en_US"
},
"components": [
{
"type": "header",
"parameters": [
{
"type": "image",
"image": {
"link": "https://example.com/summer-sale.jpg"
}
}
]
},
{
"type": "body",
"parameters": [
{ "type": "text", "parameter_name": "customer_name", "text": "Mark" },
{ "type": "text", "parameter_name": "discount_amount", "text": "25%" },
{ "type": "text", "parameter_name": "discount_code", "text": "SUMMER25" }
]
}
]
}
}'
```
# Simple text
Source: https://docs.kapso.ai/docs/whatsapp/templates/simple-text
Create and send templates with body text parameters
## Create
```typescript Create template - TypeScript SDK icon="square-js" theme={null}
import { WhatsAppClient } from '@kapso/whatsapp-cloud-api';
const client = new WhatsAppClient({
baseUrl: 'https://api.kapso.ai/meta/whatsapp',
kapsoApiKey: process.env.KAPSO_API_KEY!
});
await client.templates.create({
businessAccountId: '123456789',
name: 'order_confirmation',
category: 'UTILITY',
language: 'en_US',
parameterFormat: 'NAMED',
components: [
{
type: 'BODY',
text: 'Hi {{customer_name}}, your order {{order_number}} has been confirmed!',
example: {
body_text_named_params: [
{ param_name: 'customer_name', example: 'Jessica' },
{ param_name: 'order_number', example: 'SKBUP2-4CPIG9' }
]
}
}
]
});
```
```bash Create template - REST API icon="code" theme={null}
curl -X POST 'https://api.kapso.ai/meta/whatsapp/v24.0/123456789/message_templates' \
-H 'X-API-Key: YOUR_API_KEY' \
-H 'Content-Type: application/json' \
-d '{
"name": "order_confirmation",
"category": "UTILITY",
"language": "en_US",
"parameter_format": "NAMED",
"components": [
{
"type": "BODY",
"text": "Hi {{customer_name}}, your order {{order_number}} has been confirmed!",
"example": {
"body_text_named_params": [
{ "param_name": "customer_name", "example": "Jessica" },
{ "param_name": "order_number", "example": "SKBUP2-4CPIG9" }
]
}
}
]
}'
```
## Send
```typescript Send template - TypeScript SDK icon="square-js" theme={null}
import { WhatsAppClient } from '@kapso/whatsapp-cloud-api';
const client = new WhatsAppClient({
baseUrl: 'https://api.kapso.ai/meta/whatsapp',
kapsoApiKey: process.env.KAPSO_API_KEY!
});
await client.messages.sendTemplate({
phoneNumberId: '647015955153740',
to: '15551234567',
template: {
name: 'order_confirmation',
language: { code: 'en_US' },
components: [
{
type: 'body',
parameters: [
{ type: 'text', parameterName: 'customer_name', text: 'Jessica' },
{ type: 'text', parameterName: 'order_number', text: 'SKBUP2-4CPIG9' }
]
}
]
}
});
```
```bash Send template - REST API icon="code" theme={null}
curl -X POST 'https://api.kapso.ai/meta/whatsapp/v24.0/647015955153740/messages' \
-H 'X-API-Key: YOUR_API_KEY' \
-H 'Content-Type: application/json' \
-d '{
"messaging_product": "whatsapp",
"to": "15551234567",
"type": "template",
"template": {
"name": "order_confirmation",
"language": {
"code": "en_US"
},
"components": [
{
"type": "body",
"parameters": [
{ "type": "text", "parameter_name": "customer_name", "text": "Jessica" },
{ "type": "text", "parameter_name": "order_number", "text": "SKBUP2-4CPIG9" }
]
}
]
}
}'
```
# Text header
Source: https://docs.kapso.ai/docs/whatsapp/templates/text-header
Create and send templates with text header, body, and footer
## Create
```typescript Create template - TypeScript SDK icon="square-js" theme={null}
import { WhatsAppClient } from '@kapso/whatsapp-cloud-api';
const client = new WhatsAppClient({
baseUrl: 'https://api.kapso.ai/meta/whatsapp',
kapsoApiKey: process.env.KAPSO_API_KEY!
});
await client.templates.create({
businessAccountId: '123456789',
name: 'seasonal_promotion',
category: 'MARKETING',
language: 'en_US',
parameterFormat: 'NAMED',
components: [
{
type: 'HEADER',
format: 'TEXT',
text: '{{sale_name}} is here!',
example: {
header_text_named_params: [
{ param_name: 'sale_name', example: 'Black Friday Sale' }
]
}
},
{
type: 'BODY',
text: 'Save big with code {{discount_code}}. Offer ends {{end_date}}.',
example: {
body_text_named_params: [
{ param_name: 'discount_code', example: 'BF2024' },
{ param_name: 'end_date', example: 'November 30th' }
]
}
},
{
type: 'FOOTER',
text: 'Terms and conditions apply'
}
]
});
```
```bash Create template - REST API icon="code" theme={null}
curl -X POST 'https://api.kapso.ai/meta/whatsapp/v24.0/123456789/message_templates' \
-H 'X-API-Key: YOUR_API_KEY' \
-H 'Content-Type: application/json' \
-d '{
"name": "seasonal_promotion",
"category": "MARKETING",
"language": "en_US",
"parameter_format": "NAMED",
"components": [
{
"type": "HEADER",
"format": "TEXT",
"text": "{{sale_name}} is here!",
"example": {
"header_text_named_params": [
{ "param_name": "sale_name", "example": "Black Friday Sale" }
]
}
},
{
"type": "BODY",
"text": "Save big with code {{discount_code}}. Offer ends {{end_date}}.",
"example": {
"body_text_named_params": [
{ "param_name": "discount_code", "example": "BF2024" },
{ "param_name": "end_date", "example": "November 30th" }
]
}
},
{
"type": "FOOTER",
"text": "Terms and conditions apply"
}
]
}'
```
## Send
```typescript Send template - TypeScript SDK icon="square-js" theme={null}
import { WhatsAppClient } from '@kapso/whatsapp-cloud-api';
const client = new WhatsAppClient({
baseUrl: 'https://api.kapso.ai/meta/whatsapp',
kapsoApiKey: process.env.KAPSO_API_KEY!
});
await client.messages.sendTemplate({
phoneNumberId: '647015955153740',
to: '15551234567',
template: {
name: 'seasonal_promotion',
language: { code: 'en_US' },
components: [
{
type: 'header',
parameters: [
{ type: 'text', parameterName: 'sale_name', text: 'Black Friday Sale' }
]
},
{
type: 'body',
parameters: [
{ type: 'text', parameterName: 'discount_code', text: 'BF2024' },
{ type: 'text', parameterName: 'end_date', text: 'November 30th' }
]
}
]
}
});
```
```bash Send template - REST API icon="code" theme={null}
curl -X POST 'https://api.kapso.ai/meta/whatsapp/v24.0/647015955153740/messages' \
-H 'X-API-Key: YOUR_API_KEY' \
-H 'Content-Type: application/json' \
-d '{
"messaging_product": "whatsapp",
"to": "15551234567",
"type": "template",
"template": {
"name": "seasonal_promotion",
"language": {
"code": "en_US"
},
"components": [
{
"type": "header",
"parameters": [
{ "type": "text", "parameter_name": "sale_name", "text": "Black Friday Sale" }
]
},
{
"type": "body",
"parameters": [
{ "type": "text", "parameter_name": "discount_code", "text": "BF2024" },
{ "type": "text", "parameter_name": "end_date", "text": "November 30th" }
]
}
]
}
}'
```
# Calls
Source: https://docs.kapso.ai/docs/whatsapp/typescript-sdk/calls
Initiate and manage WhatsApp voice calls with the TypeScript SDK
The SDK includes helpers for Meta’s Calling API and Kapso’s call logs.
## Initiate a call
```ts theme={null}
await client.calls.connect({
phoneNumberId: '123',
to: '14085551234',
session: { sdpType: 'offer', sdp: 'v=0...' },
bizOpaqueCallbackData: 'tracking'
});
```
`connect` also accepts `recipient` (a business-scoped user ID) instead of, or alongside, `to`. The phone number wins when both are present.
## Pre-accept / Accept
```ts theme={null}
await client.calls.preAccept({
phoneNumberId: '123',
callId: 'wacid....',
session: { sdpType: 'answer', sdp: 'v=0' }
});
await client.calls.accept({
phoneNumberId: '123',
callId: 'wacid....',
session: { sdpType: 'answer', sdp: 'v=0' }
});
```
## Reject / Terminate
```ts theme={null}
await client.calls.reject({ phoneNumberId: '123', callId: 'wacid....' });
await client.calls.terminate({ phoneNumberId: '123', callId: 'wacid....' });
```
## Check call permission
```ts theme={null}
const perm = await client.calls.permissions.get({
phoneNumberId: '123',
userWaId: '15551234567'
});
```
## Managed billing
User-initiated calls are free. Meta charges business-initiated calls based on duration and destination.
Kapso managed billing for Calling is available for eligible USD WABAs, including active Multi-partner Solutions. Calling fees are settled from Meta Call Analytics and appear as aggregate USD usage rather than an exact fee for each call.
When calls use Kapso infrastructure, Kapso checks billing health and project balance before starting a business-initiated call. When a Tech Provider uses its own infrastructure, Kapso checks Call Analytics about every five minutes and cannot block calls made through that integration.
See [Multi-partner Solutions](/docs/platform/tech-providers/multi-partner-solutions#whatsapp-calling) for details.
## Call logs (Kapso)
```ts theme={null}
import { buildKapsoFields } from '@kapso/whatsapp-cloud-api';
const logs = await client.calls.list({
phoneNumberId: '123',
direction: 'INBOUND',
limit: 20,
fields: buildKapsoFields()
});
```
Calling is gradually rolling out in WhatsApp and may not be enabled for all numbers.
> For a complete list of Kapso fields and presets, see [Kapso Extensions](/docs/whatsapp/typescript-sdk/kapso-extensions).
# Contacts
Source: https://docs.kapso.ai/docs/whatsapp/typescript-sdk/contacts
List, get, and update WhatsApp contacts with the TypeScript SDK
Kapso keeps a directory of contacts observed in conversations. You can filter by tenant (`customerId`) and WA ID.
## List contacts
```ts theme={null}
import { buildKapsoFields } from '@kapso/whatsapp-cloud-api';
const contacts = await client.contacts.list({
phoneNumberId: '123',
customerId: '00000000-0000-0000-0000-000000000000',
limit: 50,
fields: buildKapsoFields(['contact_name'])
});
contacts.data.forEach((c) => {
console.log(c.id, c.waId, c.profileName, c.customerId);
});
```
Optional filters
* `waId`: WhatsApp ID (E.164 without plus). The SDK accepts `56911112222` and normalizes server-side.
* `customerId`: UUID to scope contacts to a tenant.
* `hasCustomer`: `true` | `false` to find linked/unlinked contacts.
## Get a contact
```ts theme={null}
const contact = await client.contacts.get({
phoneNumberId: '123',
waId: '56911112222'
});
```
## Update a contact
```ts theme={null}
await client.contacts.update({
phoneNumberId: '123',
waId: '56911112222',
profileName: 'Alice',
metadata: { tags: ['vip'], source: 'import' }
});
```
Contacts APIs are provided by Kapso. Use the proxy base URL to enable them.
> For a complete list of Kapso fields and presets, see [Kapso Extensions](/docs/whatsapp/typescript-sdk/kapso-extensions).
# Conversations
Source: https://docs.kapso.ai/docs/whatsapp/typescript-sdk/conversations
List, get, and update WhatsApp conversations with the TypeScript SDK
Kapso exposes conversation history via the Meta proxy. Results include the phone number and the Kapso `phoneNumberId` that owns the conversation.
## List conversations
```ts theme={null}
import { buildKapsoFields } from '@kapso/whatsapp-cloud-api';
const page = await client.conversations.list({
phoneNumberId: '123',
status: 'active',
lastActiveSince: '2025-01-01T00:00:00Z',
limit: 20,
fields: buildKapsoFields([
'contact_name',
'messages_count',
'last_message_id',
'last_message_type',
'last_message_timestamp',
'last_message_text',
'last_inbound_at',
'last_outbound_at'
])
});
page.data.forEach((c) => {
console.log(c.id, c.phoneNumber, c.status, c.phoneNumberId);
});
```
Common filters
* `status`: `active` | `ended`
* `lastActiveSince` / `lastActiveUntil`: ISO-8601 timestamps
* `phoneNumber`: filter by customer phone (E.164)
* `assignedUserId`: filter by active assignee user ID (must be a project member)
* `unassigned`: filter conversations with no active assignment (boolean, cannot be combined with `assignedUserId`)
* `fields`: specify Kapso extras (use `buildKapsoFields()` for the full set or `kapso()` to omit them). See [Kapso Extensions](/docs/whatsapp/typescript-sdk/kapso-extensions) for the conversation field list.
## Get a conversation
```ts theme={null}
const conv = await client.conversations.get({ conversationId: 'conv-123' });
// Example shape
// {
// id: 'conv-123',
// phoneNumber: '+15551234567',
// status: 'active',
// lastActiveAt: '2025-01-01T12:00:00Z',
// phoneNumberId: '123',
// metadata: {}
// }
```
## Update status
```ts theme={null}
await client.conversations.updateStatus({
conversationId: 'conv-123',
status: 'ended'
});
```
Conversations are available when using the Kapso proxy base URL. When calling Meta directly, this data is not provided by Graph.
# Flows
Source: https://docs.kapso.ai/docs/whatsapp/typescript-sdk/flows
Author, deploy, preview, and send WhatsApp Flows with the TypeScript SDK
Requires a WhatsApp Business Account (WABA) with Flow feature enabled. Works with Meta access tokens or Kapso API keys.
## Send a flow message
```ts theme={null}
await client.messages.sendInteractiveFlow({
phoneNumberId: '123',
to: '56961567267',
bodyText: 'Start the onboarding',
parameters: {
flowId: '1197715005513101',
flowCta: 'Open', // required, 1-20 chars
flowMessageVersion: '3', // defaults to "3"
flowAction: 'navigate',
flowActionPayload: { screen: 'WELCOME' }
}
});
```
## Create a flow
```ts theme={null}
const { id } = await client.flows.create({
wabaId: 'WABA_ID',
name: 'Onboarding',
categories: ['OTHER'],
endpointUri: 'https://example.com/flows/endpoint',
flowJson: {
version: '7.2',
dataApiVersion: '3.0',
routingModel: { WELCOME: [] },
screens: [
{
id: 'WELCOME',
terminal: true,
layout: {
type: 'SingleColumnLayout',
children: [
{ type: 'TextHeading', text: 'Welcome!' },
{ type: 'Footer', label: 'Finish', onClickAction: { name: 'complete', payload: {} } }
]
}
}
]
}
});
```
Write your Flow JSON in camelCase—the SDK converts it for you:
```ts theme={null}
const myFlowJson = {
version: '7.2',
dataApiVersion: '3.0',
routingModel: { BOOKING: [] },
screens: [
{
id: 'BOOKING',
title: 'Booking',
terminal: true,
data: {
availableSlots: { type: 'array', items: { type: 'object', properties: { id: { type: 'string' }, title: { type: 'string' } } }, __example__: [{ id: '1', title: '09:00' }] },
isDropdownVisible: { type: 'boolean', __example__: false }
},
layout: { type: 'SingleColumnLayout', children: [{ type: 'TextHeading', text: 'Pick a time' }] }
}
]
};
```
Meta's fixed keys (like component `type`) stay as-is. Your custom keys can be camelCase.
## Update flow JSON
```ts theme={null}
await client.flows.updateAsset({
flowId: id,
json: myFlowJson, // or file: Blob/ArrayBuffer
phoneNumberId: '1234567890' // when using Kapso proxy
});
```
Safe to call repeatedly during development.
## Publish & deprecate
```ts theme={null}
await client.flows.publish({ flowId: id, phoneNumberId: '1234567890' });
await client.flows.deprecate({ flowId: id, phoneNumberId: '1234567890' });
```
## Preview flows
```ts theme={null}
const { preview } = await client.flows.preview({
flowId: id,
phoneNumberId: '1234567890',
interactive: true
});
console.log(preview.previewUrl);
```
## Get & list flows
```ts theme={null}
const metadata = await client.flows.get<{ id: string; name: string }>({ flowId: id, fields: 'id,name' });
const list = await client.flows.list<{ data: Array<{ id: string }> }>({ wabaId: 'WABA_ID', limit: 10 });
```
## Handle data endpoint callbacks
```ts theme={null}
import express from 'express';
import { receiveFlowEvent, respondToFlow } from '@kapso/whatsapp-cloud-api/server';
const app = express();
app.post('/flows/onboarding', express.raw({ type: '*/*' }), async (req, res) => {
try {
const ctx = await receiveFlowEvent({
rawBody: req.body as Buffer,
phoneNumberId: process.env.PHONE_ID!,
getPrivateKey: async () => process.env.FLOW_PRIVATE_KEY_PEM!
});
const reply = respondToFlow({ screen: ctx.screen, data: {} });
res.status(reply.status).set(reply.headers).send(reply.body);
} catch (e: any) {
const status = e?.status ?? 500;
res.status(status).set(e?.headers ?? {}).send(e?.body ?? { error: 'unexpected' });
}
});
```
## Using Kapso proxy
```ts theme={null}
const client = new WhatsAppClient({
baseUrl: 'https://app.kapso.ai/api/meta/',
kapsoApiKey: process.env.KAPSO_API_KEY!
});
await client.flows.publish({ flowId: 'FLOW', phoneNumberId: '1234567890' });
```
You can use either `phoneNumberId` or `businessAccountId` when publishing.
## Errors & types
Meta returns validation errors with snake\_case field names. The SDK converts them to camelCase and adds helpful hints.
Export `FlowInteractiveInput` for typing. The `flowMessageVersion` defaults to `"3"`.
# Interactive messages
Source: https://docs.kapso.ai/docs/whatsapp/typescript-sdk/interactive
Send buttons, lists, products, flows, and more interactive types
Every interactive builder accepts `recipient` (a business-scoped user ID) instead of, or alongside, `to`. See [Addressing](/docs/whatsapp/typescript-sdk/messages#addressing-to-and-recipient).
## Buttons
```ts theme={null}
await client.messages.sendInteractiveButtons({
phoneNumberId: '123',
to: '56961567267',
bodyText: 'Pick an option',
buttons: [
{ id: 'accept', title: 'Accept' },
{ id: 'decline', title: 'Decline' }
],
header: { type: 'text', text: 'Confirm your choice' },
footerText: 'Reply with a button'
});
```
## Lists
```ts theme={null}
await client.messages.sendInteractiveList({
phoneNumberId: '123',
to: '56961567267',
bodyText: 'Choose a category',
buttonText: 'View options',
sections: [
{
title: 'Main menu',
rows: [
{ id: 'opt1', title: 'Option 1', description: 'First option' },
{ id: 'opt2', title: 'Option 2', description: 'Second option' }
]
}
]
});
```
## Products
### Single product
```ts theme={null}
await client.messages.sendInteractiveProduct({
phoneNumberId: '123',
to: '56961567267',
catalogId: 'CAT123',
productRetailerId: 'SKU-1',
bodyText: 'Check out this product'
});
```
### Product list
```ts theme={null}
await client.messages.sendInteractiveProductList({
phoneNumberId: '123',
to: '56961567267',
catalogId: 'CAT123',
bodyText: 'Our catalog',
sections: [
{
title: 'Featured items',
productItems: [
{ productRetailerId: 'SKU-1' },
{ productRetailerId: 'SKU-2' }
]
}
]
});
```
## Flows
```ts theme={null}
await client.messages.sendInteractiveFlow({
phoneNumberId: '123',
to: '56961567267',
bodyText: 'Complete the form',
parameters: {
flowId: 'FLOW123',
flowCta: 'Open', // required by Meta (1-20 chars)
flowAction: 'navigate',
flowActionPayload: { screen: 'welcome' }
}
});
```
## CTA URL
```ts theme={null}
await client.messages.sendInteractiveCtaUrl({
phoneNumberId: '123',
to: '56961567267',
bodyText: 'Check out our latest deals',
parameters: {
displayText: 'Visit Website', // max 20 chars
url: 'https://example.com/deals'
}
});
```
## Carousel
Free-form interactive carousels are media cards sent inside the 24-hour customer
service window. They are different from approved carousel templates, which use
`sendTemplate` and a template `carousel` component.
```ts theme={null}
await client.messages.sendInteractiveCarousel({
phoneNumberId: '123',
to: '56961567267',
bodyText: 'Choose an option',
cards: [
{
cardIndex: 0,
header: { type: 'image', image: { link: 'https://example.com/one.jpg' } },
bodyText: 'First card',
action: {
type: 'cta_url',
displayText: 'View',
url: 'https://example.com/one'
}
},
{
cardIndex: 1,
header: { type: 'video', video: { link: 'https://example.com/two.mp4' } },
bodyText: 'Second card',
action: {
displayText: 'View',
url: 'https://example.com/two'
}
}
]
});
```
For quick replies, every card must use the same number of quick-reply buttons:
```ts theme={null}
await client.messages.sendInteractiveCarousel({
phoneNumberId: '123',
to: '56961567267',
bodyText: 'Pick a product',
cards: [
{
cardIndex: 0,
header: { type: 'image', image: { link: 'https://example.com/a.jpg' } },
action: {
buttons: [
{ id: 'product_a', title: 'Select' }
]
}
},
{
cardIndex: 1,
header: { type: 'image', image: { link: 'https://example.com/b.jpg' } },
action: {
buttons: [
{ id: 'product_b', title: 'Select' }
]
}
}
]
});
```
## Location request
```ts theme={null}
await client.messages.sendInteractiveLocationRequest({
phoneNumberId: '123',
to: '56961567267',
bodyText: 'Please share your location',
parameters: {
requestMessage: 'Please share your location'
}
});
```
## Address message
```ts theme={null}
// India only
await client.messages.sendInteractiveAddress({
phoneNumberId: '123',
to: '91XXXXXXXXXX',
bodyText: 'Please confirm your delivery address',
parameters: {
country: 'IN',
// Optional: pre-fill values
values: {
name: 'John Doe',
phoneNumber: '+919876543210',
inPinCode: '400063',
city: 'Mumbai'
}
}
});
```
## Call permission request
```ts theme={null}
await client.messages.sendInteractiveCallPermission({
phoneNumberId: '123',
to: '56961567267',
bodyText: 'We would like to call you about your order',
parameters: {
phoneNumber: '+15551234567'
}
});
```
## Catalog message
```ts theme={null}
await client.messages.sendInteractiveCatalogMessage({
phoneNumberId: '123',
to: '56961567267',
bodyText: 'Browse our full catalog',
parameters: {
thumbnailProductRetailerId: 'SKU_FEATURED' // optional
}
});
```
# Quickstart
Source: https://docs.kapso.ai/docs/whatsapp/typescript-sdk/introduction
Use the open-source library to call the WhatsApp Cloud API
The SDK mirrors Meta’s official Cloud API surface so anything you build here works against Meta directly.
When you point it at Kapso, you can opt into additional fields with `fields=kapso(...)`, store history, and query conversations.
## Installation
```bash theme={null}
npm install @kapso/whatsapp-cloud-api
# or
yarn add @kapso/whatsapp-cloud-api
```
## Quickstart
**Direct Meta API (default)**
```ts theme={null}
import { WhatsAppClient } from '@kapso/whatsapp-cloud-api';
const client = new WhatsAppClient({ accessToken: process.env.META_ACCESS_TOKEN! });
await client.messages.sendText({ phoneNumberId: '647015955153740', to: '56961567267', body: 'Hello!' });
```
**Via Kapso proxy**
```ts theme={null}
const client = new WhatsAppClient({ baseUrl: 'https://app.kapso.ai/api/meta/', kapsoApiKey: process.env.KAPSO_API_KEY! });
// Kapso adds storage/query APIs and optional kapso(...) fields in responses
```
When using `accessToken` without the Kapso base URL, Kapso storage and query features are not used.
## Support matrix
| Feature | WhatsApp token (direct Graph) | Kapso base URL + API key |
| ----------------------------------------------------------------------- | :---------------------------: | :----------------------: |
| Send text | ✅ | ✅ |
| Send media (image, video, audio, document, sticker) | ✅ | ✅ |
| Send location | ✅ | ✅ |
| Send interactive (buttons, list, product, product list, flow, carousel) | ✅ | ✅ |
| Send template | ✅ | ✅ |
| Reactions | ✅ | ✅ |
| Mark read | ✅ | ✅ |
| Media: upload | ✅ | ✅ |
| Media: get/delete | ✅ | ✅ |
| Templates: list (by WABA) | ✅ | ✅ |
| Phone numbers: settings/profile | ✅ | ✅ |
| Calling: connect/pre‑accept/accept/reject/terminate | ✅ | ✅ |
| Calling: permissions.get | ✅ | ✅ |
| Conversations: list/get | ❌ | ✅ |
| Messages history: query/list by conversation | ❌ | ✅ |
| Contacts: list/get/update | ❌ | ✅ |
| Call logs: list | ❌ | ✅ |
| Storage of conversations/messages | ❌ | ✅ |
| Supabase sync | ❌ | ✅ |
## Next steps
Check out the guides to learn how to send different types of messages and use advanced features.
Source code, examples, and issue tracker for the SDK.
REST endpoints if you want to build your own client.
# Kapso extensions
Source: https://docs.kapso.ai/docs/whatsapp/typescript-sdk/kapso-extensions
Get extra fields with both REST API and TypeScript SDK
Kapso returns Meta-compatible objects by default. Opt in to extra fields using `fields=kapso(...)` on list endpoints.
Works with:
* **REST API**: Add `?fields=kapso(...)` query parameter
* **TypeScript SDK**: Pass `fields` option with helper functions or string
These extensions add valuable context like message direction, media URLs, conversation details, and more.
## Usage
### TypeScript SDK
The SDK includes helper functions for common field sets:
```ts theme={null}
import {
buildKapsoFields,
buildKapsoMessageFields
} from '@kapso/whatsapp-cloud-api';
// All default Kapso message fields
const page = await client.messages.query({
phoneNumberId: '123',
limit: 20,
fields: buildKapsoFields()
});
// Subset of fields
const subset = await client.messages.query({
phoneNumberId: '123',
limit: 20,
fields: buildKapsoMessageFields('flow_response', 'flow_token')
});
// Or use string format
const messages = await client.messages.query({
phoneNumberId: '123',
limit: 20,
fields: 'kapso(default)'
});
```
### REST API
Add the `fields` query parameter:
```bash theme={null}
# All default Kapso fields
GET /v24.0/{phone_number_id}/messages?fields=kapso(default)
# Specific fields only
GET /v24.0/{phone_number_id}/messages?fields=kapso(direction,media_url,contact_name)
# Omit Kapso fields (Meta-only)
GET /v24.0/{phone_number_id}/messages?fields=kapso()
```
**Available selectors:**
* `kapso(default)` or `kapso(*)` or `kapso(all)` - All default Kapso fields
* `kapso(field1,field2,...)` - Specific fields only
* `kapso()` - Omit Kapso fields entirely
## Message fields
| Field | Description |
| -------------------------- | --------------------------------------------------------------------------------------------- |
| `direction` | Inbound or outbound based on the business number. |
| `status` | Server-side message status when available. |
| `processing_status` | Internal processing status when available. |
| `phone_number` | Business phone number for the message. |
| `has_media` | True when a media blob is attached. |
| `media_data` | URL, filename, content type, and byte size for stored media. |
| `media_url` | Direct URL to the attached media. Inbound: immediate. Outbound: appears shortly after send. |
| `whatsapp_conversation_id` | Internal conversation identifier. |
| `contact_name` | Display name when known. |
| `message_type_data` | Per-type hints such as caption, sticker info, or location values. |
| `content` | Original message fragment stored by Kapso (for example, entire interactive/catalog payloads). |
| `flow_response` | Parsed Flows response JSON. |
| `flow_token` | Token from a Flows response when present. |
| `flow_name` | Name from a Flows response when present. |
| `order_text` | Optional note from order messages. |
## Conversation fields
Use these with conversations endpoint.
| Field | Description |
| ------------------------ | ------------------------------------------------------------------------- |
| `contact_name` | Display name for the contact linked to the conversation. |
| `messages_count` | Total number of stored messages in the conversation. |
| `last_message_id` | WAMID of the most recent message in the conversation. |
| `last_message_type` | Type of the most recent message (for example text, image, reaction). |
| `last_message_timestamp` | ISO 8601 timestamp of the most recent message. |
| `last_message_text` | Text or caption preview when available (may be empty for non-text types). |
| `last_inbound_at` | ISO 8601 timestamp of the most recent inbound message. |
| `last_outbound_at` | ISO 8601 timestamp of the most recent outbound message. |
## Examples
### All Kapso fields
```ts TypeScript SDK theme={null}
await client.messages.query({
phoneNumberId: '123',
limit: 20,
fields: buildKapsoFields()
});
```
```bash REST API theme={null}
GET /v24.0/123/messages?fields=kapso(default)
```
### Subset of fields
```ts TypeScript SDK theme={null}
await client.messages.query({
phoneNumberId: '123',
limit: 20,
fields: 'kapso(flow_response,flow_token,media_url)'
});
```
```bash REST API theme={null}
GET /v24.0/123/messages?fields=kapso(flow_response,flow_token,media_url)
```
### Omit Kapso fields
```ts TypeScript SDK theme={null}
await client.messages.query({
phoneNumberId: '123',
limit: 20,
fields: 'kapso()'
});
```
```bash REST API theme={null}
GET /v24.0/123/messages?fields=kapso()
```
## Webhooks
The SDK includes a normalization helper for Meta webhooks. It returns the same structure as history queries and sets `kapso.direction`. SMB echoes are tagged with `kapso.source = "smb_message_echo"`. Other webhook fields are available under `events.raw.`.
```ts theme={null}
import { normalizeWebhook, verifySignature } from '@kapso/whatsapp-cloud-api/server';
app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => {
if (!verifySignature({
appSecret: process.env.META_APP_SECRET!,
rawBody: req.body,
signatureHeader: req.headers['x-hub-signature-256'] as string
})) return res.status(401).end();
const events = normalizeWebhook(JSON.parse(req.body.toString('utf8')));
events.messages.forEach(m => console.log(m.type, m.kapso?.direction));
res.sendStatus(200);
});
```
# Media upload
Source: https://docs.kapso.ai/docs/whatsapp/typescript-sdk/media
Upload and manage media files
## Upload media
```ts theme={null}
const imageBlob = new Blob([/* binary data */], { type: 'image/png' });
const uploadResult = await client.media.upload({
phoneNumberId: '123',
type: 'image',
file: imageBlob,
fileName: 'photo.png'
});
console.log(uploadResult.id); // Media ID
```
## Use uploaded media
```ts theme={null}
await client.messages.sendImage({
phoneNumberId: '123',
to: '56961567267',
image: { id: uploadResult.id, caption: 'Uploaded image' }
});
```
## Get media metadata
```ts theme={null}
const metadata = await client.media.get({
mediaId: 'MEDIA_ID',
phoneNumberId: '123' // Required when using Kapso proxy
});
```
## Delete media
```ts theme={null}
await client.media.delete({
mediaId: 'MEDIA_ID',
phoneNumberId: '123' // Required when using Kapso proxy
});
```
## Display media
URL‑first with Kapso (recommended):
```ts theme={null}
import { buildKapsoMessageFields } from '@kapso/whatsapp-cloud-api';
const fields = buildKapsoMessageFields('media_url');
const page = await client.messages.listByConversation({
phoneNumberId: '123',
conversationId: 'conv-123',
fields
});
const msg = page.data.find(m => m.type === 'image');
const src = msg?.kapso?.mediaUrl ?? msg?.image?.link; // render this URL
```
Raw bytes fallback (works for Meta and Kapso):
```ts theme={null}
// Returns Blob in browsers when as: 'blob'
const blob = await client.media.download({ mediaId: 'MEDIA_ID', phoneNumberId: '123', as: 'blob' });
const src = URL.createObjectURL(blob);
```
Notes:
* The SDK downloads from WhatsApp CDNs without auth headers and adds auth only for Kapso hosts. No special configuration is needed.
* Kapso mirrors outbound media shortly after send; inbound media is available immediately.
# Messages
Source: https://docs.kapso.ai/docs/whatsapp/typescript-sdk/messages
Send text, media, location, contacts and reactions with the TypeScript SDK
## Addressing: to and recipient
Every send accepts `to` (a phone number), `recipient` (a business-scoped user ID, including the parent `ENT` variant), or both. When both are present, Meta delivers to the phone number, so sending both is the recommended default once you have both identifiers.
```ts theme={null}
// By BSUID (the phone number never arrived)
await client.messages.sendText({
phoneNumberId: '123',
recipient: 'US.13491208655302741918',
body: 'Hola desde Kapso!'
});
// Both identifiers
await client.messages.sendText({
phoneNumberId: '123',
to: '56961567267',
recipient: 'US.13491208655302741918',
body: 'Hola desde Kapso!'
});
```
One limit from Meta: one-tap, zero-tap and copy-code authentication templates require a phone number and cannot be delivered to a BSUID. See [Business-scoped user IDs](/docs/whatsapp/business-scoped-user-ids) for the full migration.
## Text
```ts theme={null}
await client.messages.sendText({
phoneNumberId: '123',
to: '56961567267',
body: 'Hola desde Kapso!',
// Optional: disable URL previews
previewUrl: false
});
```
## Image
```ts theme={null}
await client.messages.sendImage({
phoneNumberId: '123',
to: '56961567267',
image: {
link: 'https://example.com/photo.jpg',
caption: 'Check this out!'
}
});
// Or with uploaded media ID
await client.messages.sendImage({
phoneNumberId: '123',
to: '56961567267',
image: { id: 'MEDIA_ID', caption: 'Uploaded photo' }
});
```
## Video
```ts theme={null}
await client.messages.sendVideo({
phoneNumberId: '123',
to: '56961567267',
video: {
link: 'https://example.com/demo.mp4',
caption: 'Product demo'
}
});
```
## Audio
```ts theme={null}
// Basic audio message
await client.messages.sendAudio({
phoneNumberId: '123',
to: '56961567267',
audio: {
link: 'https://example.com/ringtone.mp3'
}
});
```
## Voice message
```ts theme={null}
// Voice note with transcription support (.ogg with OPUS codec)
await client.messages.sendAudio({
phoneNumberId: '123',
to: '56961567267',
audio: {
id: 'MEDIA_ID',
voice: true
}
});
```
## Document
```ts theme={null}
await client.messages.sendDocument({
phoneNumberId: '123',
to: '56961567267',
document: {
link: 'https://example.com/report.pdf',
filename: 'report.pdf',
caption: 'Q4 Report'
}
});
```
## Sticker
```ts theme={null}
await client.messages.sendSticker({
phoneNumberId: '123',
to: '56961567267',
sticker: {
id: 'STICKER_MEDIA_ID'
}
});
```
## Location
```ts theme={null}
await client.messages.sendLocation({
phoneNumberId: '123',
to: '56961567267',
location: {
latitude: -33.4489,
longitude: -70.6693,
name: 'Santiago',
address: 'CL'
}
});
```
## Contacts
```ts theme={null}
await client.messages.sendContacts({
phoneNumberId: '123',
to: '56961567267',
contacts: [
{
name: { formatted_name: 'Ada Lovelace' },
org: { company: 'Analytical Engines' },
phones: [{ phone: '+44123456789', type: 'WORK' }]
}
]
});
```
## Reaction
```ts theme={null}
await client.messages.sendReaction({
phoneNumberId: '123',
to: '56961567267',
reaction: { messageId: 'wamid.SOME_ID', emoji: '👍' }
});
```
## Mark as read
```ts theme={null}
await client.messages.markRead({
phoneNumberId: '123',
messageId: 'wamid.SOME_ID'
});
// With typing indicator (dismissed on send or after ~25s)
await client.messages.markRead({
phoneNumberId: '123',
messageId: 'wamid.SOME_ID',
typingIndicator: { type: 'text' }
});
```
## Message history
```ts theme={null}
import { buildKapsoFields } from '@kapso/whatsapp-cloud-api';
// Query by time and direction
const page = await client.messages.query({
phoneNumberId: '123',
direction: 'inbound',
since: '2025-01-01T00:00:00Z',
limit: 50,
fields: buildKapsoFields() // include Kapso extras (direction, flow_response, media_data, ...)
});
// List messages for a single conversation
const convMessages = await client.messages.listByConversation({
phoneNumberId: '123',
conversationId: 'conv-123',
limit: 25,
fields: buildKapsoFields(['media_url', 'flow_response']) // request only the fields you need
});
// Graph-style cursors are exposed under page.paging.cursors.before and page.paging.cursors.after
const nextCursor = page.paging.cursors.after;
> Tip: Include `media_url` to render images/documents directly when Kapso has attached them. See [Kapso Extensions](/docs/whatsapp/typescript-sdk/kapso-extensions) for the full list.
```
# Phone numbers
Source: https://docs.kapso.ai/docs/whatsapp/typescript-sdk/phone-numbers
Manage WhatsApp phone number settings and business profile
## Settings
```ts theme={null}
// Read settings
const s = await client.phoneNumbers.settings.get({ phoneNumberId: '123' });
// Update settings
await client.phoneNumbers.settings.update({
phoneNumberId: '123',
fallbackLanguage: 'en_US'
});
```
## Business profile
```ts theme={null}
// Read profile
const p = await client.phoneNumbers.businessProfile.get({ phoneNumberId: '123' });
// Update profile
await client.phoneNumbers.businessProfile.update({
phoneNumberId: '123',
about: 'My business',
email: 'hello@example.com',
websites: ['https://example.com']
});
```
These methods call the Meta Graph API and work with or without the Kapso proxy.
# Templates
Source: https://docs.kapso.ai/docs/whatsapp/typescript-sdk/templates
Build and send approved WhatsApp templates
## Build with components
```ts theme={null}
import { buildTemplatePayload } from '@kapso/whatsapp-cloud-api';
const template = buildTemplatePayload({
name: 'order_confirmation',
language: 'en_US', // or { code: 'en_US', policy: 'deterministic' }
components: [
{
type: 'body',
parameters: [
{ type: 'text', text: 'Ada' },
{ type: 'text', text: '#1234' }
]
}
]
});
await client.messages.sendTemplate({
phoneNumberId: '123',
to: '56961567267',
template
});
```
The SDK validates structures with Zod and converts your keys to snake\_case for the Meta API.
## Typed helper (optional)
Prefer typed guardrails? Use `buildTemplateSendPayload`. It outputs the same Meta structure with compile‑time guidance.
```ts theme={null}
import { buildTemplateSendPayload } from '@kapso/whatsapp-cloud-api';
const template = buildTemplateSendPayload({
name: 'order_confirmation',
language: 'en_US',
body: [
{ type: 'text', text: 'Ada' },
{ type: 'text', text: '#1234' }
]
});
```
### Flow buttons
```ts theme={null}
const withFlow = buildTemplateSendPayload({
name: 'flow_cta',
language: 'en_US',
buttons: [
{
type: 'button',
subType: 'flow',
index: 0,
parameters: [{ type: 'action', action: { flow_token: 'FT_123', flow_action_data: { step: 'one' } } }]
}
]
});
```
If you pass an object for language, include policy: "deterministic".
## Tips
* Ensure the template name and language match what’s approved in your WABA.
* Provide parameters in the same order they appear in the template body and buttons.
* For creation, examples are required when your header/body strings contain variables.
# Utilities
Source: https://docs.kapso.ai/docs/whatsapp/typescript-sdk/utilities
Conversations, contacts, calls, and phone settings
## Conversations
```ts theme={null}
import { buildKapsoFields } from '@kapso/whatsapp-cloud-api';
// List
const conversations = await client.conversations.list({
phoneNumberId: '123',
status: 'active',
lastActiveSince: '2025-01-01T00:00:00Z',
limit: 20,
fields: buildKapsoFields(['contact_name'])
});
// Get one (includes phoneNumberId when using Kapso proxy)
const conv = await client.conversations.get({ conversationId: 'conv-123' });
// Update status
await client.conversations.updateStatus({ conversationId: 'conv-123', status: 'ended' });
```
## Contacts
```ts theme={null}
// List (Kapso proxy)
const contacts = await client.contacts.list({
phoneNumberId: '123',
customerId: '00000000-0000-0000-0000-000000000000',
limit: 50,
fields: buildKapsoFields(['contact_name'])
});
// Get one
const contact = await client.contacts.get({ phoneNumberId: '123', waId: '56911112222' });
// Update
await client.contacts.update({
phoneNumberId: '123',
waId: '56911112222',
profileName: 'Alice',
metadata: { tags: ['vip'] }
});
```
## Calls (Calling API)
```ts theme={null}
// Initiate (business-initiated)
await client.calls.connect({
phoneNumberId: '123',
to: '14085551234',
session: { sdpType: 'offer', sdp: 'v=0...' },
bizOpaqueCallbackData: 'tracking'
});
// Pre-accept / Accept / Reject / Terminate
await client.calls.preAccept({ phoneNumberId: '123', callId: 'wacid...', session: { sdpType: 'answer', sdp: 'v=0' } });
await client.calls.accept({ phoneNumberId: '123', callId: 'wacid...', session: { sdpType: 'answer', sdp: 'v=0' } });
await client.calls.reject({ phoneNumberId: '123', callId: 'wacid...' });
await client.calls.terminate({ phoneNumberId: '123', callId: 'wacid...' });
// Permission check
const perm = await client.calls.permissions.get({ phoneNumberId: '123', userWaId: '15551234567' });
// Kapso call logs
const callPage = await client.calls.list({
phoneNumberId: '123',
direction: 'INBOUND',
limit: 20,
fields: buildKapsoFields()
});
```
## Phone number settings & profile
```ts theme={null}
// Settings
const settings = await client.phoneNumbers.settings.get({ phoneNumberId: '123' });
await client.phoneNumbers.settings.update({ phoneNumberId: '123', fallbackLanguage: 'en_US' });
// Business profile
const profile = await client.phoneNumbers.businessProfile.get({ phoneNumberId: '123' });
await client.phoneNumbers.businessProfile.update({ phoneNumberId: '123', about: 'My business' });
```
## Kapso field helpers
```ts theme={null}
import {
KAPSO_MESSAGE_FIELDS,
buildKapsoFields,
buildKapsoMessageFields
} from '@kapso/whatsapp-cloud-api';
// Request every Kapso message extra (same as fields: 'kapso(default)')
const selector = buildKapsoFields();
// Opt into a subset
const lightweight = buildKapsoMessageFields('flow_response', 'flow_token');
const history = await client.messages.query({
phoneNumberId: '123',
limit: 20,
fields: lightweight
});
// Backend also accepts the raw strings 'kapso(default)' or 'kapso(*)'
```
## Business-scoped user IDs
`isBusinessScopedUserId` checks whether a value has the shape of a BSUID, including the parent `ENT` variant. Use it at your boundaries, for example to keep BSUIDs out of phone number columns:
```ts theme={null}
import { isBusinessScopedUserId, BSUID_PATTERN } from '@kapso/whatsapp-cloud-api';
isBusinessScopedUserId('US.13491208655302741918'); // true
isBusinessScopedUserId('US.ENT.11815799212886844830'); // true
isBusinessScopedUserId('56961567267'); // false
```
The send builders deliberately do not enforce the shape, so a future format change on Meta's side doesn't turn valid sends into client-side errors.
## Webhooks
```ts theme={null}
import express from 'express';
import { normalizeWebhook, verifySignature } from '@kapso/whatsapp-cloud-api/server';
const app = express();
app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => {
if (!verifySignature({
appSecret: process.env.META_APP_SECRET!,
rawBody: req.body,
signatureHeader: req.headers['x-hub-signature-256'] as string
})) {
return res.status(401).end();
}
const payload = JSON.parse(req.body.toString('utf8'));
const events = normalizeWebhook(payload);
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()` unwraps Meta’s Graph payload, converts it to the same structure returned by `messages.query`, and sets `kapso.direction` (`"inbound"`/`"outbound"`). SMB echoes are tagged with `kapso.source = "smb_message_echo"`, and every other webhook field (for example `accountAlerts`, `templateCategoryUpdate`) is available under `events.raw.`.
Identity fields are typed on every event: `message.fromUserId` (the sender's BSUID, present on every inbound message), `message.from` (the phone number, which can be absent once the sender adopts a username), `contact.userId` and `contact.profile.username`, and `status.recipientUserId`. Direction inference works even when no phone number arrives.
> See the full list of Kapso fields in [Kapso Extensions](/docs/whatsapp/typescript-sdk/kapso-extensions).
`verifySignature` depends on Node’s `crypto` module and is intended for server runtimes. If your bundler shows a warning about `node:crypto` being external, that’s expected — mark it as external or set the bundler platform to `node`.
# Build in the canvas
Source: https://docs.kapso.ai/docs/workflows/build-in-canvas
Create, connect, test, and activate a workflow from the dashboard
The canvas is the default way to build a workflow. No CLI, no local setup.
Open **Workflows** in the dashboard and create one, either from scratch or from a **workflow template**.
## Add and connect nodes
Pick a node type from the node picker to drop it on the canvas. The available types are:
`start` · `send_text` · `send_template` · `send_interactive` · `wait_for_response` · `decide` · `agent` · `function` · `webhook` · `call` · `handoff` · `emit_event`
Every workflow needs exactly one `start` node. Drag from a node's handle to another node to create an edge. Execution follows those edges.
See [Step types](/docs/flows/step-types/start-node) for what each node does and how to configure it.
## Configure a node
Select a node to open its form. Fields depend on the type: message body for `send_text`, template and parameters for `send_template`, model and prompt for `agent`, and so on.
Message fields accept template variables like `{{last_user_input}}`. See [Variables and context](/docs/flows/variables-and-context).
## Branch with conditions
Edges leaving a `decide` node carry conditions. The label the node returns picks which edge runs. Edit conditions on the edge itself, not the node.
See [Edges](/docs/flows/edges).
## Set a trigger
A workflow does nothing until something starts it. Open the triggers panel to add one, most commonly an inbound message on a phone number.
See [Triggers](/docs/flows/triggers).
## Workflow settings
The settings sheet holds everything that is not a node:
* **Workflow name** and **Slug**: the slug is generated from the name and is what the API and other workflows use to reference this workflow
* **Description**
* **Inbound read behavior**: whether incoming messages are marked as read
* **Message debounce**: how long to wait for more messages before running, so a user sending three messages in a row triggers one run instead of three
## Test before activating
Run the workflow from the test modal without messaging a real number. It shows the **execution stack** so you can see which nodes ran, in what order, and where a run stopped. Use **Copy events** to grab the full event trail.
For runs that already happened, open the workflow logs.
## Draft and Active
A workflow is **Draft** until you activate it. Draft workflows do not respond to triggers, so you can keep editing a live workflow's replacement without affecting production.
Save your changes, test, then activate.
## Observer mode
Observer mode lets a workflow watch a conversation without sending messages. Useful to validate routing logic against real traffic before letting it reply.
## Move to code later
Anything built in the canvas can be pulled into a local repo, edited as JSON or TypeScript, and pushed back. The canvas has a **Build this workflow locally** action that gives you the commands.
See [Build locally](/docs/workflows/build-locally).
# Build locally
Source: https://docs.kapso.ai/docs/workflows/build-locally
Pull workflows into a local repo, edit them as JSON or code, and push them back to Kapso.
Use the Kapso CLI to keep workflows and functions in a local repository. You can edit the generated `definition.json` directly, or use `@kapso/workflows` to author the workflow in JavaScript or TypeScript.
The Platform API stores workflow definitions as JSON with `snake_case` config keys. The JavaScript library uses idiomatic `camelCase` inputs and compiles them to the same JSON shape before `kapso push`.
## Start a local repo
```bash theme={null}
# 1. Create a local workspace
mkdir kapso-workflows
cd kapso-workflows
# 2. Install the Kapso CLI and workflow library
npm init -y
npm install -g @kapso/cli
npm install --save-dev @kapso/workflows
# 3. Link this directory to your Kapso project
kapso login
kapso link --project
kapso pull
# 4. Install Kapso agent skills
npx skills add gokapso/agent-skills
```
`kapso link` binds the current directory to one Kapso project. `kapso pull` downloads source files for workflows and functions.
## Repository layout
After pulling, the repo includes project binding metadata and one directory per source object.
```text theme={null}
kapso.yaml
.kapso/project.json
.kapso/remote-map.json
functions/
send-slack-message/
function.yaml
index.js
workflows/
support-router/
workflow.yaml
definition.json
workflow.js
```
Commit the files you want to version. Keep `.kapso/remote-map.json` in shared repos because it stores the last pulled remote state for dirty-file and stale-update checks.
## Choose JSON or code
Both paths are supported:
* Edit `definition.json` when you want the exact Platform API graph payload.
* Edit `workflow.js` or `workflow.ts` when you want a boring code API for constructing the graph.
* Keep or ignore generated `definition.json` as a team decision. The CLI works either way.
`kapso pull` creates `workflow.js` next to `definition.json` when no workflow source file exists. After you edit that source file, future pulls preserve it and update only the remote-owned JSON/YAML files.
## Build with code
```javascript theme={null}
import { START, Workflow } from "@kapso/workflows";
const workflow = new Workflow("support-router", {
name: "Support Router",
status: "draft"
});
workflow.addTrigger({
type: "inbound_message",
phoneNumberId: ""
});
workflow.addNode(START, {
position: { x: 100, y: 100 }
});
workflow.addNode("intro", {
type: "send_text",
message: "Hi {{vars.customer_name}}, how can we help?"
}, {
position: { x: 360, y: 100 }
});
workflow.addNode("wait_reply", {
type: "wait_for_response",
saveResponseTo: "latest_reply",
timeoutSeconds: 300
}, {
position: { x: 620, y: 100 }
});
workflow.addNode("classify", {
type: "decide",
decisionType: "ai",
providerModel: "gpt-5-mini",
conditions: [
{ label: "billing", description: "Billing, invoices, or payments" },
{ label: "technical", description: "Product or technical support" }
]
}, {
position: { x: 880, y: 100 }
});
workflow.addEdge(START, "intro");
workflow.addEdge("intro", "wait_reply");
workflow.addEdge("wait_reply", "classify");
export default workflow;
```
Build the workflow JSON before pushing:
```bash theme={null}
kapso build
```
If a pulled `workflow.js` file is unchanged, `kapso push` uses `workflow.yaml` and `definition.json`. Once you edit `workflow.js` or add `workflow.ts`, `kapso push` compiles that source before uploading.
## Push changes
Preview changes before writing to Kapso:
```bash theme={null}
kapso push --dry-run
```
Push one workflow:
```bash theme={null}
kapso push workflow support-router
```
Push one function:
```bash theme={null}
kapso push function send-slack-message
```
Push everything in the local repo:
```bash theme={null}
kapso push
```
Workflows and functions are matched by slug. If a remote object with the same slug exists but your repo does not have a pull baseline, run `kapso pull` first.
## Node reference
Each node has a stable `id`, a `type`, and optional display/position options. Edges reference node IDs.
```javascript theme={null}
workflow.addNode("node_id", { type: "send_text", message: "Hello" });
workflow.addEdge("node_id", "next_node");
```
The table below shows the library shape and the Platform API `node_type`.
| Node | API `node_type` | Library config |
| ----------------- | ------------------- | ------------------------------------------------------------------------------------------------------------------------------------- |
| Start | `start` | `workflow.addNode(START, { position })` |
| Send text | `send_text` | `{ type: "send_text", message, phoneNumberId?, toPhoneNumber?, delaySeconds?, providerModel?, aiFields? }` |
| Send template | `send_template` | `{ type: "send_template", templateId, parameters?, phoneNumberId?, toPhoneNumber?, providerModel?, aiFields? }` |
| Send interactive | `send_interactive` | `{ type: "send_interactive", interactiveType, bodyText?, buttons?, listSections?, ctaUrl?, flowId?, phoneNumberId?, toPhoneNumber? }` |
| Wait for response | `wait_for_response` | `{ type: "wait_for_response", saveResponseTo?, timeoutSeconds? }` |
| Decide | `decide` | `{ type: "decide", decisionType: "ai" / "function", conditions, providerModel? }` or `{ functionSlug }` for function decisions |
| Function | `function` | `{ type: "function", functionSlug, saveResponseTo? }` |
| Webhook | `webhook` | `{ type: "webhook", url, method?, headers?, bodyTemplate?, saveResponseTo?, providerModel?, aiFields? }` |
| Agent | `agent` | `{ type: "agent", systemPrompt, providerModel?, functionTools?, webhooks?, mcpServers?, resources? }` |
| Call workflow | `call` | `{ type: "call", workflowSlug, saveErrorTo? }` |
| Handoff | `handoff` | `{ type: "handoff", reason?, contextData? }` |
| Set variable | `set_variable` | `{ type: "set_variable", variableName, variableValue, valueType? }` |
| Raw | any | `{ type: "raw", nodeType, config }` for fields not yet modeled by the library |
Use `rawConfig` on typed nodes when you need an extra Platform API field that the library has not modeled yet:
```javascript theme={null}
workflow.addNode("send", {
type: "send_text",
message: "Hello",
rawConfig: {
experimental_field: true
}
});
```
## References by slug
Local source should use stable slugs where possible:
* Function nodes use `functionSlug`.
* Function decision nodes use `functionSlug`.
* Agent function tools use `functionSlug`.
* Call workflow nodes use `workflowSlug`.
During `kapso push`, the CLI resolves those slugs to remote IDs before calling the Platform API. Direct Platform API requests should use `function_id` and `workflow_id`.
## API definition shape
A workflow definition is a graph:
```json theme={null}
{
"nodes": [
{
"id": "start",
"type": "flow-node",
"position": { "x": 100, "y": 100 },
"data": {
"node_type": "start",
"config": {},
"display_name": "Start"
}
}
],
"edges": []
}
```
When using the Platform API directly:
* Use `snake_case` config keys.
* Use `GET /workflows/{workflow_id}/definition` to read the expanded graph.
* Use `PATCH /workflows/{workflow_id}` to update metadata and graph collections.
* Include the latest `lock_version` to avoid overwriting remote changes.
* The update response returns workflow metadata, not the expanded definition. Refetch the definition when you need the saved graph.
Example update:
```bash theme={null}
curl -X PATCH "https://api.kapso.ai/platform/v1/workflows/" \
-H "X-API-Key: " \
-H "Content-Type: application/json" \
-d '{
"workflow": {
"lock_version": 3,
"definition": {
"nodes": [
{
"id": "start",
"type": "flow-node",
"position": { "x": 100, "y": 100 },
"data": { "node_type": "start", "config": {} }
},
{
"id": "intro",
"type": "flow-node",
"position": { "x": 360, "y": 100 },
"data": {
"node_type": "send_text",
"config": {
"message": "Hi {{vars.customer_name}}",
"phone_number_id": ""
}
}
}
],
"edges": [
{ "source": "start", "target": "intro", "label": "next" }
]
}
}
}'
```
Inside `definition`, `nodes` and `edges` are replacement sets when present. If you send only one node in `definition.nodes`, every other node is removed. Omit `definition.nodes` or `definition.edges` when you want that collection to remain unchanged.
## Sync behavior
The CLI protects local and remote work during sync:
* `kapso pull` refuses to overwrite dirty local remote-owned files.
* `kapso pull --diff` shows blocked incoming diffs without writing files.
* `kapso pull --overwrite` replaces local edits in remote-owned files with the remote version.
* `kapso pull` preserves authored `workflow.js` and `workflow.ts` files.
* `kapso push` checks the last pulled remote version before updating existing objects.
* Remote-only workflows and functions are reported, not deleted.
* Local-only workflows and functions are created on push.
## Build with AI
The setup block above installs the Kapso agent skills. For AI-assisted edits, point your coding assistant at the workflow directory and ask it to edit `workflow.js` or `workflow.ts`.
Kapso docs are available to AI tools through the MCP endpoint:
```text theme={null}
https://docs.kapso.ai/mcp
```
# Examples
Source: https://docs.kapso.ai/docs/workflows/examples
Workflow examples and use cases
## AI agent in 2 minutes
## AI agent connected to Google Calendar
## WhatsApp to Slack summary
## Login with WhatsApp Flows
# Introduction
Source: https://docs.kapso.ai/docs/workflows/introduction
Automate WhatsApp conversations with visual flows and AI steps
A workflow is a directed graph where nodes execute in sequence. Each node performs one action: sending a message, waiting for input, routing logic, running code, or handing off to a human.
Workflows run on a trigger, most often an inbound WhatsApp message.
More in [Examples](/docs/workflows/examples).
## Node categories
**Start node** - Entry point
* Triggers when a message arrives or when the API calls the workflow
* Every workflow has exactly one Start node
* Automatically advances to the first connected node
**Action nodes** - Execute and continue
* Send messages (text, templates, interactive)
* Execute functions
* Call webhooks
* Set workflow variables
* Call other workflows
* Transfer to human agents (handoff)
* Execute immediately and advance to the next node
**Wait nodes** - Pause for input
* Halt execution until the user responds
* Store the response in `{{last_user_input}}` or a custom variable
* Resume when a message arrives
**Decide nodes** - Route execution
* Branch to different paths
* Use AI to interpret intent, or custom function logic
* The returned label determines which edge to follow
**Agent nodes** - AI conversations
* Multi-turn conversations with tool access
* Can call webhooks and functions
* Stay at the node until the agent calls `complete_task`
* New user messages are injected automatically during the conversation
## Execution flow
Execution follows the edges between nodes and continues until:
* A Wait node pauses for user input
* An Agent node is reached, staying there until it calls `complete_task`
* A Handoff node stops the run and transfers to a human
* No connected nodes remain, and the workflow completes
## Two ways to build
Drag nodes, connect them, and publish from the dashboard. No setup required.
Pull workflows into a repo, edit them as JSON or TypeScript, and push them back.
Both produce the same workflow. You can start in the canvas and pull it into code later, or push from code and keep editing in the canvas.
## Learn more
Every node type and its configuration
Access user data and workflow state with template variables
Start workflows from messages or API calls
Serverless JavaScript your workflows can call
# Start and resume via API
Source: https://docs.kapso.ai/docs/workflows/start-and-resume-via-api
Start workflow executions and resume waiting ones with the Platform API
Two endpoints cover the whole lifecycle from an external system:
* `POST /platform/v1/workflows/{workflow_id}/executions` — start an execution
* `POST /platform/v1/workflow_executions/{execution_id}/resume` — resume an execution that is waiting for input
Both are asynchronous: they queue the work and return immediately.
## Before you start
* The workflow status must be `active`
* The workflow needs an active [API trigger](/docs/flows/triggers#api-trigger)
* Every request needs an `X-API-Key` header
## Start an execution
```bash theme={null}
curl -X POST "https://api.kapso.ai/platform/v1/workflows/$WORKFLOW_ID/executions" \
-H "X-API-Key: $KAPSO_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"workflow_execution": {
"phone_number": "+14155552671",
"phone_number_id": "123456789012345",
"variables": {
"customer_name": "Alice Smith",
"order_total": 149.99
},
"context": {
"source": "checkout_flow"
}
}
}'
```
| Field | Description |
| -------------------- | ------------------------------------------------------------------------------------------ |
| `phone_number` | E.164 number of the participant. Required unless you send `recipient`. |
| `recipient` | BSUID or parent BSUID of the participant. Required unless you send `phone_number`. |
| `phone_number_id` | WhatsApp number that handles the conversation. Defaults to the project's WhatsApp config. |
| `whatsapp_config_id` | Deprecated. Use `phone_number_id`. |
| `variables` | Initial workflow variables, available as `{{var_name}}` or `{{vars.var_name}}`. |
| `context` | Extra contextual data merged into the `context` namespace, outside the variable namespace. |
| `initial_data` | Custom payload passed to the workflow's first step. |
Response is `202 Accepted`:
```json theme={null}
{
"data": {
"message": "Workflow execution initiated",
"workflow_id": "workflow_uuid",
"id": "execution_uuid",
"tracking_id": "tracking_uuid"
}
}
```
Keep `id` to fetch or resume the execution, and `tracking_id` to correlate it with your own systems.
### Start by BSUID
When you only have a [business-scoped user ID](/docs/whatsapp/business-scoped-user-ids), send `recipient` instead of `phone_number`:
```bash theme={null}
curl -X POST "https://api.kapso.ai/platform/v1/workflows/$WORKFLOW_ID/executions" \
-H "X-API-Key: $KAPSO_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"workflow_execution": {
"recipient": "US.13491208655302741918",
"phone_number_id": "123456789012345"
}
}'
```
If you send both identifiers, the phone number routes the conversation and the BSUID is kept as the participant identity. BSUID recipients are not supported on sandbox numbers.
### Execution context
An API-triggered execution starts with:
```
{{system.trigger_type}} # "api_call"
{{system.tracking_id}} # tracking_id from the response
{{system.api_key_id}} # API key used
{{system.trigger_whatsapp_config_id}} # WhatsApp config handling the conversation
{{context.channel}} # "api"
{{context.phone_number}} # normalized phone number, when sent
{{context.whatsapp_business_scoped_user_id}} # when recipient is a BSUID
{{context.whatsapp_parent_business_scoped_user_id}} # when recipient is a parent BSUID (US.ENT.…)
{{vars.your_variable}} # from the variables object
```
See [Triggers](/docs/flows/triggers#workflow-context) for the full context reference.
## Check execution status
```bash theme={null}
curl "https://api.kapso.ai/platform/v1/workflow_executions/$EXECUTION_ID" \
-H "X-API-Key: $KAPSO_API_KEY"
```
The execution row is created when the queued job picks up the request, so a lookup fired immediately after the `202` can return `404`. Retry after a moment.
To find executions that need input, filter the workflow's execution list:
```bash theme={null}
curl "https://api.kapso.ai/platform/v1/workflows/$WORKFLOW_ID/executions?status=waiting" \
-H "X-API-Key: $KAPSO_API_KEY"
```
## Resume a waiting execution
An execution enters `waiting` when it reaches a [wait for response](/docs/flows/step-types/wait-for-response-node) step. Resume it with the input the workflow was waiting for:
```bash theme={null}
curl -X POST "https://api.kapso.ai/platform/v1/workflow_executions/$EXECUTION_ID/resume" \
-H "X-API-Key: $KAPSO_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"message": {
"kind": "payload",
"data": "yes, proceed"
},
"variables": {
"confirmed_at": "2026-08-11T18:00:00Z"
}
}'
```
| Field | Description |
| -------------- | ---------------------------------------------------------------------------- |
| `message.data` | Required. String, object, or array passed to the waiting step. |
| `message.kind` | Optional. Defaults to `payload`. |
| `variables` | Optional. Merged into the execution's `vars`; existing keys are overwritten. |
Note that `message` and `variables` sit at the top level, not under `workflow_execution`.
The response is `200 OK` with the execution's `id`, `status`, `tracking_id`, `current_step`, and timestamps. Resume work runs in the background, so the returned status can still be `waiting` — poll `GET /platform/v1/workflow_executions/{id}` to see it advance.
Only one resume can be pending at a time. A second request while the first is still pending returns `409 Conflict`.
## Errors
| Status | When |
| ------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `400` | Resume request without a `message` object or without `message.data`. |
| `404` | Workflow or execution not found in this project. |
| `409` | A resume is already pending, or the execution is being processed by another request. Retry after a short delay. |
| `422` | Workflow not active, no active API trigger, neither `phone_number` nor `recipient` sent, `recipient` is not a valid BSUID or parent BSUID, `recipient` used with a sandbox number, `phone_number_id` not in this project, or the execution is not in `waiting` status. |
| `429` | Burst rate limit exceeded. Retry after the seconds in `Retry-After`. |
The start endpoint has a burst limiter on top of the general [rate limits](/api/rate-limits), scoped by API key and workflow, resetting every second:
* `legacy` / `free`: 5 requests per second
* `pro`: 15 requests per second
* `enterprise` / `platform`: 30 requests per second
Successful responses carry `X-Burst-RateLimit-Limit` and `X-Burst-RateLimit-Remaining`.