# Campaign Detail
Source: https://docs.apifycloud.io/api-reference/analytics/campaigns-detail
Fetch a single campaign and its stats
## Endpoint
`GET https://api.apifycloud.io/api/v1/analytics/{appId}/campaigns/{campaignId}`
## Authentication
Bearer token required.
Scope: `campaigns:read`
## Path parameters
* `appId` (UUID, required)
* `campaignId` (UUID, required)
## Response
```json theme={null}
{
"data": {
"id": "uuid",
"name": "string",
"status": "string",
"template_name": "string",
"scheduled_at": "2024-01-01T00:00:00.000Z",
"completed_at": "2024-01-01T00:00:00.000Z",
"total_contacts": 0,
"sent_count": 0,
"delivered_count": 0,
"read_count": 0,
"failed_count": 0,
"created_at": "2024-01-01T00:00:00.000Z",
"metadata": {}
},
"meta": {
"timestamp": "2024-01-01T00:00:00.000Z"
}
}
```
## Errors
* `403 forbidden` unauthorized for app or missing scope
* `404 not_found` campaign not found
* `400 validation_error` invalid path parameters
* `429 rate_limit_exceeded`
* `500 server_error`
## Example
```bash theme={null}
curl "https://api.apifycloud.io/api/v1/analytics/{appId}/campaigns/{campaignId}" \
-H "Authorization: Bearer {access_token}"
```
# List Campaigns
Source: https://docs.apifycloud.io/api-reference/analytics/campaigns-list
List campaigns with filters and cursor pagination
## Endpoint
`GET https://api.apifycloud.io/api/v1/analytics/{appId}/campaigns`
## Authentication
Bearer token required.
Scope: `campaigns:read`
## Path parameters
* `appId` (UUID, required)
## Query parameters
* `limit` (number, optional) page size for cursor pagination
* `cursor` (string, optional) base64 cursor from the previous response
* `status` (string, optional) one of `all`, `active`, `completed`, `paused`, `failed`, `scheduled`, `running`
* `search` (string, optional) search by campaign name
* `tags` (string, optional) comma-separated tags (e.g. `vip,launch`)
* `startDate` (string, optional) ISO date (YYYY-MM-DD)
* `endDate` (string, optional) ISO date (YYYY-MM-DD)
## Response
```json theme={null}
{
"data": [
{
"id": "uuid",
"name": "string",
"status": "string",
"total_contacts": 0,
"sent_count": 0,
"created_at": "2024-01-01T00:00:00.000Z"
}
],
"meta": {
"timestamp": "2024-01-01T00:00:00.000Z"
},
"pagination": {
"limit": 25,
"hasMore": false,
"nextCursor": null
}
}
```
## Errors
* `403 forbidden` unauthorized for app or missing scope
* `400 validation_error` invalid query parameters
* `429 rate_limit_exceeded`
* `500 server_error`
## Example
```bash theme={null}
curl "https://api.apifycloud.io/api/v1/analytics/{appId}/campaigns?limit=25&status=running&tags=vip,launch" \
-H "Authorization: Bearer {access_token}"
```
# Search Campaigns
Source: https://docs.apifycloud.io/api-reference/analytics/campaigns-search
Search campaigns with advanced filters and cursor pagination
## Endpoint
`POST https://api.apifycloud.io/api/v1/analytics/{appId}/campaigns/search`
## Authentication
Bearer token required.
Scope: `campaigns:read`
## Request body
* `limit` (number, optional)
* `cursor` (string, optional)
* `filter.status` (array of strings, optional)
* `filter.name` (string, optional)
* `filter.tags` (array of strings, optional)
* `filter.startDate` (string, optional, ISO datetime)
* `filter.endDate` (string, optional, ISO datetime)
```json theme={null}
{
"limit": 25,
"filter": {
"status": ["running", "scheduled"],
"name": "launch",
"tags": ["vip"],
"startDate": "2026-02-01T00:00:00.000Z",
"endDate": "2026-02-28T23:59:59.999Z"
}
}
```
## Response
```json theme={null}
{
"data": [
{
"id": "uuid",
"name": "Launch 1",
"status": "running",
"total_contacts": 2000,
"sent_count": 1500,
"created_at": "2026-02-18T00:00:00.000Z"
}
],
"meta": {
"timestamp": "2026-02-18T00:00:00.000Z"
},
"pagination": {
"limit": 25,
"hasMore": false,
"nextCursor": null
}
}
```
## Errors
* `400 invalid_json` invalid JSON body
* `400 validation_error` invalid request body
* `403 forbidden` unauthorized for app or missing scope
* `429 rate_limit_exceeded`
* `500 server_error`
# Contact History
Source: https://docs.apifycloud.io/api-reference/analytics/contacts-history
Get campaign message history for a contact
## Endpoint
`GET https://api.apifycloud.io/api/v1/analytics/{appId}/contacts/{contactId}/history`
## Authentication
Bearer token required.
Scope: `analytics:read`
## Path parameters
* `appId` (UUID, required)
* `contactId` (string, required)
## Query parameters
* `limit` (number, optional)
* `cursor` (string, optional)
## Response
```json theme={null}
{
"data": [
{
"id": "uuid",
"campaign_name": "Launch",
"template_name": "hsm_odontologia_v0",
"status": "delivered",
"sent_at": "2026-02-18T00:00:00.000Z",
"delivered_at": "2026-02-18T00:01:00.000Z",
"read_at": null,
"failed_at": null,
"error_message": null,
"created_at": "2026-02-18T00:00:00.000Z"
}
],
"meta": {
"timestamp": "2026-02-18T00:00:00.000Z"
},
"pagination": {
"limit": 25,
"hasMore": false,
"nextCursor": null
}
}
```
## Errors
* `403 forbidden` unauthorized for app or missing scope
* `400 validation_error` invalid path/query parameters
* `429 rate_limit_exceeded`
* `500 server_error`
# Message Detail
Source: https://docs.apifycloud.io/api-reference/analytics/messages-detail
Fetch a single connector message by ID
## Endpoint
`GET https://api.apifycloud.io/api/v1/analytics/{appId}/messages/{messageId}`
## Authentication
Bearer token required.
Scope: `messages:read`
## Path parameters
* `appId` (UUID, required)
* `messageId` (string, required)
## Response
```json theme={null}
{
"data": {
"id": "string",
"app_id": "uuid",
"source": "string",
"destination": "string",
"provider_from": "string",
"provider_to": "string",
"provider_from_id": "string",
"provider_to_id": "string",
"channel": "whatsapp",
"direction": "outbound",
"message_type": "template",
"user_phone": "573001112233",
"business_id": "string",
"conversation_key": "string",
"external_ids": {},
"source_payload": {},
"outgoing_payload": {},
"response_payload": {},
"provider_message_id": "string",
"status": "string",
"error_message": null,
"sent_at": "2024-01-01T00:00:00.000Z",
"failed_at": "2024-01-01T00:00:00.000Z",
"created_at": "2024-01-01T00:00:00.000Z",
"updated_at": "2024-01-01T00:00:00.000Z"
},
"meta": {
"timestamp": "2024-01-01T00:00:00.000Z"
}
}
```
## Errors
* `403 forbidden` unauthorized for app or missing scope
* `404 not_found` message not found
* `400 validation_error` invalid path parameters (`messageId` is required)
* `429 rate_limit_exceeded`
* `500 server_error`
## Example
```bash theme={null}
curl "https://api.apifycloud.io/api/v1/analytics/{appId}/messages/{messageId}" \
-H "Authorization: Bearer {access_token}"
```
# List Messages
Source: https://docs.apifycloud.io/api-reference/analytics/messages-list
List connector messages with filters and cursor pagination
## Endpoint
`GET https://api.apifycloud.io/api/v1/analytics/{appId}/messages`
## Authentication
Bearer token required.
Scope: `messages:read`
## Path parameters
* `appId` (UUID, required)
## Query parameters
* `limit` (number, optional)
* `cursor` (string, optional)
* `status` (string, optional)
* `direction` (string, optional) e.g. `inbound`, `outbound`
* `channel` (string, optional) e.g. `whatsapp`
* `messageType` (string, optional) provider message type
* `source` (string, optional) connector source system
* `destination` (string, optional) connector destination system
* `search` (string, optional) matches `user_phone`, `provider_message_id`, `conversation_key`, `provider_from`, `provider_to`
* `startDate` (string, optional) ISO date (YYYY-MM-DD)
* `endDate` (string, optional) ISO date (YYYY-MM-DD)
## Response
```json theme={null}
{
"data": [
{
"id": "string",
"source": "string",
"destination": "string",
"provider_from": "string",
"provider_to": "string",
"channel": "whatsapp",
"direction": "outbound",
"message_type": "template",
"user_phone": "573001112233",
"conversation_key": "string",
"provider_message_id": "string",
"status": "string",
"sent_at": "2024-01-01T00:00:00.000Z",
"failed_at": null,
"error_message": null,
"created_at": "2024-01-01T00:00:00.000Z",
// ... more connector fields are available in detail endpoint
}
],
"meta": {
"timestamp": "2024-01-01T00:00:00.000Z"
},
"pagination": {
"limit": 25,
"hasMore": false,
"nextCursor": null
}
}
```
## Errors
* `403 forbidden` unauthorized for app or missing scope
* `400 validation_error` invalid query parameters
* `429 rate_limit_exceeded`
* `500 server_error`
## Example
```bash theme={null}
curl "https://api.apifycloud.io/api/v1/analytics/{appId}/messages?limit=25&direction=outbound&channel=whatsapp&search=57300" \
-H "Authorization: Bearer {access_token}"
```
# Search Messages
Source: https://docs.apifycloud.io/api-reference/analytics/messages-search
Search connector messages with complex filters
## Endpoint
`POST https://api.apifycloud.io/api/v1/analytics/{appId}/messages/search`
## Authentication
Bearer token required.
Scope: `messages:read`
## Path parameters
* `appId` (UUID, required)
## Request body
* `limit` (number, optional)
* `cursor` (string, optional)
* `status` (string, optional)
* `direction` (string, optional) e.g. `inbound`, `outbound`
* `channel` (string, optional)
* `messageType` (string, optional)
* `source` (string, optional)
* `destination` (string, optional)
* `search` (string, optional) matches `user_phone`, `provider_message_id`, `conversation_key`, `provider_from`, `provider_to`
* `startDate` (string, optional) ISO date (YYYY-MM-DD)
* `endDate` (string, optional) ISO date (YYYY-MM-DD)
```json theme={null}
{
"limit": 25,
"status": "pending",
"direction": "outbound",
"channel": "whatsapp",
"messageType": "text",
"source": "api",
"destination": "gupshup",
"search": "57300"
}
```
## Response
```json theme={null}
{
"data": [
{
"id": "string",
"source": "string",
"destination": "string",
"provider_from": "string",
"provider_to": "string",
"channel": "whatsapp",
"direction": "outbound",
"message_type": "text",
"user_phone": "573001112233",
"conversation_key": "string",
"provider_message_id": "string",
"status": "string",
"error_message": null,
"sent_at": "2024-01-01T00:00:00.000Z",
"failed_at": null,
"created_at": "2024-01-01T00:00:00.000Z",
// ... more connector fields are available in detail endpoint
}
],
"meta": {
"timestamp": "2024-01-01T00:00:00.000Z"
},
"pagination": {
"limit": 25,
"hasMore": false,
"nextCursor": null
}
}
```
## Errors
* `400 invalid_request` invalid JSON body
* `403 forbidden` unauthorized for app or missing scope
* `400 validation_error` invalid request parameters
* `429 rate_limit_exceeded`
* `500 server_error`
## Example
```bash theme={null}
curl -X POST "https://api.apifycloud.io/api/v1/analytics/{appId}/messages/search" \
-H "Authorization: Bearer {access_token}" \
-H "Content-Type: application/json" \
-d '{
"limit": 25,
"status": "pending",
"direction": "outbound",
"channel": "whatsapp",
"search": "57300"
}'
```
# Daily Metrics
Source: https://docs.apifycloud.io/api-reference/analytics/metrics-daily
Get daily aggregated metrics for an app
## Endpoint
`GET https://api.apifycloud.io/api/v1/analytics/{appId}/metrics/daily`
## Authentication
Bearer token required.
Scope: `analytics:read`
## Query parameters
* `startDate` (string, optional, ISO datetime)
* `endDate` (string, optional, ISO datetime)
* `timezone` (string, optional, default `UTC`)
## Response
```json theme={null}
{
"data": [
{
"date": "2026-02-18",
"sent": 100,
"delivered": 90,
"read": 70,
"failed": 10,
"cost": 0
}
],
"meta": {
"timestamp": "2026-02-18T00:00:00.000Z"
}
}
```
## Errors
* `403 forbidden` unauthorized for app or missing scope
* `400 validation_error` invalid query parameters
* `429 rate_limit_exceeded`
* `500 server_error`
# Metrics Summary
Source: https://docs.apifycloud.io/api-reference/analytics/metrics-summary
Get summary metrics for an app in a date range
## Endpoint
`GET https://api.apifycloud.io/api/v1/analytics/{appId}/metrics/summary`
## Authentication
Bearer token required.
Scope: `analytics:read`
## Query parameters
* `startDate` (string, optional, ISO datetime)
* `endDate` (string, optional, ISO datetime)
## Response
```json theme={null}
{
"data": {
"sent": 1000,
"delivered": 900,
"read": 700,
"failed": 100,
"cost": 0,
"contacts": 1200
},
"meta": {
"timestamp": "2026-02-18T00:00:00.000Z"
}
}
```
## Errors
* `403 forbidden` unauthorized for app or missing scope
* `400 validation_error` invalid query parameters
* `429 rate_limit_exceeded`
* `500 server_error`
# Outbound Message Detail
Source: https://docs.apifycloud.io/api-reference/analytics/outbound-messages-detail
Fetch a single outbound message by ID
## Endpoint
`GET https://api.apifycloud.io/api/v1/analytics/{appId}/outbound-messages/{messageId}`
## Authentication
Bearer token required.
Scope: `messages:read`
## Path parameters
* `appId` (UUID, required)
* `messageId` (UUID, required)
## Response
```json theme={null}
{
"data": {
"id": "uuid",
"campaign_id": "uuid",
"recipient_phone": "string",
"template_name": "string",
"status": "string",
"sent_at": "2024-01-01T00:00:00.000Z",
"delivered_at": "2024-01-01T00:00:00.000Z",
"read_at": "2024-01-01T00:00:00.000Z",
"failed_at": "2024-01-01T00:00:00.000Z",
"error_code": "string",
"error_reason": "string",
"cost": 0,
"currency": "USD",
"created_at": "2024-01-01T00:00:00.000Z",
"metadata": {}
},
"meta": {
"timestamp": "2024-01-01T00:00:00.000Z"
}
}
```
## Errors
* `403 forbidden` unauthorized for app or missing scope
* `404 not_found` message not found
* `400 validation_error` invalid path parameters
* `429 rate_limit_exceeded`
* `500 server_error`
## Example
```bash theme={null}
curl "https://api.apifycloud.io/api/v1/analytics/{appId}/outbound-messages/{messageId}" \
-H "Authorization: Bearer {access_token}"
```
# List Outbound Messages
Source: https://docs.apifycloud.io/api-reference/analytics/outbound-messages-list
List outbound messages (campaign + direct) with filters and cursor pagination
## Endpoint
`GET https://api.apifycloud.io/api/v1/analytics/{appId}/outbound-messages`
## Authentication
Bearer token required.
Scope: `messages:read`
## Path parameters
* `appId` (UUID, required)
## Query parameters
* `limit` (number, optional)
* `cursor` (string, optional)
* `status` (string, optional)
* `campaign` (string, optional) campaign ID
* `template` (string, optional) template name (partial match)
* `source` (string, optional) one of `all`, `campaign`, `direct`
* `startDate` (string, optional) ISO date (YYYY-MM-DD)
* `endDate` (string, optional) ISO date (YYYY-MM-DD)
## Response
```json theme={null}
{
"data": [
{
"id": "uuid",
"status": "string",
"recipient_phone": "string",
"template_name": "string",
"campaign_id": "uuid",
"sent_at": "2024-01-01T00:00:00.000Z",
"delivered_at": "2024-01-01T00:00:00.000Z",
"read_at": "2024-01-01T00:00:00.000Z",
"created_at": "2024-01-01T00:00:00.000Z",
"cost": 0,
"currency": "USD",
"source": "campaign"
}
],
"meta": {
"timestamp": "2024-01-01T00:00:00.000Z"
},
"pagination": {
"limit": 25,
"hasMore": false,
"nextCursor": null
}
}
```
## Errors
* `403 forbidden` unauthorized for app or missing scope
* `400 validation_error` invalid query parameters
* `429 rate_limit_exceeded`
* `500 server_error`
## Example
```bash theme={null}
curl "https://api.apifycloud.io/api/v1/analytics/{appId}/outbound-messages?limit=25&source=campaign" \
-H "Authorization: Bearer {access_token}"
```
# Search Outbound Messages
Source: https://docs.apifycloud.io/api-reference/analytics/outbound-messages-search
Search outbound messages (campaign + direct) with complex filters
## Endpoint
`POST https://api.apifycloud.io/api/v1/analytics/{appId}/outbound-messages/search`
## Authentication
Bearer token required.
Scope: `messages:read`
## Path parameters
* `appId` (UUID, required)
## Request body
* `limit` (number, optional)
* `cursor` (string, optional)
* `status` (string, optional) one of `all`, `queued`, `sent`, `delivered`, `read`, `failed`
* `search` (string, optional) matches phone number, template, or campaign name
* `campaign` (string, optional) campaign name search
* `template` (string, optional) template name search
* `source` (string, optional) one of `all`, `campaign`, `direct`
* `tags` (array of strings, optional)
* `startDate` (string, optional) ISO date (YYYY-MM-DD)
* `endDate` (string, optional) ISO date (YYYY-MM-DD)
```json theme={null}
{
"limit": 25,
"status": "sent",
"search": "promo",
"source": "campaign",
"tags": ["vip"]
}
```
## Response
```json theme={null}
{
"data": [
{
"id": "uuid",
"phone_number": "string",
"status": "string",
"source": "campaign",
"template_name": "string",
"campaign_name": "string",
"created_at": "2024-01-01T00:00:00.000Z",
"sent_at": "2024-01-01T00:00:00.000Z",
"delivered_at": "2024-01-01T00:00:00.000Z",
"read_at": "2024-01-01T00:00:00.000Z"
}
],
"meta": {
"timestamp": "2024-01-01T00:00:00.000Z"
},
"pagination": {
"limit": 25,
"hasMore": false,
"nextCursor": null
}
}
```
## Errors
* `400 invalid_request` invalid JSON body
* `403 forbidden` unauthorized for app or missing scope
* `400 validation_error` invalid request parameters
* `429 rate_limit_exceeded`
* `500 server_error`
## Example
```bash theme={null}
curl -X POST "https://api.apifycloud.io/api/v1/analytics/{appId}/outbound-messages/search" \
-H "Authorization: Bearer {access_token}" \
-H "Content-Type: application/json" \
-d '{
"limit": 25,
"status": "sent",
"source": "campaign",
"tags": ["vip"]
}'
```
# Delivery Report
Source: https://docs.apifycloud.io/api-reference/analytics/reports-delivery
Get delivery performance report by campaign
## Endpoint
`GET https://api.apifycloud.io/api/v1/analytics/{appId}/reports/delivery-performance`
## Authentication
Bearer token required.
Scope: `analytics:read`
## Query parameters
* `startDate` (string, optional, ISO datetime)
* `endDate` (string, optional, ISO datetime)
## Response
```json theme={null}
{
"data": [
{
"campaign_id": "uuid",
"campaign_name": "Launch",
"created_at": "2026-02-18T00:00:00.000Z",
"sent_count": 100,
"delivered_count": 90,
"read_count": 70,
"failed_count": 10,
"delivery_rate": 90.0,
"read_rate": 77.78
}
],
"meta": {
"timestamp": "2026-02-18T00:00:00.000Z"
}
}
```
## Errors
* `403 forbidden` unauthorized for app or missing scope
* `400 validation_error` invalid query parameters
* `429 rate_limit_exceeded`
* `500 server_error`
# Engagement Report
Source: https://docs.apifycloud.io/api-reference/analytics/reports-engagement
Get engagement report grouped by template
## Endpoint
`GET https://api.apifycloud.io/api/v1/analytics/{appId}/reports/engagement`
## Authentication
Bearer token required.
Scope: `analytics:read`
## Query parameters
* `startDate` (string, optional, ISO datetime)
* `endDate` (string, optional, ISO datetime)
## Response
```json theme={null}
{
"data": [
{
"template_name": "hsm_odontologia_v0",
"campaign_count": 4,
"total_sent": 2000,
"total_read": 1300,
"read_rate_avg": 72.22
}
],
"meta": {
"timestamp": "2026-02-18T00:00:00.000Z"
}
}
```
## Errors
* `403 forbidden` unauthorized for app or missing scope
* `400 validation_error` invalid query parameters
* `429 rate_limit_exceeded`
* `500 server_error`
# List Tags
Source: https://docs.apifycloud.io/api-reference/analytics/tags-list
List distinct campaign tags
## Endpoint
`GET https://api.apifycloud.io/api/v1/analytics/{appId}/tags`
## Authentication
Bearer token required.
Scope: `analytics:read`
## Path parameters
* `appId` (UUID, required)
## Response
```json theme={null}
{
"data": ["vip", "launch", "promo"],
"meta": {
"timestamp": "2024-01-01T00:00:00.000Z"
}
}
```
## Errors
* `403 forbidden` unauthorized for app or missing scope
* `400 validation_error` invalid path parameters
* `429 rate_limit_exceeded`
* `500 server_error`
## Example
```bash theme={null}
curl "https://api.apifycloud.io/api/v1/analytics/{appId}/tags" \
-H "Authorization: Bearer {access_token}"
```
# Tag Stats
Source: https://docs.apifycloud.io/api-reference/analytics/tags-stats
Get campaign statistics for a specific tag
## Endpoint
`GET https://api.apifycloud.io/api/v1/analytics/{appId}/tags/{tag}/stats`
## Authentication
Bearer token required.
Scope: `analytics:read`
## Path parameters
* `appId` (UUID, required)
* `tag` (string, required) URL-encoded tag value
## Response
```json theme={null}
{
"data": {
"campaign_count": 0,
"sent": 0,
"delivered": 0,
"read": 0,
"failed": 0,
"last_used": "2024-01-01T00:00:00.000Z"
},
"meta": {
"timestamp": "2024-01-01T00:00:00.000Z"
}
}
```
## Errors
* `403 forbidden` unauthorized for app or missing scope
* `400 validation_error` invalid path parameters
* `429 rate_limit_exceeded`
* `500 server_error`
## Example
```bash theme={null}
curl "https://api.apifycloud.io/api/v1/analytics/{appId}/tags/vip/stats" \
-H "Authorization: Bearer {access_token}"
```
# Template Detail
Source: https://docs.apifycloud.io/api-reference/analytics/templates-detail
Get the template entity details
## Endpoint
`GET https://api.apifycloud.io/api/v1/analytics/{appId}/templates/{templateId}`
## Authentication
Bearer token required.
Scope: `analytics:read`
## Path parameters
* `appId` (UUID, required)
* `templateId` (UUID, required)
## Response
```json theme={null}
{
"data": {
"id": "uuid",
"app_id": "uuid",
"name": "hsm_odontologia_v0",
"category": "UTILITY",
"language": "es_MX",
"status": "APPROVED",
"header_type": "TEXT",
"header_text": "Header",
"body_text": "Body {{name}}",
"body_variables": [
{ "index": 1, "name": "name", "type": "TEXT", "example": "Alex" }
],
"footer_text": "Footer",
"buttons": [],
"gupshup_template_id": "string",
"gupshup_status": "APPROVED",
"gupshup_rejection_reason": null,
"gupshup_quality": "GREEN",
"button_count": 0,
"quick_reply_count": 0,
"created_at": "2024-01-01T00:00:00.000Z",
"updated_at": "2024-01-01T00:00:00.000Z"
},
"meta": {
"timestamp": "2024-01-01T00:00:00.000Z"
}
}
```
## Errors
* `403 forbidden` unauthorized for app or missing scope
* `400 validation_error` invalid path parameters
* `404 not_found` template does not exist
* `429 rate_limit_exceeded`
* `500 server_error`
## Example
```bash theme={null}
curl "https://api.apifycloud.io/api/v1/analytics/{appId}/templates/{templateId}" \
-H "Authorization: Bearer {access_token}"
```
# Templates List
Source: https://docs.apifycloud.io/api-reference/analytics/templates-list
List templates for an app with cursor pagination
## Endpoint
`GET https://api.apifycloud.io/api/v1/analytics/{appId}/templates`
## Authentication
Bearer token required.
Scope: `analytics:read`
## Query parameters
* `limit` (number, optional): page size
* `cursor` (string, optional): cursor for next page
* `search` (string, optional): case-insensitive filter by template name
* `status` (string, optional): filter by template status
* `category` (string, optional): filter by template category
* `language` (string, optional): filter by template language
## Response
The list returns the **full template structure** for every item — the same fields as
[Template Detail](/api-reference/analytics/templates-detail), so you don't need a second
request to read a template's content. Analytics-only fields (such as `quick_reply_count`)
are available **only** in the detail endpoint.
Each entry in `body_variables` has `index`, `type` (e.g. `TEXT`), `example`, and — for
**NAMED** templates — `name`. **POSITIONAL** templates omit `name` (use `index`). Header
and button variables are not returned as a separate field — they appear as `{{...}}`
tokens inside `header_text` and `buttons[].text` / `buttons[].url`.
```json theme={null}
{
"data": [
{
"id": "uuid",
"app_id": "uuid",
"name": "hsm_odontologia_v0",
"category": "UTILITY",
"language": "es_MX",
"status": "APPROVED",
"header_type": "TEXT",
"header_text": "Hi {{name}}",
"header_media_url": null,
"header_media_filename": null,
"header_location_latitude": null,
"header_location_longitude": null,
"header_location_name": null,
"header_location_address": null,
"body_text": "Your appointment is on {{date}}",
"body_variables": [
{ "index": 1, "name": "date", "type": "TEXT", "example": "2026-02-20" }
],
"footer_text": "Dental Clinic",
"buttons": [],
"gupshup_template_id": "string",
"gupshup_status": "APPROVED",
"gupshup_rejection_reason": null,
"gupshup_quality": "GREEN",
"button_count": 0,
"created_at": "2026-02-18T00:00:00.000Z",
"updated_at": "2026-02-18T00:00:00.000Z"
}
],
"pagination": {
"nextCursor": "base64cursor",
"hasMore": true,
"limit": 20
},
"meta": {
"timestamp": "2026-02-18T00:00:00.000Z"
}
}
```
## Errors
* `403 forbidden` unauthorized for app or missing scope
* `400 validation_error` invalid query/path parameters
* `429 rate_limit_exceeded`
* `500 server_error`
# Templates Stats
Source: https://docs.apifycloud.io/api-reference/analytics/templates-stats
Get top templates by usage volume (last 90 days)
## Endpoint
`GET https://api.apifycloud.io/api/v1/analytics/{appId}/templates/stats`
## Authentication
Bearer token required.
Scope: `analytics:read`
## Response
```json theme={null}
{
"data": [
{
"template_name": "hsm_odontologia_v0",
"template_id": "uuid",
"campaign_count": 12,
"sent": 1200,
"delivered": 1000,
"read": 800,
"failed": 200
}
],
"meta": {
"timestamp": "2026-02-18T00:00:00.000Z"
}
}
```
## Errors
* `403 forbidden` unauthorized for app or missing scope
* `400 validation_error` invalid path parameters
* `429 rate_limit_exceeded`
* `500 server_error`
# OAuth Revoke
Source: https://docs.apifycloud.io/api-reference/authentication/oauth-revoke
Revoke an access token (RFC 7009 behavior)
## Endpoint
`POST https://api.apifycloud.io/api/v1/oauth/revoke`
## Authentication
Bearer token required.
Scope: `oauth:revoke`
## Request body
* `token` (string, required) access token to revoke
```json theme={null}
{
"token": "eyJhbGciOi..."
}
```
## Response
Returns success even if token is invalid or already revoked.
```json theme={null}
{
"data": {},
"meta": {
"timestamp": "2026-02-18T00:00:00.000Z"
}
}
```
## Errors
* `400 invalid_request` missing token or invalid JSON body
* `401 unauthorized` invalid auth context
* `403 forbidden` missing scope
* `429 rate_limit_exceeded`
* `500 server_error`
## Example
```bash theme={null}
curl -X POST "https://api.apifycloud.io/api/v1/oauth/revoke" \
-H "Authorization: Bearer {access_token}" \
-H "Content-Type: application/json" \
-d '{"token":"{token_to_revoke}"}'
```
# OAuth Token
Source: https://docs.apifycloud.io/api-reference/authentication/oauth-token
Exchange client credentials for an access token
## Endpoint
`POST https://api.apifycloud.io/api/v1/oauth/token`
## Authentication
No bearer token required.
## Request body
* `grant_type` (string, required) must be `client_credentials`
* `client_id` (string, required)
* `client_secret` (string, required)
```json theme={null}
{
"grant_type": "client_credentials",
"client_id": "your_client_id",
"client_secret": "your_client_secret"
}
```
## Response
```json theme={null}
{
"data": {
"access_token": "string",
"token_type": "Bearer",
"expires_in": 3600
},
"meta": {
"timestamp": "2024-01-01T00:00:00.000Z"
}
}
```
## Errors
* `400 invalid_request` invalid JSON body
* `400 unsupported_grant_type` only `client_credentials` supported
* `400 invalid_client` missing `client_id` or `client_secret`
* `401 invalid_client` client not found or invalid secret
* `500 server_error`
## Example
```bash theme={null}
curl -X POST "https://api.apifycloud.io/api/v1/oauth/token" \
-H "Content-Type: application/json" \
-d '{
"grant_type": "client_credentials",
"client_id": "your_client_id",
"client_secret": "your_client_secret"
}'
```
# Introduction
Source: https://docs.apifycloud.io/api-reference/introduction
ApifyCloud API reference and response format
## Base URL
All endpoints in this reference use the base URL: `https://api.apifycloud.io/api/v1`.
## Authentication
Most endpoints require a Bearer token:
```
Authorization: Bearer {access_token}
```
Get a token using the OAuth client credentials flow in `POST /oauth/token`.
## Response format
Successful responses follow this shape:
```json theme={null}
{
"data": {},
"meta": {
"timestamp": "2024-01-01T00:00:00.000Z"
},
"pagination": {
"limit": 25,
"hasMore": false,
"nextCursor": null
}
}
```
`pagination` is only present on list endpoints that support cursor-based pagination.
Errors follow this shape:
```json theme={null}
{
"error": {
"type": "validation_error",
"message": "Invalid request parameters",
"timestamp": "2024-01-01T00:00:00.000Z",
"details": [
{
"field": "limit",
"message": "Expected number, received string",
"code": "invalid_type"
}
]
}
}
```
## Rate limits
Every authenticated response includes informational headers describing the
rate-limit bucket the API decided to surface:
| Header | Description |
| ----------------------- | -------------------------------------------------------------------- |
| `X-RateLimit-Limit` | Maximum requests allowed in the surfaced window. |
| `X-RateLimit-Remaining` | Requests remaining in the current window. |
| `X-RateLimit-Reset` | Unix epoch seconds when the surfaced window will have free capacity. |
| `X-RateLimit-Window` | Which window is being surfaced: `minute`, `hour`, or `day`. |
| `Retry-After` | On `429` only. Seconds until the next request is likely to succeed. |
When a request exceeds a limit, the API responds with `429 Too Many Requests`
and `error.type: "rate_limit_exceeded"`. See the [Rate limits guide](/guides/rate-limits/overview)
for the bucket model, the algorithm used, and recommended client-side
backoff strategy.
# Send Message
Source: https://docs.apifycloud.io/api-reference/messages/send
Overview and message type guides for POST /messages/{appId}/send
## Endpoint
`POST https://api.apifycloud.io/api/v1/messages/{appId}/send`
## Authentication
Bearer token required.
Scope: `messages:send`
## Path parameters
* `appId` (UUID, required)
## Shared request fields
* `to` (string, required) recipient phone number, **or** the recipient's WhatsApp user ID (for customers who reach you using a username and whose phone number is not available). Both formats are accepted in the same field.
* `type` (string, required) message type
* `tags` (array of strings, optional)
* `metadata` (object or string, optional)
## Response shapes
Template:
```json theme={null}
{
"data": {
"messageId": "uuid",
"status": "sent",
"providerMessageId": "string"
},
"meta": {
"timestamp": "2024-01-01T00:00:00.000Z",
"mode": "sync"
}
}
```
Non-template:
```json theme={null}
{
"data": {
"messageId": "uuid",
"status": "queued",
"logId": "uuid"
},
"meta": {
"timestamp": "2024-01-01T00:00:00.000Z"
}
}
```
## Message type guides
* [Template](/api-reference/messages/send-template)
* [Text](/api-reference/messages/send-text)
* [Media](/api-reference/messages/send-media)
* [Buttons](/api-reference/messages/send-buttons)
* [List](/api-reference/messages/send-list)
* [Flow](/api-reference/messages/send-flow)
* [Product](/api-reference/messages/send-product)
* [Product List](/api-reference/messages/send-product-list)
* [Product Carousel](/api-reference/messages/send-carousel)
* [Catalog](/api-reference/messages/send-catalog)
* [Address Request](/api-reference/messages/send-address-request)
* [Location](/api-reference/messages/send-location)
* [Location Request](/api-reference/messages/send-location-request)
* [Contacts](/api-reference/messages/send-contacts)
* [Sticker](/api-reference/messages/send-sticker)
* [CTA URL](/api-reference/messages/send-cta-url)
* [Reaction](/api-reference/messages/send-reaction)
## Errors
* `400 invalid_json` invalid JSON body
* `400 validation_error` invalid request parameters
* `400 configuration_error` provider not configured for this app
* `400 invalid_template` template is not approved
* `400 invalid_request` missing required fields for selected type
* `403 forbidden` unauthorized for app or missing scope
* `404 not_found` template not found
* `429 rate_limit_exceeded`
* `502 provider_error`
* `500 server_error`
# Address Request
Source: https://docs.apifycloud.io/api-reference/messages/send-address-request
Send a WhatsApp address request message
## Request
Use `type` as `interactive` and set `interactive.type` to `address_message`.
### Field types
* `to`: string (required)
* `type`: string = `interactive` (required)
* `interactive`: object (required)
* `interactive.type`: string = `address_message` (required)
* `interactive.body`: object (required)
* `interactive.body.text`: string (required)
* `interactive.action`: object (required)
* `interactive.action.name`: string (optional, defaults to `address_message`)
* `interactive.action.parameters`: object (required)
* `interactive.action.parameters.country`: string (required)
* `interactive.action.parameters.values`: object (optional)
* `interactive.action.parameters.saved_addresses`: array of objects (optional)
* `interactive.action.parameters.validation_errors`: object (optional)
```json theme={null}
{
"to": "+573001112233",
"type": "interactive",
"interactive": {
"type": "address_message",
"body": {
"text": "Comparte tu direccion de entrega"
},
"action": {
"name": "address_message",
"parameters": {
"country": "CO"
}
}
}
}
```
# Buttons
Source: https://docs.apifycloud.io/api-reference/messages/send-buttons
Send a WhatsApp interactive buttons message
## Request
Use `type` as `interactive` and set `interactive.type` to `button`.
### Field types
* `to`: string (required)
* `type`: string = `interactive` (required)
* `interactive`: object (required)
* `interactive.type`: string = `button` (required)
* `interactive.body`: object (required)
* `interactive.body.text`: string (required)
* `interactive.footer`: object (optional)
* `interactive.footer.text`: string (optional)
* `interactive.action`: object (required)
* `interactive.action.buttons`: array of objects (required, 1-3)
* `interactive.action.buttons[].type`: string = `reply` (required)
* `interactive.action.buttons[].reply`: object (required)
* `interactive.action.buttons[].reply.id`: string (required)
* `interactive.action.buttons[].reply.title`: string (required)
### Field behavior and limits
* `interactive.body.text` is required, max 1024 characters.
* `interactive.action.buttons` supports 1 to 3 buttons.
* `interactive.action.buttons[].reply.id` is required, max 256 characters, and should be unique per button.
* `interactive.action.buttons[].reply.title` is required, max 20 characters, and should be unique per button.
* `interactive.footer.text` is optional, max 60 characters.
* `interactive.header` is optional and supports `text`, `image`, `video`, or `document`.
* In this API, media headers use public `link` URLs (uploaded media `id` is not supported in this endpoint).
```json theme={null}
{
"to": "+573001112233",
"type": "interactive",
"interactive": {
"type": "button",
"body": { "text": "Selecciona una opcion" },
"footer": { "text": "Footer opcional" },
"action": {
"buttons": [
{ "type": "reply", "reply": { "id": "opt_1", "title": "Opcion 1" } },
{ "type": "reply", "reply": { "id": "opt_2", "title": "Opcion 2" } }
]
}
}
}
```
# Product Carousel
Source: https://docs.apifycloud.io/api-reference/messages/send-carousel
Send a horizontally scrollable carousel of 2 to 10 product cards
## Request
Use `type` as `interactive` and set `interactive.type` to `carousel`.
A product carousel displays 2 to 10 product cards from the same catalog in a horizontally scrollable format. Each card supports the same Single-Product Message actions — view, add to cart, send order.
### Field types
* `to`: string (required)
* `type`: string = `interactive` (required)
* `interactive`: object (required)
* `interactive.type`: string = `carousel` (required)
* `interactive.body`: object (required)
* `interactive.body.text`: string (required)
* `interactive.action`: object (required)
* `interactive.action.cards`: array of objects (required, 2 to 10)
* `interactive.action.cards[].card_index`: integer 0-9 (required, unique per message)
* `interactive.action.cards[].type`: string = `product` (required)
* `interactive.action.cards[].action`: object (required)
* `interactive.action.cards[].action.product_retailer_id`: string (required)
* `interactive.action.cards[].action.catalog_id`: string (required)
* `tags`: array of strings (optional)
* `metadata`: object | string (optional)
### Field behavior and limits
* `interactive.body.text` is required, max 1024 characters.
* `interactive.action.cards` must contain between 2 and 10 cards.
* `interactive.action.cards[].card_index` must be a unique integer between 0 and 9 across the cards in the message.
* Each card must reference the **same** `catalog_id` — a carousel cannot mix products from different catalogs.
* Each card must be of `type: "product"`.
* Do not include a `header`, `footer`, or `buttons` on the interactive object — the carousel message only accepts `body` and `action`.
* Product data (image, name, price, stock) is fetched by WhatsApp at delivery time — the recipient always sees the latest catalog state.
* Cannot be sent as a notification — only inside an existing conversation.
```json theme={null}
{
"to": "+573001112233",
"type": "interactive",
"interactive": {
"type": "carousel",
"body": { "text": "Nuestros productos destacados" },
"action": {
"cards": [
{
"card_index": 0,
"type": "product",
"action": {
"product_retailer_id": "SKU-001",
"catalog_id": "1234567890"
}
},
{
"card_index": 1,
"type": "product",
"action": {
"product_retailer_id": "SKU-002",
"catalog_id": "1234567890"
}
}
]
}
}
}
```
# Catalog
Source: https://docs.apifycloud.io/api-reference/messages/send-catalog
Send a message with a "View catalog" button that opens the full product catalog in WhatsApp
## Request
Use `type` as `interactive` and set `interactive.type` to `catalog_message`.
A catalog message shows a thumbnail image of a chosen product plus body text and a **View catalog** button. When the recipient taps the button, the full product catalog opens inside WhatsApp.
### Field types
* `to`: string (required)
* `type`: string = `interactive` (required)
* `interactive`: object (required)
* `interactive.type`: string = `catalog_message` (required)
* `interactive.body`: object (required)
* `interactive.body.text`: string (required)
* `interactive.footer`: object (optional)
* `interactive.footer.text`: string (optional)
* `interactive.action`: object (required)
* `interactive.action.name`: string = `catalog_message` (required)
* `interactive.action.parameters`: object (optional)
* `interactive.action.parameters.thumbnail_product_retailer_id`: string (optional)
* `tags`: array of strings (optional)
* `metadata`: object | string (optional)
### Field behavior and limits
* `interactive.body.text` is required, max 1024 characters.
* `interactive.footer.text` is optional, max 60 characters.
* `interactive.action.name` must be `catalog_message`.
* `interactive.action.parameters.thumbnail_product_retailer_id` is optional. If provided, WhatsApp uses the image of that product as the message header thumbnail. If omitted, WhatsApp uses the image of the first product in the catalog.
* A catalog must be uploaded to Meta Commerce Manager and connected to the WhatsApp Business account before this message type can be used.
* Cannot be sent as a notification — only inside an existing conversation.
```json theme={null}
{
"to": "+573001112233",
"type": "interactive",
"interactive": {
"type": "catalog_message",
"body": {
"text": "¡Hola! Gracias por tu interés. Ordenar es fácil: visitá nuestro catálogo y agregá productos al carrito."
},
"footer": {
"text": "Las mejores ofertas por WhatsApp"
},
"action": {
"name": "catalog_message",
"parameters": {
"thumbnail_product_retailer_id": "SKU-featured"
}
}
}
}
```
# Contacts
Source: https://docs.apifycloud.io/api-reference/messages/send-contacts
Send a WhatsApp contacts message
## Request
`type` must be `contacts`.
### Field types
* `to`: string (required)
* `type`: string = `contacts` (required)
* `contacts`: array of objects (required, at least one)
* `contacts[].name`: object (recommended)
* `contacts[].name.formatted_name`: string (recommended)
* `contacts[].name.first_name`: string (optional)
* `contacts[].name.last_name`: string (optional)
* `contacts[].phones`: array of objects (optional)
* `contacts[].phones[].phone`: string (optional)
* `contacts[].phones[].type`: string (optional)
* `contacts[].phones[].wa_id`: string (optional)
* `contacts[].emails`: array of objects (optional)
* `contacts[].emails[].email`: string (optional)
* `contacts[].urls`: array of objects (optional)
* `contacts[].urls[].url`: string (optional)
* `contacts[].addresses`: array of objects (optional)
* `contacts[].org`: object (optional)
* `contacts[].birthday`: string (optional)
```json theme={null}
{
"to": "+573001112233",
"type": "contacts",
"contacts": [
{
"name": {
"formatted_name": "Juan Perez",
"first_name": "Juan",
"last_name": "Perez"
},
"phones": [
{
"phone": "+573001112233",
"type": "WORK",
"wa_id": "573001112233"
}
]
}
]
}
```
# CTA URL
Source: https://docs.apifycloud.io/api-reference/messages/send-cta-url
Send a WhatsApp CTA URL message
## Request
Use `type` as `interactive` and set `interactive.type` to `cta_url`.
### Field types
* `to`: string (required)
* `type`: string = `interactive` (required)
* `interactive`: object (required)
* `interactive.type`: string = `cta_url` (required)
* `interactive.header`: object (optional)
* `interactive.header.type`: string (optional, `text` | `image` | `video` | `document`)
* `interactive.header.text`: string (optional, when header type is `text`)
* `interactive.header.image`: object (optional)
* `interactive.header.image.link`: string (optional)
* `interactive.header.video`: object (optional)
* `interactive.header.video.link`: string (optional)
* `interactive.header.document`: object (optional)
* `interactive.header.document.link`: string (optional)
* `interactive.body`: object (required)
* `interactive.body.text`: string (required)
* `interactive.footer`: object (optional)
* `interactive.footer.text`: string (optional)
* `interactive.action`: object (required)
* `interactive.action.name`: string (optional, defaults to `cta_url`)
* `interactive.action.parameters`: object (required)
* `interactive.action.parameters.display_text`: string (required)
* `interactive.action.parameters.url`: string (required)
### Field behavior and limits
* `interactive.body.text` is required, max 1024 characters.
* `interactive.action.parameters.display_text` is required, max 20 characters.
* `interactive.action.parameters.url` is required and must be a valid public URL.
* `interactive.footer.text` is optional, max 60 characters.
* `interactive.header.text` is required only when header type is `text`, max 60 characters.
* `interactive.header.image.link`, `interactive.header.video.link`, and `interactive.header.document.link` are required when using those header media types.
* In this API, media headers use public `link` URLs (uploaded media `id` is not supported in this endpoint).
```json theme={null}
{
"to": "+573001112233",
"type": "interactive",
"interactive": {
"type": "cta_url",
"body": { "text": "Abre este enlace" },
"action": {
"name": "cta_url",
"parameters": {
"display_text": "Abrir",
"url": "https://example.com"
}
}
}
}
```
# Flow
Source: https://docs.apifycloud.io/api-reference/messages/send-flow
Send a WhatsApp interactive Flow message
## Request
Use `type` as `interactive` and set `interactive.type` to `flow`.
Use this endpoint to send a Flow **inside the 24h Customer Service Window**. To send a Flow **outside the 24h window**, wrap it in an approved template with a Flow button and use [Send Template](/api-reference/messages/send-template) instead.
### Field types
* `to`: string (required)
* `type`: string = `interactive` (required)
* `interactive`: object (required)
* `interactive.type`: string = `flow` (required)
* `interactive.header`: object (optional)
* `interactive.header.type`: string (optional, usually `text`)
* `interactive.header.text`: string (optional)
* `interactive.body`: object (required)
* `interactive.body.text`: string (required)
* `interactive.footer`: object (optional)
* `interactive.footer.text`: string (optional)
* `interactive.action`: object (required)
* `interactive.action.name`: string = `flow` (required)
* `interactive.action.parameters`: object (required)
* `interactive.action.parameters.flow_message_version`: string = `3` (required)
* `interactive.action.parameters.flow_token`: string (required, unique per send)
* `interactive.action.parameters.flow_id`: string (required if `flow_name` is not provided)
* `interactive.action.parameters.flow_name`: string (required if `flow_id` is not provided)
* `interactive.action.parameters.flow_cta`: string (required)
* `interactive.action.parameters.flow_action`: string = `navigate` or `data_exchange` (optional, default `navigate`)
* `interactive.action.parameters.flow_action_payload`: object (optional when `flow_action` is `navigate`, must be omitted when `flow_action` is `data_exchange`)
* `interactive.action.parameters.flow_action_payload.screen`: string (optional, default `FIRST_ENTRY_SCREEN`)
* `interactive.action.parameters.flow_action_payload.data`: object (optional, must be a non-empty object when provided)
* `interactive.action.parameters.mode`: string = `published` or `draft` (optional, default `published`)
* `tags`: array of strings (optional)
* `metadata`: object | string (optional)
### Field behavior and limits
* `interactive.body.text` is required, max 1024 characters.
* `interactive.header.text` is optional, max 60 characters.
* `interactive.footer.text` is optional, max 60 characters.
* `interactive.action.parameters.flow_cta` is the button label, max 20 characters and must not contain emojis or markdown.
* `interactive.action.parameters.flow_token` must be unique per send. It is echoed back in the inbound payload when the user submits the Flow, so use it to correlate the response with the original send.
* `interactive.action.parameters.flow_id` is the ID issued by Meta when the Flow is published. `flow_name` is supported as an alternative.
* `interactive.action.parameters.mode` defaults to `published`. Use `draft` only while testing — `draft` mode delivers only to numbers registered as testers in Meta.
* `interactive.action.parameters.flow_action_payload.screen` is optional. If omitted, Meta uses `FIRST_ENTRY_SCREEN`. When provided, it must match the ID of the first screen to render.
* `interactive.action.parameters.flow_action_payload.data` is the input data for the first screen. When provided, it must be a non-empty object.
* When `flow_action` is `data_exchange`, `flow_action_payload` must be omitted. Meta calls the Flow's data endpoint to populate the first screen.
* Flow messages render on the WhatsApp native mobile app. WhatsApp Web shows a simplified fallback.
```json theme={null}
{
"to": "+573001112233",
"type": "interactive",
"interactive": {
"type": "flow",
"header": { "type": "text", "text": "Agendamiento" },
"body": { "text": "Por favor completá los datos para reservar tu cita." },
"footer": { "text": "Tomará menos de 1 minuto" },
"action": {
"name": "flow",
"parameters": {
"flow_message_version": "3",
"flow_token": "f4c3b1a0-8d2e-4a51-9c77-2a18bd0e6f12",
"flow_id": "1234567890123456",
"flow_cta": "Reservar",
"flow_action": "navigate",
"flow_action_payload": {
"screen": "APPOINTMENT_DETAILS",
"data": {
"patient_name": "Juan",
"preferred_specialty": "cardiology"
}
}
}
}
},
"tags": ["appointment"],
"metadata": { "source": "crm-webhook" }
}
```
## Handling the response
When the user completes and submits the Flow, an inbound event arrives with the structured fields the user filled in, plus the same `flow_token` you sent. Use the token to correlate the response with the original send.
For the conceptual model, screen design, and pricing, see [WhatsApp Flows](/guides/whatsapp/flow-messages). For the authoritative Flow JSON schema and screen elements, see [Meta's Flow documentation](https://developers.facebook.com/docs/whatsapp/flows).
# List
Source: https://docs.apifycloud.io/api-reference/messages/send-list
Send a WhatsApp interactive list message
## Request
Use `type` as `interactive` and set `interactive.type` to `list`.
### Field types
* `to`: string (required)
* `type`: string = `interactive` (required)
* `interactive`: object (required)
* `interactive.type`: string = `list` (required)
* `interactive.header`: object (optional)
* `interactive.header.type`: string (optional, usually `text`)
* `interactive.header.text`: string (optional)
* `interactive.body`: object (required)
* `interactive.body.text`: string (required)
* `interactive.footer`: object (optional)
* `interactive.footer.text`: string (optional)
* `interactive.action`: object (required)
* `interactive.action.button`: string (required)
* `interactive.action.sections`: array of objects (required)
* `interactive.action.sections[].title`: string (required)
* `interactive.action.sections[].rows`: array of objects (required)
* `interactive.action.sections[].rows[].id`: string (required)
* `interactive.action.sections[].rows[].title`: string (required)
* `interactive.action.sections[].rows[].description`: string (optional)
### Field behavior and limits
* `interactive.action.button` is required, max 20 characters.
* `interactive.body.text` is required, max 4096 characters.
* `interactive.footer.text` is optional, max 60 characters.
* `interactive.header.text` is optional, max 60 characters.
* `interactive.action.sections` requires at least 1 section and supports up to 10 sections.
* `interactive.action.sections[].title` is required, max 24 characters.
* `interactive.action.sections[].rows` requires at least 1 row and supports up to 10 total rows across sections.
* `interactive.action.sections[].rows[].id` is required, max 200 characters.
* `interactive.action.sections[].rows[].title` is required, max 24 characters.
* `interactive.action.sections[].rows[].description` is optional, max 72 characters.
```json theme={null}
{
"to": "+573001112233",
"type": "interactive",
"interactive": {
"type": "list",
"header": { "type": "text", "text": "Menu" },
"body": { "text": "Selecciona una opcion" },
"footer": { "text": "Footer opcional" },
"action": {
"button": "Ver opciones",
"sections": [
{
"title": "Opciones",
"rows": [
{ "id": "row_1", "title": "Item 1", "description": "Descripcion" }
]
}
]
}
}
}
```
# Location
Source: https://docs.apifycloud.io/api-reference/messages/send-location
Send a WhatsApp location message
## Request
`type` must be `location`.
### Field types
* `to`: string (required)
* `type`: string = `location` (required)
* `location`: object (required)
* `location.latitude`: number (required)
* `location.longitude`: number (required)
* `location.name`: string (optional)
* `location.address`: string (optional)
```json theme={null}
{
"to": "+573001112233",
"type": "location",
"location": {
"latitude": 4.60971,
"longitude": -74.08175,
"name": "Bogota",
"address": "Bogota, Colombia"
}
}
```
# Location Request
Source: https://docs.apifycloud.io/api-reference/messages/send-location-request
Send a WhatsApp location request message
## Request
Use `type` as `interactive` and set `interactive.type` to `location_request_message`.
### Field types
* `to`: string (required)
* `type`: string = `interactive` (required)
* `interactive`: object (required)
* `interactive.type`: string = `location_request_message` (required)
* `interactive.body`: object (required)
* `interactive.body.text`: string (required)
* `interactive.action`: object (optional)
* `interactive.action.name`: string (optional, defaults to `send_location`)
* `interactive.action.parameters`: object (optional)
* `interactive.action.parameters.display_text`: string (optional)
### Field behavior and limits
* `interactive.body.text` is required, max 1024 characters.
* `interactive.action.name` defaults to `send_location` when omitted.
* `interactive.action.parameters.display_text` is optional, max 20 characters when provided.
```json theme={null}
{
"to": "+573001112233",
"type": "interactive",
"interactive": {
"type": "location_request_message",
"body": {
"text": "Comparte tu ubicacion actual"
},
"action": {
"name": "send_location",
"parameters": {
"display_text": "Compartir ubicacion"
}
}
}
}
```
# Media
Source: https://docs.apifycloud.io/api-reference/messages/send-media
Send WhatsApp image, audio, video, and document messages
## Request
Supported values for `type`:
* `image`
* `audio`
* `video`
* `document`
For all media types, only `link` is supported.
### Field types
* `to`: string (required)
* `type`: string = `image` | `audio` | `video` | `document` (required)
* `image`: object (required when `type=image`)
* `image.link`: string (required)
* `image.caption`: string (optional)
* `audio`: object (required when `type=audio`)
* `audio.link`: string (required)
* `audio.voice`: boolean (optional)
* `video`: object (required when `type=video`)
* `video.link`: string (required)
* `video.caption`: string (optional)
* `document`: object (required when `type=document`)
* `document.link`: string (required)
* `document.filename`: string (optional)
* `document.caption`: string (optional)
### Field behavior and limits
* `link` is required for every media subtype and must be a public URL.
* `image.caption` is optional, max 1024 characters.
* `video.caption` is optional, max 1024 characters.
* `document.caption` is optional, max 1024 characters.
* `document.filename` is optional; include extension (for example `invoice.pdf`), max 240 characters.
* `audio.voice` is optional boolean:
* `true`: marks the audio as a voice note.
* `false` or omitted: sends as regular audio.
* For voice notes, use OGG/OPUS compatible media.
### Supported formats and max size
Audio (`max 16 MB`):
* AAC: `.aac` (`audio/aac`)
* AMR: `.amr` (`audio/amr`)
* MP3: `.mp3` (`audio/mpeg`)
* MP4 Audio: `.m4a` (`audio/mp4`)
* OGG Audio: `.ogg` (`audio/ogg`) with OPUS codec only and mono input
Document (`max 100 MB`):
* Text: `.txt` (`text/plain`)
* Microsoft Excel: `.xls` (`application/vnd.ms-excel`)
* Microsoft Excel: `.xlsx` (`application/vnd.openxmlformats-officedocument.spreadsheetml.sheet`)
* Microsoft Word: `.doc` (`application/msword`)
* Microsoft Word: `.docx` (`application/vnd.openxmlformats-officedocument.wordprocessingml.document`)
* Microsoft PowerPoint: `.ppt` (`application/vnd.ms-powerpoint`)
* Microsoft PowerPoint: `.pptx` (`application/vnd.openxmlformats-officedocument.presentationml.presentation`)
* PDF: `.pdf` (`application/pdf`)
Image (`max 5 MB`):
* JPEG: `.jpeg` (`image/jpeg`)
* PNG: `.png` (`image/png`)
* Images must be 8-bit RGB or RGBA.
Video (`max 16 MB`):
* 3GPP: `.3gp` (`video/3gpp`)
* MP4 Video: `.mp4` (`video/mp4`)
* Codec constraints: H.264 video and AAC audio only.
* Recommended for compatibility: H.264 Main without B-frames or H.264 Baseline, and `faststart` (`moov` before `mdat`).
For sticker-specific constraints (`.webp` and size limits), see [Sticker](/api-reference/messages/send-sticker).
### Image
```json theme={null}
{
"to": "+573001112233",
"type": "image",
"image": {
"link": "https://example.com/image.jpg",
"caption": "Imagen de referencia"
}
}
```
### Audio
```json theme={null}
{
"to": "+573001112233",
"type": "audio",
"audio": {
"link": "https://example.com/audio.ogg",
"voice": true
}
}
```
### Video
```json theme={null}
{
"to": "+573001112233",
"type": "video",
"video": {
"link": "https://example.com/video.mp4",
"caption": "Video"
}
}
```
### Document
```json theme={null}
{
"to": "+573001112233",
"type": "document",
"document": {
"link": "https://example.com/file.pdf",
"filename": "file.pdf",
"caption": "Documento"
}
}
```
# Product
Source: https://docs.apifycloud.io/api-reference/messages/send-product
Send a single product from your catalog as an interactive message
## Request
Use `type` as `interactive` and set `interactive.type` to `product`.
A single-product message displays one product from your catalog. The recipient can view the product details, add it to their WhatsApp cart, and send the cart back as an order.
### Field types
* `to`: string (required)
* `type`: string = `interactive` (required)
* `interactive`: object (required)
* `interactive.type`: string = `product` (required)
* `interactive.body`: object (optional)
* `interactive.body.text`: string (optional)
* `interactive.footer`: object (optional)
* `interactive.footer.text`: string (optional)
* `interactive.action`: object (required)
* `interactive.action.catalog_id`: string (required)
* `interactive.action.product_retailer_id`: string (required)
* `tags`: array of strings (optional)
* `metadata`: object | string (optional)
### Field behavior and limits
* `interactive.action.catalog_id` is the ID of the Meta catalog connected to the WhatsApp Business account.
* `interactive.action.product_retailer_id` is the retailer ID (SKU) of the product within the catalog.
* `interactive.body.text` is optional, max 1024 characters.
* `interactive.footer.text` is optional, max 60 characters.
* Product data (image, name, price, stock) is fetched by WhatsApp at delivery time — the recipient always sees the latest catalog state.
* If the `product_retailer_id` does not exist in the catalog, the message is not delivered and an error is returned.
* Cannot be sent as a notification — only inside an existing conversation.
```json theme={null}
{
"to": "+573001112233",
"type": "interactive",
"interactive": {
"type": "product",
"body": { "text": "Este es el producto que te recomendamos." },
"footer": { "text": "Envío gratis hoy" },
"action": {
"catalog_id": "1234567890",
"product_retailer_id": "SKU-123"
}
}
}
```
# Product List
Source: https://docs.apifycloud.io/api-reference/messages/send-product-list
Send up to 30 products from your catalog, organized in sections
## Request
Use `type` as `interactive` and set `interactive.type` to `product_list`.
A multi-product message displays products from your catalog grouped in sections. The recipient can browse, view details, add items to their WhatsApp cart, and send the cart back as an order.
### Field types
* `to`: string (required)
* `type`: string = `interactive` (required)
* `interactive`: object (required)
* `interactive.type`: string = `product_list` (required)
* `interactive.header`: object (required)
* `interactive.header.type`: string = `text` (required)
* `interactive.header.text`: string (required)
* `interactive.body`: object (required)
* `interactive.body.text`: string (required)
* `interactive.footer`: object (optional)
* `interactive.footer.text`: string (optional)
* `interactive.action`: object (required)
* `interactive.action.catalog_id`: string (required)
* `interactive.action.sections`: array of objects (required)
* `interactive.action.sections[].title`: string (required)
* `interactive.action.sections[].product_items`: array of objects (required)
* `interactive.action.sections[].product_items[].product_retailer_id`: string (required)
* `tags`: array of strings (optional)
* `metadata`: object | string (optional)
### Field behavior and limits
* `interactive.header.text` is required, max 60 characters. The header type must be `text`.
* `interactive.body.text` is required, max 1024 characters.
* `interactive.footer.text` is optional, max 60 characters.
* `interactive.action.catalog_id` is the ID of the Meta catalog connected to the WhatsApp Business account.
* `interactive.action.sections` requires at least 1 section.
* Across all sections, at most 30 `product_items` in total.
* Product data (image, name, price, stock) is fetched by WhatsApp at delivery time — the recipient always sees the latest catalog state.
* If none of the `product_retailer_id` values match a product in the catalog, the message is not delivered and an error is returned. If at least one matches, the message is sent and missing items are dropped silently.
* Cannot be sent as a notification — only inside an existing conversation.
```json theme={null}
{
"to": "+573001112233",
"type": "interactive",
"interactive": {
"type": "product_list",
"header": { "type": "text", "text": "Nuestro menú" },
"body": { "text": "Elige uno o más productos" },
"footer": { "text": "Envío disponible" },
"action": {
"catalog_id": "1234567890",
"sections": [
{
"title": "Entradas",
"product_items": [
{ "product_retailer_id": "SKU-001" },
{ "product_retailer_id": "SKU-002" }
]
},
{
"title": "Platos fuertes",
"product_items": [
{ "product_retailer_id": "SKU-101" },
{ "product_retailer_id": "SKU-102" },
{ "product_retailer_id": "SKU-103" }
]
}
]
}
}
}
```
# Reaction
Source: https://docs.apifycloud.io/api-reference/messages/send-reaction
Send a WhatsApp reaction message
## Request
`type` must be `reaction`.
### Field types
* `to`: string (required)
* `type`: string = `reaction` (required)
* `reaction`: object (required)
* `reaction.message_id`: string (required)
* `reaction.emoji`: string (required)
```json theme={null}
{
"to": "+573001112233",
"type": "reaction",
"reaction": {
"message_id": "wamid.HBgL...",
"emoji": "\\ud83d\\udc4d"
}
}
```
# Sticker
Source: https://docs.apifycloud.io/api-reference/messages/send-sticker
Send a WhatsApp sticker message
## Request
`type` must be `sticker`.
### Field types
* `to`: string (required)
* `type`: string = `sticker` (required)
* `sticker`: object (required)
* `sticker.link`: string (required)
### Supported formats and max size
* Static sticker: `.webp` (`image/webp`), max 100 KB
* Animated sticker: `.webp` (`image/webp`), max 500 KB
* WebP files are only supported for sticker messages.
```json theme={null}
{
"to": "+573001112233",
"type": "sticker",
"sticker": {
"link": "https://example.com/sticker.webp"
}
}
```
# Template
Source: https://docs.apifycloud.io/api-reference/messages/send-template
Send a WhatsApp template message
## Request
`type` must be `template`.
### Field types
* `to`: string (required)
* `type`: string = `template` (required)
* `templateName`: string (required)
* `language`: string (optional)
* `variables`: array of strings OR object with string values (optional)
* `tags`: array of strings (optional)
* `metadata`: object | string (optional)
```json theme={null}
{
"to": "+573001112233",
"type": "template",
"templateName": "order_update",
"language": "es",
"variables": ["Juan", "A123"],
"tags": ["vip"],
"metadata": { "source": "api" }
}
```
## Response
Template sends are synchronous.
```json theme={null}
{
"data": {
"messageId": "uuid",
"status": "sent",
"providerMessageId": "string"
},
"meta": {
"timestamp": "2024-01-01T00:00:00.000Z",
"mode": "sync"
}
}
```
# Text
Source: https://docs.apifycloud.io/api-reference/messages/send-text
Send a WhatsApp text message
## Request
`type` must be `text`.
### Field types
* `to`: string (required)
* `type`: string = `text` (required)
* `text`: object (required)
* `text.body`: string (required)
* `text.preview_url`: boolean (optional)
* `tags`: array of strings (optional)
* `metadata`: object | string (optional)
### Field behavior and limits
* `text.body` is required.
* `text.body` maximum length: 4096 characters.
* URLs in `text.body` are automatically hyperlinked by WhatsApp clients.
* `text.preview_url` is optional:
* `true`: attempts to render a link preview for URLs in `text.body`.
* `false` or omitted: no link preview is requested.
```json theme={null}
{
"to": "+573001112233",
"type": "text",
"text": {
"body": "Hola, este es un mensaje de prueba",
"preview_url": true
}
}
```
## Response
Text sends are queued asynchronously.
```json theme={null}
{
"data": {
"messageId": "uuid",
"status": "queued",
"logId": "uuid"
},
"meta": {
"timestamp": "2024-01-01T00:00:00.000Z"
}
}
```
# Get Availability
Source: https://docs.apifycloud.io/api-reference/scheduled-calls/availability
Query open slots for one or more agents
## Endpoint
`GET https://api.apifycloud.io/api/v1/video/{appId}/availability`
## Authentication
Bearer token required.
Scope: `scheduling:read`
## Path parameters
* `appId` (UUID, required)
## Query parameters
| Name | Type | Required | Notes |
| --------------------- | ------- | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `fromUtc` | string | yes | Window start (ISO-8601 UTC). |
| `toUtc` | string | yes | Window end (ISO-8601 UTC). |
| `durationMinutes` | number | yes | 5–480. Size of each candidate slot. |
| `agentId` | string | no | Limit to a single agent. Defaults to every member with configured working hours. |
| `stepMinutes` | number | no | Grid resolution. Defaults to `durationMinutes`. |
| `bufferBeforeMinutes` | number | no | Default 0. |
| `bufferAfterMinutes` | number | no | Default 0. |
| `respectWorkingHours` | boolean | no | Default `true`. When `true`, only slots inside an agent's configured working hours (or inside an `available_extra` exception) are returned — matches the console UI. Set to `false` to include slots outside those windows (conflict checks still apply). |
## Behavior
* Respects existing scheduled calls, blocked `agent_availability_exceptions`, and `agent_dnd_windows`.
* `available_extra` exceptions are honored as positive availability.
* Working hours are enforced by default (`respectWorkingHours=true`). Pass `respectWorkingHours=false` if your integration owns its own hours model and only wants the raw conflict data.
## Caps
* Max 400 candidate slots per request (time window × step × agents). Larger queries return `400 invalid_request`.
* No cached results — each call is fresh off the database.
## Response
```json theme={null}
{
"data": {
"slots": [
{
"startUtc": "2026-05-01T14:00:00.000Z",
"endUtc": "2026-05-01T14:30:00.000Z",
"agentId": "user_abc"
},
{
"startUtc": "2026-05-01T14:30:00.000Z",
"endUtc": "2026-05-01T15:00:00.000Z",
"agentId": "user_abc"
}
]
}
}
```
## Errors
* `400 invalid_request` invalid range, duration out of bounds, or too many candidate slots
* `403 forbidden` unauthorized for app or missing scope
* `429 rate_limit_exceeded`
* `500 server_error`
## Example
```bash theme={null}
curl "https://api.apifycloud.io/api/v1/video/{appId}/availability?fromUtc=2026-05-01T12:00:00Z&toUtc=2026-05-01T20:00:00Z&durationMinutes=30&agentId=user_abc" \
-H "Authorization: Bearer {access_token}"
```
# Cancel Scheduled Call
Source: https://docs.apifycloud.io/api-reference/scheduled-calls/cancel
Soft-cancel a scheduled call
## Endpoint
`DELETE https://api.apifycloud.io/api/v1/video/{appId}/scheduled-calls/{id}`
## Authentication
Bearer token required.
Scope: `scheduling:write`
## Path parameters
* `appId` (UUID, required)
* `id` (UUID, required) — the scheduled call id
## Request body
Optional. A cancellation reason can be supplied either in the body or as `?reason=...` query parameter.
```json theme={null}
{ "reason": "Customer no longer interested" }
```
## Behavior
* The row is never hard-deleted; only `status` transitions to `cancelled`.
* Pending reminders for the call are cancelled.
* A `cancelled` event is appended to the timeline, and any configured webhook subscription receives `scheduled.cancelled`.
* Idempotent — calling `DELETE` on an already-cancelled row returns the row unchanged.
## Response
```json theme={null}
{
"data": {
"call": {
"id": "uuid",
"status": "cancelled",
"cancelled_at": "2026-04-22T12:00:00.000Z",
"cancellation_reason": "Customer no longer interested"
}
}
}
```
## Errors
* `403 forbidden` unauthorized for app or missing scope
* `404 not_found` call does not belong to this app
* `500 server_error`
## Example
```bash theme={null}
curl -X DELETE "https://api.apifycloud.io/api/v1/video/{appId}/scheduled-calls/{id}" \
-H "Authorization: Bearer {access_token}" \
-H "Content-Type: application/json" \
-d '{ "reason": "Customer no longer interested" }'
```
# Create Scheduled Call
Source: https://docs.apifycloud.io/api-reference/scheduled-calls/create
Create a new scheduled video call
## Endpoint
`POST https://api.apifycloud.io/api/v1/video/{appId}/scheduled-calls`
## Authentication
Bearer token required.
Scope: `scheduling:write`
## Path parameters
* `appId` (UUID, required)
## Request body
| Field | Type | Required | Description |
| --------------------- | ------- | ----------- | ----------------------------------------------------------------------- |
| `guestName` | string | yes | Guest display name. |
| `scheduledAtUtc` | string | yes | ISO-8601 UTC timestamp for the start of the call. |
| `appTimezone` | string | yes | IANA timezone used for display (e.g. `America/Bogota`). |
| `guestPhone` | string | conditional | Required if `guestEmail` is not provided. |
| `guestEmail` | string | conditional | Required if `guestPhone` is not provided. |
| `assignedAgentId` | string | optional | User id of the agent to assign the call to. |
| `assignmentMethod` | string | optional | `manual`, `self_selected`, `round_robin`, `pool`. Defaults to `manual`. |
| `guestTimezone` | string | optional | IANA timezone of the guest. |
| `guestIntakeNotes` | string | optional | Free-form notes attached to the booking. |
| `durationMinutes` | number | optional | Default 30. |
| `bufferBeforeMinutes` | number | optional | Default 0. |
| `bufferAfterMinutes` | number | optional | Default 0. |
| `requireConfirmation` | boolean | optional | Forces the call into `pending_confirmation` until the guest confirms. |
### Notes
* `bookingSource` is always stamped as `api` server-side.
* Recurrence is not exposed in v1. Create each instance individually.
* When `assignedAgentId` is provided the endpoint takes an advisory lock on `(agent, minute)` so concurrent POSTs cannot double-book.
* Customer rate limits configured on the app preset apply. Hitting the limit returns `429` with `nextAllowedAt` in the error details.
## Response
```json theme={null}
{
"data": {
"call": {
"id": "uuid",
"app_id": "uuid",
"booking_source": "api",
"assigned_agent_id": "uuid",
"guest_name": "Jane Doe",
"guest_phone": "+57 300 555 0100",
"guest_email": "jane@example.com",
"scheduled_at": "2026-05-01T15:00:00.000Z",
"duration_minutes": 30,
"app_timezone": "America/Bogota",
"status": "scheduled",
"created_at": "2026-04-22T12:00:00.000Z"
}
}
}
```
Returns `201 Created`.
## Errors
* `400 invalid_request` validation failed (past date, missing required fields)
* `403 forbidden` scheduling not enabled on the app's active preset, or missing scope
* `404 app_not_found`
* `409 conflict` slot overlaps an existing call, blocked exception, or DND window
* `429 rate_limit_exceeded` either client rate limit or per-customer booking limit
* `500 server_error`
## Example
```bash theme={null}
curl -X POST "https://api.apifycloud.io/api/v1/video/{appId}/scheduled-calls" \
-H "Authorization: Bearer {access_token}" \
-H "Content-Type: application/json" \
-d '{
"assignedAgentId": "user_abc",
"assignmentMethod": "manual",
"guestName": "Jane Doe",
"guestPhone": "+57 300 555 0100",
"guestEmail": "jane@example.com",
"guestTimezone": "America/Bogota",
"guestIntakeNotes": "Wants to discuss contract renewal",
"scheduledAtUtc": "2026-05-01T15:00:00.000Z",
"durationMinutes": 30,
"appTimezone": "America/Bogota",
"bufferBeforeMinutes": 5,
"bufferAfterMinutes": 5,
"requireConfirmation": false
}'
```
# Get Scheduled Call
Source: https://docs.apifycloud.io/api-reference/scheduled-calls/get
Fetch a single scheduled call plus its full event timeline
## Endpoint
`GET https://api.apifycloud.io/api/v1/video/{appId}/scheduled-calls/{id}`
## Authentication
Bearer token required.
Scope: `scheduling:read`
## Path parameters
* `appId` (UUID, required)
* `id` (UUID, required) — the scheduled call id
## Response
```json theme={null}
{
"data": {
"call": {
"id": "uuid",
"app_id": "uuid",
"assigned_agent_id": "uuid",
"booking_source": "api",
"guest_name": "Jane Doe",
"guest_phone": "+57 300 555 0100",
"guest_email": "jane@example.com",
"guest_timezone": "America/Bogota",
"scheduled_at": "2026-05-01T15:00:00.000Z",
"duration_minutes": 30,
"app_timezone": "America/Bogota",
"status": "scheduled",
"created_at": "2026-04-22T12:00:00.000Z"
},
"events": [
{
"id": "uuid",
"event_type": "created",
"actor_type": "api",
"actor_id": "client_abc",
"payload": {},
"created_at": "2026-04-22T12:00:00.000Z"
}
]
}
}
```
## Errors
* `403 forbidden` unauthorized for app or missing scope
* `404 not_found` call does not belong to this app
* `500 server_error`
## Example
```bash theme={null}
curl "https://api.apifycloud.io/api/v1/video/{appId}/scheduled-calls/{id}" \
-H "Authorization: Bearer {access_token}"
```
# List Scheduled Calls
Source: https://docs.apifycloud.io/api-reference/scheduled-calls/list
List scheduled video calls for an app with filters
## Endpoint
`GET https://api.apifycloud.io/api/v1/video/{appId}/scheduled-calls`
## Authentication
Bearer token required.
Scope: `scheduling:read`
## Path parameters
* `appId` (UUID, required)
## Query parameters
* `agentId` (string, optional) filter to a single assigned agent
* `status` (string, optional) comma-separated: `scheduled`, `pending_confirmation`, `confirmed`, `cancelled`, `completed`, `no_show`
* `bookingSource` (string, optional) comma-separated: `admin`, `agent`, `public_booking`, `api`
* `dateFrom` (string, optional) ISO-8601 inclusive lower bound on `scheduled_at`
* `dateTo` (string, optional) ISO-8601 exclusive upper bound on `scheduled_at`
* `search` (string, optional) substring match across `guest_name` / `guest_email` / `guest_phone`
* `limit` (number, optional) 1–200, default 50
* `offset` (number, optional) default 0
## Response
```json theme={null}
{
"data": {
"calls": [
{
"id": "uuid",
"app_id": "uuid",
"assigned_agent_id": "uuid",
"booking_source": "api",
"guest_name": "Jane Doe",
"guest_phone": "+57 300 555 0100",
"guest_email": "jane@example.com",
"guest_timezone": "America/Bogota",
"scheduled_at": "2026-05-01T15:00:00.000Z",
"duration_minutes": 30,
"app_timezone": "America/Bogota",
"status": "scheduled",
"created_at": "2026-04-22T12:00:00.000Z"
}
]
},
"pagination": {
"total": 120,
"limit": 50,
"offset": 0,
"hasMore": true
}
}
```
## Errors
* `403 forbidden` unauthorized for app or missing scope
* `400 validation_error` invalid query parameters
* `429 rate_limit_exceeded`
* `500 server_error`
## Example
```bash theme={null}
curl "https://api.apifycloud.io/api/v1/video/{appId}/scheduled-calls?status=scheduled,confirmed&dateFrom=2026-05-01T00:00:00Z&limit=50" \
-H "Authorization: Bearer {access_token}"
```
# Send Reminder
Source: https://docs.apifycloud.io/api-reference/scheduled-calls/remind
Queue an ad-hoc reminder for a scheduled call
## Endpoint
`POST https://api.apifycloud.io/api/v1/video/{appId}/scheduled-calls/{id}/reminders`
## Authentication
Bearer token required.
Scope: `scheduling:write`
## Path parameters
* `appId` (UUID, required)
* `id` (UUID, required) — the scheduled call id
## Request body
All fields optional.
| Field | Type | Description |
| ----------- | ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `kind` | string | Defaults to `reminder_custom`. Other accepted values match the lifecycle kinds (`invitation`, `reminder_24h`, `reminder_1h`, `reminder_15m`, `cancellation`). |
| `channel` | string | v1 only dispatches `whatsapp`; the row inserts regardless. |
| `recipient` | string | Overrides the recipient identifier. Falls back to the call's `guest_phone` → `guest_email`. |
The reminder is inserted with `status='pending'` and picked up by the scheduled-calls reminder cron on its next tick (\~1 min cadence), bypassing the cron's normal schedule.
## Response
Returns `202 Accepted`.
```json theme={null}
{
"data": {
"reminder": {
"id": "uuid",
"fire_at": "2026-04-22T12:00:00.000Z",
"reminder_kind": "reminder_custom",
"channel": "whatsapp",
"status": "pending"
}
}
}
```
## Errors
* `400 invalid_request` unknown `kind` / `channel` or invalid payload
* `403 forbidden` unauthorized for app or missing scope
* `404 not_found` call does not belong to this app
* `409 conflict` call is in a final state and cannot receive reminders
* `429 rate_limit_exceeded`
* `500 server_error`
## Example
```bash theme={null}
curl -X POST "https://api.apifycloud.io/api/v1/video/{appId}/scheduled-calls/{id}/reminders" \
-H "Authorization: Bearer {access_token}" \
-H "Content-Type: application/json" \
-d '{
"kind": "reminder_custom",
"channel": "whatsapp",
"recipient": "+57 300 555 0100"
}'
```
# Update Scheduled Call
Source: https://docs.apifycloud.io/api-reference/scheduled-calls/update
Reschedule, reassign, or edit guest info on an existing scheduled call
## Endpoint
`PATCH https://api.apifycloud.io/api/v1/video/{appId}/scheduled-calls/{id}`
## Authentication
Bearer token required.
Scope: `scheduling:write`
## Path parameters
* `appId` (UUID, required)
* `id` (UUID, required) — the scheduled call id
## Request body
All fields optional; at least one must be present.
| Field | Type | Description |
| ------------------ | -------------- | --------------------------------------------------------------------------------------------- |
| `scheduledAtUtc` | string | New start time (ISO-8601 UTC). Triggers a `rescheduled` event and re-plans pending reminders. |
| `assignedAgentId` | string | Reassign the call to a different agent. Triggers an `assigned` event. |
| `guestName` | string | Update guest display name. |
| `guestPhone` | string \| null | Update or clear the guest phone. |
| `guestEmail` | string \| null | Update or clear the guest email. |
| `guestTimezone` | string | Update the guest's IANA timezone. |
| `guestIntakeNotes` | string | Replace intake notes. |
| `reason` | string | Optional free-text reason attached to the emitted event. |
Each mutation emits its own event so the call timeline narrates what changed.
Recurring series: this endpoint operates on a single instance in v1. Bulk edits require iteration.
## Response
```json theme={null}
{
"data": {
"call": {
"id": "uuid",
"scheduled_at": "2026-05-02T16:00:00.000Z",
"status": "scheduled"
}
}
}
```
## Errors
* `400 invalid_request` no editable fields provided or payload invalid
* `403 forbidden` unauthorized for app or missing scope
* `404 not_found` call does not belong to this app
* `409 conflict` new slot unavailable, or call is already in a final state (`cancelled` / `completed` / `no_show`)
* `429 rate_limit_exceeded`
* `500 server_error`
## Example
```bash theme={null}
curl -X PATCH "https://api.apifycloud.io/api/v1/video/{appId}/scheduled-calls/{id}" \
-H "Authorization: Bearer {access_token}" \
-H "Content-Type: application/json" \
-d '{
"scheduledAtUtc": "2026-05-02T16:00:00.000Z",
"reason": "Customer requested new time"
}'
```
# Get an Access Token
Source: https://docs.apifycloud.io/guides/authentication/get-token
Use OAuth client credentials to obtain a Bearer token
## Overview
ApifyCloud APIs use Bearer tokens. You can obtain a token using the OAuth client credentials flow.
## Step 1: Prepare credentials
You need your `client_id` and `client_secret`.
## Step 2: Request a token
Send a POST request to `/oauth/token` with the `client_credentials` grant type.
```bash theme={null}
curl -X POST "https://api.apifycloud.io/api/v1/oauth/token" \
-H "Content-Type: application/json" \
-d '{
"grant_type": "client_credentials",
"client_id": "your_client_id",
"client_secret": "your_client_secret"
}'
```
## Step 3: Use the token
Add the token to the `Authorization` header in subsequent requests:
```
Authorization: Bearer {access_token}
```
## Token lifetime
The response includes `expires_in` (in seconds). Request a new token when it expires.
# Audio quality
Source: https://docs.apifycloud.io/guides/click-to-call/audio-quality
Understanding the Network indicator and what users and admins can do to keep calls sounding great
Click to Call measures the live network conditions of every call and
surfaces them as a simple **Network indicator** inside the widget. This
page explains what the indicator means and what each audience can do when
quality drops.
## The Network indicator
A small meter in the call UI with four states:
| State | Colour | What it means |
| --------- | ------ | ------------------------------------------- |
| Excellent | Green | Low latency, minimal jitter, no packet loss |
| Good | Green | Normal conditions — most calls land here |
| Fair | Amber | Occasional audio artefacts possible |
| Poor | Red | Stuttering or dropouts likely |
The indicator is a block in **Call Studio** — you can add it to any state
of the widget, style its position, or hide it entirely if you prefer a
minimal UI.
## What's being measured
Every call samples these metrics continuously:
* **Round-trip latency (RTT)** — how long a packet takes to go to the
other side and come back
* **Jitter** — variation in packet arrival time
* **Packet loss** — percentage of media packets that never arrive
* **Audio level** — from the local microphone, to detect silence or
clipping
The indicator blends these into a single rating. A single bad metric is
enough to downgrade to **Fair** or **Poor**; you don't need all three to
be bad.
## What the user can do when quality drops
These are the top five fixes, in order of effectiveness. Consider surfacing
them as a tip inside the widget (Call Studio supports a `paragraph` block
conditional on state):
1. **Move closer to the Wi-Fi router.** Poor Wi-Fi signal is the most
common cause of call issues.
2. **Switch from Wi-Fi to cellular** (or vice versa). If Wi-Fi is
congested, cellular is often better.
3. **Close other apps** — especially video streaming, cloud backups, and
background downloads.
4. **Disconnect other devices** from the same network.
5. **Use wired headphones.** Bluetooth headsets add latency and sometimes
stutter; a call that's poor on AirPods can be fine on wired earbuds.
## What the admin can do
If you see a pattern — many users reporting poor calls — the issue is
usually one of:
### Corporate network policies
If your agents are behind a restrictive corporate firewall:
* Confirm UDP is allowed outbound — see
[Network requirements](/guides/click-to-call/network-requirements)
* Check if TLS inspection is interfering with WebSocket upgrades
* Test a call from outside the corporate network to isolate
### Agent headset quality
A surprisingly common culprit: cheap USB headsets with unstable mic
gain. If possible, audit the hardware your agents use. Business-grade
USB or wireless headsets designed for unified communications deliver
dramatically more consistent results than consumer earbuds.
## What's next
What your network needs to support reliable calls.
Specific fixes for specific symptoms.
# Browser & device support
Source: https://docs.apifycloud.io/guides/click-to-call/browser-support
Supported browsers and platforms for the Click to Call widget
Click to Call is built on standards-based real-time voice technology and
runs in modern web browsers on phones, tablets, and computers. Visitors
do not need to install anything.
## Supported browsers
The widget is officially supported on the latest stable release of:
* Google Chrome
* Microsoft Edge
* Mozilla Firefox
* Safari
Both desktop and mobile versions of these browsers are covered.
Older releases may work but are not officially tested.
## Supported operating systems
* **Mobile** — iOS and iPadOS on iPhone and iPad, Android on phones
and tablets
* **Desktop** — macOS, Windows, and Linux
On iOS and iPadOS, every browser is built on Safari's engine and
therefore behaves identically for voice calls.
## Requirements for your website
If you embed the widget on your own site, the page must be served over
a secure connection. This is a rule enforced by every modern browser
for any feature that accesses a microphone — not something specific to
Click to Call.
If you share the widget as a direct link instead of embedding it, no
setup on your side is required.
## Microphone access
When a visitor starts their first call, their browser asks permission
to use the microphone. Granting it is a one-time action per browser
and site.
If the visitor declines, the widget clearly explains how to re-enable
access from their browser settings. Some devices also restrict
microphone access at the operating system level — managed phones,
corporate laptops — in which case the visitor must adjust their system
privacy settings to allow the browser to use the microphone.
## What's next
What visitors' and agents' networks need for reliable calls.
Diagnose and resolve the most common issues.
# Business hours & availability
Source: https://docs.apifycloud.io/guides/click-to-call/business-hours
Configure when calls can be placed and what users see outside of hours
Business hours let you restrict when the call button is active and
customise what users see outside of your operating window. Configuration
lives at two levels — app-wide defaults and per-profile overrides — so
teams with multiple queues or regions can mix and match without
duplicating data.
## Where it's configured
In the console under **Call Profiles**, each profile has a **Business
hours** section with two modes:
* **Inherit from app** — use the app-level schedule (the default)
* **Custom** — override with a schedule specific to this profile
App-level hours live in **Settings** and apply to every profile that
inherits.
## What you can configure
All times are stored and evaluated in a single timezone per schedule.
Pick the one your team operates from (e.g. `America/New_York`,
`Europe/Madrid`). Daylight saving is handled automatically.
Seven rows (Monday – Sunday) with one or more open/close windows per
day. A day with zero windows is treated as closed. Split shifts
(e.g. 9:00–13:00 and 15:00–19:00) are supported by adding two
windows to the same day.
A list of specific dates that override the weekly pattern. Use this
for public holidays, company events, or one-off closures. Each entry
has an optional label shown to the user ("Closed for holidays") and
can either close the day entirely or specify custom hours.
For always-on teams, toggle 24/7 on the app or profile. The business
hours gate is disabled and the call button is always active.
## What the user sees
Outside of hours, the call button is disabled and the widget shows
your closure message. Visitors can still see your branding and any
other content you've placed in the widget.
The closure message is configurable in **Call Studio** — place a
`paragraph` block with the text you want, conditional on the `closed`
state.
## Inheritance rules
When a profile is set to **Inherit from app**:
* The profile uses whatever is currently configured at the app level.
* Changes to the app schedule take effect immediately for inheriting
profiles — no per-profile update needed.
When a profile is set to **Custom**:
* The profile has its own fully independent schedule.
* App-level changes do not affect this profile.
* You can switch back to inherit at any time; custom hours are
preserved but not applied while inheriting.
## Timezone handling for the user
The user's own timezone is **not** consulted. Business hours are
evaluated in the schedule's timezone only. If you want to display
"open hours in your local time" in the widget, use Call Studio text
interpolation with the user's locale — contact support if you need
help with this pattern.
## What's next
Listen for the `widget_loaded` event to know when visitors arrive.
URL context and pre-fill.
# Compliance & privacy
Source: https://docs.apifycloud.io/guides/click-to-call/compliance
What data Click to Call processes, consent patterns, and how to handle sensitive use cases
Click to Call processes voice and personal data on behalf of both you
(the controller) and ApifyCloud (the processor). This page covers what
data is involved and the consent patterns you can build into the
widget.
## Personal data processed
The widget touches four categories of data:
| Category | Examples |
| ----------------- | ------------------------------------------------------------------------------- |
| Identifiers | IP address, user agent, session token |
| Call metadata | Call id, duration, timestamps |
| Voluntary content | URL context (`orderId`, `customerTier`), survey responses, pre-call form fields |
| Audio stream | The call audio itself |
URL context is whatever you pass to the widget — you control what goes
in it, and you're responsible for the lawful basis to send it to us.
Don't include sensitive categories (health, financial account numbers,
biometrics) unless you've done the legal analysis on your side.
## Consent patterns
### Microphone permission
Handled by the browser's native permission prompt. No extra layer is
added. When the visitor taps the call button for the first time, the
browser shows its standard "allow microphone access" dialog.
### Before-call disclosure
Regulated industries (finance, health) often require disclosing data
collection before the microphone is active. Place a text block in the
idle state of the widget (designed in Call Studio) with your disclosure
text — it renders above the call button, so the visitor reads it
before tapping.
### Cookie consent (for custom code / analytics)
Any analytics pixels you add via [Custom code](/guides/click-to-call/custom-code)
live inside a sandbox with an opaque origin. They can't read or write
cookies on your domain, so they don't fall under your site's cookie
banner — they get their own cookie jar per sandbox.
That said, if you ship analytics pixels inside the sandbox, their
tracking is still attributable to the visitor on the pixel provider's
own domains. You should disclose their presence in your privacy
notice.
## PCI, HIPAA, and similar
* **PCI** — we're not a payment processor and we do not accept card
data through the widget. Don't paste card numbers into URL context
or survey fields.
* **HIPAA** — Click to Call is not currently a HIPAA-eligible service.
Do not use it to transmit Protected Health Information (PHI).
## Contact
For privacy questions or compliance review requests, contact support.
# Custom code
Source: https://docs.apifycloud.io/guides/click-to-call/custom-code
Inject your own HTML and JavaScript into the widget — safely, inside a sandbox
Every Click to Call widget has a **Custom code** section where you can
paste HTML and JavaScript snippets. The code runs inside a secure
sandbox, so it can react to widget events and call its own APIs
without putting your visitors or your account at risk.
## Where to configure it
In the console under **Call Studio → Settings → Custom code**, two
text areas:
* **Head HTML** — injected at the top of the sandbox document. Use for
`
```
### Meta Pixel — fire Lead on call start
```html theme={null}
```
### Google Tag Manager — push events to the dataLayer
```html theme={null}
```
### Custom events — emit your own signals
Use `window.c2c.emit` to surface custom signals from the sandbox. They
reach the parent page as `postMessage` and integrations as
`custom:`.
```html theme={null}
```
### Conditional tracking based on URL context
The `context` object is available on every event and on
`window.c2c.context`. Use it to only fire a tag for specific visitor
cohorts.
```html theme={null}
```
## Limitations to keep in mind
Because the sandbox has an opaque origin, it has its own cookie
jar. Any SDK that depends on first-party cookies on your domain
will create them inside the sandbox instead, not on the embedding
page.
You can't read URL parameters from the embedding page, inspect its
DOM, or modify it. Pass anything you need via
[URL context](/guides/click-to-call/embedding#url-context) when
loading the widget.
Communication with the widget is event-based and one-way per direction
(sandbox → widget and widget → sandbox). There's no request/response
RPC pattern built in.
WebRTC audio streams live in the widget, not in the sandbox. Custom
code cannot tap into mic input or call audio.
## Debugging custom code
The sandbox is a normal iframe — open your browser devtools and select
it in the iframe dropdown to inspect its console, network, and
sources. Errors in your custom code appear there, not in the main page
console.
If a script fails silently:
1. Check the Network tab for blocked requests (CSP on your side, ad
blockers, mixed-content warnings).
2. Check the Console for thrown exceptions inside handlers.
3. Verify the iframe's sandbox attributes are what you expect — by
design they should be `allow-scripts` only.
## What's next
The full catalogue of events you can subscribe to.
Why the sandbox is shaped this way and what it protects against.
# Embedding the widget
Source: https://docs.apifycloud.io/guides/click-to-call/embedding
Two ways to use Click to Call — as an iframe on your site or as a shared link
Click to Call is delivered as a hosted widget. You can use it in two
ways: embed it on your site with an iframe, or share its URL directly.
## Option 1 — Iframe embed
Drop the widget into any page on your site:
```html theme={null}
```
The `allow="microphone"` attribute is required. Without it the browser
will refuse the microphone request inside the iframe.
### Routing to a specific profile
If you have multiple call profiles configured in the console, pass the
profile id in the query string:
```
https://c2c.apifycloud.io/w/YOUR_APP_ID?r=PROFILE_ID
```
When omitted, the widget uses the app's default profile.
### Language
The widget auto-detects the browser language. Override with `?lang=en`
or `?lang=es`.
### Nested iframes
If your page is itself embedded in another iframe, the outer container
must delegate microphone access down the chain. Set this HTTP header on
your outer page:
```
Permissions-Policy: microphone=(self "https://c2c.apifycloud.io")
```
## Option 2 — Direct link
For landing pages, campaigns, SMS links, QR codes, or any "tap to call
us" surface, use the widget's hosted URL directly:
```
https://c2c.apifycloud.io/w/YOUR_APP_ID
```
No site of your own is required. The widget renders full-page. Share
the link however you like — printed on a ticket as a QR code, sent in
an email, pasted in a link-in-bio.
## URL context
Any query parameter you add to the widget URL becomes available inside
the widget as **context**:
```
https://c2c.apifycloud.io/w/YOUR_APP_ID?orderId=A-12345&customerTier=gold
```
Rules:
* Keys must match `^[a-zA-Z0-9_\-.]{1,64}$` — other keys are ignored.
* Values are capped at 512 bytes each.
* Total context is capped at 2 KB.
* A hard maximum of 32 context keys per session.
* These reserved keys are stripped: `session`, `preview`, `configId`,
`r`, `lang`.
Where context flows:
| Destination | Used how |
| ------------------------ | ------------------------------------------------------- |
| Pre-call form | Field whose `key` matches a context key gets pre-filled |
| Widget labels / headings | `{keyName}` interpolation in Call Studio text blocks |
| SIP headers | Forwarded to the contact centre |
| Widget events | Included in the `context` property of every event |
| Custom code | Readable as `window.c2c.context` |
## Receiving events in the parent page
When you embed via iframe, the widget can emit `postMessage` events to
your parent page — `call_started`, `call_ended`, etc. For security, the
widget only emits to origins you've explicitly allowlisted in the
console under **Call Studio → Embed**. With no origins configured, the
widget emits no `postMessage` events to the parent at all.
Once configured, listen in your parent page:
```js theme={null}
window.addEventListener('message', (event) => {
// Always verify the origin.
if (event.origin !== 'https://c2c.apifycloud.io') return;
if (event.data?.type !== 'c2c:event') return;
switch (event.data.name) {
case 'call_started':
// Your analytics / tracking code
break;
case 'call_ended':
// ...
break;
}
});
```
See [Events](/guides/click-to-call/events) for the list of events.
### Firefox handshake
Firefox does not expose the iframe's parent origin to the widget at
load time, so for security the widget waits for the parent to initiate
a handshake:
```js theme={null}
const iframe = document.querySelector('iframe');
iframe.addEventListener('load', () => {
iframe.contentWindow.postMessage(
{ type: 'c2c:handshake' },
'https://c2c.apifycloud.io',
);
});
```
You only need this if you care about parent-frame events on Firefox.
## Content Security Policy
If your site has a CSP, allow the widget's origin as an iframe source:
```
frame-src https://c2c.apifycloud.io;
```
The widget's internal signalling and media traffic run from within the
iframe itself and are not subject to your parent page's CSP.
## What's next
What the widget emits and when.
Inject your own HTML and JavaScript into the widget.
Origin allowlisting, sandbox, and data boundaries.
# Events & lifecycle
Source: https://docs.apifycloud.io/guides/click-to-call/events
Every event the widget emits, when it fires, and what each payload contains
The widget emits a stream of **lifecycle events** during a call. You
can consume them in three places:
* **Parent page** — via `postMessage` from the iframe (see [Embedding](/guides/click-to-call/embedding))
* **Custom code** — via `window.c2c.on(name, handler)` inside injected
scripts (see [Custom code](/guides/click-to-call/custom-code))
* **Integrations** — forwarded server-side to your webhooks (see
[Integrations](/guides/click-to-call/integrations))
## Common payload shape
Every event carries a common envelope:
```ts theme={null}
{
name: string; // e.g. "call_started"
data: { // event-specific fields (see below)
...
context: { // URL context — always included
[key: string]: string;
};
};
timestamp: string; // ISO 8601
}
```
The `context` object is the URL context you passed to the widget (see
[Embedding — URL context](/guides/click-to-call/embedding#url-context)).
It's attached to every event so downstream systems always have
correlation data like `orderId` or `customerTier`.
## Event catalogue
### `widget_loaded`
Fired once, when the widget finishes initial render. Useful to verify
the widget is actually present on pages where it should be, or to fire
a pageview-equivalent in analytics.
```json theme={null}
{
"name": "widget_loaded",
"data": {
"isPreview": false,
"context": { "orderId": "A-12345" }
}
}
```
* `isPreview` — `true` when rendered inside the Call Studio preview,
`false` in production.
***
### `call_started`
Fired when the user taps the call button and the outbound call request
has been accepted.
```json theme={null}
{
"name": "call_started",
"data": {
"destinationId": "dest_xyz",
"routingKey": null,
"hasFormValues": false,
"context": { "orderId": "A-12345" }
}
}
```
* `destinationId` — the profile the call is routed to, or `null` for
the default profile.
* `routingKey` — routing key supplied via the session, if any.
* `hasFormValues` — `true` if the pre-call form was used.
***
### `call_ended`
Fired when the call is terminated cleanly — either the visitor tapped
hang-up or the agent (or IVR / queue) hung up. This is the most
important event for most integrations.
If the call fails because of a network or media issue, you get
[`call_error`](#call_error) instead — `call_ended` is not emitted on
error paths.
```json theme={null}
{
"name": "call_ended",
"data": {
"duration": 187,
"reason": "user_hangup",
"context": { "orderId": "A-12345" }
}
}
```
* `duration` — call length in seconds. Measured from the moment the
visitor started the call.
* `reason` — one of:
| Value | Meaning |
| --------------- | --------------------------------------- |
| `user_hangup` | The visitor tapped hang-up |
| `remote_hangup` | The agent, IVR, or queue ended the call |
***
### `call_error`
Fired when the call fails to start, or when it drops mid-call because
of a media failure. A spike in these events is your early warning
sign that something is wrong on the infrastructure or network side.
```json theme={null}
{
"name": "call_error",
"data": {
"errorCode": "media_failed",
"errorMessage": "Media connection lost",
"context": { "orderId": "A-12345" }
}
}
```
* `errorCode` — machine-readable label. Currently one of:
* `media_failed` — the WebRTC media connection dropped
* other codes may surface depending on the failure path; always
check `errorMessage` for details.
* `errorMessage` — human-readable description.
***
### `survey_submitted`
Fired when the visitor completes the post-call survey.
```json theme={null}
{
"name": "survey_submitted",
"data": {
"rating": 5,
"answerCount": 2,
"hasComment": true,
"context": { "orderId": "A-12345" }
}
}
```
* `rating` — average rating across the visitor's answers (1–5).
* `answerCount` — how many survey questions the visitor answered.
* `hasComment` — whether the visitor added free-text feedback.
The actual answers and comment text are stored server-side on the
call record and are available through the console — they are not
included in the event payload to keep events small.
***
### `custom_button_clicked`
Fired when the visitor interacts with a custom button you added in
Call Studio.
```json theme={null}
{
"name": "custom_button_clicked",
"data": {
"label": "Open chat",
"action": "link",
"context": { "orderId": "A-12345" }
}
}
```
* `label` — the label configured on the button in Call Studio.
* `action` — the action type configured (e.g. `link`, `copy`, etc.).
## State transitions
The widget has a finite state machine:
```
idle ──▶ calling ──▶ ended ──▶ survey ──▶ survey_submitted ──▶ closed
│
└──▶ error
```
| From → to | Triggers event |
| ----------------------------- | --------------------------- |
| `idle` → `calling` | `call_started` |
| `calling` → `ended` | `call_ended` |
| `calling` → `error` | `call_error` |
| `ended` → `survey` | (transition only, no event) |
| `survey` → `survey_submitted` | `survey_submitted` |
## Forwarding to your server
Every event above can be forwarded to your webhook endpoints through
**Integrations**. Unlike `postMessage` or `window.c2c.on` (both
client-only), integrations are delivered server-side with retries and
a circuit breaker.
See [Integrations](/guides/click-to-call/integrations) for setup.
## What's next
Listen for these events from your own injected scripts.
Forward events to your webhooks and servers.
# Integrations (webhooks)
Source: https://docs.apifycloud.io/guides/click-to-call/integrations
Forward widget events to your webhooks, analytics, and third-party destinations
Integrations let you forward widget events to any HTTP destination — your
own server, an analytics platform, a CRM, a Slack channel — without
writing any client-side code. Events are sent to our server using a
keepalive request and dispatched from there to your integrations with
retries and a circuit breaker — so they survive the visitor closing the
page in most cases.
## How it works
When the widget emits an event, ApifyCloud fans it out to each active
integration configured on the app:
```
Widget event
│
▼
ApifyCloud runtime
│
├──▶ Integration 1 (your analytics endpoint)
├──▶ Integration 2 (your team chat webhook)
└──▶ Integration 3 (your CRM)
```
Each integration:
* Has its own URL, headers, and authentication
* Is filtered to the events you want
* Retries on failure with exponential backoff and jitter
* Trips a circuit breaker after sustained failures
* Encrypts its secrets at rest
## Creating an integration
In the console under **Call Studio → Integrations**:
HTTPS only. HTTP URLs are rejected to prevent plaintext leakage of
PII.
By default no events are selected. Choose specifically which ones
you want delivered — most integrations only need a subset.
Static headers (e.g. `Authorization: Bearer ...`) go in the headers
list. Values stored under the **Secrets** section are encrypted at
rest and can be referenced from URLs or headers.
You can have up to **3 active integrations per app**. This cap is
deliberate — more integrations means more latency on the shared
dispatch loop. If you need more, contact support.
## Request format
We POST a JSON body with the event envelope:
```json theme={null}
{
"name": "call_ended",
"data": {
"duration": 187,
"reason": "user_hangup",
"context": { "orderId": "A-12345" }
},
"timestamp": "2026-04-19T14:08:29.446Z",
"app": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"name": "My Support Line"
}
}
```
The body is UTF-8 JSON with `Content-Type: application/json`.
## Securing your endpoint
Add an `Authorization: Bearer ` header (or any other auth
header you prefer) when creating the integration. Your endpoint rejects
any request without the matching token.
## Secrets
Secrets are values you don't want to see again after saving — API keys,
bearer tokens, signing keys. Store them in the **Secrets** section of
the integration:
* Values are encrypted at rest with AES-256-GCM.
* Reveal is one-time-only, with a full audit log (who revealed what,
when, from which IP).
* Referenced in URLs and headers using the `{secret.keyName}` syntax.
* Reveals are rate-limited (10 per hour per integration) to slow down
an attacker with compromised console access.
Rotating a secret: edit the integration, find the secret in the list,
replace its value, and save. The new value takes effect on the next
delivery.
## Delivery reliability
### Timeouts
Each delivery has a configurable timeout, up to **10 seconds**. If your
endpoint doesn't respond within the configured window, it counts as a
failure.
### Retries
On transient failure (5xx response, connection reset, timeout) we
retry with exponential backoff and ±20 % jitter. After up to
**3 attempts** the delivery is marked failed.
### Short-circuit on 4xx
Any `4xx` response stops retries immediately. A `4xx` means the
request was rejected by your endpoint and retrying won't fix it —
fix the integration config or your endpoint.
### Circuit breaker
After **20 consecutive failures** across any events, the integration
is automatically deactivated and stops dispatching. Fix the issue
(wrong URL, expired token, endpoint down) and reactivate the
integration from the console.
This protects you from:
* Burning retries against a dead endpoint
* Accumulating a retry backlog that delivers out-of-order
* Triggering rate limits on third-party services
### Body size cap
Request bodies are capped at **64 KB**. Events are well under this —
it's a defensive limit only.
## Debugging a failing integration
The integration list shows a badge — **Active** or **Paused** — and
a success-rate percentage. If the integration is **Paused**, the
circuit breaker tripped and you need to reactivate it after fixing
the cause. If the success rate is low, your endpoint is rejecting
deliveries.
Did the request reach you? If yes, check the response status — that's
the value our dispatcher sees. If no, the issue is DNS or a firewall
on your side.
If live events don't arrive, check the event allowlist on the
integration. It's easy to enable it for `call_started` but forget
`call_ended`.
See [Request format](#request-format) above and compare against
what your endpoint expects.
## What's next
Catalogue of events you can subscribe to.
How integration secrets and event data are protected.
When deliveries fail and you can't tell why.
# Network requirements
Source: https://docs.apifycloud.io/guides/click-to-call/network-requirements
Connectivity required for reliable calls, from the visitor side and the agent side
Click to Call uses real-time voice over the web. Most home and mobile
networks work without any configuration. Corporate networks with strict
outbound policies may need a short allowlist.
## From the visitor's side
Visitors on public networks (home Wi-Fi, mobile data, café, hotel) can
usually place calls without any setup. If quality is poor, it is almost
always a local-network issue — Wi-Fi signal strength, background traffic,
or a congested ISP — rather than a Click to Call–specific requirement.
The [Audio quality guide](/guides/click-to-call/audio-quality) covers
what visitors can do when a call sounds bad.
## NAT and ICE
Most networks use NAT (home routers, office gateways, carrier-grade
NAT on mobile). Click to Call uses the standard ICE negotiation to find
the best media path automatically, and falls back to a media relay when
a direct path isn't possible. No configuration is required for any of
this.
## HTTPS is mandatory
Browsers only allow microphone access on pages served over HTTPS. If
you embed the widget on your own site, your page must be HTTPS. The
hosted widget URL we provide is always HTTPS.
## What's next
Understanding the Network indicator and how to act on it.
Specific fixes when calls fail despite meeting the requirements.
# Overview
Source: https://docs.apifycloud.io/guides/click-to-call/overview
What Click to Call is, when to use it, and how it fits into your product
**Click to Call** is an embeddable voice widget. Visitors on your website or app
tap a button and a voice call is established from their browser to your agents
— no phone dialer, no installed app, no native client on either side.
## What it is
A hosted voice widget you can use in two ways:
* **Embed** it on any website or app with a single `