# 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 `