# Agent API Source: https://docs.sagepilot.ai/api-reference/agent-api/overview Run Sagepilot AI agents and human support agents inside any channel or app you own. The Agent API turns a surface you control — such as a mobile app, support portal, kiosk, or IVR deflection flow — into a Sagepilot support channel. Your backend sends customer-message events to Sagepilot, and Sagepilot delivers AI or human-agent replies to an HTTPS callback you configure. **Agent API** is the product name. The API uses `custom_channel` as its backend channel type and endpoint identifier. Agent API V1 is asynchronous. The inbound request is acknowledged after Sagepilot authenticates, validates, and accepts the customer message. Replies arrive later as signed `support.message.created` webhook events. To send non-message JSON events that start journeys (orders, leads, payments), use [Custom webhook](/api-reference/webhooks/custom-webhooks) instead. ## How it works 1. Create an **Agent API** channel in the Sagepilot dashboard, select the default AI agent, and enter your HTTPS response URL. 2. Save the one-time signing key shown during channel creation. Sagepilot cannot show it again. 3. Send each customer message to the shared `POST /webhooks/custom_channel` endpoint. Include the channel ID in the signed JSON body. 4. Sagepilot resolves the customer from `customer.external_id`, manages the internal chat, and processes the message through its normal inbox and AI-assignment flow. 5. Sagepilot sends eligible AI and human-agent replies to your response URL as signed webhook events. The same signing key serves two purposes: * Your backend signs inbound requests to Sagepilot. * Your backend verifies reply callbacks sent by Sagepilot. ## Identity and conversations `customer.external_id` is your stable customer identifier and must be unique within the Sagepilot workspace. Optional customer details such as name, email, phone, and Instagram identifiers belong in `customer.properties`; Sagepilot never uses those mutable properties as identity keys. Do not send a conversation ID, thread ID, or chat ID. Sagepilot manages chats internally using the customer, workspace, channel, and the normal reopen policy. Reply callbacks include Sagepilot's resolved `payload.chat_id` so your system can correlate delivered replies, but that value is output-only and must not be sent back in inbound events. ## Attachments Inbound messages can contain text, attachments, or both. Each attachment must be available at a directly downloadable HTTPS URL that requires no authentication. Sagepilot validates and copies the file into Sagepilot-owned storage before processing the message. Reply callbacks contain safe attachment metadata and a short-lived Sagepilot download URL. They never expose internal storage keys or the original client URL. ## Next steps Create a channel, sign a customer-message event, and send it to Sagepilot. Verify signed reply callbacks and process the public event payload. # Quickstart Source: https://docs.sagepilot.ai/api-reference/agent-api/quickstart Create an Agent API channel and send your first signed customer message. ## Prerequisites * A Sagepilot workspace with at least one published AI agent. * Admin access to **Settings → Channels**. * An HTTPS response URL that Sagepilot can reach. * A backend that can generate HMAC-SHA256 signatures. If you send attachments, their HTTPS host must be allowlisted for your Sagepilot environment and the files must be downloadable without authentication. ## 1. Create the channel 1. In the dashboard, go to **Settings → Channels** and click **Add channel**. 2. Choose **Agent API**. 3. Enter a channel name and select the default AI agent. 4. Enter the HTTPS **Response URL** where Sagepilot should deliver replies. 5. Click **Save**. 6. Copy the shared inbound endpoint, channel ID, and one-time signing key. 7. Confirm that you saved the signing key before closing the dialog. Store the signing key in your secret manager. It is shown only once. If channel creation succeeds but your browser does not receive the key, do not create a duplicate channel; contact Sagepilot support. ## 2. Build a customer-message event Send this strict V1 JSON shape. Unknown fields are rejected. ```json theme={null} { "version": "v1", "event_type": "message", "channel_id": "2c4bc69a-77f7-47ea-9d9a-44019e77a36e", "event_id": "evt-order-help-001", "occurred_at": "2026-08-16T10:30:00Z", "customer": { "external_id": "cust-4821", "properties": { "name": "Priya", "email": "priya@example.com", "phone": "+919876543210", "instagram_id": "17841400000000000", "plan": "premium" } }, "message": { "external_id": "msg-001", "text": "Where is my order?", "attachments": [] } } ``` ### Event fields | Field | Required | Description | | ---------------------- | ----------- | ------------------------------------------------------------------------------------------------------------------------------------- | | `version` | Yes | Must be `v1`. | | `event_type` | Yes | Must be `message` in V1. | | `channel_id` | Yes | Agent API channel UUID copied from the dashboard. | | `event_id` | Yes | Stable ID for this delivery attempt. Reuse it only when retrying the identical event. | | `occurred_at` | Yes | RFC 3339 timestamp for the customer event. | | `customer.external_id` | Yes | Stable customer ID, unique within your Sagepilot workspace. | | `customer.properties` | No | Up to 50 non-null string, number, or boolean profile values. Values enrich the customer record but never control identity or routing. | | `message.external_id` | Yes | Stable ID for this customer message. Reuse it only when retrying the identical message. | | `message.text` | Conditional | UTF-8 text, up to 10 KiB. Text, attachments, or both must be present. | | `message.attachments` | No | Up to five directly downloadable HTTPS attachment descriptors. | Do not include `conversation_id`, `thread_id`, or `chat_id`. These are not accepted inbound fields. ### Attachment object ```json theme={null} { "external_id": "attachment-001", "url": "https://files.example.com/order-label.pdf", "file_name": "order-label.pdf", "mime_type": "application/pdf", "size": 4096 } ``` Attachment rules: * `url` must use HTTPS, contain no credentials or fragment, require no authentication, and return the file directly without redirects. * The URL hostname must be included in Sagepilot's configured attachment-source allowlist. * `size` is the exact byte size and cannot exceed 10 MiB. * Supported MIME types are PDF, PNG, JPEG, GIF, and WebP. * `sha256` is optional. When supplied, it must be the lowercase SHA-256 digest of the file bytes. ## 3. Sign the exact request body Generate one timestamp as Unix seconds or RFC 3339. Build the canonical byte sequence: ```text theme={null} POST\n/webhooks/custom_channel\n{timestamp}\n{exact_raw_json_body} ``` Compute a lowercase hexadecimal HMAC-SHA256 using the channel signing key, then send it as `v1={hex_digest}`. ```javascript theme={null} import { createHmac } from "node:crypto"; const endpoint = process.env.SAGEPILOT_AGENT_API_ENDPOINT; const signingKey = process.env.SAGEPILOT_AGENT_API_SIGNING_KEY; if (!endpoint || !signingKey) { throw new Error("Agent API endpoint and signing key are required"); } const timestamp = Math.floor(Date.now() / 1000).toString(); const event = { version: "v1", event_type: "message", channel_id: "2c4bc69a-77f7-47ea-9d9a-44019e77a36e", event_id: "evt-order-help-001", occurred_at: new Date().toISOString(), customer: { external_id: "cust-4821", properties: { name: "Priya", email: "priya@example.com" }, }, message: { external_id: "msg-001", text: "Where is my order?", attachments: [], }, }; const rawBody = JSON.stringify(event); const canonical = `POST\n/webhooks/custom_channel\n${timestamp}\n${rawBody}`; const signature = createHmac("sha256", signingKey) .update(canonical) .digest("hex"); const response = await fetch(endpoint, { method: "POST", headers: { "Content-Type": "application/json", "X-Sagepilot-Timestamp": timestamp, "X-Sagepilot-Signature": `v1=${signature}`, }, body: rawBody, }); console.log(response.status, await response.json()); ``` Set `SAGEPILOT_AGENT_API_ENDPOINT` to the shared endpoint shown in your dashboard. Its path must be exactly `/webhooks/custom_channel`, and the signed raw bytes must be identical to the HTTP request body. Requests outside the configured replay window are rejected. ## 4. Handle the acknowledgment A newly accepted event returns HTTP `202`: ```json theme={null} { "accepted": true, "idempotent_replay": false } ``` Retrying the identical accepted event with the same IDs returns `202` with `idempotent_replay: true`. Acknowledgment means Sagepilot accepted the inbound message; the AI or human-agent reply arrives later at your response URL. See [Receiving replies](/api-reference/agent-api/receiving-replies) for the callback contract. ## Inbound error responses | Status | Meaning | | ------ | --------------------------------------------------------------------------------------------------------------------------------------------------------- | | `400` | Invalid JSON, channel ID, event shape, or field value. | | `401` | Missing, stale, malformed, or invalid request signature. | | `403` | Unknown or disabled Agent API channel. | | `413` | Event body exceeds the 256 KiB limit. | | `422` | The accepted event cannot be processed, such as an invalid attachment. | | `503` | Ingress, configuration lookup, attachment download, or intake is temporarily unavailable. Retry the identical event and honor `Retry-After` when present. | # Receiving replies Source: https://docs.sagepilot.ai/api-reference/agent-api/receiving-replies Verify and process asynchronous Agent API reply callbacks. Agent API V1 delivers replies asynchronously. After Sagepilot accepts a customer message, eligible AI-agent and human-agent replies are sent to the channel's configured HTTPS response URL as `support.message.created` events. ## Callback request ```json theme={null} { "id": "evt_custom_channel_reply_01", "type": "support.message.created", "version": "v1", "occurred_at": "2026-08-16T10:30:03Z", "workspace_id": "8920e14b-176b-483e-a079-8ecb4f731aaa", "channel_id": "2c4bc69a-77f7-47ea-9d9a-44019e77a36e", "channel_type": "custom_channel", "payload": { "chat_id": "8c89f8e2-3ef7-4d1a-bcf8-2ec5fcb67e21", "customer": { "id": "b8240e37-272d-4c60-806d-ddd0ac8f93f8", "external_id": "cust-4821", "name": "Priya", "email": "priya@example.com", "phone": "+919876543210" }, "message": { "id": "0d9f6a3e-3fd5-4d72-bd36-5e2224535844", "role": "pilot", "content": "Your order shipped yesterday and arrives tomorrow.", "attachments_count": 1, "attachment_types": ["application/pdf"], "type": "message", "in_reply_to_external_message_id": "msg-001", "attachments": [ { "id": "sage-attachment-id", "file_name": "invoice.pdf", "mime_type": "application/pdf", "size": 4096, "url": "https://sagepilot-download.example.com/signed-download" } ] } } } ``` Important fields: * `id` is the stable webhook event ID. Use it to deduplicate retries. * `channel_type` is `custom_channel`, the backend identifier for Agent API. * `payload.chat_id` is Sagepilot's resolved chat ID. It is output-only and must not be sent in later inbound message events. * `payload.customer.external_id` is the workspace-unique customer ID supplied by your backend. * `payload.message.role` is `pilot` for an AI-agent reply or `agent` for a human-agent reply. * `payload.message.in_reply_to_external_message_id`, when present, refers to your inbound `message.external_id`. * Attachment `url` values are short-lived Sagepilot download URLs, currently valid for 30 minutes. Download or copy files promptly. Optional or unknown values may be omitted or `null`. Do not depend on customer profile fields always being present. ## Verify the callback signature Sagepilot sends these headers: | Header | Description | | ------------------------ | ---------------------------------------------- | | `X-Sagepilot-Event-Id` | Same value as the body `id`. | | `X-Sagepilot-Event-Type` | Same value as the body `type`. | | `X-Sagepilot-Timestamp` | Timestamp generated for this delivery attempt. | | `X-Sagepilot-Signature` | Lowercase hexadecimal HMAC-SHA256 signature. | Compute the expected signature over the timestamp, one period, and the exact raw request body: ```text theme={null} {X-Sagepilot-Timestamp}.{exact_raw_request_body} ``` ```javascript theme={null} import { createHmac, timingSafeEqual } from "node:crypto"; /** Verify a Sagepilot Agent API callback against its exact raw body. */ function verifyAgentApiCallback({ rawBody, timestamp, signature, signingKey }) { const expected = createHmac("sha256", signingKey) .update(`${timestamp}.${rawBody}`) .digest("hex"); const suppliedBytes = Buffer.from(signature, "utf8"); const expectedBytes = Buffer.from(expected, "utf8"); return ( suppliedBytes.length === expectedBytes.length && timingSafeEqual(suppliedBytes, expectedBytes) ); } ``` Verify the signature before parsing or processing the JSON body. Also confirm that the event and header IDs/types match and reject timestamps outside your replay window. Inbound request signatures use `v1={hex_digest}` over the method, path, timestamp, and raw body. Callback signatures contain the raw hexadecimal digest and use the separate `{timestamp}.{raw_body}` framing shown above. ## Acknowledge and deduplicate Return a `2xx` response quickly after durably accepting the event. Process slow application work asynchronously. Sagepilot retries callback delivery for transport errors, timeouts, HTTP `429`, and HTTP `5xx`. Other HTTP `4xx` responses are treated as non-retryable. Because delivery is at least once, store and deduplicate on the event `id` before applying side effects. Your response body is ignored and should not contain customer data or secrets. # Get analytics catalog Source: https://docs.sagepilot.ai/api-reference/analytics/catalog GET /platform-api/v1/analytics/catalog List available analytics datasets, metrics, dimensions, and filters. ```json 200 theme={null} { "datasets": [ { "name": "tickets", "label": "Tickets", "description": "Ticket lifecycle, ownership, and backlog analytics.", "default_time_anchor": "created_at", "filter_fields": [ "channel_ids", "channel_kinds", "statuses", "priorities", "assignee_ids", "assignee_types", "team_ids", "tag_ids", "queue_statuses", "unread", "response_status", "no_assignee", "inferred_csat_scores", "user_csat_scores", "custom_fields" ], "measures": [ { "name": "ticket_count", "label": "Ticket count", "description": "", "kind": "count", "type": null }, { "name": "open_ticket_count", "label": "Open ticket count", "description": "", "kind": "count", "type": null }, { "name": "closed_ticket_count", "label": "Closed ticket count", "description": "", "kind": "count", "type": null }, { "name": "ai_handled_ticket_count", "label": "AI handled ticket count", "description": "", "kind": "count", "type": null }, { "name": "human_assigned_ticket_count", "label": "Human assigned ticket count", "description": "", "kind": "count", "type": null }, { "name": "human_involved_ticket_count", "label": "Human involved ticket count", "description": "", "kind": "count", "type": null }, { "name": "human_resolved_ticket_count", "label": "Human resolved ticket count", "description": "", "kind": "count", "type": null }, { "name": "unassigned_ticket_count", "label": "Unassigned ticket count", "description": "", "kind": "count", "type": null }, { "name": "escalated_ticket_count", "label": "Escalated ticket count", "description": "", "kind": "count", "type": null }, { "name": "ai_deflection_rate", "label": "AI deflection rate", "description": "", "kind": "ratio", "type": null } ], "dimensions": [ { "name": "status", "label": "Status", "description": "", "kind": null, "type": "string" }, { "name": "priority", "label": "Priority", "description": "", "kind": null, "type": "string" }, { "name": "sub_status", "label": "Sub status", "description": "", "kind": null, "type": "string" }, { "name": "channel_kind", "label": "Channel kind", "description": "", "kind": null, "type": "string" }, { "name": "channel_id", "label": "Channel ID", "description": "", "kind": null, "type": "string" }, { "name": "tag_name", "label": "Tag name", "description": "", "kind": null, "type": "string" }, { "name": "tag_icon", "label": "Tag icon", "description": "", "kind": null, "type": "string" }, { "name": "open_age_bucket", "label": "Open age bucket", "description": "", "kind": null, "type": "string" }, { "name": "closed_age_bucket", "label": "Closed age bucket", "description": "", "kind": null, "type": "string" }, { "name": "current_assignee_id", "label": "Current assignee ID", "description": "", "kind": null, "type": "string" }, { "name": "current_assignee_role", "label": "Current assignee role", "description": "", "kind": null, "type": "string" }, { "name": "custom_field_value", "label": "Custom field value", "description": "", "kind": null, "type": "string" } ], "query_shapes": [ { "name": "summary", "label": "Ticket summary", "description": "Workspace or filtered ticket summary totals.", "measures": [ "ticket_count", "open_ticket_count", "closed_ticket_count", "ai_handled_ticket_count", "human_assigned_ticket_count", "human_involved_ticket_count", "human_resolved_ticket_count", "unassigned_ticket_count", "escalated_ticket_count", "ai_deflection_rate" ], "dimensions": [], "requires_time": false, "allowed_grains": [] }, { "name": "timeseries", "label": "Ticket trend", "description": "Ticket trends over time.", "measures": [ "ticket_count", "open_ticket_count", "ai_handled_ticket_count", "human_involved_ticket_count", "ai_deflection_rate" ], "dimensions": [], "requires_time": true, "allowed_grains": [ "day", "week", "month" ] }, { "name": "timeseries_by_channel", "label": "Ticket trend by channel", "description": "Ticket counts grouped by channel kind over time.", "measures": [ "ticket_count" ], "dimensions": [ "channel_kind" ], "requires_time": true, "allowed_grains": [ "day", "week", "month" ] }, { "name": "tag_trend", "label": "Tag trend", "description": "Ticket counts grouped by tag over time.", "measures": [ "ticket_count" ], "dimensions": [ "tag_name", "tag_icon" ], "requires_time": true, "allowed_grains": [ "day", "week", "month" ] }, { "name": "priority_breakdown", "label": "Priority breakdown", "description": "Ticket count by priority.", "measures": [ "ticket_count" ], "dimensions": [ "priority" ], "requires_time": false, "allowed_grains": [] }, { "name": "sub_status_breakdown", "label": "Sub-status breakdown", "description": "Ticket count by sub status.", "measures": [ "ticket_count" ], "dimensions": [ "sub_status" ], "requires_time": false, "allowed_grains": [] }, { "name": "open_age_breakdown", "label": "Open age breakdown", "description": "Open ticket count bucketed by age.", "measures": [ "open_ticket_count" ], "dimensions": [ "open_age_bucket" ], "requires_time": false, "allowed_grains": [] }, { "name": "open_member_breakdown", "label": "Open owner breakdown", "description": "Open ticket count by age bucket and current assignee.", "measures": [ "open_ticket_count" ], "dimensions": [ "open_age_bucket", "current_assignee_role", "current_assignee_id" ], "requires_time": false, "allowed_grains": [] }, { "name": "closed_age_breakdown", "label": "Closed age breakdown", "description": "Closed ticket count bucketed by time to close.", "measures": [ "closed_ticket_count" ], "dimensions": [ "closed_age_bucket" ], "requires_time": true, "allowed_grains": [ "day", "week", "month" ] }, { "name": "closed_member_breakdown", "label": "Closed owner breakdown", "description": "Closed ticket count by time-to-close bucket and final assignee.", "measures": [ "closed_ticket_count" ], "dimensions": [ "closed_age_bucket", "current_assignee_role", "current_assignee_id" ], "requires_time": true, "allowed_grains": [ "day", "week", "month" ] }, { "name": "open_channel_breakdown", "label": "Open channel breakdown", "description": "Open ticket count by age bucket and channel.", "measures": [ "open_ticket_count" ], "dimensions": [ "open_age_bucket", "channel_kind", "channel_id" ], "requires_time": false, "allowed_grains": [] }, { "name": "custom_field_trend", "label": "Custom field trend", "description": "Ticket counts over time grouped by custom field value.", "measures": [ "ticket_count" ], "dimensions": [ "custom_field_value" ], "requires_time": true, "allowed_grains": [ "day", "week", "month" ] } ] }, { "name": "messages", "label": "Messages", "description": "Inbound, AI, and human message activity analytics.", "default_time_anchor": "message_created_at", "filter_fields": [ "channel_ids", "channel_kinds", "statuses", "priorities", "assignee_ids", "assignee_types", "team_ids", "tag_ids", "queue_statuses", "unread", "response_status", "no_assignee", "custom_fields" ], "measures": [ { "name": "message_count", "label": "Message count", "description": "", "kind": "count", "type": null }, { "name": "conversation_count", "label": "Conversation count", "description": "", "kind": "count", "type": null }, { "name": "customer_message_count", "label": "Customer message count", "description": "", "kind": "count", "type": null }, { "name": "ai_message_count", "label": "AI message count", "description": "", "kind": "count", "type": null }, { "name": "human_message_count", "label": "Human message count", "description": "", "kind": "count", "type": null }, { "name": "template_message_count", "label": "Template message count", "description": "", "kind": "count", "type": null }, { "name": "average_message_count", "label": "Average message count", "description": "", "kind": "average", "type": null }, { "name": "average_conversation_count", "label": "Average conversation count", "description": "", "kind": "average", "type": null } ], "dimensions": [ { "name": "channel_kind", "label": "Channel kind", "description": "", "kind": null, "type": "string" }, { "name": "sender_role", "label": "Sender role", "description": "", "kind": null, "type": "string" }, { "name": "hour_of_day", "label": "Hour of day", "description": "", "kind": null, "type": "integer" }, { "name": "day_of_week", "label": "Day of week", "description": "", "kind": null, "type": "string" }, { "name": "sender_agent_id", "label": "Sender agent ID", "description": "", "kind": null, "type": "string" }, { "name": "sender_agent_name", "label": "Sender agent name", "description": "", "kind": null, "type": "string" }, { "name": "sender_agent_avatar", "label": "Sender agent avatar", "description": "", "kind": null, "type": "string" } ], "query_shapes": [ { "name": "summary", "label": "Message summary", "description": "Message and conversation totals.", "measures": [ "message_count", "conversation_count", "customer_message_count", "ai_message_count", "human_message_count" ], "dimensions": [], "requires_time": true, "allowed_grains": [ "day", "week", "month" ] }, { "name": "message_breakdown", "label": "Message breakdown trend", "description": "Customer, AI, and human message trends over time.", "measures": [ "conversation_count", "customer_message_count", "ai_message_count", "human_message_count" ], "dimensions": [], "requires_time": true, "allowed_grains": [ "day", "week", "month" ] }, { "name": "busiest_times", "label": "Busiest times", "description": "Conversation and message volume by weekday and hour.", "measures": [ "conversation_count", "message_count" ], "dimensions": [ "day_of_week", "hour_of_day" ], "requires_time": true, "allowed_grains": [] }, { "name": "agent_hourly_activity", "label": "Agent hourly activity", "description": "Human outbound and message activity by agent and hour.", "measures": [ "conversation_count", "human_message_count", "template_message_count" ], "dimensions": [ "sender_agent_id", "sender_agent_name", "sender_agent_avatar", "hour_of_day" ], "requires_time": true, "allowed_grains": [] }, { "name": "hourly_averages", "label": "Hourly averages", "description": "Average conversation and message counts by hour.", "measures": [ "average_conversation_count", "average_message_count" ], "dimensions": [ "hour_of_day" ], "requires_time": true, "allowed_grains": [] } ] }, { "name": "workforce_activity", "label": "Workforce activity", "description": "Assignee, response-time, and resolution analytics for AI and human handlers.", "default_time_anchor": "created_at", "filter_fields": [ "channel_ids", "channel_kinds", "statuses", "priorities", "assignee_ids", "assignee_types", "team_ids", "tag_ids", "queue_statuses", "unread", "response_status", "no_assignee", "inferred_csat_scores", "user_csat_scores", "custom_fields", "actor_ids", "actor_types", "first_response_target_seconds", "resolution_target_seconds" ], "measures": [ { "name": "ticket_count", "label": "Ticket count", "description": "", "kind": "count", "type": null }, { "name": "closed_ticket_count", "label": "Closed ticket count", "description": "", "kind": "count", "type": null }, { "name": "first_response_count", "label": "First response count", "description": "", "kind": "count", "type": null }, { "name": "first_response_within_target_count", "label": "First response within target", "description": "", "kind": "count", "type": null }, { "name": "resolution_count", "label": "Resolution count", "description": "", "kind": "count", "type": null }, { "name": "resolution_within_target_count", "label": "Resolution within target", "description": "", "kind": "count", "type": null }, { "name": "duration_count", "label": "Duration count", "description": "", "kind": "count", "type": null }, { "name": "median_first_response_seconds", "label": "Median first response seconds", "description": "", "kind": "median", "type": null }, { "name": "median_first_response_business_hours_seconds", "label": "Median first response BH seconds", "description": "", "kind": "median", "type": null }, { "name": "median_response_seconds", "label": "Median response seconds", "description": "", "kind": "median", "type": null }, { "name": "median_response_business_hours_seconds", "label": "Median response BH seconds", "description": "", "kind": "median", "type": null }, { "name": "median_resolution_seconds", "label": "Median resolution seconds", "description": "", "kind": "median", "type": null }, { "name": "median_resolution_business_hours_seconds", "label": "Median resolution BH seconds", "description": "", "kind": "median", "type": null }, { "name": "tickets_touched", "label": "Tickets touched", "description": "", "kind": "count", "type": null }, { "name": "resolved_of_touched", "label": "Resolved of touched", "description": "", "kind": "count", "type": null }, { "name": "deflected_of_touched", "label": "Deflected of touched", "description": "", "kind": "count", "type": null }, { "name": "deflection_rate", "label": "Deflection rate", "description": "", "kind": "ratio", "type": null }, { "name": "message_count", "label": "Message count", "description": "", "kind": "count", "type": null }, { "name": "outbound_message_count", "label": "Outbound message count", "description": "", "kind": "count", "type": null }, { "name": "response_pair_count", "label": "Response pair count", "description": "", "kind": "count", "type": null }, { "name": "average_response_minutes", "label": "Average response minutes", "description": "", "kind": "average", "type": null }, { "name": "average_response_business_hours_minutes", "label": "Average response BH minutes", "description": "", "kind": "average", "type": null }, { "name": "escalation_response_count", "label": "Escalation response count", "description": "", "kind": "count", "type": null }, { "name": "average_escalation_response_minutes", "label": "Average escalation response minutes", "description": "", "kind": "average", "type": null }, { "name": "average_escalation_response_business_hours_minutes", "label": "Average escalation response BH minutes", "description": "", "kind": "average", "type": null }, { "name": "escalation_response_within_target_count", "label": "Escalation response within target", "description": "", "kind": "count", "type": null }, { "name": "time_to_close_from_escalation_count", "label": "Close-from-escalation count", "description": "", "kind": "count", "type": null }, { "name": "average_time_to_close_from_escalation_minutes", "label": "Average close-from-escalation minutes", "description": "", "kind": "average", "type": null }, { "name": "time_to_close_from_escalation_within_target_count", "label": "Close-from-escalation within target", "description": "", "kind": "count", "type": null }, { "name": "time_to_close_from_creation_count", "label": "Close-from-creation count", "description": "", "kind": "count", "type": null }, { "name": "average_time_to_close_from_creation_minutes", "label": "Average close-from-creation minutes", "description": "", "kind": "average", "type": null }, { "name": "time_to_close_from_escalation_business_hours_count", "label": "Close-from-escalation BH count", "description": "", "kind": "count", "type": null }, { "name": "average_time_to_close_from_escalation_business_hours_minutes", "label": "Average close-from-escalation BH minutes", "description": "", "kind": "average", "type": null }, { "name": "time_to_close_from_creation_business_hours_count", "label": "Close-from-creation BH count", "description": "", "kind": "count", "type": null }, { "name": "average_time_to_close_from_creation_business_hours_minutes", "label": "Average close-from-creation BH minutes", "description": "", "kind": "average", "type": null }, { "name": "inferred_response_count", "label": "Inferred CSAT response count", "description": "", "kind": "count", "type": null }, { "name": "inferred_average_score", "label": "Inferred CSAT average score", "description": "", "kind": "average", "type": null }, { "name": "survey_response_count", "label": "Survey CSAT response count", "description": "", "kind": "count", "type": null }, { "name": "survey_average_score", "label": "Survey CSAT average score", "description": "", "kind": "average", "type": null }, { "name": "inferred_score_1_count", "label": "Inferred score 1 count", "description": "", "kind": "count", "type": null }, { "name": "inferred_score_2_count", "label": "Inferred score 2 count", "description": "", "kind": "count", "type": null }, { "name": "inferred_score_3_count", "label": "Inferred score 3 count", "description": "", "kind": "count", "type": null }, { "name": "inferred_score_4_count", "label": "Inferred score 4 count", "description": "", "kind": "count", "type": null }, { "name": "inferred_score_5_count", "label": "Inferred score 5 count", "description": "", "kind": "count", "type": null }, { "name": "survey_score_1_count", "label": "Survey score 1 count", "description": "", "kind": "count", "type": null }, { "name": "survey_score_2_count", "label": "Survey score 2 count", "description": "", "kind": "count", "type": null }, { "name": "survey_score_3_count", "label": "Survey score 3 count", "description": "", "kind": "count", "type": null }, { "name": "survey_score_4_count", "label": "Survey score 4 count", "description": "", "kind": "count", "type": null }, { "name": "survey_score_5_count", "label": "Survey score 5 count", "description": "", "kind": "count", "type": null } ], "dimensions": [ { "name": "current_assignee_id", "label": "Current assignee ID", "description": "", "kind": null, "type": "string" }, { "name": "current_assignee_role", "label": "Current assignee role", "description": "", "kind": null, "type": "string" }, { "name": "channel_kind", "label": "Channel kind", "description": "", "kind": null, "type": "string" }, { "name": "actor_type", "label": "Actor type", "description": "", "kind": null, "type": "string" }, { "name": "actor_id", "label": "Actor ID", "description": "", "kind": null, "type": "string" }, { "name": "actor_name", "label": "Actor name", "description": "", "kind": null, "type": "string" }, { "name": "actor_avatar", "label": "Actor avatar", "description": "", "kind": null, "type": "string" }, { "name": "duration_metric", "label": "Duration metric", "description": "", "kind": null, "type": "string" }, { "name": "duration_bucket", "label": "Duration bucket", "description": "", "kind": null, "type": "string" } ], "query_shapes": [ { "name": "issues_by_assignee_by_week", "label": "Issues by assignee by week", "description": "Weekly issue counts by current assignee.", "measures": [ "ticket_count" ], "dimensions": [ "current_assignee_id", "current_assignee_role" ], "requires_time": true, "allowed_grains": [ "week" ] }, { "name": "issues_by_source_by_assignee", "label": "Issues by source by assignee", "description": "Issue counts by channel and current assignee.", "measures": [ "ticket_count" ], "dimensions": [ "channel_kind", "current_assignee_id", "current_assignee_role" ], "requires_time": true, "allowed_grains": [] }, { "name": "median_first_response_by_assignee", "label": "Median first response by assignee", "description": "Median first response time by current assignee.", "measures": [ "median_first_response_seconds", "median_first_response_business_hours_seconds" ], "dimensions": [ "current_assignee_id", "current_assignee_role" ], "requires_time": true, "allowed_grains": [] }, { "name": "median_resolution_by_assignee", "label": "Median resolution by assignee", "description": "Median resolution time by current assignee.", "measures": [ "median_resolution_seconds", "median_resolution_business_hours_seconds" ], "dimensions": [ "current_assignee_id", "current_assignee_role" ], "requires_time": true, "allowed_grains": [] }, { "name": "assignee_summary", "label": "Assignee summary", "description": "Issue counts and response/resolution medians by assignee.", "measures": [ "ticket_count", "closed_ticket_count", "median_first_response_seconds", "median_first_response_business_hours_seconds", "median_response_seconds", "median_response_business_hours_seconds", "median_resolution_seconds", "median_resolution_business_hours_seconds" ], "dimensions": [ "current_assignee_id", "current_assignee_role" ], "requires_time": true, "allowed_grains": [] }, { "name": "org_daily_metrics", "label": "Org daily metrics", "description": "Daily response and resolution medians by handler role.", "measures": [ "median_first_response_seconds", "median_first_response_business_hours_seconds", "median_response_seconds", "median_response_business_hours_seconds", "median_resolution_seconds" ], "dimensions": [ "current_assignee_role" ], "requires_time": true, "allowed_grains": [ "day" ] }, { "name": "actor_performance", "label": "Actor performance", "description": "Touched-ticket, response, close, and CSAT measures grouped by actor over a reporting window.", "measures": [ "tickets_touched", "resolved_of_touched", "deflected_of_touched", "deflection_rate", "message_count", "outbound_message_count", "response_pair_count", "average_response_minutes", "average_response_business_hours_minutes", "escalation_response_count", "average_escalation_response_minutes", "average_escalation_response_business_hours_minutes", "escalation_response_within_target_count", "time_to_close_from_escalation_count", "average_time_to_close_from_escalation_minutes", "time_to_close_from_escalation_within_target_count", "time_to_close_from_creation_count", "average_time_to_close_from_creation_minutes", "time_to_close_from_escalation_business_hours_count", "average_time_to_close_from_escalation_business_hours_minutes", "time_to_close_from_creation_business_hours_count", "average_time_to_close_from_creation_business_hours_minutes", "inferred_response_count", "inferred_average_score", "survey_response_count", "survey_average_score", "inferred_score_1_count", "inferred_score_2_count", "inferred_score_3_count", "inferred_score_4_count", "inferred_score_5_count", "survey_score_1_count", "survey_score_2_count", "survey_score_3_count", "survey_score_4_count", "survey_score_5_count" ], "dimensions": [ "actor_type", "actor_id", "actor_name", "actor_avatar" ], "requires_time": true, "allowed_grains": [] }, { "name": "duration_distribution_by_handler", "label": "Duration distribution by handler", "description": "First response and resolution duration buckets grouped by AI and human handlers.", "measures": [ "duration_count" ], "dimensions": [ "duration_metric", "duration_bucket", "current_assignee_role" ], "requires_time": true, "allowed_grains": [] } ] } ], "templates": [ { "name": "dashboard.ticket_breakdown", "dataset": "tickets", "measures": [ "open_ticket_count", "human_assigned_ticket_count", "ai_handled_ticket_count", "unassigned_ticket_count" ], "dimensions": [] }, { "name": "dashboard.conversation_trend", "dataset": "tickets", "measures": [ "ticket_count", "open_ticket_count" ], "dimensions": [] }, { "name": "dashboard.message_breakdown", "dataset": "messages", "measures": [ "customer_message_count", "ai_message_count", "human_message_count" ], "dimensions": [] } ] } ``` Use this endpoint to list the analytics datasets, measures, dimensions, query shapes, and dashboard templates available to the API client. ## Required access | Authentication | Scope | Dataset grant | | ------------------------------------------------------------ | ----------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | [API client v2](/api-reference/authentication/api-client-v2) | `analytics.query` | Any analytics dataset you want returned in the catalog, such as `tickets`, `messages`, `outbound_messages`, `marketing_attribution`, `csat`, or `workforce_activity`. | ## Journey and campaign reporting | Dataset | What to discover | | ----------------------- | ------------------------------------------------------------------------------------------------------------ | | `marketing_attribution` | `attribution_revenue`, `estimated_roas`, and existing `attribution_cost`, grouped by time bucket and entity. | | `outbound_messages` | Delivery measures grouped by `template_category`, optionally with campaign or journey ID. | Each dataset lists its `filter_fields`, measures, dimensions and supported `query_shapes`. The shape's dimension order matters. For template-category delivery, `time_bucket` is returned automatically and must not be added to the request dimension list. Start with [journey and campaign analytics](/api-reference/analytics/journeys-campaigns) for working request examples and metric definitions, then use [Find entity IDs](/api-reference/analytics/members) to resolve names. ## Notes * This endpoint uses API client v2 credentials. See [API client v2](/api-reference/authentication/api-client-v2). # Journey and campaign analytics Source: https://docs.sagepilot.ai/api-reference/analytics/journeys-campaigns Query attributed revenue, estimated ROAS, and delivery rates by WhatsApp template category. Use the [analytics query endpoint](/api-reference/analytics/query) to report on journeys and campaigns over a selected period. The same request shapes are available through the MCP `query_analytics` tool. ## Before you start Create an [API client v2](/api-reference/authentication/api-client-v2) with `analytics.query` and the datasets you need: | Dataset | Reports | | ----------------------- | ------------------------------------------------------- | | `marketing_attribution` | Attributed revenue, orders, and estimated ROAS. | | `outbound_messages` | Message counts and delivery rates by template category. | Use [Find entity IDs](/api-reference/analytics/members) to resolve campaign or journey names. Replace the example UUIDs below with IDs from your workspace. The [catalog](/api-reference/analytics/catalog) lists query shapes available to your client. ## Attributed revenue and estimated ROAS Send this JSON body to `POST /platform-api/v1/analytics/query`: ```json theme={null} { "dataset": "marketing_attribution", "measures": ["attribution_revenue", "estimated_roas"], "dimensions": ["time_bucket", "entity_type", "entity_id"], "filters": { "campaign_ids": ["22222222-2222-4222-8222-222222222222"] }, "time": { "start": "2026-09-01T00:00:00Z", "end": "2026-09-07T23:59:59.999Z", "timezone": "UTC", "grain": "day", "anchor": "order_created_at" }, "result_format": "rows", "limit": 100 } ``` For a journey, replace `campaign_ids` with `journey_ids`. To compare selected campaigns and journeys, provide both arrays. Attribution returns either selected entity type, further restricted by `entity_types` if provided. An empty ID array imposes no restriction; each array accepts up to 100 UUIDs. An example row in `rows`: ```json theme={null} { "time_bucket": "2026-09-03", "entity_type": "campaign", "entity_id": "22222222-2222-4222-8222-222222222222", "attribution_type": "delivery", "window_hours": 72, "attribution_revenue": { "by_currency": [{"currency": "INR", "revenue": 910, "orders": 2}] }, "estimated_roas": { "by_currency": [{"currency": "INR", "roas": 10}] } } ``` These illustrative values use INR 91 in estimated messaging cost. `roas: 10` means ten units of attributed revenue per unit of estimated spend; it is a ratio, not a percentage. ### Attribution settings and pagination * Omit `filters.attribution_type` and `filters.window_hours` to use your workspace attribution settings. Override the event with `delivery`, `read`, or `clicked`, and the lookback with 1–8760 hours. The lookback is separate from the reporting period. * `meta.applied_filters` reports the effective attribution event, lookback, and entity types even when no rows match. * Use the exact dimensions `["time_bucket", "entity_type", "entity_id"]`. Choose `day`, `week`, or `month`; `order_created_at` is the supported time anchor. * Request `attribution_revenue`, `estimated_roas`, or both. The existing `attribution_cost` measure can also be requested. Only requested measures are returned. * Results use a fixed order by time bucket, entity type, and entity ID. Custom sort and `summary` are unsupported. Follow `meta.cursor` with the same query until it is null. The default and maximum page size is 1000. Selecting one entity does not reassign orders that were attributed elsewhere. Attribution selects the winning eligible interaction before applying campaign/journey ID filters. ### How to interpret estimated ROAS Estimated ROAS divides attributed revenue by estimated messaging cost **in the same currency**. The existing estimate covers delivered/read WhatsApp marketing messages at INR 0.91 and utility messages at INR 0.15. Authentication messages and other channels are outside this estimate. These are estimation inputs, not actual invoiced charges or a pricing quotation. | Situation in a bucket | `roas` | | -------------------------------------------------------- | --------------------------------- | | Revenue and positive estimated cost in the same currency | Revenue divided by cost. | | Positive estimated cost with no revenue in any currency | `0`. | | Zero or missing estimated cost | `null`. | | Revenue only in USD and estimated cost only in INR | `null` for both currency entries. | No currency conversion is performed. Keep currency breakdowns separate. Revenue uses gross attributed order totals and includes cancelled/refunded orders; it is not net revenue. Do not average daily ROAS values to calculate a period's ROAS. Where matching revenue and cost are available, sum each in the same currency and divide those totals. Revenue buckets use order creation time; spend buckets use message send time. A reporting period can therefore include spend for orders attributed in another period. Campaign and journey spend can overlap, so do not add their costs together as a workspace total. ## Delivery by template category Send this body to the same query endpoint: ```json theme={null} { "dataset": "outbound_messages", "measures": ["sent_count", "delivered_like_count", "read_count", "failed_count", "delivery_rate"], "dimensions": ["journey_id", "template_category"], "filters": { "journey_ids": ["33333333-3333-4333-8333-333333333333"] }, "time": { "start": "2026-09-01T00:00:00Z", "end": "2026-09-07T23:59:59.999Z", "timezone": "UTC", "grain": "day", "anchor": "sent_at" }, "result_format": "rows" } ``` An example row in `rows`: ```json theme={null} { "time_bucket": "2026-09-03", "journey_id": "33333333-3333-4333-8333-333333333333", "template_category": "MARKETING", "sent_count": 100, "delivered_like_count": 80, "read_count": 50, "failed_count": 5, "delivery_rate": 0.8 } ``` Choose one of these dimension lists, in the order shown. `time_bucket` is added to each response row automatically; do not include it in these request dimensions. | Dimensions | Grouping | | -------------------------------------- | ------------------------------------------- | | `["template_category"]` | Category across matching outbound messages. | | `["campaign_id", "template_category"]` | Campaign and category. | | `["journey_id", "template_category"]` | Journey and category. | Campaign/journey groupings include only messages linked to that entity type. Category-only grouping also includes messages without campaign or journey IDs. You can request any nonempty subset of the five measures in the example. `sent_count` counts messages whose current status is sent, delivered, or read. `delivered_like_count` counts delivered or read; `read_count` counts read; `failed_count` counts failed. `delivery_rate` is `delivered_like_count / sent_count`: **0–1**, so `0.8` means 80%. A zero denominator returns `0`. Failed messages are reported separately and are not in this denominator. For a combined delivery rate, divide the summed counts; do not average the row rates. ### Category and filter rules * Categories are `MARKETING`, `UTILITY`, `AUTHENTICATION`, and `UNKNOWN`. They reflect the **current WhatsApp template category**, so reclassification can change historical reports. * Missing/deleted templates and non-WhatsApp messages use `UNKNOWN`. For a WhatsApp-only report, add `channel_kinds: ["whatsapp"]` to `filters`. * `template_categories` filters categories and requires one of the category groupings above. Category values are case-sensitive. * `campaign_ids`, `journey_ids`, `has_campaign`, `has_journey`, `channel_ids`, and `channel_kinds` combine with AND on message rows. With both ID arrays supplied, each message must match both. * Results contain observed buckets only, with no empty-date/category rows added. `summary` and cursors are unsupported. Omit `limit` to receive all matching buckets; a limit truncates the result without a next page. ## Time windows and errors Use ISO 8601 timestamps with an explicit offset. Timestamps without an offset are treated as UTC. `time.timezone` controls day/week/month grouping; it does not reinterpret start/end offsets. Revenue and category delivery include both start and end timestamps. Estimated cost includes the start and excludes the end. Category delivery uses send time, falling back to message creation time for messages not yet sent. These boundaries and different event times matter when reconciling reports. Marketing attribution and category queries reject reversed windows, unknown timezones, `aggregation`, and `business_hours`. Unsupported or misspelled filters return an error rather than an unfiltered total. A missing scope/dataset grant returns `403`; invalid combinations return `400`, and malformed fields return `422`. For MCP examples and the distinction between date-bounded analytics and lifetime product overviews, see [Journey and campaign tools](/mcp/journeys-campaigns). # Find campaigns and journeys Source: https://docs.sagepilot.ai/api-reference/analytics/members POST /platform-api/v1/analytics/v2/members Resolve campaign and journey names to IDs for analytics filters. ```bash cURL theme={null} curl --request POST \ --url https://app.sagepilot.ai/platform-api/v1/analytics/v2/members \ --header "Authorization: Bearer $SAGEPILOT_API_TOKEN" \ --header 'Content-Type: application/json' \ --data '{ "dataset": "marketing_attribution", "dimension": "journey_id", "search": "Welcome", "limit": 20 }' ``` Use this endpoint to find campaign or journey IDs before [querying analytics](/api-reference/analytics/journeys-campaigns). Results include names and IDs from your workspace, including entities with no activity during a reporting period. Set `SAGEPILOT_API_TOKEN` to your API client token before running the cURL example, and use your [regional base URL](/api-reference/authentication/api-client-v2#base-url). ## Required access Use an [API client v2 token](/api-reference/authentication/api-client-v2) with `analytics.query` and the dataset named in your request: `marketing_attribution` or `outbound_messages`. These grants allow campaign and journey lookup. They do not grant access to ticket member lists, ticket SQL, or journey flow definitions. ## Body Set `marketing_attribution` for revenue/ROAS reports or `outbound_messages` for delivery reports. If omitted, the endpoint defaults to `tickets` and requires that dataset's access. Use `campaign_id` or `journey_id`. Other dimensions are rejected for marketing/outbound datasets. Optional case-insensitive name search. Omit to list names in alphabetical order. Maximum matches, from 1 to 50. There is no cursor; narrow your search if several entities have similar names. ## Example request ```json theme={null} { "dataset": "marketing_attribution", "dimension": "journey_id", "search": "Welcome", "limit": 20 } ``` ```json 200 theme={null} { "dimension": "journey_id", "members": [ { "value": "33333333-3333-4333-8333-333333333333", "label": "Welcome journey" } ] } ``` Use a returned `value` in `filters.journey_ids` or `filters.campaign_ids`. No matches returns an empty `members` array. A missing scope or dataset grant returns `403`; an unsupported dimension returns `400`. # Analytics Source: https://docs.sagepilot.ai/api-reference/analytics/overview Discover analytics datasets, query reports, and find campaign and journey IDs. Use the Analytics API to discover available datasets and query reports with your API client's dataset grants. Discover datasets, measures, dimensions, filters, and supported query shapes. Build reports with measures, dimensions, filters, and a reporting period. Look up campaign and journey IDs by name for use in analytics queries. Query attributed revenue, estimated ROAS, and delivery rates by template category. For credentials and dataset grants, see [API client v2](/api-reference/authentication/api-client-v2). # Query analytics Source: https://docs.sagepilot.ai/api-reference/analytics/query POST /platform-api/v1/analytics/query Run an analytics query with selected measures, dimensions, filters, and time windows. ```json 200 theme={null} { "dataset": "tickets", "rows": [], "summary": { "ticket_count": 1128 }, "meta": { "applied_filters": { "channel_ids": [], "channel_kinds": [], "statuses": [], "priorities": [], "assignee_ids": [], "assignee_types": [], "team_ids": [], "tag_ids": [], "excluded_tag_ids": [], "queue_statuses": [], "inferred_csat_scores": [], "user_csat_scores": [], "custom_fields": [], "score_sources": [], "actor_ids": [], "actor_types": [], "entity_types": [] }, "grain": null, "timezone": "UTC", "time_anchor": "created_at", "requested_measures": [ "ticket_count" ], "requested_dimensions": [], "generated_at": "2026-05-29T16:16:53.572328Z", "cursor": null } } ``` Use this endpoint to run analytics queries for a granted dataset. Query results can be returned as summary totals or grouped rows. For campaign or journey attribution, estimated ROAS, and delivery by template category, use the [journey and campaign analytics guide](/api-reference/analytics/journeys-campaigns). Discover supported combinations with the [analytics catalog](/api-reference/analytics/catalog). ## Required access | Authentication | Scope | Dataset grant | | ------------------------------------------------------------ | ----------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- | | [API client v2](/api-reference/authentication/api-client-v2) | `analytics.query` | The dataset used in the request body, such as `tickets`, `messages`, `outbound_messages`, `marketing_attribution`, `csat`, or `workforce_activity`. | ## Body Dataset to query. For example, `tickets`, `messages`, `outbound_messages`, `marketing_attribution`, `csat`, or `workforce_activity`. Measures to calculate. For example, `["ticket_count", "open_ticket_count"]` for the `tickets` dataset or `["message_count", "customer_message_count"]` for the `messages` dataset. Optional dimensions to group results. For example, `["channel_kind"]`, `["priority"]`, or `["day_of_week", "hour_of_day"]`. Optional filter object. For example, `{ "channel_ids": [""], "statuses": ["open"] }`. For channel, agent, pilot, team, or workspace IDs, see [Resource IDs](/api-reference/authentication/api-client-v2#resource-ids). Campaign UUIDs, up to 100. Supported by `marketing_attribution` and `outbound_messages`. Resolve names with [analytics member lookup](/api-reference/analytics/members). An empty array imposes no restriction. Journey UUIDs, up to 100. Supported by `marketing_attribution` and `outbound_messages`. With campaign IDs, attribution selects either requested entity type; outbound queries require both filters to match each message. For `marketing_attribution`, use `campaign`, `journey`, or both. Defaults to both. This filter also applies when you specify entity IDs. For `marketing_attribution`, use `delivery`, `read`, or `clicked`. Omit to use your workspace attribution setting. Attribution lookback override for `marketing_attribution`, from 1 to 8760 hours. Omit to use your workspace setting. This is separate from the reporting period in `time`. For outbound category queries only: `MARKETING`, `UTILITY`, `AUTHENTICATION`, or `UNKNOWN`. Requires a supported `template_category` grouping. Values are case-sensitive. Optional time window. Include this for trend queries or date-bounded summaries. ISO start timestamp. For example, `2026-05-01T00:00:00.000Z`. ISO end timestamp. For example, `2026-05-29T23:59:59.999Z`. Timezone used to group time buckets. For example, `Asia/Kolkata` or `UTC`. Explicit offsets in start/end timestamps determine the reporting boundaries; timestamps without an offset are treated as UTC. Time grain for grouped trend results. Use `day`, `week`, or `month`. Timestamp field used for the query window. For example, `created_at`, `closed_at`, `message_created_at`, `sent_at`, or `order_created_at`. Optional business-hours configuration for business-hours duration measures. Business-hours timezone. For example, `Asia/Kolkata`. Local business day start time. For example, `09:00`. Local business day end time. For example, `18:00`. Working days as weekday numbers. For example, `[1, 2, 3, 4, 5]` for Monday through Friday. Response shape, where supported by the chosen query. Use `summary` for aggregate totals or `rows` for grouped rows. Marketing attribution and template-category queries return rows and reject `summary`. Optional sort instructions. For example, `[{ "field": "ticket_count", "direction": "desc" }]`. Field to sort by, usually a requested measure or dimension. Sort direction. Use `asc` or `desc`. Maximum number of rows to return, from 1 to 1000. Marketing attribution defaults to 1000 rows per page. Category queries apply a limit without pagination; omit it when you need all matching buckets. For marketing attribution, use `meta.cursor` from the previous response with the same filters, measures, and time settings. Stop when it is null. Template-category queries reject cursors. ## Notes * This endpoint uses API client v2 credentials. See [API client v2](/api-reference/authentication/api-client-v2). * Marketing attribution and template-category queries require a time window and reject reversed ranges, unknown timezones, `business_hours`, and `aggregation`. Marketing attribution also rejects custom sort. * Unsupported or misspelled filters on these datasets return an error. A missing scope or dataset grant returns `403`; invalid query combinations return `400`, and malformed request fields return `422`. # API client v2 Source: https://docs.sagepilot.ai/api-reference/authentication/api-client-v2 Use API client v2 credentials for analytics, ticket list APIs, ticket exports, voice call exports, customer read APIs, and new platform integrations. ## Base URL * Global region: `https://app.sagepilot.ai` * Europe region: `https://eu.sagepilot.ai` ## Create an API client In the Sagepilot dashboard, go to **Settings** > **API Details**. In **API client v2**, select **Create key**. Fill in the label, principal type, and description so the client is identifiable later. Select only the actions this integration needs. Select only the datasets this integration needs. Copy the generated `sp_...` token. It is shown once. ## Create form fields Human-readable name for the client, such as `BI warehouse sync` or `Support MCP client`. Principal category for the client. Use `external` for third-party systems, customer-managed automations, and external integrations. Use `internal` for Sagepilot-managed internal services. Optional description of what the client is used for. Action permissions granted to the client. Dataset grants that restrict which data the client can read or export. ## Current scopes The API Details page currently exposes these API client v2 scope options: | Scope | Use | | -------------------- | ----------------------------------------------------------------------------- | | `analytics.query` | Run analytics catalog and query endpoints on granted datasets. | | `analytics.raw.read` | Reserved for future raw analytics row access. | | `ticket.read` | Legacy UI-visible ticket read scope. For new ticket APIs, use `tickets:read`. | | `tickets.export` | Export raw ticket rows as CSV. | | `tickets:read` | Read ticket lists, ticket details, and ticket messages. | | `tickets:write` | Update ticket status and writable ticket fields. | | `tickets:assign` | Assign tickets to agents, AI agents, teams, or unassigned state. | | `customers:read` | Search and read customer profile data. | ## Current datasets The API Details page currently exposes these dataset grants: | Dataset | Use | | ----------------------- | -------------------------------------------------------------------------- | | `tickets` | Ticket list APIs and ticket analytics. | | `customers` | Customer search and customer profile APIs. | | `messages` | Message analytics. | | `workforce_activity` | Workforce activity analytics. | | `csat` | CSAT analytics. | | `outbound_messages` | Outbound message analytics, including delivery rates by template category. | | `marketing_attribution` | Campaign/journey attribution and estimated ROAS. | | `tickets.raw` | Raw ticket and voice call CSV export. | New API clients should use these canonical dataset names. ## Send credentials Send the API client token in either header form: ```bash theme={null} Authorization: Bearer sp_. ``` ```bash theme={null} X-API-Key: sp_. ``` Optional for API client v2 requests. If you send it, it must match the workspace bound to the API client. Find the current workspace ID in **Settings > API Details > Channels**. Treat `sp_...` API client tokens as secrets. Do not commit them to source control or expose them in client-side code. ## Required access by API | API area | Required scope | Required dataset | | ------------------------------------ | --------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- | | Analytics catalog and query | `analytics.query` | Matching analytics dataset, such as `tickets`, `messages`, `outbound_messages`, `marketing_attribution`, `csat`, or `workforce_activity`. | | Campaign/journey analytics ID lookup | `analytics.query` | `marketing_attribution` or `outbound_messages`, matching the lookup request. | | Ticket list APIs | `tickets:read` | `tickets` | | Mark ticket read | - | `tickets` | | Ticket assignment | `tickets:assign` | `tickets` | | Ticket status updates | `tickets:write` | `tickets` | | Ticket messages | Any ticket scope: `tickets:read`, `tickets:write`, `tickets:assign`, or `tickets:reply` | `messages` | | Ticket exports | `tickets.export` | `tickets.raw` | | Voice call exports | `tickets.export` | `tickets.raw` | | Customer read APIs | `customers:read` | `customers` | ## Resource IDs Use **Settings > API Details** in the Sagepilot dashboard to find the IDs used in API requests. | Resource | Where to find it | Used for | | ---------------------- | --------------------------------------------------- | -------------------------------------------------------------------------------------------------------------- | | Campaign or journey ID | [Find entity IDs](/api-reference/analytics/members) | `filters.campaign_ids` and `filters.journey_ids` in analytics queries. | | Workspace ID | **Settings > API Details > Channels** | `X-Workspace-Id` headers and optional `workspaceId` body fields. This is the current workspace UUID. | | Channel ID | **Settings > API Details > Channels** | `channel_id`, `filters.channel_ids`, and channel filters. | | Human agent ID | **Settings > API Details > Agents** | `agent_id`, `assignee_id` when `assignee_type` is `agent`, and agent assignment filters. | | AI agent ID / pilot ID | **Settings > API Details > Agents** | `pilot_id`, `assignee_id` when `assignee_type` is `pilot`, and AI agent assignment filters. | | Team ID | **-** | `assignee_id` when `assignee_type` is `team`, and team filters. | | Customer ID | Customer module | Open a customer profile and use the last UUID in the URL: `/dashboard/[workspace_id]/customers/[customer_id]`. | Use **Settings > API Details > Channels** as the current reference for workspace IDs. # Legacy workspace API key Source: https://docs.sagepilot.ai/api-reference/authentication/legacy-workspace-api-key Use the legacy workspace API key only for existing WhatsApp template-send integrations. ## Base URL * Global region: `https://app.sagepilot.ai` * Europe region: `https://eu.sagepilot.ai` ## Setup flow In the Sagepilot dashboard, go to **Settings** > **API Details**. Locate the legacy workspace API key section. You must be a workspace admin to view or generate this credential. Copy the workspace ID from **Settings** > **API Details** > **Channels**. Add the legacy API key in `Authorization` and the workspace ID in `X-SP-Workspace-Id`. Use this flow only for the existing WhatsApp template-send route. ## Required headers ```bash theme={null} Authorization: Bearer X-SP-Workspace-Id: ``` Bearer token using the legacy workspace API key. Workspace ID for the request. Find it in **Settings > API Details > Channels**. This header is always required for the legacy flow. This flow is for the legacy WhatsApp template-send route only. Do not use it for analytics, exports, ticket list APIs, customer APIs, or new integrations. # Calls Source: https://docs.sagepilot.ai/api-reference/calls Reference material for Sagepilot voice call APIs. Use these endpoints to export and work with voice call data from Sagepilot. Download filtered voice call rows as a streamed CSV attachment. ## Related For credential setup, supported auth headers, and access requirements, see [API client v2](/api-reference/authentication/api-client-v2). # Export voice calls Source: https://docs.sagepilot.ai/api-reference/calls/export-voice-calls POST /platform-api/v1/exports/voice_calls Download filtered voice call rows as a streamed CSV attachment. Use the voice call export API to download filtered call rows as a CSV file. ## Required access | Authentication | Scope | Dataset grant | | ------------------------------------------------------------ | ---------------- | ------------- | | [API client v2](/api-reference/authentication/api-client-v2) | `tickets.export` | `tickets.raw` | ## Body Export format. Currently only `csv` is supported. Voice call filters to apply before exporting rows. Optional object mapping voice-call custom field IDs to filter values. Custom field value filter for the given Sagepilot custom field ID. Timezone used for formatted datetime columns. Maximum number of rows to stream in the CSV export. ## Filters Call status. Supported values include `pending`, `ended`, `missed`, `failed`, `active`, `scheduled`, or `all`. Comma-separated call modes, such as `ai`, `human`, `ai_to_human`, or `human_to_ai`. Call direction. Use `inbound` or `outbound`. Comma-separated channel UUIDs. Find channel IDs in **Settings > API Details > Channels**. See [Resource IDs](/api-reference/authentication/api-client-v2#resource-ids). Comma-separated channel kinds, such as `voice` or `whatsapp`. Customer UUID to filter calls by. Comma-separated human agent or AI agent UUIDs. Use `current` where the API request is associated with a user context. See [Resource IDs](/api-reference/authentication/api-client-v2#resource-ids). Comma-separated team UUIDs. See [Resource IDs](/api-reference/authentication/api-client-v2#resource-ids). Comma-separated disposition values. Comma-separated terminal reasons, such as `voice_mail`. Comma-separated tag IDs. Comma-separated call sources, such as `direct`, `journey`, or `campaign`. Comma-separated campaign IDs. Comma-separated journey IDs. Comma-separated messaging template IDs sent during the call. Comma-separated sentiment values, such as `positive`, `neutral`, or `negative`. Use `true` or `false` to filter by recording availability. Use `true` or `false` to filter by transcript availability. Minimum call duration in seconds. Maximum call duration in seconds. Date preset or explicit range. Supported values include `today`, `yesterday`, `last_7_days`, `last_30_days`, and `start|end`. Date field used with `date_range`. Supported values are `created_at`, `answered_at`, and `ended_at`. Field used for ordering. Supported values include `created_at`, `answered_at`, `ended_at`, and `duration_seconds`. Sort direction. Use `asc` or `desc`. Use `true` to return unassigned calls. ## Default columns The CSV includes these default columns, followed by enabled voice-call custom fields: * `Call Code` * `Call ID` * `Direction` * `Mode` * `Status` * `Created At` * `Answered At` * `Ended At` * `Duration` * `Hangup Reason` * `Ended By` * `Customer Name` * `Customer Phone` * `Customer Email` * `Customer Company` * `From Number` * `To Number` * `Agent` * `Pilot` * `Team` * `Channel` * `Channel Type` * `Disposition` * `Disposition Set By` * `Summary` * `Sentiment` * `Recording URL` * `Has Transcript` * `Source` * `Campaign ID` * `Journey ID` * `Scheduled At` * `Scheduled By` * `Tags` * `Transcript` ## Notes * Use voice call export when you need row-level CSV data for calls, recordings, transcripts, dispositions, and call-level custom fields. * The endpoint appends enabled voice-call custom fields after the default CSV columns. # Customers Source: https://docs.sagepilot.ai/api-reference/customers Reference material for customer read APIs. Use these endpoints to read customer profiles, related tickets, calls, notes, and linked contacts. List, search, and sort customers. Get a customer by exact ID, email, or phone. List recent chats for a customer. List recent voice calls for a customer. ## Access Customer APIs use API client v2 with `customers:read` scope plus the `customers` dataset grant. # Get backoffice tickets Source: https://docs.sagepilot.ai/api-reference/customers/backoffice-tickets POST /platform-api/v1/customers/backoffice_tickets List backoffice tickets for a customer. ```json 200 theme={null} [ { "id": "48ee1866-6e3e-47cf-8d85-701b788cd7bd", "code": 40, "workspace_id": "ab1360ce-ef91-4397-8fbd-3ed77ef80e08", "title": "Test Backoffice ticket", "status": "new", "priority": null, "source": "manual", "due_date": "2026-05-29T18:30:00Z", "created_at": "2026-05-29T17:31:52.862791Z", "updated_at": "2026-05-29T17:31:52.862791Z", "tags": "[]", "assignee_name": "anushkakpawar25" } ] ``` Get backoffice tickets for a customer in the authenticated workspace. ## Required access | Authentication | Scope | Dataset grant | | ------------------------------------------------------------ | ---------------- | ------------- | | [API client v2](/api-reference/authentication/api-client-v2) | `customers:read` | `customers` | ## Body Customer UUID to fetch tickets for. Open a customer in the Customer module and use the last UUID in the URL: `/dashboard/[workspace_id]/customers/[customer_id]`. See [Resource IDs](/api-reference/authentication/api-client-v2#resource-ids). Optional workspace UUID. The authenticated workspace is used by default. Find the current workspace ID in **Settings > API Details > Channels**. See [Resource IDs](/api-reference/authentication/api-client-v2#resource-ids). Include tickets from linked contacts. ## Query parameters Page number. Records per page. Maximum value is `200`. ## Response Backoffice ticket records for the customer. # Get customer Source: https://docs.sagepilot.ai/api-reference/customers/get POST /platform-api/v1/customers/get Get a customer by exact ID, email, or phone. ```json 200 theme={null} { "id": "88d44689-1ce7-4033-8256-655a7cd8f90b", "name": "Vikram Singh Chundawat", "email": "vikram@sagepilot.ai", "phone": "+918003213447", "custom_fields": "{\"country\": \"Rajasthan\", \"location\": \"Bhilwara\", \"popup_submitted\": true, \"campaign.country\": \"Rajasthan\", \"popup_source_url\": \"https://sagepilot-test-account.myshopify.com/\", \"campaign.location\": \"Bhilwara\", \"popup_submitted_at\": \"2025-10-29T10:24:04.225Z\"}", "created_at": "2025-02-18T19:49:12.573277+00:00", "instagram_id": null, "instagram_username": null, "channel_name": "New Playground Channel", "channel_kind": "playground" } ``` Get a single customer by exact ID, email, or phone. ## Required access | Authentication | Scope | Dataset grant | | ------------------------------------------------------------ | ---------------- | ------------- | | [API client v2](/api-reference/authentication/api-client-v2) | `customers:read` | `customers` | ## Body One of `customerId`, `email`, or `phone` is required. Optional workspace UUID. The authenticated workspace is used by default. Find the current workspace ID in **Settings > API Details > Channels**. See [Resource IDs](/api-reference/authentication/api-client-v2#resource-ids). Customer UUID to look up. Open a customer in the Customer module and use the last UUID in the URL: `/dashboard/[workspace_id]/customers/[customer_id]`. See [Resource IDs](/api-reference/authentication/api-client-v2#resource-ids). Customer email to look up. Customer phone number to look up. ## Response Customer ID. Customer name. Customer email. Customer phone number. # Get customer by ID Source: https://docs.sagepilot.ai/api-reference/customers/get-by-id GET /platform-api/v1/customers/{customer_id} Get a customer by path ID. ```json 200 theme={null} { "id": "88d44689-1ce7-4033-8256-655a7cd8f90b", "name": "Vikram Singh Chundawat", "email": "vikram@sagepilot.ai", "phone": "+918003213447", "custom_fields": "{\"country\": \"Rajasthan\", \"location\": \"Bhilwara\", \"popup_submitted\": true, \"campaign.country\": \"Rajasthan\", \"popup_source_url\": \"https://sagepilot-test-account.myshopify.com/\", \"campaign.location\": \"Bhilwara\", \"popup_submitted_at\": \"2025-10-29T10:24:04.225Z\"}", "created_at": "2025-02-18T19:49:12.573277+00:00", "instagram_id": null, "instagram_username": null, "channel_name": "New Playground Channel", "channel_kind": "playground" } ``` Get a customer by ID within the authenticated workspace. ## Required access | Authentication | Scope | Dataset grant | | ------------------------------------------------------------ | ---------------- | ------------- | | [API client v2](/api-reference/authentication/api-client-v2) | `customers:read` | `customers` | ## Path parameters Customer UUID. Open a customer in the Customer module and use the last UUID in the URL: `/dashboard/[workspace_id]/customers/[customer_id]`. See [Resource IDs](/api-reference/authentication/api-client-v2#resource-ids). ## Response Customer ID. Customer custom fields, when present. Customer creation timestamp. # Get linked contacts Source: https://docs.sagepilot.ai/api-reference/customers/linked-contacts POST /platform-api/v1/customers/linked_contacts List contacts linked to a customer by tracker. ```json 200 theme={null} { "success": true, "contacts": [ { "id": "8258ac65-4073-44b8-83f5-b459ab9ff68e", "name": "Vikram Singh Chundawat", "email": "vikram@sagepilot.ai", "phone": null, "is_primary": false, "created_at": "2025-03-18T20:49:09.137699Z", "source": "playground" }, { "id": "70c42a1c-da1e-488e-b24f-ece6b2f3a5fe", "name": "Vikram Singh Chundawat", "email": "vikram@sagepilot.ai", "phone": "918003213447", "is_primary": false, "created_at": "2025-01-06T19:44:10.643789Z", "source": "web_widget" } ] } ``` Get contacts linked to a customer through shared identity tracking. ## Required access | Authentication | Scope | Dataset grant | | ------------------------------------------------------------ | ---------------- | ------------- | | [API client v2](/api-reference/authentication/api-client-v2) | `customers:read` | `customers` | ## Body Contact/customer UUID used to find linked contacts. Open a customer in the Customer module and use the last UUID in the URL: `/dashboard/[workspace_id]/customers/[customer_id]`. See [Resource IDs](/api-reference/authentication/api-client-v2#resource-ids). ## Response Whether the request succeeded. Linked contacts. # List customers Source: https://docs.sagepilot.ai/api-reference/customers/list POST /platform-api/v1/customers/list List, search, and sort customers. ```json 200 theme={null} { "records": [ { "id": "c52c9352-a1bb-4bab-bc53-a7651a5eec54", "name": "vikram", "email": "vikram2000b@gmail.com", "phone": "918003213447", "instagram_id": null, "instagram_username": null, "channel_name": "Sagepilot Website", "channel_kind": "web_widget", "custom_fields": { "city": null, "region": null, "source": "shopify_out_of_stock", "country": null, "product_url": "https://sagepilot-test-account.myshopify.com/products/the-3p-fulfilled-snowboard", "country_code": null, "product_name": "The 3p Fulfilled Snowboard", "submitted_at": "2025-10-31T06:22:19.648Z", "shopify_product_id": "9657337184546", "shopify_variant_id": "49698626765090", "shopify.customer.id": "8738967847202", "shopify.customer.name": "Vikram Singh Chundawat", "shopify.customer.note": "", "shopify.customer.tags": [ "READ", "VIP" ], "shopify.customer.email": "vikram2000b@gmail.com", "shopify.customer.phone": "+918003213447", "shopify.customer.state": "DISABLED", "sagepilot.customer.name": "Vikram Singh Chundawat", "sagepilot.customer.email": "vikram2000b@gmail.com", "sagepilot.customer.phone": "+918003213447", "shopify.customer.last_order": { "id": "6367398134050", "name": "#1032", "closed": false, "confirmed": true, "created_at": "2025-06-12T16:02:40Z", "total_price": 29.45, "cancelled_at": null, "processed_at": "2025-06-12T16:02:40Z", "currency_code": "INR", "return_status": "NO_RETURN", "financial_status": "PENDING", "fulfillment_status": "UNFULFILLED" }, "shopify.customer.currency_code": "INR", "shopify.customer.recent_orders": [ { "id": "6367398134050", "name": "#1032", "closed": false, "confirmed": true, "created_at": "2025-06-12T16:02:40Z", "total_price": 29.45, "cancelled_at": null, "processed_at": "2025-06-12T16:02:40Z", "currency_code": "INR", "return_status": "NO_RETURN", "financial_status": "PENDING", "fulfillment_status": "UNFULFILLED" }, { "id": "6348544737570", "name": "#1031", "closed": false, "confirmed": true, "created_at": "2025-05-29T19:48:27Z", "total_price": 29.45, "cancelled_at": null, "processed_at": "2025-05-29T19:48:26Z", "currency_code": "INR", "return_status": "NO_RETURN", "financial_status": "PAID", "fulfillment_status": "UNFULFILLED" }, { "id": "6348540510498", "name": "#1030", "closed": false, "confirmed": true, "created_at": "2025-05-29T19:44:02Z", "total_price": 29.45, "cancelled_at": null, "processed_at": "2025-05-29T19:44:02Z", "currency_code": "INR", "return_status": "NO_RETURN", "financial_status": "PAID", "fulfillment_status": "UNFULFILLED" }, { "id": "6268534620450", "name": "#1029", "closed": false, "confirmed": true, "created_at": "2025-05-13T20:11:05Z", "total_price": 29.45, "cancelled_at": null, "processed_at": "2025-05-13T20:11:05Z", "currency_code": "INR", "return_status": "NO_RETURN", "financial_status": "PENDING", "fulfillment_status": "UNFULFILLED" }, { "id": "6248299069730", "name": "#1028", "closed": false, "confirmed": true, "created_at": "2025-05-01T21:24:55Z", "total_price": 825.95, "cancelled_at": null, "processed_at": "2025-05-01T21:24:53Z", "currency_code": "INR", "return_status": "NO_RETURN", "financial_status": "PAID", "fulfillment_status": "UNFULFILLED" } ], "shopify.customer.lifetime_spent": 15856.85, "shopify.customer.last_order_date": "2025-06-12T16:02:40Z", "shopify.customer.lifetime_orders": 21, "shopify.customer.predicted_spend_tier": null, "shopify.customer.sms_marketing_consent": "SUBSCRIBED", "shopify.customer.email_marketing_consent": "SUBSCRIBED", "shopify.customer.last_order_repurchase_link": "https://sagepilot-test-account.myshopify.com/cart/49698626568482:1?storefront=true&utm_campaign=ai&utm_source=sagepilot-ai&utm_medium=whatsapp" }, "created_at": "2025-02-22T15:54:39.814426Z", "workspace_id": "ab1360ce-ef91-4397-8fbd-3ed77ef80e08", "company": null, "stage": "", "last_chat_id": "b18398f8-a00c-4c70-b463-8c681d5a64bf", "channel_id": "30cdb75b-14a2-40de-bc98-acf13610d714", "company_id": null, "external_id": null, "chats": 1, "blocked": false, "segments": [ "6990ac04-3cb7-4aa8-abe9-d10becb53a86", "198afd6e-3dc4-4f66-921b-100b8a9f407e" ], "whatsapp_optout": false, "tracker_id": "0fd87fac-1c57-4bcf-9e03-e0fdd8c079a8", "is_primary": false, "test_user": false, "email_optout": false, "profile_id": "66b246d1-d87f-4983-bf45-11caec0f545e", "shopify_customer_id": 8738967847202, "profile_updated_at": "2026-05-25T13:03:31.632136Z", "es_sync_status": "synced", "es_synced_at": "2026-05-13T20:47:57.219727Z", "es_sync_attempts": 0, "es_sync_error": null, "es_sync_locked_at": null, "es_index_version": 1, "channels": { "name": "Sagepilot Website", "kind": "web_widget" } } ], "total": 1 } ``` List customers with optional search, sorting, and pagination. ## Required access | Authentication | Scope | Dataset grant | | ------------------------------------------------------------ | ---------------- | ------------- | | [API client v2](/api-reference/authentication/api-client-v2) | `customers:read` | `customers` | ## Body A JSON body is required. Send `{}` when you are not filtering or sorting customers. Optional search text for customer lookup. Field used to sort customers. Sort direction. Use `asc` or `desc`. ## Query parameters Page number. Records per page. Maximum value is `100`. ## Response Customer records. Total matching customer count. # Get customer notes Source: https://docs.sagepilot.ai/api-reference/customers/notes POST /platform-api/v1/customers/notes List notes for a customer. ```json 200 theme={null} { "success": true, "notes": [ { "id": "54b9f80f-e4b4-47b5-9b41-132bc13e6f3f", "customer_id": "88d44689-1ce7-4033-8256-655a7cd8f90b", "note": "new note", "role": "agent", "agent_id": "818568b8-71ad-47e7-a73d-84a4cb5d6fd1", "created_at": "2026-05-29T17:26:57.482363Z", "updated_at": "2026-05-29T17:26:57.482363Z" } ] } ``` Get notes for a customer. ## Required access | Authentication | Scope | Dataset grant | | ------------------------------------------------------------ | ---------------- | ------------- | | [API client v2](/api-reference/authentication/api-client-v2) | `customers:read` | `customers` | ## Body Customer UUID to fetch notes for. Open a customer in the Customer module and use the last UUID in the URL: `/dashboard/[workspace_id]/customers/[customer_id]`. See [Resource IDs](/api-reference/authentication/api-client-v2#resource-ids). Include notes from linked contacts. ## Query parameters Page number. Records per page. Maximum value is `200`. ## Response Whether the request succeeded. Customer notes. # Get recent calls Source: https://docs.sagepilot.ai/api-reference/customers/recent-calls POST /platform-api/v1/customers/recent_calls List recent voice calls for a customer. ```json 200 theme={null} [ { "id": "56cd9f21-e1dd-4837-b031-b313f17fba6e", "created_at": "2026-01-26T11:26:02.895027Z", "workspace_id": "ab1360ce-ef91-4397-8fbd-3ed77ef80e08", "status": "missed", "customer_id": "88d44689-1ce7-4033-8256-655a7cd8f90b", "agent_id": null, "type": "inbound", "call_id": "56cd9f21-e1dd-4837-b031-b313f17fba6e", "from_number": "+918003213447", "to_number": "+912261891120", "answered_at": null, "ended_at": "2026-01-26T05:56:10.348382Z", "duration_seconds": 0, "mode": "human", "recordings": "{}", "channel_id": "53f55362-a793-4679-b9b4-40cbb899b8c0", "metadata": "{\"provider\": \"sip_trunk_audiosocket\", \"sip_headers\": {\"X-Sage-SIP-CallID\": \"3F4F776960918B1900@ngn.ttl.in\"}, \"termination_reason\": \"outside_business_hours\", \"outside_hours_recording_url\": \"https://sagepilot-outbound-attachments.s3.ap-south-1.amazonaws.com/workspaces/ab1360ce-ef91-4397-8fbd-3ed77ef80e08/voice_assets/outside_hours_787ccc04-1449-489b-83fc-d5f560b3cfcc\"}", "updated_at": "2026-01-26T11:26:11.540439Z", "pilot_id": null, "usage_stats": null, "hangup_reason": "failed", "dial_status": "failed", "ended_by": "system", "asterisk_ip": null, "asterisk_ari_port": 8088, "conference_id": null, "controller_task_id": null, "disposition": null, "transcription": null, "disposition_set_by": null, "disposition_set_at": null, "customer_journey_id": null, "journey_id": null, "campaign_id": null, "scheduled_at": null, "scheduled_by": null, "schedule_metadata": "{}", "participant_count": 0, "code": 551, "team_id": null, "summary": null, "sentiment": null, "source": "direct", "customer_name": "Vikram Singh Chundawat", "customer_phone": "+918003213447" } ] ``` Get recent voice calls for a customer in the authenticated workspace. ## Required access | Authentication | Scope | Dataset grant | | ------------------------------------------------------------ | ---------------- | ------------- | | [API client v2](/api-reference/authentication/api-client-v2) | `customers:read` | `customers` | ## Body Customer UUID to fetch calls for. Open a customer in the Customer module and use the last UUID in the URL: `/dashboard/[workspace_id]/customers/[customer_id]`. See [Resource IDs](/api-reference/authentication/api-client-v2#resource-ids). Optional workspace UUID. The authenticated workspace is used by default. Find the current workspace ID in **Settings > API Details > Channels**. See [Resource IDs](/api-reference/authentication/api-client-v2#resource-ids). Include calls from linked contacts. ## Query parameters Page number. Records per page. Maximum value is `200`. ## Response Recent voice call records for the customer. # Get recent chats Source: https://docs.sagepilot.ai/api-reference/customers/recent-chats POST /platform-api/v1/customers/recent_chats List recent ticket chats for a customer. ```json 200 theme={null} [ { "id": "c8beea3e-8046-4dff-835f-ac2e12238a4a", "workspace_id": "ab1360ce-ef91-4397-8fbd-3ed77ef80e08", "code": 1000, "created_at": "2026-02-07T08:33:06.697712Z", "status": "closed", "subject": "Initial greeting and introduction in a demo environment", "sub_status": null, "priority": "", "agent_id": null, "unread_status": false, "channel_kind": "playground", "message_content": "Hi — I’m here to help with QA and testing of the Sagepilot AI agent in this demo environment. I can run test scenarios, execute specific function calls, reproduce and diagnose errors, validate features or workflows, and fetch product/order data for testing.\n\nWhat would you like me to do right now? If you have a specific function, parameters, or a feature to validate, please share them and I’ll run the test.", "message_role": "pilot", "last_message_created_at": "2026-02-07T08:33:20.521000Z", "message_agent_id": null, "message_pilot_id": "8ecf3ded-7090-4997-9444-6c9f8c367f54", "customer_name": "Vikram Singh Chundawat" } ] ``` Get recent chats for a customer in the authenticated workspace. ## Required access | Authentication | Scope | Dataset grant | | ------------------------------------------------------------ | ---------------- | ------------- | | [API client v2](/api-reference/authentication/api-client-v2) | `customers:read` | `customers` | ## Body Customer UUID to fetch chats for. Open a customer in the Customer module and use the last UUID in the URL: `/dashboard/[workspace_id]/customers/[customer_id]`. See [Resource IDs](/api-reference/authentication/api-client-v2#resource-ids). Optional workspace UUID. The authenticated workspace is used by default. Find the current workspace ID in **Settings > API Details > Channels**. See [Resource IDs](/api-reference/authentication/api-client-v2#resource-ids). Include chats from linked contacts. ## Query parameters Page number. Records per page. Maximum value is `200`. ## Response Recent chat records for the customer. # Get social activity Source: https://docs.sagepilot.ai/api-reference/customers/social-activity POST /platform-api/v1/customers/social_activity List social comments and activity for a customer. ```json 200 theme={null} [ { "id": "d4926456-6ce4-4d26-8d92-a871d914c459", "comment_content": "Hii", "created_at": "2025-10-17T14:19:37.669529Z", "post_caption": null, "post_type": "FEED", "channel_kind": "instagram" } ] ``` Get social activity for a customer. ## Required access | Authentication | Scope | Dataset grant | | ------------------------------------------------------------ | ---------------- | ------------- | | [API client v2](/api-reference/authentication/api-client-v2) | `customers:read` | `customers` | ## Body Customer UUID to fetch social activity for. Open a customer in the Customer module and use the last UUID in the URL: `/dashboard/[workspace_id]/customers/[customer_id]`. See [Resource IDs](/api-reference/authentication/api-client-v2#resource-ids). Optional workspace UUID. The authenticated workspace is used by default. Find the current workspace ID in **Settings > API Details > Channels**. See [Resource IDs](/api-reference/authentication/api-client-v2#resource-ids). Include social activity from linked contacts. ## Query parameters Page number. Records per page. Maximum value is `200`. ## Response Social activity records for the customer. # Get ticket stats Source: https://docs.sagepilot.ai/api-reference/customers/tickets-stats POST /platform-api/v1/customers/tickets_stats Get ticket status counts for a customer. ```json 200 theme={null} { "closed": 92, "open": 23 } ``` Get ticket status counts for a customer in the authenticated workspace. ## Required access | Authentication | Scope | Dataset grant | | ------------------------------------------------------------ | ---------------- | ------------- | | [API client v2](/api-reference/authentication/api-client-v2) | `customers:read` | `customers` | ## Body Customer UUID to calculate ticket counts for. Open a customer in the Customer module and use the last UUID in the URL: `/dashboard/[workspace_id]/customers/[customer_id]`. See [Resource IDs](/api-reference/authentication/api-client-v2#resource-ids). Optional workspace UUID. The authenticated workspace is used by default. Find the current workspace ID in **Settings > API Details > Channels**. See [Resource IDs](/api-reference/authentication/api-client-v2#resource-ids). Include tickets from linked contacts. ## Response Count of tickets for each returned status key. # Send WhatsApp template Source: https://docs.sagepilot.ai/api-reference/messages/send-whatsapp-template POST https://api.sagepilot.ai/send-whatsapp-template Send an approved WhatsApp template message through the legacy template-send API. Use this endpoint to send an approved WhatsApp template from one of your connected WhatsApp channels to a customer. This endpoint uses the legacy template-send contract. You pass the template name and a Meta-style `parameters` component list. Marketing templates may require the recipient to start or reopen a WhatsApp conversation with your business before testing. Ask the test recipient to send a message such as `Hi` to your connected WhatsApp number first. ## Required access | Authentication | Required headers | | ---------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------- | | [Legacy workspace API key](/api-reference/authentication/legacy-workspace-api-key) | `Authorization: Bearer ` and `X-SP-Workspace-Id: `. | ## Headers Bearer token using the legacy workspace API key. Workspace ID for the request. This header is required so Sagepilot can resolve the workspace, channel, template, and customer record before sending the WhatsApp template. Send the workspace ID in the `X-SP-Workspace-Id` header. Do not send it as `workspace_id` inside the JSON body. ## Before you send In Sagepilot, go to **Settings > API Details**. Copy your workspace ID and generate or copy a legacy workspace API key. Use the `channel_id` for the WhatsApp number you want to send from. Each connected number has its own channel ID. Use the template name exactly as it appears in Sagepilot or Meta, for example `checkout_1`. Template names are case-sensitive. For each `{{1}}`, `{{2}}`, or media/button placeholder in the template, add the matching entry in `parameters`. Send `customer_phone` as digits only with the country code first, such as `918888888888` for India or `14155550123` for the United States. Confirm that `X-SP-Workspace-Id`, `customer_name`, `customer_phone`, `channel_id`, `template_name`, `parameter_type`, and `parameters` are present before sending the request. ## Body WhatsApp channel UUID from **Settings > API Details > Channels**. This decides which connected WhatsApp number sends the message. Destination phone number with country code at the start. Use digits only. Do not include `+`, spaces, brackets, or hyphens. Examples: `918888888888`, `14155550123`, `447700900123`. Must be `template`. Approved WhatsApp template name. Use the exact name from Sagepilot or Meta. Customer name to associate with the recipient. This is required for the legacy send flow because Sagepilot creates or updates the customer record before queueing the WhatsApp template. This does not automatically fill a template variable unless you also pass the same value in `parameters`. Must be `list`. This tells Sagepilot that `parameters` is already in Meta's positional component format. Meta-style template component payload. Supported component types include `body`, `header`, and `button`. For a body template such as `Hi {{1}}`, pass: ```json theme={null} [ { "type": "body", "parameters": [ { "type": "text", "text": "Ankita" } ] } ] ``` For an image header and one body variable, pass: ```json theme={null} [ { "type": "header", "parameters": [ { "type": "image", "image": { "link": "https://example.com/product-image.png" } } ] }, { "type": "body", "parameters": [ { "type": "text", "text": "Ankita" } ] } ] ``` ## Phone number format Always include the country code at the start of `customer_phone`. | Customer location | Correct | Incorrect | | ----------------- | -------------- | --------------------------------- | | India | `918888888888` | `8080365185`, `+91 8080365185` | | United States | `14155550123` | `4155550123`, `+1 (415) 555-0123` | | United Kingdom | `447700900123` | `07700900123`, `+44 7700 900123` | If the phone number is missing the country code, WhatsApp cannot reliably route the message. ## How template variables map WhatsApp templates use positional placeholders such as `{{1}}`, `{{2}}`, and `{{3}}`. The API does not use the placeholder names from your internal systems. It uses component order. ### Body text variables For this template body: ```text theme={null} Hi {{1}}, your order {{2}} is ready. ``` Send this body component: ```json theme={null} { "type": "body", "parameters": [ { "type": "text", "text": "Ankita" }, { "type": "text", "text": "#1001" } ] } ``` `Ankita` fills `{{1}}`. `#1001` fills `{{2}}`. ### Header media variables If the template has an image header, send: ```json theme={null} { "type": "header", "parameters": [ { "type": "image", "image": { "link": "https://example.com/product-image.png" } } ] } ``` For a document header, use `document` instead: ```json theme={null} { "type": "header", "parameters": [ { "type": "document", "document": { "link": "https://example.com/invoice.pdf" } } ] } ``` The media URL must be publicly accessible by WhatsApp. ### Dynamic URL buttons If the template has a URL button with a dynamic placeholder, add a `button` component. ```json theme={null} { "type": "button", "sub_type": "url", "index": 0, "parameters": [ { "type": "text", "text": "https%3A%2F%2Fstore.example.com%2Fcart%3Fid%3Dtest123" } ] } ``` Use `index: 0` for the first button, `index: 1` for the second button, and so on. For Sagepilot CTA buttons such as `https://app.sagepilot.ai/cta?redirect={{1}}`, pass only the encoded destination URL as the button text. Do not pass the full Sagepilot CTA wrapper URL. ## Complete example This example sends an abandoned checkout template with: * An image header. * One body variable for the customer name. * One dynamic URL button for the checkout link. ```bash theme={null} curl --request POST \ --url "https://api.sagepilot.ai/send-whatsapp-template" \ --header "Authorization: Bearer " \ --header "X-SP-Workspace-Id: " \ --header "Content-Type: application/json" \ --data '{ "channel_id": "", "customer_phone": "918888888888", "message_type": "template", "template_name": "checkout_1", "customer_name": "Ankita", "parameter_type": "list", "parameters": [ { "type": "header", "parameters": [ { "type": "image", "image": { "link": "https://example.png" } } ] }, { "type": "body", "parameters": [ { "type": "text", "text": "Ankita" } ] }, { "type": "button", "sub_type": "url", "index": 0, "parameters": [ { "type": "text", "text": "https%3A%2F%2Fstore.example.com%2Fcart%3Fid%3Dtest123" } ] } ] }' ``` ## OTP / authentication template example Use this shape for authentication templates that send a one-time password and include a Copy Code button. For example, the approved authentication template `otp_authentication` expects the OTP in the body and in the OTP button parameter. Use the same code in both places. ```bash theme={null} curl --request POST \ --url "https://api.sagepilot.ai/send-whatsapp-template" \ --header "Authorization: Bearer " \ --header "X-SP-Workspace-Id: " \ --header "Content-Type: application/json" \ --data '{ "channel_id": "", "customer_phone": "918888888888", "message_type": "template", "template_name": "otp_authentication", "customer_name": "Ankita", "parameter_type": "list", "parameters": [ { "type": "body", "parameters": [ { "type": "text", "text": "123456" } ] }, { "type": "button", "sub_type": "url", "index": 0, "parameters": [ { "type": "text", "text": "123456" } ] } ] }' ``` ## Body-only example Use this shape when the template has only body text placeholders and no header media or buttons. ```bash theme={null} curl --request POST \ --url "https://api.sagepilot.ai/send-whatsapp-template" \ --header "Authorization: Bearer " \ --header "X-SP-Workspace-Id: " \ --header "Content-Type: application/json" \ --data '{ "channel_id": "", "customer_phone": "918888888888", "message_type": "template", "template_name": "order_shipped_3291", "customer_name": "Anushka Pawar", "parameter_type": "list", "parameters": [ { "type": "body", "parameters": [ { "type": "text", "text": "Anushka" }, { "type": "text", "text": "#1234" }, { "type": "text", "text": "Rs. 100" }, { "type": "text", "text": "https://sagepilot.ai" } ] } ] }' ``` ## Successful response ```json 200 theme={null} { "message_id": "0fa3bc4b-50b0-446c-842f-cc7b36f57151", "success": true } ``` ## Common errors | Error | What it means | How to fix it | | ---------------------------------------------- | ---------------------------------------------------------------------------------------- | ------------------------------------------------------------------------- | | `Template not found` | The template name does not exist for the selected channel or workspace. | Confirm the exact `template_name`, `channel_id`, and `X-SP-Workspace-Id`. | | Missing workspace ID | The legacy route cannot resolve the workspace context. | Send `X-SP-Workspace-Id` as a request header, not as a body field. | | Missing customer name | The legacy route creates or updates a customer profile before queueing the template. | Send `customer_name` in the request body. | | Missing or invalid phone number | `customer_phone` is empty or not routable. | Pass digits only with country code at the start. | | Template variable mismatch | The template has placeholders that are missing from `parameters`, or the order is wrong. | Match each Meta component and placeholder in order. | | Media cannot be fetched | WhatsApp cannot access the header media URL. | Use a public HTTPS URL for image, video, or document media. | | Marketing template not received during testing | The recipient may not have an open WhatsApp conversation with the business. | Ask the recipient to message your business number first, then retry. | ## Meta template component reference The `parameters` array follows Meta's WhatsApp template component structure. Refer to Meta's WhatsApp template documentation when mapping body placeholders, media headers, and URL buttons: * [Meta WhatsApp message templates](https://developers.facebook.com/docs/whatsapp/business-management-api/message-templates) * [Meta Cloud API template messages](https://developers.facebook.com/documentation/business-messaging/whatsapp/templates/overview) # API reference Source: https://docs.sagepilot.ai/api-reference/overview Start here for supported Sagepilot API functions. Use this section for the API functions Sagepilot currently supports publicly. Start with authentication, then choose the workflow you are building. ## 1. Authenticate Every supported API path starts with the right credential model and regional host. Use API client v2 tokens for analytics, ticket exports, customer APIs, voice call exports, and new platform integrations. Use the legacy workspace key only for the existing WhatsApp template-send integration. ## 2. Send messages Use the currently supported message-send API for WhatsApp template delivery. Send a WhatsApp template message through the legacy template-send integration. ## 3. Query analytics Use analytics APIs to discover available datasets and run analytics queries. List available analytics datasets, metrics, dimensions, and filters. Run an analytics query with selected metrics, dimensions, filters, date ranges, and timezone. ## 4. Export ticket data Use ticket APIs when you need paginated support summaries or raw support data for spreadsheets, warehouse jobs, or offline reporting. Understand the ticket API surface and related authentication requirements. List paginated ticket summaries with filters, sorting, and cursor pagination. Download raw ticket rows as a streamed CSV attachment. ## 5. Work with support resources Use customer and call APIs when you need support context outside the ticket list. List, search, create, update, and retrieve customer context. Export voice call rows for reporting and operations workflows. ## Related developer surfaces These are not REST API endpoint pages, but they are part of the developer documentation set. Embed Sagepilot chat in iOS and Android apps. Connect MCP-compatible AI clients to Sagepilot workspace tools. POST JSON events from any app to identify customers and start journeys. Send customer messages from a channel you own and receive signed replies. # Tickets Source: https://docs.sagepilot.ai/api-reference/tickets Reference material for ticket export APIs. Use these endpoints to list, update, assign, and export ticket data from Sagepilot. List paginated ticket summaries with filters, sorting, and cursor pagination. Download raw ticket rows as a streamed CSV attachment. Assign a ticket to an agent, AI agent, team queue, or unassigned state. List paginated messages for a ticket chat. ## Related For credential setup, supported auth headers, and access requirements, see [API client v2](/api-reference/authentication/api-client-v2). # Assign ticket Source: https://docs.sagepilot.ai/api-reference/tickets/assign POST /platform-api/v1/chats/assign Assign a ticket to an agent, AI agent, team queue, or unassigned state. ```json 200 theme={null} { "success": true, "chat": { "id": "0e14f204-0f57-4a8d-a105-4e38982238b2", "workspace_id": "ab1360ce-ef91-4397-8fbd-3ed77ef80e08", "status": "open", "sub_status": null, "priority": "", "last_message_created_at": "2026-05-29T16:49:38+00:00", "pilot_id": null, "agent_id": null, "created_at": "2026-05-29T16:49:32.959247+00:00", "updated_at": "2026-05-29T16:55:57.987982+00:00", "assignee_type": "agent", "code": "1143", "unread_status": false, "subject": null, "summary": null, "channel_id": "47ef5902-f2c8-4b97-97d9-fb9b1ba585f5", "team_id": null, "queue_status": null, "closed_at": null, "customers": { "id": "12290791-dcd4-464f-a2a3-79dce74d01e4", "name": "anushka.pawar", "phone": null, "email": "anushka.pawar@sagepilot.ai", "company": "sagepilot.ai", "instagram_username": null }, "channels": { "id": "47ef5902-f2c8-4b97-97d9-fb9b1ba585f5", "name": "New Playground Channel", "kind": "playground" }, "chat_tags": [], "chat_custom_field_values": [] } } ``` Assign a ticket to a human agent, AI agent, team queue, or unassigned state. ## Required access | Authentication | Scope | Dataset grant | | ------------------------------------------------------------ | ---------------- | ------------- | | [API client v2](/api-reference/authentication/api-client-v2) | `tickets:assign` | `tickets` | ## Body Ticket chat UUID to assign. Use the `records[].id` value from [List tickets](/api-reference/tickets/list); do not pass the human-readable ticket `code`. Assignment type. Supported values are `agent`, `pilot`, `team`, and `unassigned`. Use `agent` for a human agent, `pilot` for an AI agent, `team` for a team queue, and `unassigned` to clear assignment. Assignee UUID. Required for `agent`, `pilot`, and `team` assignments. For `assignee_type: "pilot"`, use an AI agent ID. For `assignee_type: "agent"`, use a human agent ID. For `assignee_type: "team"`, use the team ID. Omit this field for `unassigned`. See [Resource IDs](/api-reference/authentication/api-client-v2#resource-ids). ## Response Whether the assignment succeeded. Updated ticket chat object. # Export tickets Source: https://docs.sagepilot.ai/api-reference/tickets/export POST /platform-api/v1/exports/tickets Download raw ticket rows as a streamed CSV attachment. Use the ticket export API to download raw ticket rows as a CSV file. ## Required access | Authentication | Scope | Dataset grant | | ------------------------------------------------------------ | ---------------- | ------------- | | [API client v2](/api-reference/authentication/api-client-v2) | `tickets.export` | `tickets.raw` | ## Body Export format. Currently only `csv` is supported. Optional list of columns to include. If omitted, Sagepilot exports the default ticket columns and appends workspace custom fields automatically. Built-in column key, or any label you want for a custom-field integration column. Optional CSV header label. Sagepilot custom field ID. Include this when exporting a workspace custom field. Same filter object used by analytics queries. Channel UUIDs to include in the export. Find channel IDs in **Settings > API Details > Channels**. See [Resource IDs](/api-reference/authentication/api-client-v2#resource-ids). Ticket statuses to include. Current constraint: at most one status per export request. Optional sort instructions. Field to sort by, such as `created_at`. Sort direction. Use `asc` or `desc`. Timezone used for formatted datetime columns. Maximum rows to export. The maximum supported value is `50000`. Optional date preset. Optional date field selector. Optional saved-view identifier. ## Built-in columns Common built-in column keys include: * `code` * `status` * `priority` * `created_at` * `updated_at` * `customer_name` * `customer_phone` * `customer_email` * `channel_id` * `tags` * `closed_at` * `agent_name` * `pilot_name` ## Response The response is a streamed CSV attachment. ## Notes * Use ticket export when you need row-level CSV data for spreadsheets, warehouse jobs, or offline analysis. * Use the analytics API when you need aggregates, ratios, trends, or grouped summaries. # List tickets Source: https://docs.sagepilot.ai/api-reference/tickets/list POST /platform-api/v1/chats/list List paginated ticket summaries with filters, sorting, and cursor pagination. ```json 200 theme={null} { "records": [ { "id": "9b7110ad-69b6-4476-a448-e99b4054ad9c", "workspace_id": "ab1360ce-ef91-4397-8fbd-3ed77ef80e08", "status": "closed", "sub_status": null, "priority": "", "last_message_created_at": "2026-05-28T18:03:11.500000+00:00", "pilot_id": "e3638c66-4100-4a13-b6ad-461f41847e46", "agent_id": null, "created_at": "2026-05-28T18:02:54.777168+00:00", "updated_at": "2026-05-29T06:32:37.091082+00:00", "chat_escalated_at": null, "assignee_type": "pilot", "code": "1141", "unread_status": true, "total_unread_messages": 1, "subject": null, "summary": null, "channel_id": "195a338d-2dfb-4653-9e4c-87e988efa818", "custom_info": null, "total_agent_messages": 0, "total_pilot_messages": 1, "total_user_messages": 1, "inferred_csat_score": null, "user_csat_score": null, "is_spam": false, "latest_entry_sort_column": "2026-05-28T18:03:11.500000+00:00", "substatus_expiry_time": null, "team_id": null, "team_name": null, "queue_status": null, "queued_at": null, "dequeued_at": null, "closed_at": "2026-05-29T06:32:37.091082+00:00", "chat_custom_fields": [], "first_agent_response_at": null, "chat_tags": [], "customers": { "id": "5307a68f-7188-4bbb-8e38-d96f52a096c3", "name": "Surender Singh", "company": "sagepilot.ai", "email": "surender.singh@sagepilot.ai", "phone": null, "created_at": "2026-02-15T22:41:15.158760+00:00", "instagram_username": null, "custom_fields": {} }, "channels": { "kind": "playground", "id": "195a338d-2dfb-4653-9e4c-87e988efa818", "name": "New Playground Channel", "config": { "is_sagepilot_provider": null, "provider": null } }, "pilots": { "name": "Jarvis", "ui": { "logo": "https://bummer.in/cdn/shop/files/mm_logo.png?crop=center&height=32&v=1759729936&width=32", "name": "Jarvis" } }, "agent_profile": null, "messages": { "content": "The result of that calculation is approximately 23,258,354,226.06. Is there anything else I can help you with today?", "role": "pilot", "created_at": "2026-05-28T18:03:11.500000+00:00" } } ], "cursor": "2026-05-28T18:03:11.500000+00:00_9b7110ad-69b6-4476-a448-e99b4054ad9c", "total": null } ``` Use this endpoint to list ticket summaries for a workspace. The response is paginated and includes the latest message, customer, channel, tag, custom-field, assignee, and queue summary fields used by the Sagepilot ticket list. If you omit the request body, Sagepilot treats it as `{}` and applies the default pagination and sorting behavior. ## Required access | Authentication | Scope | Dataset grant | | ------------------------------------------------------------ | -------------- | ------------- | | [API client v2](/api-reference/authentication/api-client-v2) | `tickets:read` | `tickets` | ## Body Number of records to return. The minimum is `1` and the maximum is `100`. Cursor from the previous response. Pass this value to fetch the next page. Whether to include the total matching record count. Set to `false` for faster cursor syncs when you only need `records` and `cursor`. Ticket status filter. Use `all` or omit the field to avoid status filtering. Supported view-style values include `open`, `reopened`, `investigating`, `snoozed`, `waiting_on_customer`, and `closed`. Comma-separated priorities to include, such as `high,medium`. Comma-separated assignee types, such as `agent` or `pilot`. Comma-separated human agent or AI agent UUIDs. Use `current` where the API request is associated with a user context. See [Resource IDs](/api-reference/authentication/api-client-v2#resource-ids). Comma-separated channel kinds, such as `email,whatsapp`. Comma-separated channel UUIDs. Find channel IDs in **Settings > API Details > Channels**. See [Resource IDs](/api-reference/authentication/api-client-v2#resource-ids). Comma-separated boolean values, such as `true` or `false`. Built-in or custom view code. Built-in values include `needs_attention`, `awaiting_reply`, `needs_first_human_reply`, and `mentions`. Comma-separated tag IDs to include. Comma-separated tag IDs to exclude. Comma-separated queue statuses. Comma-separated team UUIDs. See [Resource IDs](/api-reference/authentication/api-client-v2#resource-ids). Use `true` to return tickets without an assigned agent or AI agent. Comma-separated inferred CSAT score values. Comma-separated user-submitted CSAT score values. Object mapping custom field IDs to filter values. For multiple accepted values on one field, pass a comma-separated string. Custom field value filter for the given Sagepilot custom field ID. Date preset or explicit range. Supported values include `today`, `yesterday`, `last_7_days`, `last_30_days`, `start|end`, `start|`, or a start-only ISO timestamp. Date field used with `date_range`. Supported values are `created_at`, `last_message_created_at`, `chat_escalated_at`, `closed_at`, and `updated_at`. Field used for ordering and cursor generation. Supported values are `created_at`, `last_message_created_at`, `chat_escalated_at`, `closed_at`, and `updated_at`. Sort direction. Use `asc` or `desc`. ## Response Paginated ticket summaries. Ticket ID. Human-readable ticket code. Ticket status. Ticket sub-status, when present. Ticket priority. Ticket subject. Ticket summary. Current assignee type, such as `agent` or `pilot`. Assigned human agent ID, when applicable. Assigned AI agent ID, when applicable. Assigned team ID, when applicable. Assigned team name, when available. Channel ID for the ticket. Whether the ticket has unread messages. Number of unread messages on the ticket. Number of human agent messages on the ticket. Number of AI agent messages on the ticket. Number of customer messages on the ticket. Queue status, when the ticket is queued. Ticket creation timestamp. Ticket update timestamp. Timestamp of the latest message attached to the ticket. Ticket close timestamp, when closed. Customer summary for the ticket. Channel summary for the ticket. Latest message summary for the ticket. AI agent summary, when assigned. Human agent profile summary, when assigned. Tags attached to the ticket. Custom field values attached to the ticket. Cursor for the next page. This is `null` when there is no next page. Total matching records when `include_total` is `true`; otherwise `null`. ## Notes * Use API client v2 credentials with `tickets:read` scope and the `tickets` dataset grant. See [API client v2](/api-reference/authentication/api-client-v2). * Multi-value filters are comma-separated strings. * Use `assignee` with `assignee_type` to filter by human agents or AI agents. # Mark ticket read Source: https://docs.sagepilot.ai/api-reference/tickets/mark-read POST /platform-api/v1/chats/mark_read Mark a ticket chat as read. Mark a ticket chat as read. The update runs in the background and the endpoint returns immediately. ## Required access | Authentication | Scope | Dataset grant | | ------------------------------------------------------------ | ----- | ------------- | | [API client v2](/api-reference/authentication/api-client-v2) | - | `tickets` | ## Body Ticket chat UUID to mark as read. Use the `records[].id` value from [List tickets](/api-reference/tickets/list); do not pass the human-readable ticket `code`. ## Response `true` when the mark-read task has been queued. # List ticket messages Source: https://docs.sagepilot.ai/api-reference/tickets/messages-list POST /platform-api/v1/messages/list List paginated messages for a ticket chat. ```json 200 theme={null} { "records": [ { "id": "8a7f57ee-0a17-4763-a8a7-cb591adabdb2", "role": "system", "content": "Conversation automatically Closed due to inactivity.", "content_html": "Conversation automatically Closed due to inactivity.", "created_at": "2026-05-29T06:32:37.091082Z", "attachments": [], "meta_data": null, "status": null, "error_code": null, "error_details": null, "chat_id": "9b7110ad-69b6-4476-a448-e99b4054ad9c", "is_feedback_message": false, "type": "message", "agent_id": null, "pilot_id": null, "links": [], "context_message": null, "outbound_context_message": null }, { "id": "209503e7-2877-40ed-89a9-4694e31d4ab4", "role": "pilot", "content": "The result of that calculation is approximately 23,258,354,226.06. Is there anything else I can help you with today?", "content_html": "The result of that calculation is approximately 23,258,354,226.06. Is there anything else I can help you with today?", "created_at": "2026-05-28T18:03:11.500000Z", "attachments": [], "meta_data": null, "status": null, "error_code": null, "error_details": null, "chat_id": "9b7110ad-69b6-4476-a448-e99b4054ad9c", "is_feedback_message": false, "type": "message", "agent_id": null, "pilot_id": "e3638c66-4100-4a13-b6ad-461f41847e46", "links": [], "context_message": null, "outbound_context_message": null }, { "id": "740d0262-7f27-46e0-906a-e39e674d9aa7", "role": "user", "content": "what is 1265284626 * 435247 / 23678", "content_html": "what is 1265284626 * 435247 / 23678", "created_at": "2026-05-28T18:02:54.847000Z", "attachments": [], "meta_data": null, "status": null, "error_code": null, "error_details": null, "chat_id": "9b7110ad-69b6-4476-a448-e99b4054ad9c", "is_feedback_message": false, "type": "message", "agent_id": null, "pilot_id": null, "links": [], "context_message": null, "outbound_context_message": null }, { "id": "112404ed-e6bc-41c9-b429-2ecf1f64794f", "role": "system", "content": "Surender Singh started a conversation via Playground", "content_html": "Surender Singh started a conversation via Playground", "created_at": "2026-05-28T18:02:54.814000Z", "attachments": [], "meta_data": null, "status": null, "error_code": null, "error_details": null, "chat_id": "9b7110ad-69b6-4476-a448-e99b4054ad9c", "is_feedback_message": false, "type": "message", "agent_id": null, "pilot_id": null, "links": [], "context_message": null, "outbound_context_message": null } ], "cursor": null } ``` List paginated messages for a ticket chat. ## Required access | Authentication | Scope | Dataset grant | | ------------------------------------------------------------ | --------------------------------------------------------------------------------------- | ------------- | | [API client v2](/api-reference/authentication/api-client-v2) | Any ticket scope: `tickets:read`, `tickets:write`, `tickets:assign`, or `tickets:reply` | `messages` | ## Body Ticket chat UUID to list messages for. Use the `records[].id` value from [List tickets](/api-reference/tickets/list); do not pass the human-readable ticket `code`. Cursor from the previous response. Number of messages to return. The minimum is `1` and the maximum is `100`. Channel kind for channel-specific message shaping, such as `whatsapp`. Whether to return platform-facing messages. ## Response Message records. Message ID. Message role. Message content. Message creation timestamp. Cursor for the next page. This is `null` when there is no next page. # Update ticket status Source: https://docs.sagepilot.ai/api-reference/tickets/status POST /platform-api/v1/chats/status Update a ticket status and optional sub-status. ```json 200 theme={null} { "success": true, "chat": { "id": "0e14f204-0f57-4a8d-a105-4e38982238b2", "workspace_id": "ab1360ce-ef91-4397-8fbd-3ed77ef80e08", "status": "closed", "sub_status": null, "priority": "", "last_message_created_at": "2026-05-29T16:49:38+00:00", "pilot_id": null, "agent_id": "912035ba-27d1-4e1c-9ee3-5414a72465cb", "created_at": "2026-05-29T16:49:32.959247+00:00", "updated_at": "2026-05-29T17:04:28.581178+00:00", "assignee_type": "agent", "code": "1143", "unread_status": false, "subject": null, "summary": null, "channel_id": "47ef5902-f2c8-4b97-97d9-fb9b1ba585f5", "team_id": null, "queue_status": null, "closed_at": "2026-05-29T17:04:28.560195+00:00", "customers": { "id": "12290791-dcd4-464f-a2a3-79dce74d01e4", "name": "anushka.pawar", "phone": null, "email": "anushka.pawar@sagepilot.ai", "company": "sagepilot.ai", "instagram_username": null }, "channels": { "id": "47ef5902-f2c8-4b97-97d9-fb9b1ba585f5", "name": "New Playground Channel", "kind": "playground" }, "chat_tags": [], "chat_custom_field_values": [] } } ``` Update a ticket status and optional sub-status. ## Required access | Authentication | Scope | Dataset grant | | ------------------------------------------------------------ | --------------- | ------------- | | [API client v2](/api-reference/authentication/api-client-v2) | `tickets:write` | `tickets` | ## Body Ticket chat UUID to update. Use the `records[].id` value from [List tickets](/api-reference/tickets/list); do not pass the human-readable ticket `code`. New ticket status. For example, `open`, `closed`, or `reopened`. Optional ticket sub-status. For example, `investigating`, `awaiting_reply`, or `snoozed`. Optional ISO timestamp for when the sub-status expires. For example, `2026-05-30T10:00:00.000Z`. ## Response Whether the status update succeeded. Updated ticket chat object. # Custom webhook Source: https://docs.sagepilot.ai/api-reference/webhooks/custom-webhooks Receive JSON events from any app, identify the customer, and start Sagepilot journeys. Custom webhooks let you POST JSON from a system Sagepilot does not integrate with natively. Sagepilot uses the payload to find or create a customer, stores the event, and can start or signal journeys. This is an inbound webhook. Sagepilot does not send these events to you. For Sagepilot-to-your-app delivery, see [Event webhooks](/settings/event-webhooks). For a support messaging channel you own, see [Agent API](/api-reference/agent-api/overview). ## Before you start * A Sagepilot workspace and permission to manage integrations. * A JSON object your system can POST over HTTPS. The payload must include an email, a phone number, or both, unless you enable push registration. * The sample payload you configure must match the live payload shape. Nested objects become journey variables; arrays stay as single values. Image placeholder: add a screenshot of **Settings → Integrations** with the **Custom Webhook** card, and the **Configure** tab after the integration is connected. ## How it works 1. You configure **Custom Webhook** once for the workspace, then add a named trigger. 2. You paste a sample JSON payload and map which fields are the customer's name, email, and phone. 3. Sagepilot gives you a unique POST URL for that trigger. 4. Each accepted POST is queued. Sagepilot then resolves the customer, records the event, and starts live journeys whose trigger is that event name. A `200` response means Sagepilot accepted the request into the queue. It does not mean a customer was found or a journey started. ## Configure the integration In Sagepilot, go to **Settings → Integrations**. Custom Webhook is listed under **Webhooks & Ops**. Open **Custom Webhook** and click **Configure Custom Webhook**. You only do this once per workspace. After setup, the page has **Analytics** and **Configure** tabs. Use **Configure** to add and manage triggers. ## Add a trigger Click **Add trigger** and complete the three steps. Trigger names must be unique in the workspace. Sagepilot stores them as lowercase with spaces converted to underscores (`Lead Created` becomes `lead_created`). Enter the trigger name and paste one JSON object exactly as your system will send it. The body must be an object, not an array. Map payload fields to **Name** (optional), **Email**, and **Phone**. Map **Email**, **Phone**, or both, unless you enable [push registration](#register-android-push-tokens). Review the fields Sagepilot will create as journey variables. Mapped contact fields become customer fields instead of extra variables. Adjust the format when Sagepilot infers it incorrectly, then click **Create trigger**. After create, copy the **Webhook URL** and the sample `curl`. You can also copy both later from the trigger page. Image placeholder: add screenshots of the Add webhook trigger dialog (Payload, Identity, Variables) and the **Webhook ready** confirmation with the URL and sample curl. ## Send events Copy the URL from the trigger page. The path is: ```text theme={null} https://api.sagepilot.ai/webhooks/custom/{trigger_name}/{workspace_id}/{integration_id} ``` Treat the URL as a secret. Sagepilot authenticates the request with the workspace ID and integration ID in the path. There is no signature header or API key. Always POST `application/json`. Do not send form-encoded bodies. ```bash theme={null} curl -X POST \ 'https://api.sagepilot.ai/webhooks/custom/payment_failed/WORKSPACE_ID/INTEGRATION_ID' \ -H 'Content-Type: application/json' \ --data-raw '{ "email": "priya@example.com", "phone": "+919876543210", "name": "Priya Sharma", "order": { "id": "ORD-4821", "status": "payment_failed", "amount": 2499 } }' ``` Use the sample curl from the dashboard so the URL and payload match the trigger you created. ### Request rules | Rule | Behavior | | ------------------ | ------------------------------------------------------------------------------------------------ | | Method | `POST` only. | | Body | A JSON object. Arrays at the root are rejected. | | Nested objects | Flattened with dots. `order.id` in a `payment_failed` trigger becomes `payment_failed.order.id`. | | Arrays and scalars | Kept as leaf values. Sagepilot does not expand array items into separate fields. | | Idempotency | None. Each POST gets a new event ID. Retrying the same payload can start another journey. | ### HTTP responses | Status | When | | ------ | ------------------------------------------------------------------------------------ | | `200` | Sagepilot queued the webhook. Body: `{"message": "Webhook received and processed"}`. | | `400` | The body was not valid JSON or could not be decoded. | | `500` | Sagepilot could not queue the event. Retry later. | ## Identity mapping Sagepilot finds or creates the customer from the mapped email and phone. Phone values are normalized before lookup. Name is optional and updates the customer profile when present. If the live payload has neither a mapped email nor a mapped phone, Sagepilot does not record a customer event and does not start a journey. The HTTP response can still be `200` because acceptance happens before customer resolution. ## Journey variables Unmapped payload fields become journey variables with source `custom`. In templates, decisions, and message actions they appear as: ```text theme={null} {{ custom.payment_failed.order.id }} ``` Mapped identity fields use the Sagepilot customer variables instead: ```text theme={null} {{ sagepilot.customer.email }} {{ sagepilot.customer.phone }} {{ sagepilot.customer.name }} ``` | Format | Typical value | Decision operators | | -------------- | -------------------------------- | ------------------------------------- | | Text | Strings and other non-URL values | `eq`, `neq` | | Number | JSON numbers | `eq`, `neq`, `gt`, `gte`, `lt`, `lte` | | URL | `http://` or `https://` links | `eq`, `neq` | | Image (URL) | Image file URLs | Use in templates as media | | Document (URL) | Document file URLs | Use in templates as media | | Video (URL) | Video file URLs | Use in templates as media | Custom webhook variables are available in journey decisions and templates. They are not available as native segment attributes. ## Use in journeys 1. Publish at least one trigger. 2. Open **Engage → Journeys** and create a journey. 3. In the trigger node, choose the event under **Custom**. The event name matches the trigger name (`payment_failed`). 4. Use the payload variables in conditions, templates, and actions. A later POST of the same trigger can start another live journey for that customer. It can also satisfy a **wait until event** step, or stop a running journey if you listed the event as a termination event. Image placeholder: add a screenshot of the journey trigger picker with the **Custom** source and a custom webhook event selected. ## Register Android push tokens Optional. On the Identity step, enable **Push registration** to store Android FCM tokens from the payload. * Select a [Push Notifications](/integrations/channels/push-notifications) channel that has Android / FCM configured. * Map the FCM token field. This is required when push registration is on. * Optionally map an external user ID field. When push registration is enabled, email and phone are optional. Sagepilot still records the token. It attaches the token to the customer when identity is present, and can attach it later when a later event resolves the same token. Push registration from custom webhooks is Android FCM only. ## Monitor events On the Custom Webhook home page: * **Analytics** shows volume over 24 hours, 7 days, or 30 days, counts by event type, and payload history. * **Configure** lists triggers. Open a trigger for its webhook URL, identity mapping, sample payload, event trend, and recent events. Recent events and analytics only include payloads that resolved a customer. Token-only posts that never include email or phone do not appear there. ## Update a trigger Open the trigger, edit the sample payload, identity mapping, variable formats, or push registration, then click **Save changes**. * New fields become new journey variables. * Fields you remove are deleted as journey variables. Journeys that still reference those variables will not resolve them. * You cannot rename a trigger. Create a new trigger if you need a new URL path. ## Troubleshooting | Issue | What to check | | ---------------------------------------- | ------------------------------------------------------------------------------------------------------- | | `400` from the webhook | Send `Content-Type: application/json` and a JSON object, not an array or form body. | | `200` but no journey and no recent event | The payload did not include the mapped email or phone, or those fields were empty. | | Trigger not in the journey picker | Confirm the trigger was created, then refresh the journey builder. | | Variable missing in a template | The live payload path must match the sample. Nested keys are dotted; arrays are not exploded. | | Duplicate journeys | Each POST is a new event. Deduplicate in your sender, or turn off concurrent traversals on the journey. | | Push token not stored | Enable push registration, map the token field, and select an Android FCM push channel. | ## Related Send Sagepilot events outbound to your HTTPS endpoint. Send customer messages from a channel you own and receive replies. Create the Android FCM channel used by webhook token registration. Start automations from custom webhook triggers. # Webhooks Source: https://docs.sagepilot.ai/api-reference/webhooks/overview Receive events from external platforms in Sagepilot or send Sagepilot events to a client-owned endpoint. Webhooks allow Sagepilot and external systems to exchange events automatically. The webhook you need depends on which system produces the event and which system receives it. | Requirement | Webhook to use | Direction | | --------------------------------------------------------------------- | -------------------------- | ------------------------------ | | Receive events from a platform with a native Sagepilot integration | Native integration webhook | Native integration → Sagepilot | | Receive events from a platform without a native Sagepilot integration | Custom webhook | Client system → Sagepilot | | Send Sagepilot events to a client-owned system | Event webhook | Sagepilot → Client system | ## Native integration webhooks Use a native integration webhook when Sagepilot already supports the external platform. After you install the integration, Sagepilot generates a webhook URL for that workspace and integration. Add this URL to the provider's webhook settings and select the events the provider should send. Sagepilot processes these events and makes supported events available for analytics and journey automation. For example, after installing ClickPost, open **Settings → Integrations → ClickPost → Webhook**, copy the generated URL, and add it in ClickPost to receive events such as shipment picked up, in transit, out for delivery, delivered, failed delivery, delayed, return-to-origin, return, exchange, and refund updates. ## When to use a custom webhook Use a Custom Webhook when a client needs to send events from a system that does not have a native Sagepilot integration. Common examples include: * A proprietary order-management system sending `order_created` * A payment system sending `payment_failed` * A CRM sending `lead_created` * A client backend sending subscription, appointment, or account events For each event, create a custom trigger using a representative JSON payload. Sagepilot uses the payload to identify the customer, define journey variables, and generate a unique webhook URL. After creating the Custom Webhook integration, go to **Settings → Integrations → Custom Webhook → Configure**. Open the required trigger to find its **Webhook URL** and sample `curl` request. The client system must send an HTTP `POST` request containing a JSON object to this URL whenever the event occurs. Sagepilot can use the event to start a journey, continue a journey waiting for that event, or stop a journey configured with it as a termination event. Create a trigger, map customer identity fields, and send JSON events to Sagepilot. ## Send Sagepilot events to a client Use an Event Webhook when a client wants to receive events produced by Sagepilot. Go to **Settings → Event Webhooks** and click **Add webhook**. Select **Own backend**, enter the client's destination URL, choose the applicable channels, and select the events Sagepilot should deliver. Available Sagepilot events include: * WhatsApp message sent, delivered, read, and failed * New customer message * New support-agent message * Conversation status changed * Conversation assignee changed Sagepilot sends the selected events to the client's endpoint as signed HTTP requests. The receiving service should verify the Sagepilot signature before processing each payload. Send Sagepilot message, delivery, and conversation events to a client-owned endpoint. ## Choose the correct webhook Use a native integration webhook when Sagepilot supports the provider. Use a Custom Webhook when an unsupported system needs to send events into Sagepilot. Use an Event Webhook when Sagepilot needs to send its events to a client. # Authentication Source: https://docs.sagepilot.ai/mcp/authentication Authenticate to Sagepilot MCP with hosted OAuth or API client tokens. Sagepilot MCP supports two authentication modes. | Mode | Use when | Credential | | ------------ | ------------------------------------------------------------------------------------ | -------------------------------------------------------- | | Hosted OAuth | The client can open a Sagepilot browser sign-in flow. | No token to copy. The user signs in and approves access. | | API token | The client cannot complete OAuth, or you are running private server-side automation. | `Authorization: Bearer sp_...` | For ChatGPT, Claude, Codex, and similar hosted clients, start with [Hosted connectors](/mcp/hosted-connectors). ## API token authentication Use this only when hosted OAuth is not available. Sign in to the Sagepilot workspace you want to connect. Open **Settings -> API Details -> API client v2**. Create a new API client with the access your MCP client needs, then copy the `sp_...` token. It is shown once. Pass the token as an Authorization header. ```text theme={null} Authorization: Bearer sp_your_token ``` Some MCP clients also support `X-API-Key: sp_your_token`. Prefer the `Authorization` header unless your client requires a separate API-key header field. Treat `sp_...` API client tokens as secrets. Do not commit them to source control or expose them in client-side code. ## Analytics and product access Your connection needs the scope and dataset for each tool it calls. `get_connection_info` returns the resolved workspace, permissions, and dataset grants so you can check the connection before requesting a report. | Task | Scope | Dataset | | ------------------------------------------------------------------------------------------ | ----------------- | --------------------------------------------------------------------------- | | Attributed revenue and estimated ROAS through `query_analytics` or `get_journey_analytics` | `analytics.query` | `marketing_attribution` | | Template-category delivery through `query_analytics` | `analytics.query` | `outbound_messages` | | Campaign/journey ID lookup through `search_analytics_entities` | `analytics.query` | The tool's selected `marketing_attribution` or `outbound_messages` dataset. | | List journeys or inspect a journey flow | `journeys:read` | `journeys` | | List campaigns or inspect a lifetime campaign overview | `campaigns:read` | `campaigns` | For API-client analytics access, select `analytics.query` and the required analytics datasets when configuring your client. Analytics grants cover reports and name lookup; they do not include journey flows or campaign configuration. Use [Hosted OAuth](/mcp/hosted-connectors) for product tools when the API-client setup does not offer the corresponding scopes, subject to your workspace role and permissions. If an existing OAuth connection lacks a required capability, reconnect and approve access again, then check `get_connection_info`. Reconnecting does not expand your Sagepilot role. If access still fails, ask your workspace administrator to review your permissions. See [Journey and campaign tools](/mcp/journeys-campaigns) for examples and [Find entity IDs](/api-reference/analytics/members) for the equivalent REST lookup. # Client setup Source: https://docs.sagepilot.ai/mcp/clients Configure Sagepilot MCP in MCP-compatible clients. ## Hosted OAuth For ChatGPT, Claude, and other hosted MCP clients, paste the MCP URL from **Settings -> API Details -> MCP connector** and complete the Sagepilot sign-in flow. See [Hosted connectors](/mcp/hosted-connectors) for the full OAuth flow. ### Codex CLI ```bash theme={null} SAGEPILOT_MCP_URL="https://app.sagepilot.ai/mcp" codex mcp add sagepilot \ --url "$SAGEPILOT_MCP_URL" \ --oauth-resource "$SAGEPILOT_MCP_URL" codex mcp login sagepilot ``` ## API-token MCP configuration Use this in any MCP client that supports remote Streamable HTTP servers and custom headers but cannot complete browser-based OAuth: ```json theme={null} { "mcpServers": { "sagepilot": { "url": "https://app.sagepilot.ai/mcp", "headers": { "Authorization": "Bearer sp_your_token" } } } } ``` For EU-hosted workspaces, replace the URL with `https://eu.sagepilot.ai/mcp`. ## Claude Code If you are using an API client token in Claude Code: ```bash theme={null} claude mcp add --transport http sagepilot https://app.sagepilot.ai/mcp \ --header "Authorization: Bearer sp_your_token" ``` Then run `/mcp` in Claude Code and confirm Sagepilot is connected. ## Cursor Add this to `~/.cursor/mcp.json`: ```json theme={null} { "mcpServers": { "sagepilot": { "url": "https://app.sagepilot.ai/mcp", "headers": { "Authorization": "Bearer sp_your_token" } } } } ``` Then open **Cursor Settings -> Tools & MCP** and enable or refresh Sagepilot. # Hosted connectors Source: https://docs.sagepilot.ai/mcp/hosted-connectors Connect ChatGPT, Claude, CLI clients, and custom MCP hosts with Sagepilot OAuth. Hosted OAuth is the recommended setup for ChatGPT, Claude, Codex, and any MCP host that can open a browser sign-in flow. ## Prerequisites * You have access to the Sagepilot workspace you want to connect. * The AI host supports remote Streamable HTTP MCP servers. * The AI host can complete browser-based OAuth sign-in. ## Connect In Sagepilot, open **Settings -> API Details -> MCP connector** and copy the MCP URL. In ChatGPT, Claude, Codex, or another MCP host, add a custom MCP connector and paste the MCP URL. The host redirects you to Sagepilot. Sign in and choose the workspace you want to connect. Review the workspace and available capabilities, then click **Allow access**. For the US app, the MCP URL is: ```text theme={null} https://app.sagepilot.ai/mcp ``` For EU-hosted workspaces, use `https://eu.sagepilot.ai/mcp`. ## What to expect * You do not need an API key, client ID, or client secret for the standard hosted connector. * Some hosts may display the full Sagepilot tool list. * Sagepilot still checks the connected user's role before a tool can read or change data. ## Troubleshooting | Issue | What to do | | --------------------------------------------------------------------- | ------------------------------------------------------------------------------- | | The host says OAuth or browser sign-in is unsupported. | Use [API token authentication](/mcp/authentication#api-token-authentication). | | The host asks for an API token while you are setting up hosted OAuth. | Switch to the OAuth/browser sign-in option, or use the API-token setup instead. | | The connector is connected but a tool returns an access error. | Check the connected user's Sagepilot role and workspace permissions. | # Journey and campaign tools Source: https://docs.sagepilot.ai/mcp/journeys-campaigns Inspect journeys, campaigns, and their performance with Sagepilot MCP. Use Sagepilot MCP to find journeys and campaigns, inspect their configuration, and query attributed revenue, estimated ROAS, and delivery rates for a selected period. All tools on this page require `workspace_id`. Call `get_connection_info` first and use the workspace ID it returns. Replace the example workspace and entity UUIDs with your own. ## Choose a tool | Task | Tool | Required scope and dataset | | ------------------------------------------------------------------- | -------------------------------- | ----------------------------------------------------- | | Find a campaign or journey ID by name | `search_analytics_entities` | `analytics.query` and the selected analytics dataset. | | Report revenue and estimated ROAS for one journey | `get_journey_analytics` | `analytics.query` and `marketing_attribution`. | | Report on selected campaigns/journeys or template-category delivery | `query_analytics` | `analytics.query` and the queried dataset. | | List journeys or inspect a journey flow | `list_journeys`, `get_journey` | `journeys:read` and `journeys`. | | List campaigns or inspect a campaign's lifetime overview | `list_campaigns`, `get_campaign` | `campaigns:read` and `campaigns`. | Analytics access supports reports and name lookup. Journey/campaign product access is separate. See [Authentication](/mcp/authentication#analytics-and-product-access) for connection permissions. ## search\_analytics\_entities Find IDs for your analytics filters without fetching journey flows or campaign configuration. The response contains `dimension` and a `members` array with `value` (ID) and `label` (name), including entities with no activity in the reporting period. | Parameter | Description | | ----------- | --------------------------------------------------------------------------------------------------- | | `dimension` | `campaign_id` or `journey_id`. Required. | | `dataset` | `marketing_attribution` (default) or `outbound_messages`. Use a dataset your connection can access. | | `search` | Optional case-insensitive name search. Omit to list names alphabetically. | | `limit` | 1–50 results. Defaults to 20; no cursor. Narrow the search if needed. | Example arguments for `search_analytics_entities`: ```json theme={null} { "workspace_id": "11111111-1111-4111-8111-111111111111", "dimension": "journey_id", "dataset": "marketing_attribution", "search": "Welcome", "limit": 20 } ``` Use a returned `value` as `journey_id` or in `filters.journey_ids`. Campaign results work the same way with `campaign_id` and `filters.campaign_ids`. The equivalent REST endpoint is [Find entity IDs](/api-reference/analytics/members). ## get\_journey\_analytics Get attributed revenue and estimated ROAS for one journey over a **required reporting period**. The tool returns the same `dataset`, `rows`, `summary`, and `meta` fields as the Analytics API, with `attribution_revenue` and `estimated_roas` in each matching row. | Parameter | Description | | -------------------------- | ----------------------------------------------------------------------------------------------------- | | `journey_id` | Journey UUID. Required. | | `start_date`, `end_date` | Reporting-period timestamps. Both required. Use ISO 8601 with an explicit timezone offset. | | `timezone` | IANA timezone for grouping. Defaults to `UTC`. | | `grain` | `day`, `week`, or `month`. Defaults to `day`. | | `attribution_type` | Optional `delivery`, `read`, or `clicked` override. Omit to use workspace settings. | | `attribution_window_hours` | Optional attribution lookback override, 1–8760 hours. Omit to use workspace settings. | | `limit` | Page size, 1–1000. Defaults to 1000. | | `cursor` | `meta.cursor` from the previous response. Repeat the same arguments with this value until it is null. | Example arguments for `get_journey_analytics`: ```json theme={null} { "workspace_id": "11111111-1111-4111-8111-111111111111", "journey_id": "33333333-3333-4333-8333-333333333333", "start_date": "2026-09-01T00:00:00Z", "end_date": "2026-09-07T23:59:59.999Z", "timezone": "UTC", "grain": "day", "limit": 100 } ``` The attribution lookback is separate from the reporting period. `meta.applied_filters` reports the effective workspace settings or overrides. For currency handling, reporting boundaries, and the estimated ROAS formula, see [Journey and campaign analytics](/api-reference/analytics/journeys-campaigns). ## query\_analytics Use `query_analytics` with `marketing_attribution` for date-scoped campaign reports or to compare several journeys and campaigns. Use `outbound_messages` for delivery rates by template category. Call `get_analytics_catalog` first to discover supported query shapes. Example arguments for campaign attribution and estimated ROAS: ```json theme={null} { "workspace_id": "11111111-1111-4111-8111-111111111111", "dataset": "marketing_attribution", "measures": ["attribution_revenue", "estimated_roas"], "dimensions": ["time_bucket", "entity_type", "entity_id"], "filters": { "campaign_ids": ["22222222-2222-4222-8222-222222222222"] }, "time": { "start": "2026-09-01T00:00:00Z", "end": "2026-09-07T23:59:59.999Z", "timezone": "UTC", "grain": "day" }, "result_format": "rows", "limit": 100 } ``` For template-category delivery, use the request shape in [Delivery by template category](/api-reference/analytics/journeys-campaigns#delivery-by-template-category) and add `workspace_id` to the MCP arguments. That page defines the supported dimensions, filters, rates, and category semantics. Marketing attribution supports cursor pagination; category delivery does not. See [Analytics query](/api-reference/analytics/query) for the shared request and response contract. ## list\_journeys List journey summaries with trigger metadata, per-journey message stats, and status counts. The response also includes `attribution_settings` and `attribution_by_journey_id`. | Parameter | Description | | ------------------------ | ------------------------------------------------------------------------------------------------------------ | | `status` | `all`, `live`, `draft`, `paused`, or `archived`. Defaults to `all`. | | `journey_classification` | `all`, `marketing`, or `utility`. Defaults to `all`. | | `source` | Journey source filter. Defaults to `all`. | | `search` | Text search over journey names. | | `start_date`, `end_date` | Optional date range. Supply both or omit both. Use `get_journey_analytics` for an explicit reporting period. | | `page`, `page_size` | Pagination. `page` defaults to 1; `page_size` defaults to 10 and is clamped to 1–100. | ## get\_journey Get one journey by ID. The response includes a flow summary with node and edge counts and counts by node type. | Parameter | Description | | -------------- | ----------------------------------------------------------------------------------------------------------------------------------- | | `journey_id` | Journey ID. Required. | | `include_flow` | Set to `true` to also return the raw flow definition. The raw flow can be large, so leave it off unless you need node-level detail. | ## list\_campaigns List campaign summaries with per-campaign message stats. The response also includes `attribution_settings`, `attribution_by_campaign_id`, and `voice_stats_by_campaign_id` where available. | Parameter | Description | | ------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------- | | `status` | `all`, `DRAFT`, `CREATED`, `SCHEDULED`, `PREPARING`, `PREPARED`, `FIRING`, `COMPLETED`, `FAILED`, `CANCELLED`, or `PAUSED`. Defaults to `all`. | | `campaign_type` | `all`, `journey`, or `single_message`. Defaults to `all`. | | `search` | Text search over campaign names. | | `start_date`, `end_date` | Optional date range. Supply both or omit both. Use `query_analytics` for an explicit attribution reporting period. | | `page`, `page_size` | Pagination. `page` defaults to 1; `page_size` defaults to 10 and is clamped to 1–100. | ## get\_campaign Get one campaign's current **lifetime overview**, including delivery stats, existing estimated cost stats, attribution, retry success count, and voice stats where applicable. The response includes `time_scope: "lifetime"`. This tool has no reporting-date arguments. For date-scoped campaign attribution or workspace attribution defaults, use `query_analytics` as shown above. | Parameter | Description | | -------------------------- | --------------------------------------------------------- | | `campaign_id` | Campaign ID. Required. | | `attribution_type` | `delivery`, `read`, or `clicked`. Defaults to `delivery`. | | `attribution_window_hours` | Attribution window in hours, 1–8760. Defaults to `72`. | # OpenAI Responses API Source: https://docs.sagepilot.ai/mcp/openai-responses-api Connect Sagepilot MCP to OpenAI Responses API. Use Sagepilot MCP as a remote MCP tool server in the OpenAI Responses API. The Responses API example below uses an API client token because the request passes authentication directly. For browser-based ChatGPT connector setup, use [Hosted connectors](/mcp/hosted-connectors). ```python theme={null} from openai import OpenAI client = OpenAI() response = client.responses.create( model="gpt-5", tools=[ { "type": "mcp", "server_label": "sagepilot", "server_url": "https://app.sagepilot.ai/mcp", "authorization": "Bearer sp_your_token", "require_approval": "never", } ], input="List the Sagepilot analytics datasets I can query.", ) print(response.output_text) ``` For EU-hosted workspaces, use: ```text theme={null} https://eu.sagepilot.ai/mcp ``` # Sagepilot MCP Source: https://docs.sagepilot.ai/mcp/overview Connect AI clients and MCP-compatible hosts to your Sagepilot workspace. Sagepilot MCP lets AI clients query workspace analytics, search support data, inspect customers and tickets, review journey and campaign performance, list channels and agents, and perform supported support actions. ## MCP URL ```text theme={null} https://app.sagepilot.ai/mcp ``` For EU-hosted workspaces, use: ```text theme={null} https://eu.sagepilot.ai/mcp ``` Use the URL exactly as shown in **Settings -> API Details -> MCP connector**. Do not add a workspace ID or a Platform API path. ## Choose a setup | If you are connecting | Use | Start here | | ------------------------------------------------------------ | ------------ | -------------------------------------------------------- | | ChatGPT, Claude, Codex, or another host with browser sign-in | Hosted OAuth | [Hosted connectors](/mcp/hosted-connectors) | | Cursor, scripts, server-side tools, or clients without OAuth | API token | [Client setup](/mcp/clients#api-token-mcp-configuration) | ## Access Hosted OAuth connectors request Sagepilot's MCP capability set. During approval, the connected person signs in, chooses a workspace, and approves access. Their Sagepilot role and permissions decide which tools can actually read or change data. API-token setup uses a long-lived `sp_...` token from **Settings -> API Details -> API client v2**. Treat it as a secret and use it only where OAuth is not available. ## Learn more Use the recommended browser sign-in flow. See available tools and access behavior. Filter ticket lists through MCP. # Ticket list filters Source: https://docs.sagepilot.ai/mcp/ticket-list-filters Filter tickets with Sagepilot MCP list_tickets. `list_tickets` supports the same filter concepts as the Sagepilot support inbox ticket list. | Parameter | Description | | | --------------------- | ------------------------------------------------------------------------------------------- | --------------------------------- | | `date_range` | `today`, `yesterday`, `last_7_days`, `last_30_days`, \`start | end\`, or an ISO start timestamp. | | `date_filter_field` | `created_at`, `last_message_created_at`, `chat_escalated_at`, `closed_at`, or `updated_at`. | | | `status` | Ticket status. Use `all` or omit to avoid status filtering. | | | `priority` | Comma-separated priorities. | | | `assignee_type` | Comma-separated assignee types, such as `agent` or `pilot`. | | | `assignee` | Comma-separated assignee IDs, or `current` where supported by the API. | | | `agent_id` | Agent ID filter. The MCP tool maps this into `assignee` + `assignee_type=agent`. | | | `pilot_id` | AI agent ID filter. The MCP tool maps this into `assignee` + `assignee_type=pilot`. | | | `channel_kind` | Comma-separated channel kinds, such as `email,whatsapp`. | | | `channel` | Comma-separated channel IDs. | | | `unread_status` | Comma-separated booleans, such as `true` or `false`. | | | `view` | Built-in/custom view code. | | | `tags` | Comma-separated tag IDs. | | | `queue_status` | Comma-separated queue statuses. | | | `team` | Comma-separated team IDs. | | | `no_assignee` | Use `true` to return unassigned tickets. | | | `inferred_csat_score` | Comma-separated inferred CSAT scores. | | | `user_csat_score` | Comma-separated user CSAT scores. | | | `custom_fields` | Object mapping custom field IDs to filter values. | | | `sort_by_field` | `created_at`, `last_message_created_at`, `chat_escalated_at`, `closed_at`, or `updated_at`. | | | `sort_order` | `asc` or `desc`. | | | `page_size` | 1 to 100. | | | `cursor` | Cursor returned by the previous page. | | # Available tools Source: https://docs.sagepilot.ai/mcp/tools Sagepilot MCP tools and access levels. All tools except `get_connection_info` require a `workspace_id` argument. Call `get_connection_info` first, then pass its returned `workspace_id` into workspace-scoped tools. ## Access model Hosted OAuth connectors request Sagepilot's full MCP capability set. The connected user's Sagepilot role and permissions decide which tools can actually use data or make changes. Some MCP hosts may display the full Sagepilot tool list. If the connected user lacks access to a tool, Sagepilot returns an access error when that tool is called. Analytics queries and entity lookup use `analytics.query` and the selected dataset grant. Journey/campaign configuration tools require their separate product scopes and dataset grants. See [Authentication](/mcp/authentication#analytics-and-product-access). | Tool | Access | Description | | ------------------------------ | ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------ | | `get_connection_info` | Read | Return MCP session details, resolved workspace ID, and supported datasets. | | `get_analytics_catalog` | Read | Discover available analytics metrics, dimensions, and filters. | | `query_analytics` | Read | Query tickets, messages, outbound messages, marketing attribution, CSAT, and workforce activity. Supports cursor pagination for marketing attribution. | | `search_analytics_entities` | Read | Find campaign or journey IDs by name for analytics filters. | | `list_tickets` | Read | List ticket summaries using Sagepilot ticket-list filters; not full-text search. | | `unified_search` | Read | Search tickets, messages, customers, and voice calls through unified OpenSearch. | | `list_channels` | Read | List connected Sagepilot channels. | | `list_agents` | Read | List AI agents and human workspace members. | | `export_tickets_csv` | Read | Export ticket rows as CSV, capped for MCP responses. | | `support_get_ticket` | Read | Get a support ticket by ID or code. | | `support_get_ticket_messages` | Read | Get messages for a support ticket. | | `support_search_customers` | Read | Search customers by name, email, phone, or social identifier. | | `support_get_customer` | Read | Get a customer by exact ID, email, or phone. | | `list_journeys` | Read | List journeys with trigger metadata, per-journey message stats, and status counts. | | `get_journey` | Read | Get one journey by ID with a summarized flow graph. | | `get_journey_analytics` | Read | Get attributed revenue and estimated ROAS for one journey over a required reporting period. | | `list_campaigns` | Read | List campaigns with per-campaign message stats. | | `get_campaign` | Read | Get one campaign's lifetime overview with delivery stats, existing estimated cost, and attribution. | | `support_assign_ticket` | Write | Assign a support ticket to an agent, AI agent, team queue, or unassigned state. | | `support_update_ticket_status` | Write | Update ticket status and optional sub-status. | ## Response shape MCP tools return structured responses with stable fields. This makes the tools easier for AI hosts to inspect, validate, and reuse across follow-up calls. `query_analytics` and `get_journey_analytics` share the [Analytics API response](/api-reference/analytics/query): `dataset`, `rows`, `summary`, and `meta`. For marketing attribution, pass a returned `meta.cursor` into the next call using the same filters and reporting period. Template-category delivery does not support cursors. See [Journey and campaign tools](/mcp/journeys-campaigns) for arguments and examples, and [Journey and campaign analytics](/api-reference/analytics/journeys-campaigns) for metric definitions. # SDK API reference Source: https://docs.sagepilot.ai/sdks/react-native/api-reference Public API surface for the Sagepilot React Native SDK. ## Method return behavior | Method | Return behavior | | ---------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------- | | `SagepilotChat.configure(config)` | `Promise` | | `SagepilotChat.identify(identity)` | `Promise` | | `SagepilotChat.logout()` | `Promise` | | `SagepilotChat.getSession()` | `Promise` | | `SagepilotChat.getUnreadCount()` | `Promise` | | `SagepilotChat.presentMessageComposer(message?, options?)` | `boolean` or `Promise` when `behavior.waitForIdentifyBeforeComposer` is enabled and identity is pending | | `SagepilotChat.present()` | `boolean` | | `SagepilotChat.presentMessages()` | `boolean` | | `SagepilotChat.dismiss()` / `hide()` / `toggle()` | `boolean` | | `SagepilotChat.isPresented()` | `boolean` | | `SagepilotChat.getIdentityState()` / `getSessionState()` / `getChannel()` | synchronous state | | `SagepilotChat.onIdentify(...)` / `onConversationCreated(...)` / lifecycle subscriptions | unsubscribe function | | `SagepilotChat.destroy()` | `void` | ## Setup * `SagepilotChat.configure(config)`: initializes the SDK, loads channel config, creates or resumes a customer session, and starts unread polling unless disabled. * `SagepilotChat.destroy()`: stops polling, clears in-memory SDK state, and removes listeners. * `SagepilotChat.getChannel()`: returns the loaded channel bootstrap data. ## Identity and session * `SagepilotChat.identify(identity)`: links the active session to a known customer. * `SagepilotChat.logout()`: removes known-customer identity from the active session. * `SagepilotChat.getSession()`: fetches the current session and returns safe session metadata. It does not expose the session token. * `SagepilotChat.getSessionState()`: returns safe local session metadata. It does not expose the session token. * `SagepilotChat.getIdentityState()`: returns local identity state, including `identified`, `pending`, and `customer`. * `SagepilotChat.onIdentify(callback)`: subscribes to successful identify events. ## Hosted chat UI * `SagepilotChat.present()`: opens the hosted chat home screen. * `SagepilotChat.presentMessages()`: opens the hosted conversations/messages screen. * `SagepilotChat.presentMessageComposer(message?, options?)`: returns `boolean` and opens immediately unless `behavior.waitForIdentifyBeforeComposer` is enabled and an `identify()` call is in flight. If both are true, it returns `Promise` and waits up to 60 seconds for a successful identify response before opening. `{ mode: "auto" }` is the default and lets Sagepilot reuse an existing conversation when one is available. Pass `{ mode: "new" }` to force a fresh conversation, or `{ chatId }` to open a specific conversation. It does not auto-send. * `SagepilotChat.dismiss()`: closes the hosted chat modal. * `SagepilotChat.hide()`: alias for `dismiss()`. * `SagepilotChat.toggle()`: opens the chat when closed and closes it when open. * `SagepilotChat.isPresented()`: returns whether the hosted chat modal is open. * `SagepilotChat.onConversationCreated(callback)`: subscribes to hosted conversations created from the React Native widget and returns `chat_id` plus any composer metadata. * `SagepilotChatProvider`: renders the hosted Sagepilot chat inside a React Native modal WebView. When `SagepilotChatProvider` is mounted, the SDK preloads a hidden hosted WebView after `configure()` so the first visible open can reuse warmed network/cache state. Disable this with `behavior.preloadWebView: false`. ## Unread state * `SagepilotChat.getUnreadCount()`: fetches and returns the current unread count. * `SagepilotChat.onUnreadChange(callback)`: subscribes to unread count changes. * `SagepilotChat.startUnreadPolling(intervalMs?)`: starts unread polling. * `SagepilotChat.stopUnreadPolling()`: stops unread polling. ## Lifecycle events * `SagepilotChat.onReady(callback)`: fires when SDK configuration completes. * `SagepilotChat.onPresent(callback)`: fires when chat opens. * `SagepilotChat.onDismiss(callback)`: fires when chat closes. * `SagepilotChat.onError(callback)`: subscribes to SDK errors. * `SagepilotChat.onStateChange(callback)`: subscribes to local SDK state changes. ## React hook * `useSagepilotChat()`: returns presentation helpers, unread count, identity state, and common actions for app-owned launchers and badges. ## Storage helpers * `createKeychainTokenStorage(keychain, options?)`: adapts `react-native-keychain` for secure session token storage. * `createAsyncStorageCacheStorage(asyncStorage)`: adapts AsyncStorage for SDK cache features such as native file-picker batch recovery. Do not use AsyncStorage for session tokens. ## Attachment helpers * `createSagepilotFilePicker(options)`: creates a native camera, gallery, and document picker adapter from app-provided picker modules. * `createSagepilotFileStore(blobUtil, options?)`: creates durable app-private storage for picked-file bytes. * `SagepilotFilePickerError`: typed error class surfaced when the picker fails for permission, camera, file-size, encoding, read, or unknown errors. The optional `@sagepilot-ai/react-native-camera-addon` package exports `createSagepilotCameraXFilePicker(options?)` for Android CameraX attachments. ## Runtime notes The React Native UI uses `react-native-webview` to show the Sagepilot-owned hosted conversation. The SDK injects mobile WebView polish for viewport scaling, text selection, tap highlighting, native bridge message forwarding, and secure hosted-auth handoff. Customers should depend on the public SDK methods and components, not WebView internals or hosted route implementation details. ## License MIT. The MIT License applies only to this SDK code. It does not grant access to Sagepilot AI services, workspaces, API credentials, hosted infrastructure, models, data, or paid features. Use of Sagepilot AI hosted services, APIs, Workspace IDs, and runtime-generated license keys is governed separately by Sagepilot AI's Terms of Service or the applicable customer agreement. # Configuration Source: https://docs.sagepilot.ai/sdks/react-native/configuration Configure the Sagepilot React Native SDK. Configure the SDK after your app knows the current workspace/channel and, if available, the current user. ```ts theme={null} await SagepilotChat.configure({ key: "workspace_id:channel_id" }); ``` If Sagepilot provides a dedicated endpoint for your workspace, pass it as an optional override: ```ts theme={null} await SagepilotChat.configure({ key: "workspace_id:channel_id", host: "https://your-sagepilot-host.com" }); ``` ## Supported options | Option | Purpose | | ---------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `key` | Public routing key in the format `workspace_id:channel_id`. | | `host` | Optional Sagepilot host override. Most apps should omit this unless Sagepilot provides a dedicated endpoint. | | `widgetHost` | Optional hosted widget origin override. Use only when Sagepilot provides a separate widget endpoint. | | `headers` | Additional headers for SDK service requests. Do not pass server API keys or long-lived secrets from a mobile app. | | `fetch` | Custom fetch implementation for runtimes that need one. | | `tokenStorage` | Secure storage adapter for SDK-created customer session tokens. | | `cacheStorage` | Optional durable cache adapter used by SDK features such as native file-picker batch recovery. | | `filePicker` | Optional native picker adapter. Use `createSagepilotCameraXFilePicker()` from the CameraX addon or `createSagepilotFilePicker(...)` with app-provided picker modules. | | `fileStore` | Optional native file-store adapter created with `createSagepilotFileStore(...)` for durable picked-file bytes. | | `anonymousId` | Optional stable anonymous identifier sent when the SDK creates a customer session. | | `metadata` | Optional app metadata merged into the session creation payload. | | `deviceInfo` | Optional adapter that returns device metadata to include during session creation. | | `presentation.style` | Modal presentation style: `"sheet"`, `"fullScreen"`, or `"push"`. | | `presentation.mobile` | Adds `mobile=1` to the hosted widget URL. Defaults to `true` for React Native. | | `presentation.showCloseButton` | Controls whether the provider renders a native close button around the WebView. Defaults to `false` so the hosted Sagepilot widget owns close behavior. | | `theme.accentColor` | Primary brand color used as the fallback launcher button color. | | `theme.logoUrl` | Optional brand logo URL exposed in SDK theme state. | | `theme.preferredColorScheme` | Optional hosted chat color scheme hint: `"light"`, `"dark"`, or `"system"`. | | `theme.launcher` | Optional chat launcher icon, label, and badge color configuration. | | `behavior.preloadWebView` | Preloads a hidden hosted chat WebView after configuration. | | `behavior.enableUnreadPolling` | Starts or disables automatic unread-count polling after configuration. | | `behavior.unreadPollIntervalMs` | Sets the unread polling interval in milliseconds. | | `behavior.waitForIdentifyBeforeComposer` | When `true`, `presentMessageComposer()` waits up to 60 seconds for an in-flight `identify()` call before opening. Defaults to `false`. | For attachment setup, see [Native file picker](/sdks/react-native/native-file-picker). For push notifications, see [Push notifications](/sdks/react-native/push-notifications). ## Basic setup ```tsx theme={null} import * as Keychain from "react-native-keychain"; import { useEffect, type ReactNode } from "react"; import { SagepilotChat, SagepilotChatProvider, createKeychainTokenStorage } from "@sagepilot-ai/react-native-sdk"; const tokenStorage = createKeychainTokenStorage(Keychain); export function SagepilotProvider({ children }: { children: ReactNode }) { useEffect(() => { let cancelled = false; async function setupSagepilot() { await SagepilotChat.configure({ key: "workspace_id:channel_id", tokenStorage, presentation: { style: "sheet", mobile: true }, behavior: { preloadWebView: true, enableUnreadPolling: true, waitForIdentifyBeforeComposer: true } }); if (!cancelled) { await SagepilotChat.identify({ userId: "user_123", email: "customer@example.com", name: "Customer Name" }); } } void setupSagepilot().catch(console.warn); return () => { cancelled = true; SagepilotChat.destroy(); }; }, []); return {children}; } ``` Mount the provider once near the top of your app: ```tsx theme={null} export function App() { return ( ); } ``` ## Keep configuration lifecycle stable `SagepilotChat.configure(...)` is asynchronous. Await it before calling `identify()` or opening chat, and avoid reconfiguring the SDK on every render. If your setup depends on auth or workspace state, use stable primitive values in the effect dependencies. Do not depend on entire `session` or `user` objects if your auth library recreates those objects frequently. ```tsx theme={null} useEffect(() => { if (!workspaceId || !channelId || !userId) return; let cancelled = false; async function setupSagepilot() { await SagepilotChat.configure({ key: `${workspaceId}:${channelId}`, tokenStorage }); if (!cancelled) { await SagepilotChat.identify({ userId, email, name }); } } void setupSagepilot().catch(console.warn); return () => { cancelled = true; SagepilotChat.destroy(); }; }, [workspaceId, channelId, userId, email, name]); ``` Mount `SagepilotChatProvider` once, and only re-run setup when the actual workspace, channel, or user values change. ## Chat launcher theme Use `theme.launcher` to configure the icon button and unread badge colors for your app-owned launcher. The SDK resolves omitted values to Sagepilot defaults. ```ts theme={null} await SagepilotChat.configure({ key: "workspace_id:channel_id", theme: { accentColor: "#173c2d", preferredColorScheme: "system", launcher: { label: "Support", buttonColor: "#173c2d", pressedButtonColor: "#225340", disabledButtonColor: "#d8cdbb", borderColor: "#2a5e49", disabledBorderColor: "#cbbda9", iconColor: "#fff8ef", iconInsideColor: "#173c2d", labelColor: "#d4e9dc", disabledContentColor: "#8d8172", unreadBadgeColor: "#e76f51", unreadBadgeTextColor: "#ffffff", unreadBadgeBorderColor: "#efe4d4" } } }); ``` # Identity Source: https://docs.sagepilot.ai/sdks/react-native/identity Identify signed-in app users and clear Sagepilot state on logout. Call `identify()` when you know the signed-in app user. ```ts theme={null} await SagepilotChat.identify({ userId: user.id, email: user.email, name: user.name, phone: user.phone, customProperties: { plan: "pro", storeId: "store_123" } }); ``` Use `behavior.waitForIdentifyBeforeComposer: true` for authenticated support flows where the composer should wait for an in-flight identity request before opening. ```ts theme={null} await SagepilotChat.configure({ key: "workspace_id:channel_id", behavior: { waitForIdentifyBeforeComposer: true } }); const identifyPromise = SagepilotChat.identify({ userId: user.id, email: user.email }); const opened = await SagepilotChat.presentMessageComposer("I need help with my order"); await identifyPromise; ``` `presentMessageComposer()` waits only while `identify()` is already pending. If identity is not pending, it opens immediately. You can inspect local identity state with `SagepilotChat.getIdentityState()` or `useSagepilotChat().identity`. The state includes `identified`, `pending`, and `customer`. ## Identity verification If identity verification is enabled for your workspace, generate `userHash` on your server and pass only the generated hash to the app: ```ts theme={null} await SagepilotChat.identify({ userId: user.id, email: user.email, userHash: serverGeneratedUserHash }); ``` Never generate `userHash` in the mobile app with a signing secret. ## Logout When the app user signs out, clear Sagepilot identity/session state: ```ts theme={null} await SagepilotChat.logout(); SagepilotChat.destroy(); ``` `logout()` removes the known customer identity from the current SDK session. `destroy()` stops polling, clears in-memory state, and removes SDK listeners. # Installation Source: https://docs.sagepilot.ai/sdks/react-native/installation Install the Sagepilot React Native SDK and required dependencies. Install the SDK and WebView dependency: ```bash theme={null} npm install @sagepilot-ai/react-native-sdk@latest react-native-webview ``` For secure persisted sessions, also install a native storage library. ## React Native Keychain ```bash theme={null} npm install react-native-keychain ``` `react-native-keychain` stores values in iOS Keychain and Android secure storage backed by Android Keystore. ## Expo SecureStore ```bash theme={null} npx expo install expo-secure-store ``` Expo SecureStore uses iOS Keychain and Android encrypted storage backed by Android Keystore. ## Optional Android CameraX addon The base SDK does not bundle Android CameraX. Install the optional addon only when your app wants Sagepilot's Android in-app CameraX picker: ```bash theme={null} npm install @sagepilot-ai/react-native-camera-addon@latest ``` After installing the addon, run a fresh native Android build. A Metro reload is not enough for React Native autolinking to register the native module. For Expo apps, use a native/dev-client build: ```bash theme={null} npx expo run:android ``` For bare React Native apps, clean and rebuild Android: ```bash theme={null} cd android && ./gradlew clean cd .. npx react-native run-android ``` The addon declares `@sagepilot-ai/react-native-sdk` as a peer dependency. Keep the base SDK installed directly in your app. ## Optional host-provided picker dependencies If you use the base SDK's custom picker adapter instead of the CameraX addon, install only the picker modules your app needs. ```bash theme={null} # Camera and photo gallery npm install react-native-image-picker # Document/file picking npm install @react-native-documents/picker # Reliable document reads and durable file storage npm install react-native-blob-util # Durable picked-file batch manifests npm install @react-native-async-storage/async-storage ``` See [Native file picker](/sdks/react-native/native-file-picker) for configuration and upload limits. ## Required values You need: * A Sagepilot workspace ID * A Sagepilot chat channel ID * A public SDK key in the format `workspace_id:channel_id` * Secure token storage for production apps The `key` is not a secret. It is safe to ship in a mobile app. Do not put server API keys, signing secrets, or private workspace secrets in a React Native app. # Native file picker Source: https://docs.sagepilot.ai/sdks/react-native/native-file-picker Use native camera, gallery, and document pickers for React Native chat attachments. By default, the hosted chat widget's attach button uses the WebView file input. On low-RAM Android devices this can be unreliable because the system camera can kill the WebView render process while capture is in progress. Configure `filePicker` so the attach button uses native pickers instead. Picked files are delivered to the hosted widget over the SDK bridge, mirrored to cache when configured, and re-delivered until the widget acknowledges them. ## Choose a picker path Use one of these approaches: * Install `@sagepilot-ai/react-native-camera-addon` for Sagepilot's Android in-app CameraX picker. * Use `createSagepilotFilePicker(...)` from the base SDK when your app already provides camera, gallery, or document picker modules. The base chat SDK does not bundle CameraX or CameraX Gradle dependencies. ## Android CameraX addon Install the addon when your Android app wants Sagepilot's in-app CameraX picker: ```bash theme={null} npm install @sagepilot-ai/react-native-camera-addon@latest ``` The addon adds these Android dependencies through its own Gradle file: | Dependency | Purpose | | -------------------------------------- | --------------------------------------------- | | `androidx.camera:camera-core` | CameraX core APIs | | `androidx.camera:camera-camera2` | Camera2 implementation | | `androidx.camera:camera-lifecycle` | Lifecycle-aware camera binding | | `androidx.camera:camera-view` | Camera preview UI | | `androidx.exifinterface:exifinterface` | Image metadata handling | | `androidx.activity:activity` | Activity result and AndroidX activity support | By default, the addon uses CameraX `1.5.3`, ExifInterface `1.4.2`, and AndroidX Activity `1.10.1`. Override them from the root Gradle project with `sagepilotCameraXVersion`, `sagepilotExifInterfaceVersion`, and `sagepilotActivityVersion` when your app needs pinned AndroidX versions. Run a fresh native Android build after installing the addon. A Metro reload is not enough for React Native autolinking to register `NativeModules.SagepilotInAppCamera`. For Expo apps, use a native/dev-client build: ```bash theme={null} npx expo run:android ``` For bare React Native apps, clean and rebuild Android: ```bash theme={null} cd android && ./gradlew clean cd .. npx react-native run-android ``` Request and confirm Android `CAMERA` permission before users open the Sagepilot CameraX picker. Do not rely on the first camera tap inside the widget to show the runtime permission prompt. On some Android devices, that first permission flow can pause the app and cause the hosted WebView to reload. Then pass the addon picker to `configure()`: ```ts theme={null} import { SagepilotChat } from "@sagepilot-ai/react-native-sdk"; import { createSagepilotCameraXFilePicker } from "@sagepilot-ai/react-native-camera-addon"; await SagepilotChat.configure({ key: "workspace_id:channel_id", filePicker: createSagepilotCameraXFilePicker({ includeCamera: true, includeLibrary: true, includeDocuments: true }) }); ``` `createSagepilotCameraXFilePicker()` returns `undefined` outside Android, so shared app setup can call it safely. On iOS, no additional setup is required for the CameraX addon unless your app provides its own native iOS picker. With this adapter enabled on Android, the hosted widget routes each attachment action to its matching native source: | Attachment action | Native source | | ----------------- | -------------------------- | | Camera | In-app CameraX capture | | Gallery | Android media/photo picker | | Files | Android document picker | When the addon advertises these sources, gallery and file actions do not use a generic WebView file chooser. ## Host-provided picker modules Use this path when your app already owns picker dependencies or needs a custom iOS picker. Install only the modules your app needs: ```bash theme={null} # Camera and photo gallery npm install react-native-image-picker # Document/file picking npm install @react-native-documents/picker # Reliable document reads and durable file storage npm install react-native-blob-util # Durable picked-file batch manifests npm install @react-native-async-storage/async-storage ``` Then pass those modules to `createSagepilotFilePicker(...)`: ```ts theme={null} import AsyncStorage from "@react-native-async-storage/async-storage"; import * as imagePicker from "react-native-image-picker"; import * as documentsPicker from "@react-native-documents/picker"; import ReactNativeBlobUtil from "react-native-blob-util"; import { SagepilotChat, createAsyncStorageCacheStorage, createSagepilotFilePicker, createSagepilotFileStore } from "@sagepilot-ai/react-native-sdk"; await SagepilotChat.configure({ key: "workspace_id:channel_id", cacheStorage: createAsyncStorageCacheStorage(AsyncStorage), fileStore: createSagepilotFileStore(ReactNativeBlobUtil), filePicker: createSagepilotFilePicker({ imagePicker, documentsPicker, fileReader: ReactNativeBlobUtil }) }); ``` The base SDK uses only the picker modules you pass into `createSagepilotFilePicker(...)`. ## Durable recovery For the strongest recovery path, pass both `cacheStorage` and `fileStore`. `cacheStorage` stores the picked-file batch manifest. `fileStore` stores picked-file bytes in app-private storage so a captured photo or document can survive an app-process restart within the hosted widget's upload limits. ## Upload limits The hosted widget enforces the final attachment limits for both WebView and native picker flows. | Limit | Value | | ------------------ | ----------------- | | Files per send | Up to 5 files | | Previewable images | Up to 5 MB total | | Non-image files | Up to 10 MB total | The native picker also applies earlier safety guards before files are read into memory. | Guard | Default | | -------------------- | ------------------ | | Gallery multi-select | 5 files | | Documents | Reject above 20 MB | | Images | Reject above 15 MB | These picker guards reduce out-of-memory risk. They do not raise the hosted widget's final upload limits. ## Platform notes * The CameraX addon is Android-only. It owns CameraX dependencies, the native module, camera activity, overlay, and image processing. * Camera and gallery images are downscaled natively to keep memory and upload sizes low. * Picker failures are surfaced with typed error codes such as `permission_denied`, `camera_unavailable`, `encode_failed`, `file_too_large`, and `read_failed`. * Android apps that use the CameraX addon must grant `CAMERA` at runtime before opening the Sagepilot picker. If permission is missing or denied, ask users to grant it from your app flow or Android settings before presenting the camera action again. * No additional iOS setup is required for the CameraX addon. Add iOS permission strings only if your app separately provides its own native iOS picker. * Apps that skip `filePicker` keep the WebView file input. The SDK can recover from WebView renderer crashes, but a photo captured at crash time cannot be restored in that mode. # Opening chat Source: https://docs.sagepilot.ai/sdks/react-native/opening-chat Connect Sagepilot chat to app-owned buttons, tabs, badges, and help screens. The SDK does not force a launcher UI. Use the hook to connect Sagepilot to your own button, tab, badge, or help screen. ```tsx theme={null} import { Pressable, Text } from "react-native"; import { useSagepilotChat } from "@sagepilot-ai/react-native-sdk"; export function SupportButton() { const { configured, unreadCount, presentMessages } = useSagepilotChat(); return ( Support{unreadCount > 0 ? ` (${unreadCount})` : ""} ); } ``` ## Open helpers ```ts theme={null} import { SagepilotChat } from "@sagepilot-ai/react-native-sdk"; SagepilotChat.present(); SagepilotChat.presentMessages(); await SagepilotChat.presentMessageComposer("I need help with my order"); await SagepilotChat.presentMessageComposer("I need help with my order", { mode: "auto" }); await SagepilotChat.presentMessageComposer("Start a new request", { mode: "new" }); await SagepilotChat.presentMessageComposer("I need help with this order", { chatId: savedChatId }); ``` `presentMessageComposer(message)` uses `{ mode: "auto" }` by default. It pre-fills the composer and lets Sagepilot reuse an existing conversation when one is available. If there is no existing conversation, Sagepilot starts a new one when the customer sends the message. Use `{ mode: "new" }` only when the button or workflow should always start a fresh conversation. Use `{ chatId }` when an app-owned surface should reopen a specific conversation, such as an order help button. `presentMessageComposer()` returns `boolean` by default. If `behavior.waitForIdentifyBeforeComposer` is `true` and `identify()` is already in flight, it returns `Promise` and waits up to 60 seconds for identity to finish before opening. You can `await` it in both cases. For authenticated support flows, enable `behavior.waitForIdentifyBeforeComposer` and call `identify()` before opening chat: ```ts theme={null} const opened = await SagepilotChat.presentMessageComposer("I need help with my order"); ``` Subscribe to conversation creation when your app needs to store a newly created chat ID: ```ts theme={null} await SagepilotChat.presentMessageComposer("I need help with this order", { mode: "new", metadata: { source: "order_help", orderId }, onConversationCreated: ({ chat_id, metadata }) => { saveChatForOrder(metadata?.orderId, chat_id); } }); const unsubscribe = SagepilotChat.onConversationCreated(({ chat_id }) => { cacheCreatedChat(chat_id); }); ``` ## Custom launcher example ```tsx theme={null} import { Pressable, Text, View } from "react-native"; import { MessageCircle } from "lucide-react-native"; import { useSagepilotChat } from "@sagepilot-ai/react-native-sdk"; export function ChatLauncher() { const { configured, unreadCount, presentMessages, theme } = useSagepilotChat(); const launcher = theme.launcher; return ( ({ alignItems: "center", backgroundColor: !configured ? launcher.disabledButtonColor : pressed ? launcher.pressedButtonColor : launcher.buttonColor, borderColor: !configured ? launcher.disabledBorderColor : launcher.borderColor, borderRadius: 28, borderWidth: 1, height: 56, justifyContent: "center", width: 56 })} > {unreadCount > 0 ? ( {unreadCount} ) : null} ); } ``` # React Native SDK Source: https://docs.sagepilot.ai/sdks/react-native/overview Add Sagepilot chat to iOS and Android apps with the official React Native SDK. `@sagepilot-ai/react-native-sdk` opens the Sagepilot-hosted chat experience inside a React Native WebView and provides APIs for setup, identity, unread count, attachments, and app-owned launchers. ## Requirements * React 18+ * React Native 0.72+ * `react-native-webview` 13+ * A Sagepilot workspace ID and chat channel ID * Secure token storage for production apps ## Install ```bash theme={null} npm install @sagepilot-ai/react-native-sdk@latest react-native-webview ``` For secure persisted sessions, install one storage library: ```bash theme={null} npm install react-native-keychain ``` Expo apps can use `expo-secure-store` instead: ```bash theme={null} npx expo install expo-secure-store ``` The base SDK does not bundle Android CameraX. Install `@sagepilot-ai/react-native-camera-addon` only if your Android app needs Sagepilot's in-app CameraX picker. ## Start here Add the SDK, WebView dependency, and secure storage package. Set the workspace/channel key, presentation behavior, and launcher theme. Use the optional CameraX addon or app-provided native pickers for more reliable uploads. Connect Sagepilot message webhooks to your app-owned push system. Use hooks and public methods to connect chat to your app-owned UI. ## Runtime model `SagepilotChatProvider` renders the Sagepilot-hosted chat UI in a React Native WebView. Most apps do not need a host override. For standard Sagepilot cloud usage, omit `host`; the SDK connects to Sagepilot-hosted endpoints automatically. Set `host` only when Sagepilot provides a dedicated endpoint for your workspace. # Push notifications Source: https://docs.sagepilot.ai/sdks/react-native/push-notifications Use Sagepilot webhooks to trigger app-owned push notifications for React Native chat. The React Native SDK does not register APNs or FCM device tokens and does not send operating-system push notifications directly. Use Sagepilot webhooks to connect customer-visible support messages to your own push-notification system. ## Recommended flow Register the device for notifications in your app and store the APNs, FCM, or provider token in your backend. Configure a Sagepilot webhook subscription for `support.message.created`. Use the webhook's customer, conversation, message, and SDK context to find the user and decide whether they should be notified. Send the push notification from your backend through APNs, FCM, or your notification provider. Keep push-token storage and notification preferences in your app/backend. ## Related setup * Configure webhook subscriptions in Sagepilot settings before relying on push delivery. * Use [Opening chat](/sdks/react-native/opening-chat) to route a push tap back into the chat experience. # Secure session storage Source: https://docs.sagepilot.ai/sdks/react-native/secure-session-storage Persist Sagepilot SDK session tokens securely in React Native apps. The SDK creates opaque customer session tokens at runtime. These tokens are not exposed through hooks or public session APIs. If `tokenStorage` is not provided, tokens are kept in memory only and the session will not persist after the app process restarts. Production apps should pass secure native storage. Do not use AsyncStorage for session tokens. ## React Native Keychain ```ts theme={null} import * as Keychain from "react-native-keychain"; import { SagepilotChat, createKeychainTokenStorage } from "@sagepilot-ai/react-native-sdk"; await SagepilotChat.configure({ key: "workspace_id:channel_id", tokenStorage: createKeychainTokenStorage(Keychain) }); ``` ## Expo SecureStore ```ts theme={null} import * as SecureStore from "expo-secure-store"; import { SagepilotChat } from "@sagepilot-ai/react-native-sdk"; await SagepilotChat.configure({ key: "workspace_id:channel_id", tokenStorage: { getItem: SecureStore.getItemAsync, setItem: SecureStore.setItemAsync, removeItem: SecureStore.deleteItemAsync } }); ```