API Reference

Slide API

Integrate email campaigns, Instagram messaging, WhatsApp campaigns, SMS, RCS, OTP verification, Shopify commerce, AI voice calling, and real-time WebSocket events into your own applications using the Slide REST API.

View rawMCP serverPaste the Markdown into Claude or Cursor, or connect over MCP.
Base URLhttps://api.slide.cx/v1
Version v1
JSON

Introduction

The Slide API is a RESTful API over HTTPS. Every response body is JSON. Requests that include a body must set Content-Type: application/json.

PropertyValue
Base URLhttps://api.slide.cx/v1
ProtocolHTTPS only
FormatJSON (application/json)
API Versionv1
Auth schemeBearer token (API key)
NoteAll timestamps in responses are ISO 8601 strings in UTC (e.g. 2026-03-24T10:00:00.000Z). Pagination follows a consistent { data, meta } envelope.
NewRead the One API overview for the business case behind the unified /messages/send endpoint, or jump straight to the technical reference below.

SDKs & Code Examples

Official client libraries for TypeScript/Node.js and Python. Both are fully typed, zero-configuration, and wrap every endpoint in a resource-based API. The latest releases add sms, rcs, and otp resources, the unified messages resource for the One API endpoint, plus WhatsApp conversation and campaign methods: client.messages.send(...), client.sms.send(...), client.otp.send(...), client.rcs.send(...), and more. If you prefer raw HTTP, jump to the Without an SDK section below.

npm@synquic/slide

TypeScript SDK with full type inference, ESM + CJS dual build, zero runtime dependencies. Works in Node.js 18+ and any modern runtime.

terminal
npm install @synquic/slide
# or: pnpm add @synquic/slide
# or: yarn add @synquic/slide
PyPIsynquic-slide

Python SDK with sync and async clients, full type hints via TypedDict, context manager support. Requires Python 3.9+ and httpx.

terminal
pip install synquic-slide

TypeScript SDK

Install the package and instantiate a client with your API key. All methods are fully typed and return typed response objects.

example.ts
import { SlideClient } from '@synquic/slide';

const slide = new SlideClient({ apiKey: process.env.SLIDE_API_KEY! });

// Send a transactional email
const sent = await slide.email.send({
  templateId: 'tmpl_welcome',
  recipient: { to: 'user@example.com', firstName: 'Rahul' },
  fromName: 'Acme Store',
  fromEmail: 'orders@acme.com',
  variables: { orderId: '#1042', total: '₹2,499' },
});
console.log(sent.messageId, sent.status);

// Send a WhatsApp template
const msg = await slide.whatsapp.sendTemplate({
  to: '+919876543210',
  templateName: 'order_shipped',
  languageCode: 'en',
});
console.log(msg.wamid);

// Initiate an outbound AI voice call
const call = await slide.voice.initiateOutboundCall({
  agentId: 'va_01JXYZ...',
  toNumber: '+919876543210',
  callContext: { customerName: 'Priya', purpose: 'appointment_confirmation' },
});
console.log(call.id, call.status); // vc_01J... INITIATED

// List contacts with pagination
const contacts = await slide.contacts.list({ page: 1, limit: 50 });
console.log(contacts.meta.total, contacts.data.length);

// Shopify - validate discount code
const discount = await slide.shopify.validateDiscount({ code: 'SAVE20' });
if (discount.valid) {
  console.log(`${discount.value}% off, expires ${discount.expiresAt}`);
}

// Instagram - get profile and send a DM
const profile = await slide.instagram.getProfile();
await slide.instagram.sendMessage({
  recipientIgsid: '123456789',
  message: 'Thanks for reaching out!',
});

// SMS, RCS, and OTP
await slide.sms.send({
  senderId: 'ACMEIN',
  to: ['919876543210'],
  message: 'Your order has shipped!',
});
const otp = await slide.otp.send({ widgetId: 'wgt_01J...', identifier: '+919876543210' });
await slide.otp.verify({ requestId: otp.requestId, otp: '123456' });
await slide.rcs.send({ to: '919876543210', templateId: 'rcst_01J...' });

// WhatsApp conversations and campaigns
const history = await slide.whatsapp.getConversationByPhone({ phone: '919876543210', limit: 50 });
const campaigns = await slide.whatsapp.listCampaigns({ page: 1, limit: 20 });

Error Handling

example.ts
import {
  SlideClient,
  SlideAuthError,
  SlideScopeError,
  SlideValidationError,
  SlideNotFoundError,
  SlideError,
} from '@synquic/slide';

const slide = new SlideClient({ apiKey: process.env.SLIDE_API_KEY! });

try {
  await slide.email.send({ /* ... */ });
} catch (err) {
  if (err instanceof SlideAuthError) {
    console.error('Invalid API key - check SLIDE_API_KEY');
  } else if (err instanceof SlideScopeError) {
    console.error('Missing scope:', err.message); // e.g. "email:send"
  } else if (err instanceof SlideValidationError) {
    console.error('Bad request:', err.body);
  } else if (err instanceof SlideNotFoundError) {
    console.error('Resource not found');
  } else if (err instanceof SlideError) {
    console.error(`HTTP ${err.statusCode}:`, err.message);
  }
}

Python SDK

The Python SDK ships two client classes: SlideClient (synchronous, backed by httpx.Client) and AsyncSlideClient (async, backed by httpx.AsyncClient). Both support context managers.

Sync Client

example.py
import os
from synquic_slide import SlideClient

slide = SlideClient(api_key=os.environ["SLIDE_API_KEY"])

# Send a transactional email
slide.email.send(
    recipient={"to": "user@example.com", "firstName": "Rahul"},
    template_id="tmpl_welcome",
    from_name="Acme Store",
    from_email="orders@acme.com",
    variables={"orderId": "#1042", "total": "₹2,499"},
)

# Send a WhatsApp template
msg = slide.whatsapp.send_template(
    to="+919876543210",
    template_name="order_shipped",
    language_code="en",
)
print(msg["wamid"])

# Initiate an outbound AI voice call
call = slide.voice.initiate_outbound_call(
    agent_id="va_01JXYZ...",
    to_number="+919876543210",
    call_context={"customerName": "Priya", "purpose": "appointment_confirmation"},
)
print(call["id"], call["status"])  # vc_01J... INITIATED

# List contacts
contacts = slide.contacts.list(page=1, limit=50)
print(contacts["meta"]["total"])

# Validate a Shopify discount code
discount = slide.shopify.validate_discount(code="SAVE20")
if discount["valid"]:
    print(f'{discount["value"]}% off, expires {discount["expiresAt"]}')

# SMS, RCS, and OTP
slide.sms.send(sender_id="ACMEIN", to=["919876543210"], message="Your order has shipped!")
otp = slide.otp.send(widget_id="wgt_01J...", identifier="+919876543210")
slide.otp.verify(request_id=otp["requestId"], otp="123456")
slide.rcs.send(to="919876543210", template_id="rcst_01J...")

Async Client

example.py
import asyncio
import os
from synquic_slide import AsyncSlideClient

async def main():
    async with AsyncSlideClient(api_key=os.environ["SLIDE_API_KEY"]) as slide:
        # Fetch contacts and send a WhatsApp message concurrently
        contacts, agents = await asyncio.gather(
            slide.contacts.list(page=1, limit=20),
            slide.voice.list_agents(),
        )
        print(f'{contacts["meta"]["total"]} contacts, {agents["meta"]["total"]} agents')

        call = await slide.voice.initiate_outbound_call(
            agent_id=agents["data"][0]["id"],
            to_number="+919876543210",
            call_context={"customerName": "Priya"},
        )
        print(call["id"], call["status"])

asyncio.run(main())

Error Handling

example.py
from synquic_slide import (
    SlideClient,
    SlideAuthError,
    SlideScopeError,
    SlideNotFoundError,
    SlideValidationError,
    SlideError,
)

slide = SlideClient(api_key="sk_live_...")

try:
    slide.email.send(...)
except SlideAuthError:
    print("Invalid API key - check SLIDE_API_KEY")
except SlideScopeError as e:
    print(f"Missing scope: {e}")      # e.g. "email:send"
except SlideValidationError as e:
    print(f"Bad request: {e.body}")
except SlideNotFoundError:
    print("Resource not found")
except SlideError as e:
    print(f"HTTP {e.status_code}: {e}")

Without an SDK

The Slide API is a standard HTTP REST API - any HTTP client works.

curl

terminal
export SLIDE_API_KEY="sk_live_YOUR_API_KEY"
export SLIDE_BASE="https://api.slide.cx/v1"

# Send an email using a template
curl -X POST "$SLIDE_BASE/email/send" \
 -H "Authorization: Bearer $SLIDE_API_KEY" \
 -H "Content-Type: application/json" \
 -d '{
 "templateId": "YOUR_TEMPLATE_ID",
 "recipient": { "to": "hello@example.com", "firstName": "Alex" },
 "variables": { "company": "Acme" },
 "fromName": "Acme",
 "fromEmail": "hi@acme.com"
 }'

# Initiate an outbound voice call
curl -X POST "$SLIDE_BASE/voice/calls/outbound" \
 -H "Authorization: Bearer $SLIDE_API_KEY" \
 -H "Content-Type: application/json" \
 -d '{ "agentId": "va_01JXYZ...", "toNumber": "+919876543210" }'

Node.js fetch

example.ts
const BASE = 'https://api.slide.cx/v1';
const KEY  = process.env.SLIDE_API_KEY!;

const headers = {
  'Authorization': `Bearer ${KEY}`,
  'Content-Type': 'application/json',
};

// Send email
const emailRes = await fetch(`${BASE}/email/send`, {
  method: 'POST',
  headers,
  body: JSON.stringify({
    templateId: 'YOUR_TEMPLATE_ID',
    recipient: { to: 'user@example.com', firstName: 'Alex' },
    variables: { company: 'Acme Inc' },
    fromName: 'Acme Inc',
    fromEmail: 'hi@acme.com',
  }),
});
const email = await emailRes.json();
console.log(email); // { messageId, status, to, templateId, templateName }

// Search Shopify products
const prodRes = await fetch(`${BASE}/shopify/products/search?q=sneakers&limit=5`, { headers });
const products = await prodRes.json();
console.log(products.data); // [{ id, title, vendor, variants }, ...]

Authentication

Every API request must include a valid API key in the Authorization header as a Bearer token. Keys are prefixed with sk_live_ for production.

To create an API key, navigate to Dashboard → API Keys or visit slide.synquic.com/dashboard/api-keys.

terminal
# Include in every request
curl https://api.slide.cx/v1/email/send \
 -H "Authorization: Bearer sk_live_YOUR_API_KEY" \
 -H "Content-Type: application/json"
WarningKeep your API key secret. Never expose it in client-side code, public repositories, or logs. Rotate keys immediately if compromised from Dashboard → API Keys.

Scopes

Each API key is issued with a specific set of scopes that determine what operations it can perform. Attempting an operation without the required scope returns 403 Forbidden.

ScopeDescription
contacts:readList and search the unified contacts database
contacts:blockBlock or unblock a contact across every channel
email:sendSend transactional or campaign emails
email:templates:readList and retrieve email templates
instagram:messages:readRead Instagram conversations and profile
instagram:messages:sendSend Instagram direct messages
instagram:insights:readRead Instagram analytics and insights

Rate Limits

Each API key has a configurable per-minute limit, rateLimitPerMinute. The default is 60 requests per minute, and it can be set anywhere from 1 to 10,000 per key in Admin Settings → API Keys (IP and rate settings).

Rate limit state is tracked on a sliding 60 second window, per key and per client IP. Two servers calling the API with the same key from different IPs each get their own window.

HeaderDescription
X-RateLimit-LimitThe configured requests-per-minute limit for this key
X-RateLimit-RemainingRequests remaining in the current 60 second window
Retry-AfterSeconds to wait before retrying (on 429 responses only)

X-RateLimit-Limit and X-RateLimit-Remaining are returned on every request. When the limit is exceeded, the API returns HTTP 429 Too Many Requests with a Retry-After header:

response.json
{
 "statusCode": 429,
 "error": "Too Many Requests",
 "message": "Rate limit exceeded. Try again in 23 seconds.",
 "retryAfter": 23
}

Recommended Client Behavior

On a 429 response, honor the Retry-After header and wait that many seconds before retrying. For repeated 429s, apply exponential backoff with jitter. Never retry in a tight loop.

example.ts
async function requestWithBackoff(url: string, init: RequestInit, maxRetries = 5) {
  for (let attempt = 0; attempt <= maxRetries; attempt++) {
    const res = await fetch(url, init);
    if (res.status !== 429) return res;

    // Honor Retry-After, fall back to exponential backoff with jitter
    const retryAfter = Number(res.headers.get('Retry-After'));
    const backoffMs = Number.isFinite(retryAfter) && retryAfter > 0
      ? retryAfter * 1000
      : Math.min(60_000, 2 ** attempt * 1000) + Math.random() * 500;
    await new Promise((r) => setTimeout(r, backoffMs));
  }
  throw new Error('Rate limited: retries exhausted');
}
NoteOTP endpoints (POST /v1/otp/send and POST /v1/otp/retry) additionally throttle per identifier: each phone number is limited to a maximum number of sends per hour, configured on the OTP widget. This applies on top of the key's per-minute limit.

IP Whitelisting & Key Security

Every API key can be locked down to a fixed set of source IP addresses. Set allowedIps (IPv4 addresses or ranges, up to 20) when you create a key, or later with Edit on the key in API Keys. An empty list allows any IP. When a list is set, requests from any other IP are rejected with 403 Forbidden, and the offending IP is included in the error message so you can update the list.

response.json
{
 "statusCode": 403,
 "error": "Forbidden",
 "message": "Request from IP 203.0.113.42 is not allowed for this API key"
}
NoteThe address checked is the one our edge saw your request come from (the last value of x-forwarded-for). An address you put in that header yourself is ignored, so a key stays locked to the true origin IP. If your servers reach us through a proxy or NAT, allow its public address.

Key Security Model

FeatureDescription
EnvironmentsKeys are issued as live (sk_live_) or test (sk_test_) and are scoped to that environment
ExpiryKeys support an optional expiresAt date, expired keys are rejected with 401
One-time displayThe plaintext key is shown exactly once at creation, only a SHA-256 hash is stored
Usage logsEvery request is logged per key: endpoint, HTTP status, source IP, and duration

Best Practices

PracticeWhy
Grant least-privilege scopesA leaked key with only sms:logs:read cannot send messages
Rotate keys periodicallyCreate a new key, migrate traffic, then revoke the old one
Use a separate key per integrationPer-key usage logs make it easy to trace and revoke a single consumer
Enable IP whitelisting on server keysEven a leaked key is useless outside your infrastructure

Error Codes

All errors follow the same JSON envelope. The message field is human-readable and safe to display to end users.

response.json
{
 "statusCode": 400,
 "error": "Bad Request",
 "message": "recipient.to must be a valid email address"
}
HTTP CodeMeaningCommon Cause
200 OKRequest succeededStandard successful response
201 CreatedResource createdContact or resource was created
400 Bad RequestInvalid inputMissing required field, bad format
401 UnauthorizedMissing or invalid API keyNo Authorization header
403 ForbiddenInsufficient scopeKey does not have required scope
404 Not FoundResource not foundInvalid ID or path
409 ConflictDuplicate resourceContact with email already exists
422 UnprocessableValidation failedField present but value invalid
429 Too Many RequestsRate limit exceededExceeded the key's configured per-minute limit
500 Internal ErrorServer errorUnexpected error contact support

Contacts API

Read and write your organization's unified contact database. Contacts are shared across all channels: WhatsApp, Instagram, email, SMS and every integration.

NoteEach endpoint below shows the scope it needs. Reading needs contacts:read, creating or updating needs contacts:write, deleting needs contacts:delete, blocking needs contacts:block, and notes have their own two scopes. Every request counts against your key's per-minute limit (see Rate Limits).

List Contacts

GET/v1/contacts

List contacts, most recently active first, with an optional search across name, phone and email, or an exact lookup by phone or email.

Required scope·contacts:read

Query Parameters

ParameterTypeDefaultDescription
pagenumber1Page number (1-indexed)
limitnumber50Results per page (max 100)
searchstring Contacts whose name, phone or email contains this text (any case). Up to 200 characters.
phonestring Exact match on the phone number, read the way POST /v1/contacts reads it. A value that is not a phone number matches nobody.
emailstring Exact match on the email address, ignoring case. With phone, the contact must match both.
includeBlockedstringfalsetrue lists blocked contacts too. They are left out otherwise.
terminal
curl "https://api.slide.cx/v1/contacts?page=1&limit=50&search=john" \
 -H "Authorization: Bearer sk_live_YOUR_API_KEY"
example.ts
const params = new URLSearchParams({ page: '1', limit: '50', search: 'john' });
const res = await fetch(`https://api.slide.cx/v1/contacts?${params}`, {
 headers: { 'Authorization': 'Bearer sk_live_YOUR_API_KEY' },
});
const { data, meta } = await res.json();

Response

response.json
{
 "data": [
 {
 "id": "4b1d2c3e-...",
 "unifiedContactId": "4b1d2c3e-...",
 "name": "John Doe",
 "firstName": "John",
 "lastName": "Doe",
 "email": "john@example.com",
 "phone": "+919876543210",
 "countryCode": "IN",
 "profilePicUrl": null,
 "tags": [{ "id": "...", "name": "VIP", "color": "#..." }],
 "customFields": { "city": "Pune" },
 "lifecycleStageId": null,
 "lifecycleStage": null,
 "isBlocked": false,
 "blockedReason": null,
 "isSubscribed": true,
 "unsubscribedAt": null,
 "isWhatsappSubscribed": true,
 "whatsappUnsubscribedAt": null,
 "lastActiveAt": "2026-10-20T09:12:00.000Z",
 "createdAt": "2026-01-15T10:30:00.000Z",
 "updatedAt": "2026-10-20T09:12:00.000Z"
 }
 ],
 "meta": {
 "total": 312,
 "page": 1,
 "limit": 50,
 "totalPages": 7
 }
}

customFields is keyed by the field id of each custom field, and every value is a string.

Get a Contact

GET/v1/contacts/:id

One contact, in the same shape as a list row. The id may be the contact id or an Instagram contact id from an older list answer; both open the same contact. A contact of another workspace is a 404.

Required scope·contacts:read
terminal
curl "https://api.slide.cx/v1/contacts/4b1d2c3e-..." \
 -H "Authorization: Bearer sk_live_YOUR_API_KEY"

Create or Update a Contact

POST/v1/contacts

Create or update ONE contact, matched by phone first, then by email. Answers 201 when the contact was created and 200 when it was updated; the body says which.

Required scope·contacts:write

Body Parameters

FieldTypeRequiredDescription
phonestringOne of phone or emailWith the country code, for example +919876543210. Up to 32 characters. A number is accepted too.
emailstringOne of phone or emailUp to 254 characters.
namestringNoUp to 128 characters. When absent, firstName and lastName are joined.
firstNamestringNoUp to 63 characters.
lastNamestringNoUp to 63 characters.
companystringNoUp to 128 characters. Kept on the email list entry, as a contact has no company field.
tagsstring[]NoTag names, at most 50, each up to 32 characters. A tag that does not exist yet is created. Tags are added, never removed.
customFieldsobjectNoKeyed by a custom field id or its display name, at most 100 keys. Values: a string up to 2,048 characters, a number, a boolean or null. Keys that match no field are listed in unmatchedCustomFields.

A blank string counts as not sent, so an empty field never wipes a value. The call never changes a contact's subscription or consent.

terminal
curl -X POST "https://api.slide.cx/v1/contacts" \
 -H "Authorization: Bearer sk_live_YOUR_API_KEY" \
 -H "Content-Type: application/json" \
 -d '{"phone": "+919876543210", "name": "John Doe", "tags": ["VIP"], "customFields": {"city": "Pune"}}'

Response

response.json
{
 "id": "4b1d2c3e-...",
 "unifiedContactId": "4b1d2c3e-...",
 "name": "John Doe",
 "phone": "+919876543210",
 "tags": [{ "id": "...", "name": "VIP", "color": "#..." }],
 "customFields": { "city": "Pune" },
 "created": true,
 "emailContact": null,
 "emailUnsubscribed": false,
 "unmatchedCustomFields": [],
 "warnings": []
}

The answer is the contact (every field of a list row; some are left out above) plus what the call did. emailUnsubscribed is true when the address has opted out of email anywhere in the workspace; this call reports it and never changes it. warnings may hold COMPANY_NEEDS_EMAIL (company was sent without an email, so it was not kept). Errors: 400 IDENTIFIER_REQUIRED with neither phone nor email, 400 INVALID_PHONE when the phone cannot be read as an international number, 409 PHONE_IN_USE when the number already belongs to another contact, and 409 CONTACT_UPSERT_IN_PROGRESS with a Retry-After header while another request is writing the same contact.

Update a Contact

PATCH/v1/contacts/:id

Change the name, email, phone, lifecycle stage or tags of ONE contact. A field you leave out stays as it is. The answer is the contact, in the same shape as GET /v1/contacts/:id.

Required scope·contacts:write

Body Parameters

FieldTypeRequiredDescription
namestringNoUp to 128 characters.
emailstringNoA valid email address, up to 254 characters.
phonestringNoWith the country code, for example +919876543210. Up to 32 characters.
lifecycleStageIdstring or nullNoThe id of a lifecycle stage of this workspace, or null to clear the stage.
tagsstring[]NoTag names, at most 50, each up to 32 characters. This REPLACES the contact’s tags with exactly these names; a name that does not exist yet is created, and [] removes every tag.

Send at least one field. A blank string counts as not sent. Email and phone cannot be cleared, because a contact is matched by them. Notes, consent and blocking are not changed here: they have their own routes. Errors: 400 NOTHING_TO_UPDATE, 400 INVALID_PHONE, 400 UNKNOWN_LIFECYCLE_STAGE, 404 for a contact of another workspace, and 409 CONTACT_VALUE_IN_USE when the phone or email already belongs to another contact.

WarningUnlike POST /v1/contacts, which only adds tags, tags here replaces them. Read the contact first if you want to keep the tags it has.
terminal
curl -X PATCH "https://api.slide.cx/v1/contacts/4b1d2c3e-..." \
 -H "Authorization: Bearer sk_live_YOUR_API_KEY" \
 -H "Content-Type: application/json" \
 -d '{"name": "John A. Doe", "tags": ["VIP", "Pune"]}'

Delete a Contact

DELETE/v1/contacts/:id

Delete ONE contact for good, with what the dashboard deletes with it: its channel records and conversations. Answers 204 with no body. A contact of another workspace is a 404.

Required scope·contacts:delete
WarningThis cannot be undone. It has its own scope, contacts:delete, which is not part of contacts:write. Give it only to a key that must remove people, for example to honour a deletion request.
terminal
curl -X DELETE "https://api.slide.cx/v1/contacts/4b1d2c3e-..." \
 -H "Authorization: Bearer sk_live_YOUR_API_KEY"

Tags and Segments

The workspace's tags and saved segments, for building a tag list or a filter in your own tool. Both take page and limit (50 a page, 100 at most).

GET/v1/contacts/tags

Every tag of the workspace, A to Z, with how many contacts carry it.

Required scope·contacts:read
response.json
{
 "data": [
 {
 "id": "9c1f...",
 "name": "VIP",
 "color": "#8b5cf6",
 "contactCount": 42,
 "createdAt": "2026-03-02T08:00:00.000Z"
 }
 ],
 "meta": { "total": 12, "page": 1, "limit": 50, "totalPages": 1 }
}
GET/v1/contacts/segments

Every saved segment, newest first. size is the member count at the last sync (lastSyncedAt), not a live count. The rule behind a segment is not returned.

Required scope·contacts:read
response.json
{
 "data": [
 {
 "id": "5a7e...",
 "name": "Pune, last 30 days",
 "size": 318,
 "lastSyncedAt": "2026-10-30T06:00:00.000Z",
 "createdAt": "2026-09-01T10:00:00.000Z",
 "updatedAt": "2026-10-30T06:00:00.000Z"
 }
 ],
 "meta": { "total": 4, "page": 1, "limit": 50, "totalPages": 1 }
}

Block a Contact

POST/v1/contacts/{id}/block

Block a contact across every channel. Requires the contacts:block scope, not contacts:read.

Required scope·contacts:block
WarningBlocking is global and takes effect immediately. The contact disappears from every inbox, their incoming messages stop raising notifications and stop triggering automations, any live bot conversation with them ends, and every outbound send, campaigns, automations, manual replies, and the send endpoints of this API, is refused with 409 CONTACT_BLOCKED. Message history is preserved and unblocking restores everything.

Body Parameters

ParameterTypeRequiredDescription
reasonstringNoUp to 500 chars. Shown in the dashboard and returned in the 409 body of every refused send.
terminal
curl -X POST "https://api.slide.cx/v1/contacts/cnt_abc123/block" \
 -H "Authorization: Bearer sk_live_YOUR_API_KEY" \
 -H "Content-Type: application/json" \
 -d '{"reason": "Spam"}'

Response

response.json
{
 "success": true,
 "contactId": "cnt_abc123",
 "isBlocked": true
}

Idempotent, blocking an already-blocked contact returns 200, never an error, so retries and duplicated webhook deliveries are safe. The id accepts either the unifiedContactId or the id returned by GET /v1/contacts, both resolve to the same contact.

NoteBlocking never stops inbound delivery. Webhooks keep firing and every incoming message is still stored, so nothing is lost and unblocking restores the full thread. What a block changes is that the contact disappears from the inboxes, raises no notification, triggers no automation, and cannot be sent to. Reading a blocked contact's history via GET /v1/whatsapp/conversations/by-phone returns 403 with code CONTACT_BLOCKED, not a 404, so your integration can tell "blocked" apart from "no such conversation".

Unblock a Contact

POST/v1/contacts/{id}/unblock

Restore a blocked contact. Conversations reappear in every inbox with their full history.

Required scope·contacts:block
terminal
curl -X POST "https://api.slide.cx/v1/contacts/cnt_abc123/unblock" \
 -H "Authorization: Bearer sk_live_YOUR_API_KEY"

Contact Notes

Free-text notes on a contact, shown on the contact in the dashboard. A note added with a key has no named author and shows on the contact's history as added by the API.

EndpointScopeWhat it does
GET /v1/contacts/:id/notescontacts:notes:readThe newest 50 notes, newest first.
POST /v1/contacts/:id/notescontacts:notes:writeAdds a note. Body: { "body": "..." }, 1 to 4,000 characters after trimming. Answers 201 with the note.
DELETE /v1/contacts/:id/notes/:noteIdcontacts:notes:writeDeletes a note. Answers 204 with no body.
response.json
// GET /v1/contacts/:id/notes
{
 "data": {
 "notes": [
 {
 "id": "...",
 "body": "Prefers a call after 6 pm",
 "createdAt": "2026-10-20T09:12:00.000Z",
 "updatedAt": "2026-10-20T09:12:00.000Z",
 "author": null
 }
 ]
 }
}

author is { id, name, email } for a note a team member wrote in the dashboard. A contact of another workspace is a 404.

Live Chat Conversations API

Read the live chat conversations of your workspace: the chats from your website and from your own app, with their messages. The same conversations, filters and order as the live chat Inbox in the dashboard. Answer a chat in text, and hand it back to your AI agent when you are done.

NoteThe three reading routes need chat:conversations:read. They keep working when live chat is switched off, so older conversations stay readable. Replying and handing back need chat:conversations:write, and a reply also needs live chat switched on.
WarningA reply is written by the key, not by a person. The visitor sees the name of the API key as the sender, so name the key what you want your customers to see. A reply takes the chat over from any AI agent on it, just as a teammate reply does, and the AI agent stays out until you call POST /v1/conversations/:id/hand-back. A reply does not assign the chat to anyone: a teammate can still be assigned to it the usual way, from the Inbox.

List Conversations

GET/v1/conversations

Conversations, most recent message first, with the same filters as the Inbox.

Required scope·chat:conversations:read

Query Parameters

ParameterTypeDefaultDescription
pagenumber1Page number (1-indexed)
limitnumber20Results per page (max 100)
isOpenstring true for open chats only, false for closed ones only.
assignedToIdstring A teammate id, or the word unassigned.
unreadstring true for chats with messages nobody has read.
needsPersonstring true for chats an AI agent handed to a person that nobody has answered yet.
contactIdstring Only the conversations of one contact (an id from /v1/contacts).
searchstring Text to look for. Up to 200 characters.
terminal
curl "https://api.slide.cx/v1/conversations?isOpen=true&needsPerson=true" \
 -H "Authorization: Bearer sk_live_YOUR_API_KEY"

Response

response.json
{
 "data": [
 {
 "id": "7d2c0b1e-...",
 "source": "website",
 "displayName": "Visitor from Pune",
 "visitor": { "name": "Asha", "email": "asha@example.com", "phone": null },
 "contactId": "4b1d2c3e-...",
 "contact": { "id": "4b1d2c3e-...", "name": "Asha Rao", "email": "asha@example.com", "phone": "+919876543210" },
 "contactClaimedByVisitor": true,
 "lastMessageAt": "2026-10-30T07:41:00.000Z",
 "lastMessageBody": "Can I change my delivery address?",
 "lastMessageDirection": "inbound",
 "unreadCount": 2,
 "isOpen": true,
 "closedAt": null,
 "handledBy": "AGENT",
 "pageUrl": "https://acme.example/orders",
 "pageTitle": "Your orders",
 "assignedToId": null,
 "assignedAt": null,
 "needsPerson": true,
 "handedOffAt": "2026-10-30T07:42:00.000Z",
 "preChatAnswers": { "Order number": "A-1042" },
 "createdAt": "2026-10-30T07:38:00.000Z",
 "updatedAt": "2026-10-30T07:42:00.000Z"
 }
 ],
 "meta": { "total": 1, "page": 1, "limit": 20, "totalPages": 1 }
}

source is website or app. visitor and preChatAnswers are what the visitor typed, and none of it is verified; when contactClaimedByVisitor is true, the link to contact rests only on a phone number or email the visitor gave. handledBy is TEAM, AGENT (an AI agent) or null.

Get a Conversation

GET/v1/conversations/:id

One conversation, in the same shape as a list row plus contactBlocked, visitorBlocked and visitorOnline. The id is a UUID; a conversation of another workspace is a 404.

Required scope·chat:conversations:read

List Messages

GET/v1/conversations/:id/messages

The messages of one conversation, newest first. Takes page and limit (30 a page unless asked, 100 at most).

Required scope·chat:conversations:read
response.json
{
 "data": [
 {
 "id": "b41e...",
 "conversationId": "7d2c0b1e-...",
 "direction": "inbound",
 "senderType": "VISITOR",
 "sender": null,
 "body": "Can I change my delivery address?",
 "readAt": null,
 "createdAt": "2026-10-30T07:41:00.000Z",
 "attachments": [
 { "id": "f02a...", "kind": "image", "mimeType": "image/jpeg", "fileName": "label.jpg", "sizeBytes": 183422, "durationMs": null }
 ]
 }
 ],
 "meta": { "total": 14, "page": 1, "limit": 30, "totalPages": 1 }
}

direction is inbound (from the visitor) or outbound. senderType is VISITOR, USER (a teammate), API_KEY (a reply written with an API key), AGENT (an AI agent) or SYSTEM, and sender names the teammate or agent when there is one. For API_KEY the sender id is null and the name is the name of the key. Attachments say what the file is, never where to download it.

Reply to a Conversation

POST/v1/conversations/:id/reply

Answer a chat in text. The visitor sees the name of the API key as the sender. It takes the chat over from any AI agent on it, until you hand it back. Answers 200.

Required scope·chat:conversations:write
NoteA reply also marks the messages of the visitor as read, reopens a closed chat, shows up in the team Inbox, and is emailed to a visitor who has left the page and gave an email address in the chat. Text only: there is no file upload. The chat stays with you, and out of reach of the AI agent, until it is handed back.

Body Parameters

FieldTypeRequiredDescription
bodystringYesThe reply, 1 to 2,000 characters after trimming. Line breaks and tabs are allowed, other control characters are not.
clientRequestIdstringYes8 to 64 characters of letters, digits, underscore and hyphen. You make it, and reuse it on a retry: one id stores and sends one reply.
terminal
curl -X POST "https://api.slide.cx/v1/conversations/7d2c0b1e-.../reply" \
 -H "Authorization: Bearer sk_live_YOUR_API_KEY" \
 -H "Content-Type: application/json" \
 -d '{"body": "Yes, we can change it. What is the new address?", "clientRequestId": "reply-0001-order-A1042"}'

Response

response.json
{
 "message": {
 "id": "b41f...",
 "conversationId": "7d2c0b1e-...",
 "direction": "outbound",
 "senderType": "API_KEY",
 "sender": { "id": null, "name": "Acme Support Bot" },
 "body": "Yes, we can change it. What is the new address?",
 "readAt": null,
 "createdAt": "2026-10-30T07:45:00.000Z",
 "attachments": []
 },
 "duplicate": false,
 "delivery": { "visitorOnline": true, "email": "NOT_SENT", "reason": "VISITOR_ONLINE" }
}

Sending the same clientRequestId again returns the stored reply with duplicate: true and sends nothing a second time, so a retry after a timeout is safe. delivery.email is QUEUED when the reply will also follow by email, otherwise NOT_SENT with a reason: VISITOR_ONLINE, NO_VISITOR_EMAIL, NO_SENDER, ADDRESS_SUPPRESSED or UNAVAILABLE.

Errors: 400 for an invalid body, 403 when the contact or the visitor is blocked, 404 for a conversation of another workspace, or 404 CHAT_NOT_AVAILABLE when live chat is switched off (nothing is written), and 409 when the visitor has another open conversation to reply in instead.

Hand a Conversation Back

POST/v1/conversations/:id/hand-back

Give the chat back to its AI agent, which answers the next message from the visitor. The same as Hand back to agent in the Inbox. Takes no body. Answers 200.

Required scope·chat:conversations:write
terminal
curl -X POST "https://api.slide.cx/v1/conversations/7d2c0b1e-.../hand-back" \
 -H "Authorization: Bearer sk_live_YOUR_API_KEY"
response.json
{
 "id": "7d2c0b1e-...",
 "handledBy": "agent",
 "agent": { "id": "a1b2c3d4-...", "name": "Maya" },
 "canHandBack": false,
 "handedOffAt": null
}

handledBy says who answers now: agent (an AI agent), person (the team) or none (no AI agent has this chat). canHandBack is true when a hand-back would still change something. With no AI agent on the conversation the call changes nothing and answers a handledBy of none. A conversation of another workspace is a 404.

Messages API

The Messages API is a single, unified endpoint for sending on any channel, WhatsApp, SMS, RCS, Email, or Instagram, through one request contract. Pick a primary channel, optionally add an ordered fallbackChannels chain for phone-based channels, and the API takes care of routing and delivery. This is purely additive: every existing per-channel endpoint documented below (/whatsapp/send-template, /sms/send, /rcs/send, /email/send, /instagram/messages) continues to work exactly as before, unchanged. Use whichever fits your integration: the unified endpoint for cross-channel routing and fallback, or a dedicated endpoint for channel-specific control.

NoteThis endpoint uses the same API key as every other endpoint on this page, no new scope purchase is needed. It dynamically requires the <channel>:send scope for whichever channel(s) you specify in channel and fallbackChannels, for example whatsapp:send together with sms:send for a WhatsApp request with an SMS fallback.

How It Works

  1. 1Every request lists a primary channel and, optionally, an ordered fallbackChannels chain.
  2. 2Before anything is attempted, the API validates that your key holds the send scope for every channel in the chain, and that the required fields for each are present, so you get a fast, cheap 400/403 instead of a partial send.
  3. 3The primary channel is attempted first through the exact same underlying send logic as its dedicated endpoint (same billing, same delivery logs).
  4. 4If it fails, the API moves to the next channel in fallbackChannels, in order, until one succeeds or the chain is exhausted.
  5. 5The response always includes an attempts array, so you can see exactly which channel(s) were tried and why any of them failed, not just the final outcome.

Send a Message (Any Channel)

POST/v1/messages/send

Send a message on any supported channel through one request contract, with an optional ordered fallback chain across phone-based channels.

Required scope·dynamic (per-channel, see above)

Request Body

FieldTypeRequiredDescription
tostringYesRecipient identifier: E.164 phone number (whatsapp/sms/rcs), email address (email), or Instagram IGSID (instagram)
channelstringYesPrimary channel: whatsapp, sms, rcs, email, or instagram
fallbackChannelsstring[]NoOrdered fallback chain, tried in order if the primary channel fails. Phone-based channels only (whatsapp, sms, rcs); cannot be combined with email or instagram
messagestringConditionalFree-text body. Required for instagram, and required for sms (sms always needs a message body)
template.namestringConditionalWhatsApp template name. Required when channel or a fallback channel is whatsapp
template.idstringConditionalRCS or Email template ID. Required for rcs, required for email
template.languageCodestringConditionalWhatsApp language code, e.g. "en". Required for whatsapp
template.variablesobjectNoTemplate merge variables (maps to RCS rcsVariables / Email merge variables)
template.componentsarrayNoAdvanced: raw WhatsApp template components array
sms.senderIdstringConditionalRequired when channel or a fallback channel is sms
sms.routestringNoSMS route override
email.subjectstringNoOverride the template subject
email.fromNamestringConditionalSender display name. Required when channel is email
email.fromEmailstringConditionalSender email address. Required when channel is email
email.replyTostringNoReply-to email address
email.firstNamestringNoMaps to {{first_name}} variable
email.lastNamestringNoMaps to {{last_name}} variable

Example 1: Simple WhatsApp Send

terminal
curl -X POST https://api.slide.cx/v1/messages/send \
 -H "Authorization: Bearer sk_live_YOUR_API_KEY" \
 -H "Content-Type: application/json" \
 -d '{
 "to": "+919876543210",
 "channel": "whatsapp",
 "template": {
 "name": "order_confirmation",
 "languageCode": "en",
 "variables": { "1": "John", "2": "ORD-12345" }
 }
 }'
example.ts
const response = await fetch('https://api.slide.cx/v1/messages/send', {
 method: 'POST',
 headers: {
 'Authorization': 'Bearer sk_live_YOUR_API_KEY',
 'Content-Type': 'application/json',
 },
 body: JSON.stringify({
 to: '+919876543210',
 channel: 'whatsapp',
 template: {
 name: 'order_confirmation',
 languageCode: 'en',
 variables: { '1': 'John', '2': 'ORD-12345' },
 },
 }),
});

const data = await response.json();
// { status: "sent", channelUsed: "whatsapp", messageId: "wamid.HBg...", attempts: [...] }

Example 2: WhatsApp with SMS Fallback

terminal
curl -X POST https://api.slide.cx/v1/messages/send \
 -H "Authorization: Bearer sk_live_YOUR_API_KEY" \
 -H "Content-Type: application/json" \
 -d '{
 "to": "+919876543210",
 "channel": "whatsapp",
 "fallbackChannels": ["sms"],
 "message": "Your order ORD-12345 has shipped!",
 "template": {
 "name": "order_shipped",
 "languageCode": "en"
 },
 "sms": { "senderId": "ACMEIN" }
 }'
example.py
import requests

response = requests.post(
 'https://api.slide.cx/v1/messages/send',
 headers={
 'Authorization': 'Bearer sk_live_YOUR_API_KEY',
 'Content-Type': 'application/json',
 },
 json={
 'to': '+919876543210',
 'channel': 'whatsapp',
 'fallbackChannels': ['sms'],
 'message': 'Your order ORD-12345 has shipped!',
 'template': {
 'name': 'order_shipped',
 'languageCode': 'en',
 },
 'sms': { 'senderId': 'ACMEIN' },
 }
)

data = response.json()
# {'status': 'sent', 'channelUsed': 'whatsapp', 'messageId': 'wamid.HBg...', 'attempts': [...]}

Response

response.json
{
 "status": "sent",
 "channelUsed": "whatsapp",
 "messageId": "wamid.HBg...",
 "attempts": [
 { "channel": "whatsapp", "status": "sent", "messageId": "wamid.HBg..." }
 ]
}

Error Responses

StatusErrorDescription
400Missing required field(s)Lists exactly which field(s) are missing for which channel, e.g. template.id, email.fromEmail
403Insufficient scopeThe API key is missing the <channel>:send scope required for the requested channel(s)
502All channels failed to deliver the messageThe primary channel and every fallback channel in the chain failed; see the attempts array for the per-channel error
response.json
// 400: missing required field(s)
{
 "message": "Missing required field(s) for channel \"email\": template.id, email.fromEmail"
}

// 403: insufficient scope
{
 "message": "Insufficient scope. Required: whatsapp:send"
}

// 502: all channels failed
{
 "message": "All channels failed to deliver the message",
 "attempts": [
 { "channel": "whatsapp", "status": "failed", "error": "..." },
 { "channel": "sms", "status": "failed", "error": "..." }
 ]
}

Email API

The Email API lets you send transactional emails and manage templates programmatically. All email operations are scoped per organization.

Send Email

POST/v1/email/send

Send a transactional email using a pre-built template. Raw HTML is not accepted create templates in the dashboard and reference them by template ID. Each send deducts 1 email credit from your wallet. A Recommended products block in the template is removed here, because its picks are written per person when a campaign is sent.

Required scope·email:send

Request Body

FieldTypeRequiredDescription
templateIdstringYesTemplate ID (get from GET /email/templates or the dashboard Templates page)
recipient.tostringYesRecipient email address
recipient.firstNamestringNoMaps to {{first_name}} variable
recipient.lastNamestringNoMaps to {{last_name}} variable
variablesobjectNoKey-value pairs for template variables, e.g. {"company": "Acme"}
subjectstringNoOverride the template subject (uses template subject if omitted)
fromNamestringYesSender display name
fromEmailstringYesSender email (must match verified SMTP or domain)
replyTostringNoReply-to email address

Important: Raw HTML is not supported in the API. Create your email templates in the dashboard (Email > Templates) using the visual editor or HTML code editor, then use the template ID here. This ensures all emails are tracked, branded, and include required unsubscribe links. Each send deducts 1 email credit from your wallet balance.

terminal
curl -X POST https://api.slide.cx/v1/email/send \
 -H "Authorization: Bearer sk_live_YOUR_API_KEY" \
 -H "Content-Type: application/json" \
 -d '{
 "templateId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
 "recipient": {
 "to": "user@example.com",
 "firstName": "John"
 },
 "variables": {
 "company": "Acme Corp",
 "order_id": "ORD-12345"
 },
 "fromName": "Your Company",
 "fromEmail": "hello@yourdomain.com"
 }'
example.ts
const response = await fetch('https://api.slide.cx/v1/email/send', {
 method: 'POST',
 headers: {
 'Authorization': 'Bearer sk_live_YOUR_API_KEY',
 'Content-Type': 'application/json',
 },
 body: JSON.stringify({
 templateId: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
 recipient: {
 to: 'user@example.com',
 firstName: 'John',
 },
 variables: {
 company: 'Acme Corp',
 order_id: 'ORD-12345',
 },
 fromName: 'Your Company',
 fromEmail: 'hello@yourdomain.com',
 }),
});

const data = await response.json();
// { messageId: "msg_01J...", status: "sent", to: "user@example.com",
// templateId: "a1b2c3d4...", templateName: "Order Confirmation" }
example.py
import requests

response = requests.post(
 'https://api.slide.cx/v1/email/send',
 headers={
 'Authorization': 'Bearer sk_live_YOUR_API_KEY',
 'Content-Type': 'application/json',
 },
 json={
 'templateId': 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
 'recipient': {
 'to': 'user@example.com',
 'firstName': 'John',
 },
 'variables': {
 'company': 'Acme Corp',
 'order_id': 'ORD-12345',
 },
 'fromName': 'Your Company',
 'fromEmail': 'hello@yourdomain.com',
 }
)

data = response.json()
# {'messageId': 'msg_01J...', 'status': 'sent', 'to': 'user@example.com',
# 'templateId': 'a1b2c3d4...', 'templateName': 'Order Confirmation'}

Response

response.json
{
 "messageId": "msg_01JXYZ...",
 "status": "sent",
 "to": "user@example.com",
 "templateId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
 "templateName": "Order Confirmation"
}

Error Responses

StatusErrorDescription
400Template not foundThe templateId does not exist or does not belong to your organization
400Insufficient email creditsYour wallet has no email credits remaining. Purchase more in Settings > Wallet
400No verified SMTP connectionSet up and verify an SMTP connection first in Email > SMTP
400Template has no HTML contentThe template is empty. Edit it in the dashboard first

List Email Templates

GET/v1/email/templates

Retrieve all email templates for the organization.

Required scope·email:templates:read
terminal
curl https://api.slide.cx/v1/email/templates \
 -H "Authorization: Bearer sk_live_YOUR_API_KEY"
example.ts
const res = await fetch('https://api.slide.cx/v1/email/templates', {
 headers: { 'Authorization': 'Bearer sk_live_YOUR_API_KEY' },
});
const { data, meta } = await res.json();

Response

response.json
{
 "data": [
 {
 "id": "tpl_01JXYZ...",
 "name": "Welcome Email",
 "subject": "Welcome to {{company_name}}",
 "createdAt": "2026-01-15T09:00:00.000Z",
 "updatedAt": "2026-02-10T12:30:00.000Z"
 }
 ],
 "meta": {
 "total": 1,
 "page": 1,
 "limit": 20,
 "totalPages": 1
 }
}

Get Email Template

GET/v1/email/templates/:id

Retrieve a single email template by its ID, including the full HTML body.

Required scope·email:templates:read
terminal
curl https://api.slide.cx/v1/email/templates/tpl_01JXYZ \
 -H "Authorization: Bearer sk_live_YOUR_API_KEY"
example.ts
const res = await fetch('https://api.slide.cx/v1/email/templates/tpl_01JXYZ', {
 headers: { 'Authorization': 'Bearer sk_live_YOUR_API_KEY' },
});
const template = await res.json();

Response

response.json
{
 "id": "tpl_01JXYZ...",
 "name": "Welcome Email",
 "subject": "Welcome to {{company_name}}",
 "html": "<h1>Welcome!</h1><p>Thanks for joining {{company_name}}.</p>",
 "text": "Welcome! Thanks for joining {{company_name}}.",
 "createdAt": "2026-01-15T09:00:00.000Z",
 "updatedAt": "2026-02-10T12:30:00.000Z"
}

List Email Contacts

GET/v1/email/contacts

List the organization's email contacts with pagination and optional search.

Required scope·email:contacts:read

Query Parameters

ParameterTypeDefaultDescription
pagenumber1Page number (1-indexed)
limitnumber50Results per page (max 100)
searchstring Optional search by name or email
terminal
curl "https://api.slide.cx/v1/email/contacts?page=1&limit=50" \
 -H "Authorization: Bearer sk_live_YOUR_API_KEY"

Response

response.json
{
 "data": [
 {
 "id": "ec_01JXYZ...",
 "email": "john@example.com",
 "firstName": "John",
 "lastName": "Doe",
 "subscribed": true,
 "createdAt": "2026-02-01T09:00:00.000Z"
 }
 ],
 "meta": { "total": 312, "page": 1, "limit": 50, "totalPages": 7 }
}

Create Email Contact

POST/v1/email/contacts

Create a new email contact. Returns 409 Conflict if a contact with the same email already exists.

Required scope·email:contacts:write

Request Body

FieldTypeRequiredDescription
emailstringYesContact email address
firstNamestringNoFirst name
lastNamestringNoLast name
terminal
curl -X POST https://api.slide.cx/v1/email/contacts \
 -H "Authorization: Bearer sk_live_YOUR_API_KEY" \
 -H "Content-Type: application/json" \
 -d '{ "email": "john@example.com", "firstName": "John", "lastName": "Doe" }'

Response

response.json
{
 "id": "ec_01JXYZ...",
 "email": "john@example.com",
 "firstName": "John",
 "lastName": "Doe",
 "subscribed": true,
 "createdAt": "2026-04-08T12:30:00.000Z"
}

Instagram API

The Instagram API provides access to your connected Instagram Business account including profile data, conversations, message sending, and analytics. Requires an active Instagram integration in your organization.

NoteInstagram API endpoints require an active Instagram Business account connection in Dashboard → Integrations → Instagram. If no account is connected, requests return 404 Not Found.

Get Instagram Profile

GET/v1/instagram/profile

Retrieve your connected Instagram Business account profile information.

Required scope·instagram:messages:read
terminal
curl https://api.slide.cx/v1/instagram/profile \
 -H "Authorization: Bearer sk_live_YOUR_API_KEY"
example.ts
const res = await fetch('https://api.slide.cx/v1/instagram/profile', {
 headers: { 'Authorization': 'Bearer sk_live_YOUR_API_KEY' },
});
const profile = await res.json();

Response

response.json
{
 "id": "17841400455970661",
 "username": "yourbusiness",
 "name": "Your Business Name",
 "biography": "Official business account",
 "followersCount": 12450,
 "followsCount": 310,
 "mediaCount": 89,
 "profilePictureUrl": "https://example.com/profile.jpg",
 "website": "https://yourbusiness.com"
}

List Conversations

GET/v1/instagram/conversations

Retrieve a paginated list of Instagram DM conversations.

Required scope·instagram:messages:read

Query Parameters

ParameterTypeDefaultDescription
pagenumber1Page number (1-indexed)
limitnumber20Conversations per page (max 50)
terminal
curl "https://api.slide.cx/v1/instagram/conversations?page=1&limit=20" \
 -H "Authorization: Bearer sk_live_YOUR_API_KEY"
example.py
import requests

res = requests.get(
 'https://api.slide.cx/v1/instagram/conversations',
 headers={'Authorization': 'Bearer sk_live_YOUR_API_KEY'},
 params={'page': 1, 'limit': 20}
)
data = res.json()

Response

response.json
{
 "data": [
 {
 "id": "conv_01JXYZ...",
 "senderId": "112233445566778",
 "senderUsername": "johndoe",
 "senderName": "John Doe",
 "lastMessage": "Thanks for the info!",
 "lastMessageAt": "2026-03-24T08:15:00.000Z",
 "unreadCount": 2
 }
 ],
 "meta": {
 "total": 87,
 "page": 1,
 "limit": 20,
 "totalPages": 5
 }
}

Send Instagram Message

POST/v1/instagram/messages

Send a direct message to an Instagram user by their page-scoped IGSID.

Required scope·instagram:messages:send
WarningThe recipientIgsid must be a valid page-scoped Instagram user ID (IGSID), not a username. You can retrieve IGSIDs from GET /v1/instagram/conversations or via the Instagram inbox.

Request Body

FieldTypeRequiredDescription
recipientIgsidstringYesPage-scoped Instagram user ID (IGSID)
messagestringYesMessage text content (max 1000 chars)
terminal
curl -X POST https://api.slide.cx/v1/instagram/messages \
 -H "Authorization: Bearer sk_live_YOUR_API_KEY" \
 -H "Content-Type: application/json" \
 -d '{
 "recipientIgsid": "112233445566778",
 "message": "Hi! Thanks for reaching out. How can we help you today?"
 }'
example.ts
const res = await fetch('https://api.slide.cx/v1/instagram/messages', {
 method: 'POST',
 headers: {
 'Authorization': 'Bearer sk_live_YOUR_API_KEY',
 'Content-Type': 'application/json',
 },
 body: JSON.stringify({
 recipientIgsid: '112233445566778',
 message: 'Hi! Thanks for reaching out. How can we help you today?',
 }),
});
const result = await res.json();
// { messageId: 'igmsg_01JXYZ...', status: 'sent' }
example.py
import requests

res = requests.post(
 'https://api.slide.cx/v1/instagram/messages',
 headers={
 'Authorization': 'Bearer sk_live_YOUR_API_KEY',
 'Content-Type': 'application/json',
 },
 json={
 'recipientIgsid': '112233445566778',
 'message': 'Hi! Thanks for reaching out.',
 }
)
print(res.json()) # {'messageId': 'igmsg_01JXYZ...', 'status': 'sent'}

Get Instagram Insights

GET/v1/instagram/insights

Retrieve Instagram account analytics and performance metrics.

Required scope·instagram:insights:read
terminal
curl https://api.slide.cx/v1/instagram/insights \
 -H "Authorization: Bearer sk_live_YOUR_API_KEY"
example.ts
const res = await fetch('https://api.slide.cx/v1/instagram/insights', {
 headers: { 'Authorization': 'Bearer sk_live_YOUR_API_KEY' },
});
const insights = await res.json();

Response

response.json
{
 "followerCount": 12450,
 "followsDelta": 23,
 "reach": 4821,
 "profileViews": 1340,
 "impressions": 9123,
 "websiteClicks": 89,
 "period": "last_7_days",
 "updatedAt": "2026-03-24T00:00:00.000Z"
}

Ads API

Read the ads account your workspace connected to Slide: its accounts, campaigns, ad sets, ads, results and lead-form leads. Make a campaign, and start, pause or resize spend. These are the same figures and the same changes as the Ads page of the dashboard.

NoteYour workspace needs the ads integration on its plan, and an ads account connected under Dashboard → Integrations. Without a plan that includes it every route answers 403. Without a connected account every route but GET /v1/ads/status answers 409 ADS_NOT_CONNECTED. Reading needs ads:read, making a campaign needs ads:write, and changing live spend needs ads:publish.
WarningStarting an ad, pausing it and changing a budget change what your account spends, and they take effect on the ad platform at once. That is why they need their own scope, ads:publish: a key that can only make a campaign can never start or resize spending. Nothing in this API deletes or archives an ad, and every new campaign is created paused.

List routes take page and limit (50 a page, 100 at most) and answer { data, meta: { total, page, limit, totalPages } }. Ids are the ones the ad platform issues: an ad account id is act_ and digits, every other id is digits. Budgets are in minor units (paise or cents).

Connection Status

GET/v1/ads/status

Whether an ads account is connected, as whom, and when the sign-in expires. Works before anything is connected.

Required scope·ads:read
response.json
{
 "connected": true,
 "connectedAs": "Priya Shah",
 "tokenExpiresAt": "2026-12-20T09:00:00.000Z",
 "lastRefreshedAt": "2026-10-30T06:00:00.000Z",
 "adAccountCount": 2,
 "pageCount": 3
}

List Ad Accounts

GET/v1/ads/accounts

The ad accounts of the connected login. insightRows says how many days of results Slide holds for each, so you can open on an account that has some.

Required scope·ads:read
response.json
{
 "data": [
 {
 "id": "act_1234567890",
 "name": "Acme Store",
 "currency": "INR",
 "timezoneName": "Asia/Kolkata",
 "accountStatus": 1,
 "businessName": "Acme Pvt Ltd",
 "syncEnabled": true,
 "insightRows": 412,
 "newestDataAt": "2026-10-30T00:00:00.000Z",
 "rowsInWindow": 87
 }
 ],
 "meta": { "total": 2, "page": 1, "limit": 50, "totalPages": 1 }
}

rowsInWindow counts result rows in the last 30 days. A positive newestDataAt with rowsInWindow of 0 means the account is quiet, not broken.

List Campaigns

GET/v1/ads/accounts/:adAccountId/campaigns

The campaigns of one ad account. Budgets, bids and the spend cap are minor units.

Required scope·ads:read
terminal
curl "https://api.slide.cx/v1/ads/accounts/act_1234567890/campaigns?limit=20" \
 -H "Authorization: Bearer sk_live_YOUR_API_KEY"
response.json
{
 "data": [
 {
 "id": "120210000000001",
 "name": "Diwali sale",
 "status": "ACTIVE",
 "effectiveStatus": "ACTIVE",
 "objective": "OUTCOME_LEADS",
 "buyingType": "AUCTION",
 "bidStrategy": "LOWEST_COST_WITHOUT_CAP",
 "dailyBudgetMinor": 50000,
 "lifetimeBudgetMinor": null,
 "budgetRemainingMinor": 31200,
 "spendCapMinor": null,
 "startTime": "2026-10-20T00:00:00+0530",
 "stopTime": null,
 "createdTime": "2026-10-19T11:02:00+0530",
 "updatedTime": "2026-10-29T18:40:00+0530"
 }
 ],
 "meta": { "total": 9, "page": 1, "limit": 20, "totalPages": 1 }
}

List Ad Sets

GET/v1/ads/campaigns/:campaignId/adsets

The ad sets of one campaign: budget, bid, what they optimise for, and their dates.

Required scope·ads:read

Each row has id, name, campaignId, status, effectiveStatus, dailyBudgetMinor, lifetimeBudgetMinor, budgetRemainingMinor, bidAmountMinor, bidStrategy, billingEvent, optimizationGoal, destinationType, startTime, endTime, createdTime and updatedTime. A value the platform does not send is null.

List Ads

GET/v1/ads/adsets/:adsetId/ads

The ads of one ad set, each with its creative when it has one.

Required scope·ads:read
response.json
{
 "data": [
 {
 "id": "120210000000003",
 "name": "Carousel A",
 "adsetId": "120210000000002",
 "campaignId": "120210000000001",
 "status": "ACTIVE",
 "effectiveStatus": "ACTIVE",
 "creative": {
 "id": "120210000000009",
 "name": "Carousel A creative",
 "title": "Festive offers",
 "body": "Up to 30% off this week",
 "callToActionType": "SHOP_NOW",
 "imageUrl": "https://...",
 "thumbnailUrl": "https://..."
 },
 "createdTime": "2026-10-19T11:05:00+0530",
 "updatedTime": "2026-10-29T18:40:00+0530"
 }
 ],
 "meta": { "total": 3, "page": 1, "limit": 50, "totalPages": 1 }
}

Results by Day

GET/v1/ads/insights

Daily results from the figures Slide holds, oldest day first, at the level you ask for. With no dates it is the last 30 days.

Required scope·ads:read

Query Parameters

ParameterTypeDefaultDescription
levelstringcampaignaccount, campaign, adset or ad.
sinceISO 8601 date30 days before untilFirst day.
untilISO 8601 datenowLast day.
adAccountIdstring Only this ad account, for example act_1234567890.
campaignIdstring Only this campaign.
response.json
{
 "data": [
 {
 "level": "campaign",
 "adAccountId": "act_1234567890",
 "campaignId": "120210000000001",
 "campaignName": "Diwali sale",
 "adsetId": null,
 "adsetName": null,
 "adId": null,
 "adName": null,
 "dateStart": "2026-10-29T00:00:00.000Z",
 "dateStop": "2026-10-29T00:00:00.000Z",
 "impressions": 18420,
 "reach": 15210,
 "frequency": "1.21",
 "clicks": 530,
 "inlineLinkClicks": 412,
 "uniqueClicks": 498,
 "spend": "4120.55",
 "cpc": "7.77",
 "cpm": "223.69",
 "ctr": "2.88",
 "leads": 34,
 "costPerLead": "121.19",
 "videoViews": 0,
 "postEngagement": 96,
 "revenue": null,
 "purchases": null
 }
 ],
 "meta": {
 "since": "2026-10-01T00:00:00.000Z",
 "until": "2026-10-30T00:00:00.000Z",
 "returned": 30,
 "truncated": false,
 "cap": 5000
 }
}

Counts are numbers; money and ratios (spend, frequency, cpc, cpm, ctr, costPerLead) are strings in the account's currency, so no digit is lost. revenue and purchases are null when the ad platform reported no purchases, never 0. One answer holds at most 5,000 rows; when it was cut, meta.truncated is true, so narrow the dates.

Results Summary

GET/v1/ads/insights/summary

One set of totals and ratios over a date range, from the campaign rows. Takes since, until and adAccountId, like the daily results. With no adAccountId it adds up every ad account of the workspace.

Required scope·ads:read
response.json
{
 "data": {
 "revenue": null,
 "purchases": null,
 "roas": null,
 "impressions": 512300,
 "reach": 301200,
 "clicks": 14230,
 "leads": 880,
 "spend": "98210.40",
 "ctr": "2.78",
 "cpc": "6.90",
 "cpl": "111.60",
 "cpm": "191.71"
 },
 "meta": { "since": "2026-10-01T00:00:00.000Z", "until": "2026-10-30T00:00:00.000Z" }
}

revenue, purchases and roas are null unless the ad platform reported purchases. cpc, cpl and cpm are null when there is nothing to divide by.

Recent Leads

GET/v1/ads/leads

The newest lead-form submissions from your ads, newest first. Takes limit only (20 unless asked, 100 at most); there is no paging.

Required scope·ads:read
response.json
{
 "data": [
 {
 "id": "ev_...",
 "occurredAt": "2026-10-30T07:41:00.000Z",
 "contactId": "4b1d2c3e-...",
 "phone": "+919876543210",
 "email": "john@example.com",
 "fullName": "John Doe",
 "formId": "5550001",
 "formName": "Diwali offer form",
 "campaignId": "120210000000001",
 "campaignName": "Diwali sale",
 "adsetId": "120210000000002",
 "adsetName": "Pune 25-40",
 "adId": "120210000000003",
 "adName": "Carousel A",
 "pageName": "Acme Store",
 "platform": "facebook",
 "status": null
 }
 ]
}

The lead is also a contact: contactId opens it with GET /v1/contacts/:id. Any field the form did not carry is null.

Create a Campaign

POST/v1/ads/accounts/:adAccountId/campaigns

Create a campaign in one ad account. It is ALWAYS created paused, with no way to ask otherwise: starting it is a separate call that needs ads:publish. Answers 201.

Required scope·ads:write

Body Parameters

FieldTypeRequiredDescription
namestringYesUp to 256 characters.
objectivestringNoUpper-case words joined by an underscore, for example OUTCOME_LEADS. Default OUTCOME_TRAFFIC.
specialAdCategoriesstring[]NoAt most 10, upper-case words joined by an underscore. The ad platform requires them for housing, credit, employment and the like.
terminal
curl -X POST "https://api.slide.cx/v1/ads/accounts/act_1234567890/campaigns" \
 -H "Authorization: Bearer sk_live_YOUR_API_KEY" \
 -H "Content-Type: application/json" \
 -d '{"name": "Winter launch", "objective": "OUTCOME_LEADS"}'
response.json
{ "id": "120210000000042", "status": "PAUSED" }

Start or Pause

POST/v1/ads/:entity/:id/status

Start (ACTIVE) or pause (PAUSED) a campaign, an ad set or an ad. :entity is campaign, adset or ad; :id is its numeric id. This changes live spend.

Required scope·ads:publish
WarningACTIVE makes the ad platform start spending against the budget at once. Pausing stops it. Neither deletes anything, and the API cannot delete or archive an ad.

Body Parameters

FieldTypeRequiredDescription
statusstringYesACTIVE or PAUSED, in any case.
terminal
curl -X POST "https://api.slide.cx/v1/ads/campaign/120210000000042/status" \
 -H "Authorization: Bearer sk_live_YOUR_API_KEY" \
 -H "Content-Type: application/json" \
 -d '{"status": "PAUSED"}'
response.json
{ "id": "120210000000042", "status": "PAUSED", "previousStatus": "ACTIVE", "changed": true }

Asking for the status the object already has changes nothing and answers changed: false, so a retried call is safe.

Change a Budget

POST/v1/ads/:entity/:id/budget

Change the daily or the lifetime budget of a campaign or an ad set, in minor units (paise or cents). Send exactly one of dailyMinor and lifetimeMinor. This changes live spend.

Required scope·ads:publish

Body Parameters

FieldTypeRequiredDescription
dailyMinorintegerOne of the twoThe new daily budget, at least 100 minor units.
lifetimeMinorintegerOne of the twoThe new lifetime budget, at least 1.
terminal
curl -X POST "https://api.slide.cx/v1/ads/campaign/120210000000042/budget" \
 -H "Authorization: Bearer sk_live_YOUR_API_KEY" \
 -H "Content-Type: application/json" \
 -d '{"dailyMinor": 75000}'
response.json
{ "id": "120210000000042", "budget": "daily", "previousMinor": 50000, "newMinor": 75000 }

Sending both fields, or neither, is a 400, because the ad platform keeps one budget type and ignores the other. previousMinor is what the budget was a moment before, or null if it had none. Each change is recorded against the key that made it.

Read the Google Ads account your workspace connected to Slide: its customers, campaigns, ad groups, ads, conversion actions, daily results and lead forms. Add a lead form, or remove one. These are the same figures as the Google Ads page of the dashboard.

NoteYour workspace needs the Google Ads integration on its plan, and a Google Ads account connected under Dashboard → Integrations. Without a plan that includes it every route answers 403. Without a connected account every route but GET /v1/google-ads/status answers 409 ADS_NOT_CONNECTED. Reading needs ads:google:read, and adding or removing a lead form needs ads:google:write. These are not the scopes of the Ads API above, which is for Meta Ads.
NoteNothing in this API changes a campaign or what your account spends. The two changes only add or remove the place where the leads of a lead form arrive in Slide. The key of a lead form is never returned, and cannot be read or rotated with an API key: whoever pastes it into Google Ads reads it in Slide, under Integrations. The connect and disconnect steps are also done in Slide, by the person who signs in to Google.

List routes take page and limit (50 a page, 100 at most) and answer { data, meta: { total, page, limit, totalPages } }. A customer id is the 10 digits Google issues, without dashes; a customer that is not one of the linked accounts of your workspace is a 404. Other Google ids are digits. Money is in full units of the currency of the account, not in minor units.

Connection Status

GET/v1/google-ads/status

Whether a Google Ads account is connected, as whom, and when the sign-in expires. Works before anything is connected.

Required scope·ads:google:read
response.json
{
 "connected": true,
 "connectedAs": "priya@acme.example",
 "tokenExpiresAt": "2026-12-20T09:00:00.000Z",
 "lastRefreshedAt": "2026-10-30T06:00:00.000Z",
 "customerCount": 2,
 "leadFormCount": 1
}

List Customers

GET/v1/google-ads/customers

The Google Ads customers (accounts) linked to your workspace. Manager accounts come first, then by name.

Required scope·ads:google:read
terminal
curl "https://api.slide.cx/v1/google-ads/customers" \
 -H "Authorization: Bearer sk_live_YOUR_API_KEY"
response.json
{
 "data": [
 {
 "customerId": "1234567890",
 "name": "Acme Store",
 "currencyCode": "INR",
 "timeZone": "Asia/Kolkata",
 "isManager": false,
 "isTestAccount": false
 }
 ],
 "meta": { "total": 1, "page": 1, "limit": 50, "totalPages": 1 }
}

List Campaigns

GET/v1/google-ads/customers/:customerId/campaigns

The campaigns of one customer, by name. dailyBudget is in full units of the currency of the account.

Required scope·ads:google:read
response.json
{
 "data": [
 {
 "id": "20100000001",
 "name": "Winter sale",
 "status": "ENABLED",
 "channelType": "SEARCH",
 "startDate": "2026-10-01",
 "endDate": null,
 "dailyBudget": 500
 }
 ],
 "meta": { "total": 1, "page": 1, "limit": 50, "totalPages": 1 }
}

List Ad Groups

GET/v1/google-ads/customers/:customerId/ad-groups

The ad groups of one customer, by name. Add campaignId (digits) to see the ad groups of one campaign only.

Required scope·ads:google:read
response.json
{
 "data": [
 {
 "id": "30200000001",
 "name": "Winter boots",
 "status": "ENABLED",
 "type": "SEARCH_STANDARD",
 "campaignId": "20100000001",
 "campaignName": "Winter sale"
 }
 ],
 "meta": { "total": 1, "page": 1, "limit": 50, "totalPages": 1 }
}

List Ads

GET/v1/google-ads/customers/:customerId/ads

The ads of one customer, by name. Add adGroupId (digits) to see the ads of one ad group only.

Required scope·ads:google:read
response.json
{
 "data": [
 {
 "id": "40300000001",
 "name": "Winter boots, free delivery",
 "type": "RESPONSIVE_SEARCH_AD",
 "status": "ENABLED",
 "adGroupId": "30200000001",
 "adGroupName": "Winter boots"
 }
 ],
 "meta": { "total": 1, "page": 1, "limit": 50, "totalPages": 1 }
}

List Conversion Actions

GET/v1/google-ads/customers/:customerId/conversion-actions

The conversion actions of one customer, by name: what Google Ads counts as a result.

Required scope·ads:google:read
response.json
{
 "data": [
 { "id": "50400000001", "name": "Lead form submit", "type": "GOOGLE_HOSTED", "category": "SUBMIT_LEAD_FORM" }
 ],
 "meta": { "total": 1, "page": 1, "limit": 50, "totalPages": 1 }
}

Results by Day

GET/v1/google-ads/insights

The daily figures Slide has stored, oldest day first, at the level you ask for. Money is in full units of the currency of the customer.

Required scope·ads:google:read

Query Parameters

ParameterTypeDefaultDescription
customerIdstring Only one customer (10 digits). Without it, every linked customer.
levelstring customer, campaign, ad_group or ad.
startDatestring First day, ISO 8601 (for example 2026-10-01).
endDatestring Last day, ISO 8601.
pagenumber1Page number (1-indexed, up to 200).
limitnumber100Rows per page (max 500).
terminal
curl "https://api.slide.cx/v1/google-ads/insights?customerId=1234567890&level=campaign&startDate=2026-10-01&endDate=2026-10-07" \
 -H "Authorization: Bearer sk_live_YOUR_API_KEY"
response.json
{
 "data": [
 {
 "level": "campaign",
 "customerId": "1234567890",
 "campaignId": "20100000001",
 "campaignName": "Winter sale",
 "adGroupId": null,
 "adGroupName": null,
 "adId": null,
 "adName": null,
 "date": "2026-10-01T00:00:00.000Z",
 "currencyCode": "INR",
 "impressions": 4210,
 "clicks": 188,
 "cost": "2350.4",
 "averageCpc": "12.5",
 "averageCpm": "558.3",
 "ctr": "0.044655",
 "conversions": "9",
 "conversionsValue": "13500",
 "costPerConversion": "261.155556",
 "leads": 9,
 "costPerLead": "261.155556",
 "videoViews": 0,
 "engagements": 0
 }
 ],
 "meta": { "total": 1, "page": 1, "limit": 100, "totalPages": 1 }
}

Counts (impressions, clicks, leads, videoViews, engagements) are numbers. Money and ratios are strings, so no digit is lost. The ids and names that do not belong to the level you asked for are null. Slide holds the days it has fetched, so a day that is not stored yet is not in the answer.

List Lead Forms

GET/v1/google-ads/lead-forms

The lead forms added to Slide, newest first, with how many leads each brought. Never the key of a form.

Required scope·ads:google:read
response.json
{
 "data": [
 {
 "id": "9c1e2f3a-...",
 "formAssetId": "6700000001",
 "formName": "Winter sale enquiry",
 "campaignId": "20100000001",
 "campaignName": "Winter sale",
 "customerId": "1234567890",
 "customerName": "Acme Store",
 "leadCount": 14,
 "lastLeadAt": "2026-10-30T07:10:00.000Z",
 "isActive": true,
 "createdAt": "2026-10-02T09:00:00.000Z"
 }
 ],
 "meta": { "total": 1, "page": 1, "limit": 50, "totalPages": 1 }
}

The leads themselves arrive as contacts in your workspace, so read them with GET /v1/contacts.

Add a Lead Form

POST/v1/google-ads/lead-forms

Add a lead form to Slide, so its leads arrive in your workspace. Every field is optional. Answers 201 with the form as the list shows it.

Required scope·ads:google:write

Body Parameters

FieldTypeRequiredDescription
formAssetIdstringNoThe id of the lead form in Google Ads. Up to 64 characters.
formNamestringNoA name to recognise it by. Up to 200 characters.
campaignIdstringNoThe campaign it belongs to. Up to 64 characters.
campaignNamestringNoThe name of that campaign. Up to 200 characters.
customerIdstringNoOne of your linked customers (10 digits, from GET /v1/google-ads/customers).
terminal
curl -X POST "https://api.slide.cx/v1/google-ads/lead-forms" \
 -H "Authorization: Bearer sk_live_YOUR_API_KEY" \
 -H "Content-Type: application/json" \
 -d '{"formName": "Winter sale enquiry", "customerId": "1234567890"}'

The answer has the same fields as a row of the list, with leadCount at 0. Slide makes the key of the form and does not return it: the person who pastes it into the lead form in Google Ads reads it in Slide, under Integrations. A customerId that is not one of your linked customers is a 404.

Remove a Lead Form

DELETE/v1/google-ads/lead-forms/:id

Remove a lead form from Slide. The leads it already brought stay as contacts. Answers 204 with no body. The id is a UUID; a form of another workspace is a 404.

Required scope·ads:google:write
terminal
curl -X DELETE "https://api.slide.cx/v1/google-ads/lead-forms/9c1e2f3a-..." \
 -H "Authorization: Bearer sk_live_YOUR_API_KEY"

WhatsApp API

Send WhatsApp template messages, retrieve message logs, and list approved templates. All messages are sent through your connected WhatsApp Business account and billed per Meta's category-based pricing (Marketing, Utility, Authentication, Service).

NoteWhatsApp requires a connected WhatsApp Business account. Connect yours in Dashboard → WhatsApp. Only approved templates can be sent via the API use the dashboard to create and submit templates for approval.

Send Template Message

POST/v1/whatsapp/send-template

Send an approved WhatsApp template message to a phone number. Supports all template types: text-only, media headers, quick-reply buttons, call-to-action buttons, and dynamic variables.

Required scope·whatsapp:send

Request Body

FieldTypeRequiredDescription
tostringYesRecipient phone number in E.164 format without + (e.g. "919876543210")
templateNamestringYesName of the approved template (e.g. "order_confirmation")
languageCodestringYesTemplate language code (e.g. "en", "en_US", "hi")
componentsarrayNoTemplate components with dynamic variables (Meta Cloud API format)
asyncbooleanNotrue answers 202 with { "requestId", "status": "queued" } before the message is handed to WhatsApp; read the outcome with GET /v1/whatsapp/send-status/:requestId. Default false: 200 with the wamid.

Components Format

Components follow the Meta Cloud API format. Each component specifies a type and parameters:

Component TypeUsageExample
headerImage, video, or document header{ "type": "header", "parameters": [{ "type": "image", "image": { "link": "https://..." } }] }
bodyDynamic variables in the template body{ "type": "body", "parameters": [{ "type": "text", "text": "John" }] }
buttonDynamic URL suffix or copy code{ "type": "button", "sub_type": "url", "index": "0", "parameters": [{ "type": "text", "text": "ORDER123" }] }
terminal
# Send a simple text template with one body variable
curl -X POST https://api.slide.cx/v1/whatsapp/send-template \
 -H "Authorization: Bearer sk_live_YOUR_API_KEY" \
 -H "Content-Type: application/json" \
 -d '{
 "to": "919876543210",
 "templateName": "order_confirmation",
 "languageCode": "en",
 "components": [
 {
 "type": "body",
 "parameters": [
 { "type": "text", "text": "John" },
 { "type": "text", "text": "ORD-12345" },
 { "type": "text", "text": "₹2,499" }
 ]
 }
 ]
 }'
example.ts
const response = await fetch('https://api.slide.cx/v1/whatsapp/send-template', {
 method: 'POST',
 headers: {
 'Authorization': 'Bearer sk_live_YOUR_API_KEY',
 'Content-Type': 'application/json',
 },
 body: JSON.stringify({
 to: '919876543210',
 templateName: 'order_confirmation',
 languageCode: 'en',
 components: [
 {
 type: 'body',
 parameters: [
 { type: 'text', text: 'John' },
 { type: 'text', text: 'ORD-12345' },
 { type: 'text', text: '₹2,499' },
 ],
 },
 ],
 }),
});

const data = await response.json();
// { wamid: "wamid.HBgM...", conversationId: "conv_01J...", status: "sent" }
example.py
import requests

response = requests.post(
 'https://api.slide.cx/v1/whatsapp/send-template',
 headers={
 'Authorization': 'Bearer sk_live_YOUR_API_KEY',
 'Content-Type': 'application/json',
 },
 json={
 'to': '919876543210',
 'templateName': 'order_confirmation',
 'languageCode': 'en',
 'components': [
 {
 'type': 'body',
 'parameters': [
 {'type': 'text', 'text': 'John'},
 {'type': 'text', 'text': 'ORD-12345'},
 {'type': 'text', 'text': '₹2,499'},
 ],
 },
 ],
 }
)

data = response.json()
# {'wamid': 'wamid.HBgM...', 'conversationId': 'conv_01J...', 'status': 'sent'}

Response

response.json
{
 "wamid": "wamid.HBgMOTE5ODc2NTQzMjEwFQIAERgSMjYxMDgx...",
 "conversationId": "conv_01JXYZ...",
 "status": "sent"
}
NoteBilling: Messages are billed based on the template category. Marketing templates are always billed. Utility and Authentication templates are free within the 24-hour service window (after the customer's last message). Service templates are always free. Costs are deducted from your organization's wallet before the message is sent. If the wallet balance is insufficient, the API returns 400 Bad Request.

Template Examples

response.json
// Text-only template (no variables)
{
 "to": "919876543210",
 "templateName": "welcome_message",
 "languageCode": "en"
}
response.json
// Template with image header + body variables
{
 "to": "919876543210",
 "templateName": "promo_offer",
 "languageCode": "en",
 "components": [
 {
 "type": "header",
 "parameters": [
 { "type": "image", "image": { "link": "https://example.com/promo.jpg" } }
 ]
 },
 {
 "type": "body",
 "parameters": [
 { "type": "text", "text": "John" },
 { "type": "text", "text": "20%" }
 ]
 }
 ]
}
response.json
// OTP / Verification template (body + copy-code button)
{
 "to": "919876543210",
 "templateName": "verification",
 "languageCode": "en",
 "components": [
 {
 "type": "body",
 "parameters": [{ "type": "text", "text": "654321" }]
 },
 {
 "type": "button",
 "sub_type": "url",
 "index": "0",
 "parameters": [{ "type": "text", "text": "654321" }]
 }
 ]
}

Get an Async Send's Status

GET/v1/whatsapp/send-status/:requestId

The outcome of a send made with async: true. Kept for 48 hours; after that, and for an id of another workspace, the answer is 404. The message log and the whatsapp.message.status webhook stay the lasting record.

Required scope·whatsapp:send
response.json
{
 "requestId": "...",
 "status": "sent",
 "to": "919876543210",
 "templateName": "order_confirmation",
 "queuedAt": "2026-10-25T07:00:00.000Z",
 "wamid": "wamid.HBgM...",
 "conversationId": "...",
 "completedAt": "2026-10-25T07:00:01.000Z"
}

status is queued, sent or failed. wamid and conversationId appear once WhatsApp has accepted the message; a failed send carries error (plain words) and errorCode (the HTTP status the same send would have answered).

Get One Message's Status

GET/v1/whatsapp/messages/:wamid

Where one message stands, by its wamid. A send is only accepted at once; WhatsApp reports the real outcome later, and a failure carries WhatsApp's own explanation.

Required scope·whatsapp:logs:read
response.json
{
 "wamid": "wamid.HBgM...",
 "status": "failed",
 "direction": "outbound",
 "type": "template",
 "templateName": "order_confirmation",
 "templateCategory": "UTILITY",
 "to": "919876543210",
 "sentAt": "2026-10-25T07:00:01.000Z",
 "failure": {
 "code": 131026,
 "label": "Not delivered",
 "reason": "...",
 "hint": "...",
 "title": "Message undeliverable"
 },
 "retryCount": 0,
 "retryOfWamid": null
}

status is pending, sent, delivered, read or failed. failure is null unless the message failed. retryCount counts automatic resends after a passing failure, and retryOfWamid is set when this message is itself such a resend. An unknown wamid is a 404.

Get Message Logs

GET/v1/whatsapp/logs

Retrieve paginated WhatsApp message logs for the organization. Includes inbound, outbound, delivery status, billing details, and message source attribution.

Required scope·whatsapp:logs:read

Query Parameters

ParameterTypeDefaultDescription
pageinteger1Page number
limitinteger50Results per page (max 100)
directionstringall"inbound" or "outbound"
statusstringall"sent", "delivered", "read", or "failed"
fromstring Start date (YYYY-MM-DD)
tostring End date (YYYY-MM-DD)
terminal
curl "https://api.slide.cx/v1/whatsapp/logs?page=1&limit=20&direction=outbound" \
 -H "Authorization: Bearer sk_live_YOUR_API_KEY"
example.ts
const res = await fetch(
 'https://api.slide.cx/v1/whatsapp/logs?page=1&limit=20&direction=outbound',
 { headers: { 'Authorization': 'Bearer sk_live_YOUR_API_KEY' } }
);
const { data, meta } = await res.json();

Response

response.json
{
 "data": [
 {
 "id": "msg_01JXYZ...",
 "wamid": "wamid.HBgM...",
 "direction": "outbound",
 "type": "template",
 "status": "delivered",
 "content": { "templateName": "order_confirmation", "languageCode": "en" },
 "templateName": "order_confirmation",
 "templateCategory": "UTILITY",
 "isTemplate": true,
 "isBillable": true,
 "billingAmountPaise": 12,
 "source": "API",
 "sourceName": "Production Key",
 "timestamp": "2026-04-08T12:30:00.000Z",
 "conversation": {
 "id": "conv_01J...",
 "customerWaId": "919876543210",
 "customerName": "John Doe"
 }
 }
 ],
 "meta": { "total": 142, "page": 1, "limit": 20, "totalPages": 8 }
}

Source Values

SourceDescription
MANUALSent by a user from the Slide dashboard
BOTSent by a WhatsApp automation/bot
AUTOMATIONSent by an automation workflow
CAMPAIGNSent as part of a bulk campaign
APISent via this Developer API
INBOUNDReceived from a customer
SYSTEMPlatform system message (e.g. OTP verification)

List Templates

GET/v1/whatsapp/templates

Retrieve all WhatsApp message templates for the organization, including their approval status, category, and language.

Required scope·whatsapp:templates:read

Query Parameters

ParameterTypeDefaultDescription
pageinteger1Page number
limitinteger25Results per page
statusstringall"APPROVED", "PENDING", or "REJECTED"
categorystringall"MARKETING", "UTILITY", or "AUTHENTICATION"
terminal
curl "https://api.slide.cx/v1/whatsapp/templates?status=APPROVED" \
 -H "Authorization: Bearer sk_live_YOUR_API_KEY"
example.ts
const res = await fetch(
 'https://api.slide.cx/v1/whatsapp/templates?status=APPROVED',
 { headers: { 'Authorization': 'Bearer sk_live_YOUR_API_KEY' } }
);
const templates = await res.json();

Response

response.json
{
 "data": [
 {
 "id": "tpl_01JXYZ...",
 "name": "order_confirmation",
 "category": "UTILITY",
 "language": "en",
 "status": "APPROVED",
 "components": [
 {
 "type": "BODY",
 "text": "Hi {{1}}, your order {{2}} of {{3}} has been confirmed!"
 }
 ],
 "createdAt": "2026-01-10T08:00:00.000Z"
 },
 {
 "id": "tpl_02ABCD...",
 "name": "verification",
 "category": "AUTHENTICATION",
 "language": "en",
 "status": "APPROVED",
 "components": [
 {
 "type": "BODY",
 "text": "Your verification code is {{1}}"
 },
 {
 "type": "BUTTONS",
 "buttons": [{ "type": "URL", "text": "Copy Code", "url": "https://..." }]
 }
 ],
 "createdAt": "2026-01-05T10:00:00.000Z"
 }
 ],
 "meta": { "total": 12, "page": 1, "limit": 25, "totalPages": 1 }
}

Submit a Template

POST/v1/whatsapp/templates

Submit a new message template to WhatsApp for review. It cannot be sent until its status is APPROVED: poll GET /v1/whatsapp/templates for the result.

Required scope·whatsapp:templates:write

Request Body

FieldTypeRequiredDescription
namestringYesLowercase letters, digits and underscores only, for example order_shipped.
languagestringYesLanguage code, for example en or en_US. One template per name and language.
categorystringYesMARKETING, UTILITY or AUTHENTICATION.
componentsarrayYesThe template DEFINITION: HEADER, BODY, FOOTER and BUTTONS blocks (and CAROUSEL cards), with {{1}}, {{2}} placeholders in the text. Not the same as the values you send with send-template.
components[].mediaUrlstringNoOn an IMAGE, VIDEO or DOCUMENT header (a carousel card's too): a public https link to a sample file, instead of an upload handle in example.header_handle. Slide downloads it and uploads it to WhatsApp for you. JPEG or PNG up to 5 MB, MP4 or PDF up to 16 MB. A link that cannot be downloaded, or a file of another type, is a 400 that names the link.
terminal
curl -X POST https://api.slide.cx/v1/whatsapp/templates \
 -H "Authorization: Bearer sk_live_YOUR_API_KEY" \
 -H "Content-Type: application/json" \
 -d '{
 "name": "order_shipped",
 "language": "en",
 "category": "UTILITY",
 "components": [
 { "type": "BODY", "text": "Hi {{1}}, your order {{2}} has shipped.", "example": { "body_text": [["Asha", "#1042"]] } }
 ]
 }'

Response (201)

response.json
{
 "id": "...",
 "metaTemplateId": "...",
 "name": "order_shipped",
 "language": "en",
 "category": "UTILITY",
 "status": "PENDING",
 "components": [ ... ],
 "parameterFormat": "POSITIONAL",
 "lastSyncedAt": "2026-10-25T07:00:00.000Z",
 "createdAt": "2026-10-25T07:00:00.000Z",
 "updatedAt": "2026-10-25T07:00:00.000Z",
 "_note": "Submitted for review. It cannot be sent until its status is APPROVED. Poll GET /v1/whatsapp/templates."
}

A template WhatsApp refuses is a 400 with its reason. The workspace needs a connected WhatsApp number.

Upload Header Media

POST/v1/whatsapp/header-media/upload

Upload an image, video, or document to use as a template header. Returns a media handle you can reference in the header component of a template send.

Required scope·whatsapp:send

Request Body

Send as multipart/form-data with a single file field.

terminal
curl -X POST https://api.slide.cx/v1/whatsapp/header-media/upload \
 -H "Authorization: Bearer sk_live_YOUR_API_KEY" \
 -F "file=@promo.jpg"

Response

response.json
{
 "mediaId": "wamedia_01JXYZ...",
 "url": "https://slide.synquic.com/media/wamedia_01JXYZ.jpg",
 "mimeType": "image/jpeg"
}

Get Conversation by Phone

GET/v1/whatsapp/conversations/by-phone

Retrieve the message history exchanged with a specific phone number. Returns the last N messages (max 50) in chronological order, oldest first.

Required scope·whatsapp:conversations:read

Query Parameters

ParameterTypeDefaultDescription
phonestring Customer phone number in international format without + (required, e.g. "919876543210")
limitnumber50Number of most recent messages to return (max 50)
terminal
curl "https://api.slide.cx/v1/whatsapp/conversations/by-phone?phone=919876543210&limit=50" \
 -H "Authorization: Bearer sk_live_YOUR_API_KEY"
example.ts
const res = await fetch(
 'https://api.slide.cx/v1/whatsapp/conversations/by-phone?phone=919876543210&limit=50',
 { headers: { 'Authorization': 'Bearer sk_live_YOUR_API_KEY' } }
);
const conversation = await res.json();
// conversation.data is chronological: oldest message first
example.py
import requests

res = requests.get(
 'https://api.slide.cx/v1/whatsapp/conversations/by-phone',
 headers={'Authorization': 'Bearer sk_live_YOUR_API_KEY'},
 params={'phone': '919876543210', 'limit': 50}
)
conversation = res.json()

Response

response.json
{
 "conversationId": "conv_01JXYZ...",
 "customerWaId": "919876543210",
 "customerName": "John Doe",
 "data": [
 {
 "id": "msg_01JXYZ...",
 "wamid": "wamid.HBgM...",
 "direction": "outbound",
 "type": "template",
 "status": "read",
 "content": { "templateName": "order_confirmation" },
 "templateName": "order_confirmation",
 "mediaUrl": null,
 "mediaMimeType": null,
 "errorCode": null,
 "errorMessage": null,
 "timestamp": "2026-04-08T12:30:00.000Z",
 "createdAt": "2026-04-08T12:30:00.000Z"
 },
 {
 "id": "msg_02ABCD...",
 "wamid": "wamid.HBgN...",
 "direction": "inbound",
 "type": "text",
 "status": "received",
 "content": { "body": "Thanks! When will it arrive?" },
 "templateName": null,
 "mediaUrl": null,
 "mediaMimeType": null,
 "errorCode": null,
 "errorMessage": null,
 "timestamp": "2026-04-08T12:31:12.000Z",
 "createdAt": "2026-04-08T12:31:12.000Z"
 }
 ]
}

List Campaigns

GET/v1/whatsapp/campaigns

Retrieve a paginated list of WhatsApp campaigns with their status and headline counts.

Required scope·whatsapp:campaigns:read

Query Parameters

ParameterTypeDefaultDescription
pagenumber1Page number
limitnumber20Results per page (max 100)
statusstringall"DRAFT", "SCHEDULED", "RUNNING", "COMPLETED", or "CANCELLED"
terminal
curl "https://api.slide.cx/v1/whatsapp/campaigns?page=1&limit=20&status=COMPLETED" \
 -H "Authorization: Bearer sk_live_YOUR_API_KEY"

Response

response.json
{
 "data": [
 {
 "id": "wac_01JXYZ...",
 "name": "April Promo",
 "status": "COMPLETED",
 "templateId": "tpl_01JXYZ...",
 "templateName": "promo_offer",
 "totalRecipients": 5000,
 "sentCount": 4980,
 "deliveredCount": 4870,
 "scheduledAt": null,
 "createdAt": "2026-04-01T09:00:00.000Z"
 }
 ],
 "meta": { "total": 6, "page": 1, "limit": 20, "totalPages": 1 }
}

Get Campaign

GET/v1/whatsapp/campaigns/:id

Retrieve a single campaign with its full configuration and current status.

Required scope·whatsapp:campaigns:read
terminal
curl "https://api.slide.cx/v1/whatsapp/campaigns/wac_01JXYZ" \
 -H "Authorization: Bearer sk_live_YOUR_API_KEY"

Get Campaign Analytics

GET/v1/whatsapp/campaigns/:id/analytics

Retrieve delivery, read, click, reply, and conversion metrics for a campaign.

Required scope·whatsapp:campaigns:read
terminal
curl "https://api.slide.cx/v1/whatsapp/campaigns/wac_01JXYZ/analytics" \
 -H "Authorization: Bearer sk_live_YOUR_API_KEY"

Response

response.json
{
 "campaignId": "wac_01JXYZ...",
 "totalRecipients": 5000,
 "sent": 4980,
 "delivered": 4870,
 "read": 3120,
 "clicked": 640,
 "replied": 212,
 "converted": 87,
 "failed": 20,
 "deliveryRate": 97.79,
 "readRate": 64.07,
 "clickRate": 13.14
}

Create Campaign

POST/v1/whatsapp/campaigns

Create a DRAFT campaign with a manual phone list audience. The campaign does not send until you call the launch endpoint (or the scheduled time is reached after launch).

Required scope·whatsapp:campaigns:write

Request Body

FieldTypeRequiredDescription
namestringNoCampaign name (auto-generated if omitted)
templateIdstringYesID of an approved WhatsApp template
phonesstring[]Yes1 to 10,000 phone numbers in international format without + (e.g. "919876543210")
templateComponentsarrayNoTemplate components with static variable values (Meta Cloud API format)
variableMappingobjectNoMap template variables to per-recipient contact fields
scheduledAtstringNoISO 8601 timestamp to schedule the send
scheduleTimezonestringNoIANA timezone for scheduledAt (e.g. "Asia/Kolkata")
terminal
curl -X POST https://api.slide.cx/v1/whatsapp/campaigns \
 -H "Authorization: Bearer sk_live_YOUR_API_KEY" \
 -H "Content-Type: application/json" \
 -d '{
 "name": "April Promo",
 "templateId": "tpl_01JXYZ...",
 "phones": ["919876543210", "919876543211"],
 "scheduledAt": "2026-04-15T10:00:00.000Z",
 "scheduleTimezone": "Asia/Kolkata"
 }'
example.ts
const res = await fetch('https://api.slide.cx/v1/whatsapp/campaigns', {
 method: 'POST',
 headers: {
 'Authorization': 'Bearer sk_live_YOUR_API_KEY',
 'Content-Type': 'application/json',
 },
 body: JSON.stringify({
 name: 'April Promo',
 templateId: 'tpl_01JXYZ...',
 phones: ['919876543210', '919876543211'],
 scheduledAt: '2026-04-15T10:00:00.000Z',
 scheduleTimezone: 'Asia/Kolkata',
 }),
});
const campaign = await res.json();
// { id: 'wac_01J...', status: 'DRAFT', totalRecipients: 2, ... }
example.py
import requests

res = requests.post(
 'https://api.slide.cx/v1/whatsapp/campaigns',
 headers={
 'Authorization': 'Bearer sk_live_YOUR_API_KEY',
 'Content-Type': 'application/json',
 },
 json={
 'name': 'April Promo',
 'templateId': 'tpl_01JXYZ...',
 'phones': ['919876543210', '919876543211'],
 }
)
campaign = res.json()
print(campaign['id'], campaign['status'])  # wac_01J... DRAFT

Response

response.json
{
 "id": "wac_01JXYZ...",
 "name": "April Promo",
 "status": "DRAFT",
 "templateId": "tpl_01JXYZ...",
 "totalRecipients": 2,
 "scheduledAt": "2026-04-15T10:00:00.000Z",
 "scheduleTimezone": "Asia/Kolkata",
 "createdAt": "2026-04-10T09:00:00.000Z"
}

Launch Campaign

POST/v1/whatsapp/campaigns/:id/launch

Launch a DRAFT campaign. Sending is queued and processed asynchronously, the endpoint returns 202 Accepted immediately. If scheduledAt is set, the campaign moves to SCHEDULED and sends at that time.

Required scope·whatsapp:campaigns:write
terminal
curl -X POST "https://api.slide.cx/v1/whatsapp/campaigns/wac_01JXYZ/launch" \
 -H "Authorization: Bearer sk_live_YOUR_API_KEY"

Response

response.json
{
 "status": "queued",
 "campaignId": "wac_01JXYZ..."
}

Cancel Campaign

POST/v1/whatsapp/campaigns/:id/cancel

Cancel a scheduled or running campaign. Messages not yet sent are stopped and the wallet is refunded for all unsent recipients.

Required scope·whatsapp:campaigns:write
terminal
curl -X POST "https://api.slide.cx/v1/whatsapp/campaigns/wac_01JXYZ/cancel" \
 -H "Authorization: Bearer sk_live_YOUR_API_KEY"

Response

response.json
{
 "success": true,
 "campaignId": "wac_01JXYZ...",
 "refundedRecipients": 1240
}

SMS API

Send transactional and promotional SMS, manage DLT templates and sender IDs, run bulk campaigns, and query delivery logs and stats. All sends go through your organization's registered DLT sender IDs and approved templates.

NoteIndian SMS traffic requires DLT registration: your sender IDs and message templates must be approved before sending. Register and manage them in Dashboard → SMS.

Send SMS

POST/v1/sms/send

Send an SMS to one or more recipients using a registered sender ID.

Required scope·sms:send

Request Body

FieldTypeRequiredDescription
senderIdstringYesRegistered sender ID (e.g. "ACMEIN")
tostring[]YesRecipient phone numbers (min 1)
messagestringYesMessage text (must match the approved DLT template)
templateIdstringNoDLT template ID for the message
routestringNo"transactional" or "promotional" (default "transactional")
terminal
curl -X POST https://api.slide.cx/v1/sms/send \
 -H "Authorization: Bearer sk_live_YOUR_API_KEY" \
 -H "Content-Type: application/json" \
 -d '{
 "senderId": "ACMEIN",
 "to": ["919876543210", "919876543211"],
 "message": "Your order ORD-12345 has shipped. Track it at https://acme.com/track",
 "route": "transactional"
 }'
example.ts
const res = await fetch('https://api.slide.cx/v1/sms/send', {
 method: 'POST',
 headers: {
 'Authorization': 'Bearer sk_live_YOUR_API_KEY',
 'Content-Type': 'application/json',
 },
 body: JSON.stringify({
 senderId: 'ACMEIN',
 to: ['919876543210'],
 message: 'Your order ORD-12345 has shipped.',
 route: 'transactional',
 }),
});
const result = await res.json();
// { status: 'sent', recipients: 1, externalIds: ['ext_...'] }
example.py
import requests

res = requests.post(
 'https://api.slide.cx/v1/sms/send',
 headers={
 'Authorization': 'Bearer sk_live_YOUR_API_KEY',
 'Content-Type': 'application/json',
 },
 json={
 'senderId': 'ACMEIN',
 'to': ['919876543210'],
 'message': 'Your order ORD-12345 has shipped.',
 'route': 'transactional',
 }
)
print(res.json())  # {'status': 'sent', 'recipients': 1, 'externalIds': ['ext_...']}

Response

response.json
{
 "status": "sent",
 "recipients": 2,
 "externalIds": ["ext_01JXYZ...", "ext_02ABCD..."]
}

List SMS Templates

GET/v1/sms/templates

Retrieve a paginated list of the organization's DLT SMS templates.

Required scope·sms:templates:read

Query Parameters

ParameterTypeDefaultDescription
pagenumber1Page number
limitnumber20Results per page (max 100)
statusstringallFilter by template status (e.g. "APPROVED", "PENDING")
terminal
curl "https://api.slide.cx/v1/sms/templates?page=1&limit=20&status=APPROVED" \
 -H "Authorization: Bearer sk_live_YOUR_API_KEY"

Response

response.json
{
 "data": [
 {
 "id": "smst_01JXYZ...",
 "name": "Order Shipped",
 "dltTemplateId": "1107160000000012345",
 "body": "Your order {#var#} has shipped. Track it at {#var#}",
 "route": "transactional",
 "status": "APPROVED",
 "createdAt": "2026-02-01T09:00:00.000Z"
 }
 ],
 "meta": { "total": 8, "page": 1, "limit": 20, "totalPages": 1 }
}

List Sender IDs

GET/v1/sms/senders

Retrieve a paginated list of the organization's registered SMS sender IDs.

Required scope·sms:senders:read

Query Parameters

ParameterTypeDefaultDescription
pagenumber1Page number
limitnumber20Results per page (max 100)
terminal
curl "https://api.slide.cx/v1/sms/senders?page=1&limit=20" \
 -H "Authorization: Bearer sk_live_YOUR_API_KEY"

Response

response.json
{
 "data": [
 {
 "id": "smss_01JXYZ...",
 "senderId": "ACMEIN",
 "entityId": "1101230000000067890",
 "status": "APPROVED",
 "createdAt": "2026-01-20T09:00:00.000Z"
 }
 ],
 "meta": { "total": 2, "page": 1, "limit": 20, "totalPages": 1 }
}

List SMS Campaigns

GET/v1/sms/campaigns

Retrieve a paginated list of SMS campaigns.

Required scope·sms:campaigns:read

Query Parameters

ParameterTypeDefaultDescription
pagenumber1Page number
limitnumber20Results per page (max 100)
terminal
curl "https://api.slide.cx/v1/sms/campaigns?page=1&limit=20" \
 -H "Authorization: Bearer sk_live_YOUR_API_KEY"

Response

response.json
{
 "data": [
 {
 "id": "smsc_01JXYZ...",
 "name": "Weekend Sale",
 "status": "COMPLETED",
 "route": "promotional",
 "senderId": "ACMEIN",
 "totalRecipients": 12000,
 "sentCount": 11940,
 "deliveredCount": 11512,
 "scheduledAt": null,
 "createdAt": "2026-03-28T09:00:00.000Z"
 }
 ],
 "meta": { "total": 4, "page": 1, "limit": 20, "totalPages": 1 }
}

Get SMS Campaign

GET/v1/sms/campaigns/:id

Retrieve a single SMS campaign with its full configuration and current counts.

Required scope·sms:campaigns:read
terminal
curl "https://api.slide.cx/v1/sms/campaigns/smsc_01JXYZ" \
 -H "Authorization: Bearer sk_live_YOUR_API_KEY"

Create SMS Campaign

POST/v1/sms/campaigns

Create a DRAFT SMS campaign. The campaign does not send until you call the launch endpoint.

Required scope·sms:campaigns:write

Request Body

FieldTypeRequiredDescription
namestringYesCampaign name
messageBodystringYesMessage text (must match the approved DLT template)
senderIdstringNoRegistered sender ID to send from
templateIdstringNoDLT template ID for the message
routestringNo"transactional" or "promotional"
audienceTypestringNoAudience selection mode (e.g. all contacts, segment, manual list)
audienceCriteriaobjectNoAudience filter criteria for the selected audienceType
scheduledAtstringNoISO 8601 timestamp to schedule the send
terminal
curl -X POST https://api.slide.cx/v1/sms/campaigns \
 -H "Authorization: Bearer sk_live_YOUR_API_KEY" \
 -H "Content-Type: application/json" \
 -d '{
 "name": "Weekend Sale",
 "messageBody": "Flat 20% off this weekend! Shop now: https://acme.com/sale",
 "senderId": "ACMEIN",
 "route": "promotional",
 "scheduledAt": "2026-04-12T04:30:00.000Z"
 }'
example.ts
const res = await fetch('https://api.slide.cx/v1/sms/campaigns', {
 method: 'POST',
 headers: {
 'Authorization': 'Bearer sk_live_YOUR_API_KEY',
 'Content-Type': 'application/json',
 },
 body: JSON.stringify({
 name: 'Weekend Sale',
 messageBody: 'Flat 20% off this weekend! Shop now: https://acme.com/sale',
 senderId: 'ACMEIN',
 route: 'promotional',
 }),
});
const campaign = await res.json();
// { id: 'smsc_01J...', status: 'DRAFT', ... }
example.py
import requests

res = requests.post(
 'https://api.slide.cx/v1/sms/campaigns',
 headers={
 'Authorization': 'Bearer sk_live_YOUR_API_KEY',
 'Content-Type': 'application/json',
 },
 json={
 'name': 'Weekend Sale',
 'messageBody': 'Flat 20% off this weekend! Shop now: https://acme.com/sale',
 'senderId': 'ACMEIN',
 'route': 'promotional',
 }
)
campaign = res.json()
print(campaign['id'], campaign['status'])  # smsc_01J... DRAFT

Response

response.json
{
 "id": "smsc_01JXYZ...",
 "name": "Weekend Sale",
 "status": "DRAFT",
 "route": "promotional",
 "senderId": "ACMEIN",
 "scheduledAt": "2026-04-12T04:30:00.000Z",
 "createdAt": "2026-04-10T09:00:00.000Z"
}

Launch SMS Campaign

POST/v1/sms/campaigns/:id/launch

Launch a DRAFT campaign. Sending is queued and processed asynchronously, the endpoint returns 202 Accepted immediately.

Required scope·sms:campaigns:write
terminal
curl -X POST "https://api.slide.cx/v1/sms/campaigns/smsc_01JXYZ/launch" \
 -H "Authorization: Bearer sk_live_YOUR_API_KEY"

Response

response.json
{
 "status": "queued",
 "campaignId": "smsc_01JXYZ..."
}

Cancel SMS Campaign

POST/v1/sms/campaigns/:id/cancel

Cancel a scheduled or running SMS campaign. Messages not yet sent are stopped.

Required scope·sms:campaigns:write
terminal
curl -X POST "https://api.slide.cx/v1/sms/campaigns/smsc_01JXYZ/cancel" \
 -H "Authorization: Bearer sk_live_YOUR_API_KEY"

Response

response.json
{
 "success": true,
 "campaignId": "smsc_01JXYZ..."
}

Get SMS Logs

GET/v1/sms/logs

Retrieve paginated SMS message logs with direction, status, and date filters.

Required scope·sms:logs:read

Query Parameters

ParameterTypeDefaultDescription
pagenumber1Page number
limitnumber50Results per page (max 100)
directionstringall"inbound" or "outbound"
statusstringallFilter by delivery status (e.g. "sent", "delivered", "failed")
startstring Start date (ISO 8601)
endstring End date (ISO 8601)
terminal
curl "https://api.slide.cx/v1/sms/logs?page=1&limit=50&direction=outbound&status=delivered" \
 -H "Authorization: Bearer sk_live_YOUR_API_KEY"

Response

response.json
{
 "data": [
 {
 "id": "smsl_01JXYZ...",
 "externalId": "ext_01JXYZ...",
 "direction": "outbound",
 "route": "transactional",
 "status": "delivered",
 "body": "Your order ORD-12345 has shipped.",
 "senderId": "ACMEIN",
 "templateId": "smst_01JXYZ...",
 "source": "API",
 "sentAt": "2026-04-08T12:30:00.000Z",
 "deliveredAt": "2026-04-08T12:30:04.000Z",
 "failedAt": null,
 "errorCode": null,
 "errorMessage": null,
 "createdAt": "2026-04-08T12:30:00.000Z",
 "phone": "919876543210",
 "customerName": "John Doe"
 }
 ],
 "meta": { "total": 1420, "page": 1, "limit": 50, "totalPages": 29 }
}

Get SMS Stats

GET/v1/sms/stats

Retrieve daily aggregated SMS delivery stats for a date range.

Required scope·sms:logs:read

Query Parameters

ParameterTypeDefaultDescription
startstring Start date (ISO 8601)
endstring End date (ISO 8601)
terminal
curl "https://api.slide.cx/v1/sms/stats?start=2026-04-01&end=2026-04-30" \
 -H "Authorization: Bearer sk_live_YOUR_API_KEY"

Response

response.json
{
 "dataPoints": [
 { "date": "2026-04-01", "sent": 480, "delivered": 462, "failed": 18 },
 { "date": "2026-04-02", "sent": 512, "delivered": 498, "failed": 14 }
 ],
 "totals": {
 "sent": 992,
 "delivered": 960,
 "failed": 32,
 "deliveryRate": 96.77
 }
}

RCS API

Send rich RCS Business Messaging content, cards, carousels, and suggested actions, with automatic SMS fallback for handsets that do not support RCS. Manage bots, templates, campaigns, and delivery logs.

NoteRCS requires an approved RCS bot for your brand. Bots and templates are provisioned and managed in Dashboard → RCS.

Send RCS Message

POST/v1/rcs/send

Send a single rich RCS message using an approved template, with an optional SMS fallback for non-RCS handsets.

Required scope·rcs:send

Request Body

FieldTypeRequiredDescription
tostringYesRecipient phone number
templateIdstringYesID of an approved RCS template
rcsVariablesobjectNoKey-value map of template variable values
smsFallbackobjectNoSMS fallback: { sender, message, templateId, route }, sent if the handset does not support RCS
ttlstringNoTime-to-live for the message before fallback or expiry (e.g. "3600s")
terminal
curl -X POST https://api.slide.cx/v1/rcs/send \
 -H "Authorization: Bearer sk_live_YOUR_API_KEY" \
 -H "Content-Type: application/json" \
 -d '{
 "to": "919876543210",
 "templateId": "rcst_01JXYZ...",
 "rcsVariables": { "name": "John", "orderId": "ORD-12345" },
 "smsFallback": {
 "sender": "ACMEIN",
 "message": "Your order ORD-12345 has shipped.",
 "route": "transactional"
 },
 "ttl": "3600s"
 }'
example.ts
const res = await fetch('https://api.slide.cx/v1/rcs/send', {
 method: 'POST',
 headers: {
 'Authorization': 'Bearer sk_live_YOUR_API_KEY',
 'Content-Type': 'application/json',
 },
 body: JSON.stringify({
 to: '919876543210',
 templateId: 'rcst_01JXYZ...',
 rcsVariables: { name: 'John', orderId: 'ORD-12345' },
 smsFallback: { sender: 'ACMEIN', message: 'Your order ORD-12345 has shipped.' },
 }),
});
const result = await res.json();
example.py
import requests

res = requests.post(
 'https://api.slide.cx/v1/rcs/send',
 headers={
 'Authorization': 'Bearer sk_live_YOUR_API_KEY',
 'Content-Type': 'application/json',
 },
 json={
 'to': '919876543210',
 'templateId': 'rcst_01JXYZ...',
 'rcsVariables': {'name': 'John', 'orderId': 'ORD-12345'},
 'smsFallback': {'sender': 'ACMEIN', 'message': 'Your order ORD-12345 has shipped.'},
 }
)
print(res.json())

List RCS Bots

GET/v1/rcs/bots

Retrieve the RCS bots provisioned for your organization.

Required scope·rcs:bots:read
terminal
curl "https://api.slide.cx/v1/rcs/bots" \
 -H "Authorization: Bearer sk_live_YOUR_API_KEY"

Response

response.json
{
 "data": [
 {
 "id": "rcsb_01JXYZ...",
 "name": "Acme Store",
 "status": "APPROVED",
 "createdAt": "2026-02-15T09:00:00.000Z"
 }
 ]
}

List RCS Templates

GET/v1/rcs/templates

Retrieve a paginated list of RCS templates.

Required scope·rcs:templates:read

Query Parameters

ParameterTypeDefaultDescription
pagenumber1Page number
limitnumber20Results per page (max 100)
terminal
curl "https://api.slide.cx/v1/rcs/templates?page=1&limit=20" \
 -H "Authorization: Bearer sk_live_YOUR_API_KEY"

Create RCS Campaign

POST/v1/rcs/campaigns

Create and dispatch a bulk RCS campaign. The bulk send is accepted and processed asynchronously, the endpoint returns 202 Accepted.

Required scope·rcs:campaigns:write

Request Body

FieldTypeRequiredDescription
botIdstringYesID of the RCS bot to send from
templateIdstringYesID of an approved RCS template
campaignNamestringNoCampaign name
numbersstring[]NoRecipient phone numbers
countrystringNoRecipient country code (default "IN")
removeDuplicatebooleanNoDeduplicate the recipient list before sending
ttlstringNoTime-to-live before fallback or expiry
scheduleCampaignbooleanNoSchedule instead of sending immediately
scheduledAtstringNoISO 8601 timestamp for the scheduled send
fallbackbooleanNoEnable SMS fallback for non-RCS handsets
fallbackMessagestringNoSMS fallback message text
terminal
curl -X POST https://api.slide.cx/v1/rcs/campaigns \
 -H "Authorization: Bearer sk_live_YOUR_API_KEY" \
 -H "Content-Type: application/json" \
 -d '{
 "botId": "rcsb_01JXYZ...",
 "templateId": "rcst_01JXYZ...",
 "campaignName": "April Launch",
 "numbers": ["919876543210", "919876543211"],
 "country": "IN",
 "removeDuplicate": true,
 "fallback": true,
 "fallbackMessage": "New arrivals are live! Shop now: https://acme.com"
 }'
example.ts
const res = await fetch('https://api.slide.cx/v1/rcs/campaigns', {
 method: 'POST',
 headers: {
 'Authorization': 'Bearer sk_live_YOUR_API_KEY',
 'Content-Type': 'application/json',
 },
 body: JSON.stringify({
 botId: 'rcsb_01JXYZ...',
 templateId: 'rcst_01JXYZ...',
 campaignName: 'April Launch',
 numbers: ['919876543210', '919876543211'],
 removeDuplicate: true,
 }),
});
// 202 Accepted, bulk send queued
example.py
import requests

res = requests.post(
 'https://api.slide.cx/v1/rcs/campaigns',
 headers={
 'Authorization': 'Bearer sk_live_YOUR_API_KEY',
 'Content-Type': 'application/json',
 },
 json={
 'botId': 'rcsb_01JXYZ...',
 'templateId': 'rcst_01JXYZ...',
 'campaignName': 'April Launch',
 'numbers': ['919876543210', '919876543211'],
 }
)
print(res.status_code)  # 202

List RCS Campaigns

GET/v1/rcs/campaigns

Retrieve a paginated list of RCS campaigns.

Required scope·rcs:campaigns:read

Query Parameters

ParameterTypeDefaultDescription
pagenumber1Page number
limitnumber20Results per page (max 100)
terminal
curl "https://api.slide.cx/v1/rcs/campaigns?page=1&limit=20" \
 -H "Authorization: Bearer sk_live_YOUR_API_KEY"

Get RCS Logs

GET/v1/rcs/logs

Retrieve paginated RCS message logs including delivery status and fallback outcomes.

Required scope·rcs:logs:read

Query Parameters

ParameterTypeDefaultDescription
pagenumber1Page number
limitnumber50Results per page (max 100)
terminal
curl "https://api.slide.cx/v1/rcs/logs?page=1&limit=50" \
 -H "Authorization: Bearer sk_live_YOUR_API_KEY"

Get RCS Stats

GET/v1/rcs/stats

Retrieve aggregated RCS delivery stats for a date range.

Required scope·rcs:logs:read

Query Parameters

ParameterTypeDefaultDescription
startstring Start date (ISO 8601)
endstring End date (ISO 8601)
terminal
curl "https://api.slide.cx/v1/rcs/stats?start=2026-04-01&end=2026-04-30" \
 -H "Authorization: Bearer sk_live_YOUR_API_KEY"

OTP API

Server-side phone verification: send one-time passwords by SMS or on WhatsApp, verify them, and prove verification to your backend with single-use access tokens. OTP length, expiry, resend cooldown, and delivery channels are configured on an OTP widget in the dashboard, the API references that widget by widgetId. SMS and WhatsApp are the only delivery channels. WhatsApp codes are sent from your own connected WhatsApp number with one of your approved authentication templates, so a widget can use WhatsApp only when your workspace has both.

NoteCreate and configure OTP widgets in Dashboard → OTP. Each widget defines the OTP length, expiry, resend rules, delivery order (a first channel and an optional second one), and per-identifier rate limits used by these endpoints. The second channel is used at once when the first one definitely could not send, and never after a timeout. When WhatsApp accepts a code and later reports that it could not be delivered (for example, the number is not on WhatsApp), a widget whose second channel is SMS sends one SMS with a new code automatically, when the failure is reported within two minutes of the WhatsApp send. That SMS does not use up one of the user's resends, it restarts the resend wait (a retry sent before the wait has passed since it answers 400), and it publishes otp.sent with channel: "sms". A widget with WhatsApp and no second channel sends nothing more to such a number. A code is charged as one WhatsApp authentication message or one SMS, and no contact is created for it.

Send OTP

POST/v1/otp/send

Send a one-time password to a phone number using an OTP widget's configuration. Returns a requestId used for retry and verification.

Required scope·otp:send

Request Body

FieldTypeRequiredDescription
widgetIdstringYesID of the OTP widget configured in the dashboard
identifierstringYesPhone number to verify (e.g. "+919876543210")
WarningSends are rate limited per identifier: each phone number can receive at most the widget's configured maximum sends per hour. The OTP length, expiry, and resend rules also come from the widget config.
terminal
curl -X POST https://api.slide.cx/v1/otp/send \
 -H "Authorization: Bearer sk_live_YOUR_API_KEY" \
 -H "Content-Type: application/json" \
 -d '{ "widgetId": "wgt_01JXYZ...", "identifier": "+919876543210" }'
example.ts
const res = await fetch('https://api.slide.cx/v1/otp/send', {
 method: 'POST',
 headers: {
 'Authorization': 'Bearer sk_live_YOUR_API_KEY',
 'Content-Type': 'application/json',
 },
 body: JSON.stringify({ widgetId: 'wgt_01JXYZ...', identifier: '+919876543210' }),
});
const { requestId } = await res.json();
example.py
import requests

res = requests.post(
 'https://api.slide.cx/v1/otp/send',
 headers={
 'Authorization': 'Bearer sk_live_YOUR_API_KEY',
 'Content-Type': 'application/json',
 },
 json={'widgetId': 'wgt_01JXYZ...', 'identifier': '+919876543210'}
)
request_id = res.json()['requestId']

Response

response.json
{
 "requestId": "otpreq_01JXYZ...",
 "channel": "sms"
}

channel is the channel the code went out on: "sms" or "whatsapp".

Retry OTP

POST/v1/otp/retry

Resend the OTP for an existing request. Respects the widget's resend cooldown and maximum resend count. A resend goes out on the widget's second channel when it has one, otherwise on its first.

Required scope·otp:send

Request Body

FieldTypeRequiredDescription
requestIdstringYesThe requestId returned by POST /otp/send
channelstringNoSend this resend on "sms" or "whatsapp" first. Honoured only when the widget sends on that channel. Any other value is not an error: the code goes out on the widget's own channel and the response says so.
terminal
curl -X POST https://api.slide.cx/v1/otp/retry \
 -H "Authorization: Bearer sk_live_YOUR_API_KEY" \
 -H "Content-Type: application/json" \
 -d '{ "requestId": "otpreq_01JXYZ...", "channel": "whatsapp" }'

Response

response.json
{
 "requestId": "otpreq_01JXYZ...",
 "channel": "whatsapp",
 "requestedChannel": "whatsapp",
 "requestedChannelAvailable": true
}
FieldTypeDescription
channelstringThe channel the code went out on: "sms" or "whatsapp"
requestedChannelstringPresent only when the request named a channel: the value you sent
requestedChannelAvailablebooleanPresent only when the request named a channel: false when this widget does not send on it
noticestringPresent only when the named channel was not the one used: why, in words
NoteEmail and voice are not delivery channels. A request that names one of them (or a channel the widget does not send on) is still accepted with 200: the code is sent on the widget's own channel, requestedChannelAvailable is false, and notice explains it. Read channel to know where the code went.

Verify OTP

POST/v1/otp/verify

Verify the OTP entered by the user. On success, returns a JWT accessToken that proves the verification. Returns 400 if the code is invalid, expired, or the request is blocked after too many attempts.

Required scope·otp:verify

Request Body

FieldTypeRequiredDescription
requestIdstringYesThe requestId returned by POST /otp/send
otpstringYesThe one-time password entered by the user
terminal
curl -X POST https://api.slide.cx/v1/otp/verify \
 -H "Authorization: Bearer sk_live_YOUR_API_KEY" \
 -H "Content-Type: application/json" \
 -d '{ "requestId": "otpreq_01JXYZ...", "otp": "123456" }'
example.ts
const res = await fetch('https://api.slide.cx/v1/otp/verify', {
 method: 'POST',
 headers: {
 'Authorization': 'Bearer sk_live_YOUR_API_KEY',
 'Content-Type': 'application/json',
 },
 body: JSON.stringify({ requestId: 'otpreq_01JXYZ...', otp: '123456' }),
});
const { accessToken } = await res.json();
// Pass accessToken to your backend, then consume it with POST /otp/verify-token
example.py
import requests

res = requests.post(
 'https://api.slide.cx/v1/otp/verify',
 headers={
 'Authorization': 'Bearer sk_live_YOUR_API_KEY',
 'Content-Type': 'application/json',
 },
 json={'requestId': 'otpreq_01JXYZ...', 'otp': '123456'}
)
access_token = res.json()['accessToken']

Response

response.json
{
 "accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}

Verify Access Token

POST/v1/otp/verify-token

Confirm server-side that an accessToken represents a real, recent verification. The token is single-use: this call consumes it, a second call with the same token fails.

Required scope·otp:verify

Request Body

FieldTypeRequiredDescription
accessTokenstringYesThe JWT returned by POST /otp/verify
terminal
curl -X POST https://api.slide.cx/v1/otp/verify-token \
 -H "Authorization: Bearer sk_live_YOUR_API_KEY" \
 -H "Content-Type: application/json" \
 -d '{ "accessToken": "eyJhbGciOiJIUzI1NiIs..." }'

Response

response.json
{
 "verified": true,
 "identifier": "+919876543210",
 "verifiedAt": "2026-04-08T12:31:12.000Z"
}
NoteTypical flow: your frontend (or the OTP widget) sends and verifies the OTP, then passes the accessToken to your backend. Your backend calls POST /v1/otp/verify-token once to confirm and consume it before trusting the phone number.

Get OTP Logs

GET/v1/otp/logs

Retrieve paginated OTP request logs, optionally filtered by widget and status.

Required scope·otp:logs:read

Query Parameters

ParameterTypeDefaultDescription
widgetIdstringallFilter by OTP widget
statusstringallFilter by request status (e.g. "sent", "verified", "expired", "blocked")
pagenumber1Page number
limitnumber50Results per page (max 100)
terminal
curl "https://api.slide.cx/v1/otp/logs?widgetId=wgt_01JXYZ&status=verified&page=1&limit=50" \
 -H "Authorization: Bearer sk_live_YOUR_API_KEY"

Response

response.json
{
 "data": [
 {
 "id": "otpreq_01JXYZ...",
 "widgetId": "wgt_01JXYZ...",
 "identifier": "+919876543210",
 "channel": "sms",
 "status": "verified",
 "attempts": 1,
 "resends": 0,
 "sentAt": "2026-04-08T12:30:00.000Z",
 "verifiedAt": "2026-04-08T12:31:12.000Z"
 }
 ],
 "meta": { "total": 940, "page": 1, "limit": 50, "totalPages": 19 }
}

Get OTP Analytics

GET/v1/otp/analytics

Retrieve success and failure rates and delivery analytics for an OTP widget.

Required scope·otp:analytics:read

Query Parameters

ParameterTypeDefaultDescription
widgetIdstringallFilter analytics to a single OTP widget
terminal
curl "https://api.slide.cx/v1/otp/analytics?widgetId=wgt_01JXYZ" \
 -H "Authorization: Bearer sk_live_YOUR_API_KEY"

Response

response.json
{
 "totalSent": 5230,
 "totalVerified": 4870,
 "totalExpired": 240,
 "totalBlocked": 46,
 "successRate": 93.12,
 "channelBreakdown": {
 "sms": { "sent": 4100, "verified": 3810 },
 "whatsapp": { "sent": 1130, "verified": 1060 }
 }
}

Shopify API

The Shopify API provides read-only access to products, collections, orders, and discount codes from your connected Shopify store. Requires an active Shopify integration in your organization.

NoteShopify endpoints require an active Shopify store connection in Dashboard → Integrations → Shopify. If no store is connected, requests return 404 Not Found.
GET/v1/shopify/products/search

Full-text search across product title, tags, vendor, and type.

Required scope·shopify:products:read

Query Parameters

ParameterTypeDefaultDescription
qstring Search query (required)
limitnumber10Max results (max 50)
terminal
curl "https://api.slide.cx/v1/shopify/products/search?q=sneakers&limit=5" \
 -H "Authorization: Bearer sk_live_YOUR_API_KEY"

Response

response.json
{
 "data": [
 {
 "id": "gid://shopify/Product/123456",
 "title": "Classic Sneakers",
 "vendor": "Nike",
 "productType": "Footwear",
 "tags": ["sneakers", "sale"],
 "variants": [
 { "id": "gid://shopify/ProductVariant/789", "title": "Size 10", "price": "89.99", "inventoryQuantity": 24 }
 ]
 }
 ],
 "meta": { "count": 1, "query": "sneakers" }
}

List Product Types

GET/v1/shopify/products/types

Get all unique product types (categories) sorted A Z.

Required scope·shopify:products:read
terminal
curl "https://api.slide.cx/v1/shopify/products/types" \
 -H "Authorization: Bearer sk_live_YOUR_API_KEY"

Response

response.json
{
 "data": ["Accessories", "Footwear", "T-Shirts", "Watches"],
 "meta": { "count": 4 }
}

Get Product

GET/v1/shopify/products/:id

Get full product detail including all variants, options, and description.

Required scope·shopify:products:read
terminal
curl "https://api.slide.cx/v1/shopify/products/gid://shopify/Product/123456" \
 -H "Authorization: Bearer sk_live_YOUR_API_KEY"

List Collections

GET/v1/shopify/collections

List all custom and smart collections sorted A Z.

Required scope·shopify:products:read
terminal
curl "https://api.slide.cx/v1/shopify/collections" \
 -H "Authorization: Bearer sk_live_YOUR_API_KEY"

Response

response.json
{
 "data": [
 { "id": "gid://shopify/Collection/111", "title": "Summer Sale", "handle": "summer-sale", "productsCount": 24 }
 ],
 "meta": { "count": 1 }
}

Get Collection Products

GET/v1/shopify/collections/:id/products

List products in a specific collection.

Required scope·shopify:products:read

Query Parameters

ParameterTypeDefaultDescription
limitnumber50Max results (max 250)
terminal
curl "https://api.slide.cx/v1/shopify/collections/gid://shopify/Collection/111/products?limit=10" \
 -H "Authorization: Bearer sk_live_YOUR_API_KEY"

Get Order Status

GET/v1/shopify/orders/status

Look up an order by order number (e.g. #1001) or customer email.

Required scope·shopify:orders:read

Query Parameters

ParameterTypeDefaultDescription
identifierstring Order number (e.g. "#1001") or customer email (required)
terminal
curl "https://api.slide.cx/v1/shopify/orders/status?identifier=%231001" \
 -H "Authorization: Bearer sk_live_YOUR_API_KEY"

Response

response.json
{
 "orderNumber": "#1001",
 "financialStatus": "paid",
 "fulfillmentStatus": "fulfilled",
 "totalPrice": "129.99",
 "currency": "INR",
 "lineItems": [
 { "title": "Classic Sneakers", "quantity": 1, "price": "89.99" },
 { "title": "Sports Socks", "quantity": 2, "price": "20.00" }
 ],
 "tracking": {
 "carrier": "BlueDart",
 "number": "BD123456789",
 "url": "https://bluedart.com/track/BD123456789"
 },
 "createdAt": "2026-03-20T10:30:00.000Z"
}

Get Customer Orders

GET/v1/shopify/customers/orders

Get a customer's order history by phone number or email.

Required scope·shopify:orders:read

Query Parameters

ParameterTypeDefaultDescription
identifierstring Phone (E.164 or local) or email (required)
terminal
curl "https://api.slide.cx/v1/shopify/customers/orders?identifier=%2B919876543210" \
 -H "Authorization: Bearer sk_live_YOUR_API_KEY"
example.ts
const res = await fetch(
 'https://api.slide.cx/v1/shopify/customers/orders?identifier=user@example.com',
 { headers: { 'Authorization': 'Bearer sk_live_YOUR_API_KEY' } }
);
const { customer, orders } = await res.json();

Validate Discount Code

GET/v1/shopify/discounts/validate

Validate a discount or promo code in real time.

Required scope·shopify:discounts:read

Query Parameters

ParameterTypeDefaultDescription
codestring Discount code to validate (required)
terminal
curl "https://api.slide.cx/v1/shopify/discounts/validate?code=SAVE20" \
 -H "Authorization: Bearer sk_live_YOUR_API_KEY"

Response

response.json
{
 "valid": true,
 "code": "SAVE20",
 "type": "percentage",
 "value": 20,
 "minimumOrderAmount": "500.00",
 "expiresAt": "2026-06-30T23:59:59.000Z",
 "usageCount": 142,
 "usageLimit": 1000
}

Shopify Store Audit API

Audit the connected Shopify store: a score per section, the problems found, and fixes Slide can suggest and write to the store for you. The same audit as the store audit in the dashboard's Shopify reports. Starting an audit, its price and fix suggestions need a connected store (404 otherwise).

Warningshopify:audit:write spends money and changes your store. One audit per store per month is free and each further audit is charged from the wallet; fix suggestions made with AI are charged by AI usage; applying a fix changes live product, collection or page data in Shopify. A wallet that cannot pay answers 402 with code (INSUFFICIENT_BALANCE or WALLET_LOCKED) and reason.

Latest Audit

GET/v1/shopify/audit

The newest audit with its section results. run is null before the first audit. A running audit shows the sections finished so far.

Required scope·shopify:audit:read
response.json
{
 "run": {
 "id": "...",
 "shopDomain": "acme.myshopify.com",
 "status": "COMPLETED",
 "trigger": "MANUAL",
 "source": "API",
 "overallScore": 72,
 "checksVersion": "...",
 "chargedPaise": 0,
 "errorMessage": null,
 "startedAt": "2026-10-25T07:00:00.000Z",
 "finishedAt": "2026-10-25T07:04:10.000Z",
 "createdAt": "2026-10-25T07:00:00.000Z",
 "weights": null,
 "sectionResults": [
 { "section": "performance", "status": "...", "score": 64, "critical": 0, "high": 2, "medium": 3, "low": 1, "checks": [ ... ], "finishedAt": "2026-10-25T07:01:30.000Z" }
 ]
 }
}

A run's status is QUEUED, RUNNING, COMPLETED, PARTIAL, FAILED or CANCELLED. Sections: performance, technical_seo, ai_visibility, conversion, ux_mobile, product_pages, cart_checkout, tracking, theme, apps, store_setup, catalog, retention, security, compliance.

Price of the Next Audit

GET/v1/shopify/audit/quote

Whether the next audit is free or what it costs. Ask before starting one.

Required scope·shopify:audit:read
example.ts
interface StoreAuditQuote {
  isFree: boolean;       // true while this month's free audit is unused
  pricePaise: number;    // what the next audit costs the wallet, in paise
  priceUsd: number;      // the same price in US dollars
  currency: 'INR';
  freeResetsOn: string;  // ISO time the next free audit becomes available (00:00 India time on the 1st)
}

List Audits, Get One

EndpointScopeWhat it does
GET /v1/shopify/audit/runsshopify:audit:readEvery audit, newest first, without section results. page (default 1) and limit (default 20, max 100); answers { data, meta }.
GET /v1/shopify/audit/runs/:idshopify:audit:readOne audit with its section results, in the shape of the latest audit above. 404 for an unknown id.

Start an Audit

POST/v1/shopify/audit/runs

Starts an audit in the background and answers 202 at once. When it is not free, the wallet is charged first. Read progress with GET /v1/shopify/audit/runs/:id.

Required scope·shopify:audit:write
response.json
{ "runId": "...", "status": "QUEUED", "isFree": true, "charged": false, "pricePaise": 0 }

402 when the wallet cannot pay (the body adds pricePaise), 409 when an audit is already running, 404 when no store is connected. Takes no body.

Issues

GET/v1/shopify/audit/issues

Every problem the audits found, most recently seen first, each with its newest finding and a fix plan (who fixes it, about how long it takes, what it changes).

Required scope·shopify:audit:read
ParameterTypeDefaultDescription
statusstring OPEN, IN_PROGRESS, FIXED or IGNORED
sectionstring One of the section ids above
severitystring critical, high, medium or low
pagenumber1Page number
limitnumber20Results per page (max 100)
response.json
{
 "data": [
 {
 "id": "...",
 "checkId": "...",
 "section": "product_pages",
 "problemKey": "...",
 "title": "...",
 "severity": "high",
 "impact": 4,
 "effort": 2,
 "status": "OPEN",
 "assigneeUserId": null,
 "firstSeenRunId": "...",
 "lastSeenRunId": "...",
 "firstSeenAt": "2026-09-02T07:00:00.000Z",
 "lastSeenAt": "2026-10-25T07:04:10.000Z",
 "resolvedAt": null,
 "priority": 2,
 "quickWin": true,
 "fixPlan": { "owner": "merchant", "ownerWords": "...", "minutes": { "min": 10, "max": 20 }, "per": "product", "totalMinutes": { "min": 490, "max": 980 }, "timeWords": "...", "expectedResult": "...", "estimate": "catalogue" },
 "latestFinding": { "id": "...", "runId": "...", "severity": "high", "affected": [ ... ], "affectedCount": 49, "evidence": { ... }, "fixGuide": { ... }, "createdAt": "2026-10-25T07:04:10.000Z" }
 }
 ],
 "meta": { "total": 31, "page": 1, "limit": 20, "totalPages": 2 }
}

impact (how much fixing it helps) and effort (how much work the fix is) run from 1 to 5. priority is impact divided by effort, and a quick win is impact 3 or more with effort 2 or less.

EndpointScopeWhat it does
GET /v1/shopify/audit/issues/:idshopify:audit:readOne issue: latestFinding in full, history (one line per finding without its evidence, newest first, up to 50) and fixes (the first page of its suggested fixes, as GET .../fixes answers it).
PATCH /v1/shopify/audit/issues/:idshopify:audit:writeChanges the status only: { "status": "OPEN" | "IN_PROGRESS" | "FIXED" | "IGNORED" }. A later audit reopens a FIXED issue it finds again; IGNORED is never changed by an audit.

Suggested Fixes

For issues Slide can fix, ask for suggestions, review or edit each one, then apply them. Nothing in the store changes until you apply, and an applied fix can be undone.

EndpointScopeWhat it does
POST /v1/shopify/audit/issues/:id/fixes/suggestshopify:audit:writeMakes suggestions in the background (202). 422 with reason no_fix when Slide cannot suggest a change for this issue; 409 while suggestions are already being made; 402 when AI suggestions cannot be paid for.
GET /v1/shopify/audit/issues/:id/fixesshopify:audit:readThe suggestions, oldest first, with generation (the progress of the latest suggest request) and access (whether the store granted the permissions each kind of change needs). status filters, page and limit page.
PATCH /v1/shopify/audit/fixes/:idshopify:audit:writeEdits a suggestion before it is applied: { "value": ... } (text, a list of tags, or { "value", "unit" } for a weight) or { "clear": true }. Changes nothing in the store.
POST /v1/shopify/audit/fixes/applyshopify:audit:writeWrites the chosen suggestions to the store in the background: { "fixIds": ["..."] }, 1 to 200. Answers 202 with { batchId, count, skipped: [{ id, reason }] }.
GET /v1/shopify/audit/fixes/batches/:batchIdshopify:audit:readProgress of an apply: { batchId, total, counts (by status), done, stale, items }.
POST /v1/shopify/audit/fixes/:id/undoshopify:audit:writePuts the value back that was in the store before the fix. 409 when the value was changed in Shopify since it was applied, or when the fix is not applied.
response.json
// One suggestion, as every fixes route returns it
{
 "id": "...",
 "issueId": "...",
 "runId": "...",
 "batchId": null,
 "kind": "...",
 "kindTitle": "Product SEO title",
 "resourceType": "...",
 "resourceId": "...",
 "parentResourceId": null,
 "resourceLabel": "Linen shirt",
 "field": "...",
 "beforeValue": "Linen shirt",
 "suggestedValue": "Linen shirt for men, breathable summer fit",
 "appliedValue": null,
 "source": "...",
 "status": "SUGGESTED",
 "error": null,
 "scopeFeature": "...",
 "accessGranted": true,
 "limits": { ... },
 "needsValue": false,
 "canEdit": true,
 "canDismiss": true,
 "canApply": true,
 "canUndo": false,
 "appliedAt": null,
 "revertedAt": null,
 "createdAt": "2026-10-25T07:10:00.000Z",
 "updatedAt": "2026-10-25T07:10:00.000Z"
}

A suggestion's status is SUGGESTED, EDITED, APPLYING, APPLIED, SKIPPED_CHANGED (the value changed in Shopify after it was suggested, so it was not written), FAILED, REVERTED or DISMISSED.

AI Search API

How AI assistants answer the questions your customers ask: whether they name your brand, in what position, which competitors they name and whether they cite your website. For every workspace, with or without Shopify. The same data as AI Search in the dashboard.

Warningaeo:write spends money. A check asks every active question of each AI assistant and is charged from the wallet by AI usage, and active questions are also checked every week, which is charged too. A wallet that cannot pay answers 402 with code and reason (insufficient_balance or wallet_locked).

Summary

GET/v1/aeo/summary

The latest answer per question and assistant over the last 30 days, with the totals: how often your brand is named, its average position, competitor mentions and how often your website is cited.

Required scope·aeo:read
response.json
{
 "label": "API-sampled; answers people see in the apps can differ.",
 "window": { "since": "2026-09-25T07:00:00.000Z", "until": "2026-10-25T07:00:00.000Z", "days": 30 },
 "configured": true,
 "brand": { "names": ["Acme"], "domains": ["acme.com"], "competitors": [{ "name": "Globex", "domain": "globex.com" }] },
 "assistants": { "available": [{ "provider": "...", "label": "...", "model": "..." }], "notAvailable": [ ... ] },
 "notChecked": [{ "name": "...", "reason": "..." }],
 "activeRun": null,
 "totals": {
 "answers": 40,
 "mentioned": 22,
 "mentionRate": 0.55,
 "averagePosition": 2.4,
 "competitorShare": 0.7,
 "competitorMentions": [{ "name": "Globex", "answers": 18 }],
 "grounded": 30,
 "groundedCitingStore": 9,
 "citationShare": 0.3,
 "byProvider": [{ "provider": "...", "label": "...", "answers": 10, "mentioned": 6, "mentionRate": 0.6, "averagePosition": 2, "grounded": 8, "groundedCitingStore": 3 }]
 },
 "prompts": [{ "id": "...", "prompt": "best linen shirts in India", "market": "India", "answers": [ ... ] }],
 "lastSampledAt": "2026-10-24T07:00:00.000Z",
 "latestRun": { "batchId": "...", "answers": 40, "chargedPaise": ..., "sampledAt": "2026-10-24T07:00:00.000Z" },
 "note": "Active questions are checked every week on each AI assistant, and each check is charged from your wallet by AI usage."
}

Each assistant is identified by provider (an id) and label (the name people know). Rates are fractions from 0 to 1, and null when there is nothing to divide. latestRun.chargedPaise is what the newest check cost the wallet, in paise.

Mention Trend

GET/v1/aeo/trend

One point per finished check, oldest first, each with its answers, mentions and mention rate, overall and per assistant. A check still running is left out until it finishes.

Required scope·aeo:read
ParameterTypeDefaultDescription
daysnumber90How many days back, 7 to 365
response.json
{
 "label": "API-sampled; answers people see in the apps can differ.",
 "window": { "since": "2026-07-27T07:00:00.000Z", "until": "2026-10-25T07:00:00.000Z", "days": 90 },
 "data": [
 { "batchId": "...", "sampledAt": "2026-10-24T07:00:00.000Z", "answers": 40, "mentioned": 22, "mentionRate": 0.55,
 "byAssistant": [{ "provider": "...", "label": "...", "answers": 10, "mentioned": 6 }] }
 ],
 "capped": false,
 "runningBatchId": null
}

At most the newest 100 checks are returned; capped is true when there were more.

Answers

GET/v1/aeo/answers

Every stored answer, newest first.

Required scope·aeo:read
ParameterTypeDefaultDescription
promptIdstring Only answers to this question
providerstring Only answers from this assistant: a provider id as the summary lists it
batchIdstring Only answers from this check
pagenumber1Page number
limitnumber20Results per page (max 100)
response.json
{
 "data": [
 {
 "id": "...",
 "promptId": "...",
 "batchId": "...",
 "promptText": "best linen shirts in India",
 "market": "India",
 "provider": "...",
 "providerLabel": "...",
 "model": "...",
 "sampledAt": "2026-10-24T07:00:00.000Z",
 "grounded": true,
 "brandMentioned": true,
 "position": 2,
 "competitors": ["Globex"],
 "products": ["Linen shirt"],
 "citations": ["https://acme.com/linen"],
 "ownDomainCited": true,
 "excerpt": "...",
 "chargedPaise": ...
 }
 ],
 "meta": { "total": 120, "page": 1, "limit": 20, "totalPages": 6 },
 "label": "API-sampled; answers people see in the apps can differ."
}

Questions and Brand Profile

EndpointScopeWhat it does
GET /v1/aeo/promptsaeo:readEvery question (at most 100 per workspace, so one page is the whole list): { data: [{ id, prompt, market, active, createdAt, updatedAt }], activeCount, maxActive, note, meta }. Active questions first.
POST /v1/aeo/promptsaeo:writeAdds a question: { "prompt": "...", "market"?: "..." }. The question is 5 to 300 characters, the market up to 60. A new question is active, so it is checked, and charged, every week; at most 20 are active at once. Answers 201 with { prompt, note }.
PATCH /v1/aeo/prompts/:idaeo:writeChanges a question: any of prompt, market (null clears it) and active (false stops the weekly check). Answers { prompt, note }.
GET /v1/aeo/settingsaeo:readThe brand profile: { configured, brandNames, domains, competitors, market, language, updatedAt, suggestions, limits }. suggestions holds values to review while nothing is saved, and is null after that.
PUT /v1/aeo/settingsaeo:writeReplaces the brand profile: brandNames (1 to 10), domains (1 to 10 public website domains), competitors (up to 10, each { name, domain? }), market and language. Answers the saved profile, as GET does.

Run a Check

POST/v1/aeo/run

Asks every active question of each available AI assistant now, in the background, and answers 202. Charged from the wallet by AI usage. Takes no body.

Required scope·aeo:write
response.json
{
 "batchId": "...",
 "status": "QUEUED",
 "questions": 6,
 "assistants": [{ "provider": "...", "label": "...", "model": "..." }],
 "notAvailable": [ ... ],
 "label": "API-sampled; answers people see in the apps can differ."
}

400 until a brand profile and at least one active question exist, 402 when the wallet cannot pay, 409 with reason: "active_run" and activeBatchId while a check is running. Read the results with GET /v1/aeo/summary or GET /v1/aeo/answers?batchId=... once it finishes.

Website Events API

Send a named event, such as pay or view_button_clicked, from your own server or any language that can make an HTTPS request, and read the events back. Events appear in the Events tab of Web Analytics, for the website you sent them to, tied to the visit behind them when you pass that on. Your website sends events from the browser with slideAnalytics.track(); this API is for everything that happens on your side.

WarningSend events with an API key from your server only. A request that carries an Origin header comes from a web page, where anyone can read the key, so it is refused with 400. In a web page use slideAnalytics.track().

Every route works on one website of the key's workspace. Name it with site (its domain, id or site key), or leave it out when the workspace has one website. The website must be verified, or the answer is 409. Each request counts as one API call against your plan, so send events in batches of up to 50.

Send Events

POST/v1/analytics/events

Send up to 50 events in one request. Each event is checked on its own: a bad event is listed in rejected with the reason, and the good ones are kept. Answers 202.

Required scope·analytics:events:write

Request Body

FieldTypeRequiredDescription
sitestringNoThe website: its domain, id or site key. Needed only when the workspace has several websites.
eventsobject[]Yes1 to 50 events, described below.

Event Fields

FieldTypeRequiredDescription
namestringYesWhat happened. Starts with a letter, up to 40 characters of Latin letters, digits, spaces and _ . : -. It is stored lower case with underscores, so "View Button Clicked" becomes view_button_clicked. A website can use up to 500 different names a day, so put changing values in properties, never in the name.
userobjectIf no visitorIdWho it was: id, email, phone, name and createContact. At least one of id, email or phone. See below.
visitorIdstringIf no userThe browser, from slideAnalytics.getIds() on your page. 6 to 64 letters, digits, dot, hyphen or underscore.
sessionIdstringNoThe visit, from slideAnalytics.getIds(). It needs the visitorId of the same browser. With it, the event joins that visit.
timenumber or stringNoWhen it happened, as epoch seconds, epoch milliseconds or an ISO date. Defaults to now. At most 7 days back; older times are rejected, never moved to today.
insertIdstringNoYour own id for this event, up to 128 characters. Sending the same insertId on the same event name never counts twice, which makes a retry safe. Generated when you leave it out, so without it a retry counts again.
valueMinorintegerNoAn amount in paise or cents, for example 199900 for Rs 1,999. Needs currency.
currencystringWith valueMinorA 3-letter code such as INR or USD.
propertiesobjectNoFree details such as plan or method. Up to 20 keys; values are text, numbers or true/false. Text longer than 200 characters is cut; an object or a list is dropped.
pathstringNoThe page it relates to, for example /checkout. Defaults to /.

Put who it was in user, never in properties. Properties whose key or value looks like personal data (an email address, a phone number, a password, a token, a card) are dropped and listed in droppedProperties. Keys that end in _id or _ref, such as order_id, keep their digits.

terminal
curl -X POST https://api.slide.cx/v1/analytics/events \
 -H "Authorization: Bearer sk_live_YOUR_API_KEY" \
 -H "Content-Type: application/json" \
 -d '{
 "site": "example.com",
 "events": [{
 "name": "pay",
 "insertId": "order_10023",
 "user": { "id": "u_123", "email": "asha@example.com" },
 "valueMinor": 199900,
 "currency": "INR",
 "properties": { "plan": "pro", "order_id": "A-10023456" }
 }]
 }'
example.ts
const res = await fetch('https://api.slide.cx/v1/analytics/events', {
 method: 'POST',
 headers: {
 Authorization: `Bearer ${process.env.SLIDE_API_KEY}`,
 'Content-Type': 'application/json',
 },
 body: JSON.stringify({
 events: [{ name: 'pay', insertId: 'order_10023', user: { id: 'u_123' }, valueMinor: 199900, currency: 'INR' }],
 }),
});
const result = await res.json(); // { accepted: 1, capped: 0, rejected: [], droppedProperties: [] }
example.py
import os
import requests

res = requests.post(
 "https://api.slide.cx/v1/analytics/events",
 headers={"Authorization": f"Bearer {os.environ['SLIDE_API_KEY']}"},
 json={"events": [{"name": "pay", "insertId": "order_10023", "user": {"id": "u_123"}, "valueMinor": 199900, "currency": "INR"}]},
 timeout=10,
)
print(res.json())  # {"accepted": 1, "capped": 0, "rejected": [], "droppedProperties": []}

Response (202)

response.json
{
 "accepted": 2,
 "capped": 0,
 "rejected": [
 { "index": 2, "code": "invalid_value", "message": "valueMinor must be a whole number of paise or cents, with a 3-letter currency." }
 ],
 "droppedProperties": [{ "index": 0, "keys": ["customer_email"] }]
}

accepted is the number of events taken in. capped is the number held back by the website's overall daily limits. rejected lists each refused event by its index in your array, with a code and a readable message. An event is never dropped silently: check rejected in your code.

Rejection codeMeaning
invalid_eventThe event is not an object.
invalid_namename is missing, or has no usable letters.
invalid_timetime is not epoch seconds, epoch milliseconds or an ISO date.
future_timetime is in the future.
too_oldtime is more than 7 days old.
no_identityThe event has neither a visitorId nor a user with an id, email or phone.
invalid_idvisitorId or sessionId is not 6 to 64 letters, digits, dot, hyphen or underscore, or a sessionId came without its visitorId.
invalid_valuevalueMinor is not a whole number, or currency is not 3 letters.
too_many_namesThe website already used 500 different event names today.
needs_contacts_scopeThe event asks for createContact, and the key does not hold contacts:write.
daily_limit_reachedThe website has used its daily limit of API events. This event was not taken in.

Limits and Errors

Limit or statusWhat it means and what to do
50 events a requestMore than 50 in one request is a 400. Split the batch.
33,000 API events a day per websiteA technical limit that keeps reports fast. Over it, a request answers 429 with code daily_limit_reached and limit. The count restarts at midnight UTC.
7 days backEvents older than 7 days are rejected with too_old.
500 event names a day per websiteCounted separately for this API and for the browser. A new name past it is rejected with too_many_names. Names already used keep working.
20 properties an eventAny more are left off and listed in droppedProperties.
429 daily_limit_reachedNothing in the request was taken in. Wait until the next UTC day, or send fewer events.
503 try_againEvents are being processed. The body has retryAfterSeconds. Send the same batch again after that time: events with the same insertId never count twice. Without insertId a resend counts again.
409The website is not verified yet. Verify it in Web Analytics first.
400The body is malformed, the request had an Origin header (a web page), or the workspace has several websites and no site was named.

The user Object

FieldTypeDescription
idstringYour own id for the person. Up to 128 characters. Events with the same id belong to one person, in every browser they use.
emailstringMatches an existing contact by email.
phonestringMatches an existing contact by phone.
namestringUsed only when a contact is created.
createContactbooleanDefault false. When true and no contact matches the email or phone, a contact is created, if the key also holds contacts:write. A contact that matches is left unchanged. It is created under the normal contact rules, which count toward your contact allowance and can start automations. Leave it off unless you want that.

A contact is matched by email or phone and linked to the visitor. A visitor already tied to a different contact is left alone. Personal details are kept in the contacts hub and never in the event itself.

How an Event Finds Its Visit

In this order: the sessionId you sent; else the visit that visitorId was on in the 30 minutes before the event; else the visit of the browser last linked to user.id, by the same rule. An event with no visit behind it (a payment confirmation, a scheduled job) still counts under its name and shows as Server in the Events tab, but it never changes visits, funnels or conversions.

Send sessionId when you have it. The browser's own events reach Slide up to 30 seconds after they happen, and a user.id is linked to its browser about a minute after it is first sent, so an event sent straight after a page loads, carrying only a visitorId or a user.id, counts with no visit behind it.

When you name a person, the visit joins their contact, and so do that browser's later visits, within about 5 minutes. A new person on the same browser replaces the old one from then on; what was already linked stays with the first person.

Link a Person to a Visit

POST/v1/analytics/identify

Tell Slide who a browser belongs to, for example when someone logs in, without sending an event. Earlier and later events of that visitor then belong to the person. Answers 202.

Required scope·analytics:events:write
FieldTypeRequiredDescription
sitestringNoThe website, as for sending events.
visitorIdstringNoThe browser, from slideAnalytics.getIds(). Without it the person has no browser yet and later events with the same user.id are grouped under them.
userobjectYesThe user object above. At least one of id, email or phone, or the answer is 400 no_identity.
terminal
curl -X POST https://api.slide.cx/v1/analytics/identify \
 -H "Authorization: Bearer sk_live_YOUR_API_KEY" \
 -H "Content-Type: application/json" \
 -d '{ "visitorId": "v_7d3e91aa0c12", "user": { "id": "u_123", "email": "asha@example.com" } }'
response.json
{ "accepted": true }

List Events

GET/v1/analytics/events

Every event name of a date range, busiest first (up to 100), with how often it happened, in how many visits, and the last day it happened. Counts are refreshed every 5 minutes and kept for good.

Required scope·analytics:events:read
ParameterTypeDefaultDescription
sitestring The website, as for sending events
fromstring29 days before toFirst day, YYYY-MM-DD (UTC)
tostringtodayLast day, YYYY-MM-DD (UTC). A range covers at most 92 days.
response.json
{
 "from": "2026-10-03",
 "to": "2026-11-01",
 "events": [
 { "name": "pay", "count": 412, "sessions": 388, "lastDay": "2026-11-01" },
 { "name": "view_button_clicked", "count": 96, "sessions": 90, "lastDay": "2026-10-31" }
 ]
}

sessions counts visits that included the event. An event sent from a server with no visit behind it is in count and not in sessions.

Get One Event

GET/v1/analytics/events/:name

One event over a date range: a daily trend, the number of people, money by currency, the most common property values and the latest events. Takes the same site, from and to parameters. A name that was never sent answers a count of 0 and empty lists; a name that cannot be an event name answers 404.

Required scope·analytics:events:read
response.json
{
 "name": "pay",
 "from": "2026-10-03",
 "to": "2026-11-01",
 "count": 412,
 "people": 377,
 "trend": [{ "day": "2026-10-31", "count": 21, "sessions": 20 }],
 "money": [{ "currency": "INR", "valueMinor": 82188800, "events": 412 }],
 "properties": [{ "key": "plan", "values": [{ "value": "pro", "count": 301 }, { "value": "starter", "count": 111 }] }],
 "sampled": 412,
 "recent": [
 {
 "id": "...",
 "name": "pay",
 "occurredAt": "2026-11-01T09:14:03.000Z",
 "source": "server",
 "userId": "u_123",
 "visitorId": "v_7d3e91aa0c12",
 "sessionId": "s_8f2k1a9c",
 "contactId": null,
 "path": "/checkout",
 "valueMinor": 199900,
 "currency": "INR",
 "properties": { "plan": "pro", "order_id": "A-10023456" }
 }
 ]
}

people is null for ranges longer than 31 days. trend leaves out days with no events. sessionId is null when the event has no visit behind it. source is server for this API and browser for slideAnalytics.track(). properties is read from the latest sampled events (at most 2,000), and recent holds the newest 50. Single events are kept for 90 days; the daily counts in trend and count are kept for good.

One Person's Events

GET/v1/analytics/users/:userId/events

The events of one person, newest first, across every browser they used and the events your server sent with their user id. :userId is the id you sent in user.id.

Required scope·analytics:events:read
ParameterTypeDefaultDescription
sitestring The website, as for sending events
fromstring29 days before toFirst day, YYYY-MM-DD (UTC)
tostringtodayLast day, YYYY-MM-DD (UTC)
pagenumber1Page number
limitnumber50Results per page (max 100)

Answers { data: [...], meta: { total, page, limit, totalPages } }, where each row has the same shape as an item of recent above.

Funnels

GET/v1/analytics/funnels

How many people got through 2 to 6 steps, in order. A step is an event name you sent or an action Slide records by itself: page_view, product_view or add_to_cart.

Required scope·analytics:events:read
ParameterTypeDefaultDescription
sitestring The website, as for sending events
stepsstring Required. The step names in order, separated by commas, 2 to 6: steps=page_view,add_to_cart,pay
fromstring29 days before toFirst day, YYYY-MM-DD (UTC). The range covers at most 31 days (400 above)
tostringtodayLast day, YYYY-MM-DD (UTC)
withinHoursnumber168How long after one step the next still counts, 1 to 720 hours
response.json
{
 "from": "2026-10-03",
 "to": "2026-11-01",
 "withinHours": 168,
 "steps": [
 { "name": "page_view", "people": 5210, "fromPrevious": null, "fromFirst": 100 },
 { "name": "add_to_cart", "people": 640, "fromPrevious": 12.3, "fromFirst": 12.3 },
 { "name": "pay", "people": 188, "fromPrevious": 29.4, "fromFirst": 3.6 }
 ]
}

Each person counts once per step, only when they did it after the step before and inside the window. fromPrevious and fromFirst are percentages with one decimal, or null where there is nobody to divide by; fromPrevious of the first step is always null. A person is a browser, so someone seen on two devices counts twice. The answer is counted from single events and cached for 5 minutes. 422 means the dates hold too much to read at once: send a shorter range.

Cohorts

GET/v1/analytics/cohorts

Weekly groups by the week a person first did one event, and the share of each group that did another event, or the same one, in the weeks after. Weeks start on Monday (UTC).

Required scope·analytics:events:read
ParameterTypeDefaultDescription
sitestring The website, as for sending events
startstring Required. The event, or built-in action, that puts a person in a week's group
returnstringstartThe event that counts as coming back
fromstring83 days before toFirst day, YYYY-MM-DD (UTC). The range covers at most 91 days (400 above)
tostringtodayLast day, YYYY-MM-DD (UTC)
response.json
{
 "from": "2026-08-10",
 "to": "2026-11-01",
 "start": "signup_done",
 "return": "pay",
 "cohorts": [
 {
 "week": "2026-10-19",
 "people": 120,
 "returned": [
 { "weeksLater": 0, "people": 30, "percent": 25 },
 { "weeksLater": 2, "people": 12, "percent": 10 }
 ]
 }
 ]
}

weeksLater is 0 for the week the person first did start. A week in which nobody came back is left out of returned (week 1 above), so read it by weeksLater and not by position. A person is a browser. Cached for 5 minutes; 422 as for funnels.

Node and Python

Website events need @synquic/slide 0.6 or synquic-slide 0.4. track returns at once and sends in batches of up to 50, retrying with the same insertId, so it never slows your request and a retry never counts twice. A serverless function must call flush() before it returns.

example.ts
import { SlideClient } from '@synquic/slide';

const slide = new SlideClient({ apiKey: process.env.SLIDE_API_KEY! });

slide.analytics.track({
  name: 'pay',
  visitorId,            // from slideAnalytics.getIds() in the browser
  sessionId,
  user: { id: 'u_123' },
  valueMinor: 199900,   // paise or cents, a whole number
  currency: 'INR',
  properties: { plan: 'pro', method: 'upi' },
});

await slide.analytics.flush();   // serverless functions only
example.py
import os
from synquic_slide import SlideClient

slide = SlideClient(api_key=os.environ["SLIDE_API_KEY"])

slide.analytics.track(
    "pay",
    visitor_id=visitor_id,   # from slideAnalytics.getIds() in the browser
    session_id=session_id,
    user_id="u_123",
    value_minor=199900,      # paise or cents, a whole number
    currency="INR",
    properties={"plan": "pro", "method": "upi"},
)

slide.analytics.flush()      # serverless functions only

In the Browser

With the Slide analytics snippet on your page, the browser sends events itself and hands you the ids of the visit, so an event your server sends afterwards joins that visit. The browser needs no API key.

example.ts
// Name an action on the page. Details are free text, numbers or true/false.
window.slideAnalytics.track('view_button_clicked', { plan: 'pro' });

// Hand this visit to your server. Both are null when the visitor has opted out.
const { visitorId, sessionId } = window.slideAnalytics.getIds();

Send visitorId and sessionId to your server with your own request, then include them in the event you send there. An event from the browser carries at most 10 properties, and the script does not ask for consent by itself: where you need consent, load it only after the visitor agrees. It stops for Do Not Track and for slideAnalytics.disable().

The site key is public, so a browser can name a person only with a signature from your server. Sign the user id (trimmed, at most 128 characters) with your website's identify secret: the signature is the HMAC-SHA256 of the id, keyed with the secret, as 64 lower-case hex characters. In Node.js that is slide.analytics.signUser(userId, secret), in Python slide.analytics.sign_user(user_id, secret); neither makes a request. The secret is shown to people who can edit widgets, in Web Analytics, under Developers, step 6.

example.ts
// On your server, for the signed-in person only
const signature = slide.analytics.signUser(userId, process.env.SLIDE_IDENTIFY_SECRET!);

// In the page, with the userId and signature your server rendered
window.slideAnalytics.identify(userId, signature);
WarningKeep the secret on your server. A secret in a page lets anyone attach any browser to any person. A signed call links a user id only: to link a contact, by email or phone, call POST /v1/analytics/identify from your server.

AI Agents API

Read and manage the AI agents that answer your customers on WhatsApp, Instagram, website chat, SMS and email: what each one got done, every run it made, and its versions. A key only ever reaches the agents of its own workspace: another workspace's agent is a 404, exactly like an id that does not exist.

NoteRuns never return what a customer wrote, what the agent replied or who the customer is. Phone numbers, email addresses and credentials are removed from tool inputs and results.

List agents

GET/v1/agents

The workspace's agents, newest first, with their live and draft versions.

Required scope·agents:read

Query Parameters

ParameterTypeDefaultDescription
pagenumber1Page number (1-indexed)
limitnumber50Results per page (max 100)
searchstring Agents whose name or description contains this text, any case. Up to 120 characters.
audiencestring CUSTOMER, PATIENT, STAFF or ACCOUNT
isActiveboolean Only active (true) or inactive (false) agents
terminal
curl "https://api.slide.cx/v1/agents?limit=10" \
 -H "Authorization: Bearer sk_live_YOUR_API_KEY"

Response

response.json
{
 "data": [
 {
 "id": "8f0c2a4e-...",
 "name": "Booking Assistant",
 "description": "Books home services and takes payment",
 "audience": "CUSTOMER",
 "isActive": true,
 "versionCount": 12,
 "boundStepCount": 1,
 "publishedVersion": { "id": "...", "version": 12, "publishedAt": "2026-10-24T09:12:00.000Z" },
 "draftVersion": null,
 "createdAt": "2026-09-13T08:00:00.000Z",
 "updatedAt": "2026-10-24T09:12:00.000Z"
 }
 ],
 "meta": { "total": 1, "page": 1, "limit": 10, "totalPages": 1 }
}

Get an agent

GET/v1/agents/:id

One agent, with its versions summarised. Read a version's full content with GET /v1/agents/:id/versions/:versionId.

Required scope·agents:read

List an agent's versions

GET/v1/agents/:id/versions

Every version of the agent, newest first, summarised: its number, status, model, limits, how many tools and knowledge items it holds and how many runs it answered.

Required scope·agents:read
ParameterTypeDefaultDescription
statusstring DRAFT, PUBLISHED or ARCHIVED
pagenumber1Page number
limitnumber20Results per page (max 100)
response.json
{
 "data": [
 {
 "id": "...",
 "version": 12,
 "status": "PUBLISHED",
 "modelRef": "...",
 "maxIterations": 6,
 "maxTurns": null,
 "hasMemory": true,
 "publishedAt": "2026-10-24T09:12:00.000Z",
 "publishedBy": { "id": "...", "name": "Asha Rao" },
 "toolCount": 5,
 "knowledgeCount": 2,
 "runCount": 1794,
 "createdAt": "2026-10-24T08:40:00.000Z",
 "updatedAt": "2026-10-24T09:12:00.000Z"
 }
 ],
 "meta": { "total": 12, "page": 1, "limit": 20, "totalPages": 1 }
}

Get one version

GET/v1/agents/:id/versions/:versionId

One version in full: its instructions, model, tools and their settings, knowledge, hand-off and session settings, and whether it can still be edited (only a DRAFT can).

Required scope·agents:read
response.json
{
 "id": "...",
 "version": 13,
 "status": "DRAFT",
 "publishedAt": null,
 "publishedBy": null,
 "createdAt": "2026-10-25T06:00:00.000Z",
 "updatedAt": "2026-10-25T06:10:00.000Z",
 "editable": true,
 "content": {
 "systemPrompt": "You book home services ...",
 "modelRef": "...",
 "maxIterations": 6,
 "maxTurns": null,
 "exitKeyword": null,
 "hasMemory": true,
 "tools": [{ "toolKey": "...", "requiresVerification": false, "needsApproval": false, "config": null }],
 "knowledgeIds": ["..."],
 "handoffPolicy": null,
 "sessionPolicy": null
 }
}

Credentials are removed from every tool's config; a config with nothing else left is null. modelRef is the slug of a model from GET /v1/ai/models.

What an agent got done

GET/v1/agents/:id/performance

The bookings, orders, payments and other results the agent's tools completed, cancellations counted apart, the share of its conversations with a result, a daily trend and the AI cost per result. Counted from successful tool calls only; nothing is estimated.

Required scope·agents:read

Query Parameters

ParameterTypeDefaultDescription
daysnumber30How many days back, 1 to 180
terminal
curl "https://api.slide.cx/v1/agents/8f0c2a4e-.../performance?days=30" \
 -H "Authorization: Bearer sk_live_YOUR_API_KEY"

Response

response.json
{
 "agentId": "8f0c2a4e-...",
 "from": "2026-09-25T07:00:00.000Z",
 "to": "2026-10-25T07:00:00.000Z",
 "objective": { "label": "Complete actions for customers", "measure": "actions completed through its tools" },
 "outcomes": { "runs": 1900, "answeredWithoutPerson": 1500, "handedOff": 310, "resolved": 1100, "failed": 12 },
 "conversations": 1794,
 "convertedConversations": 140,
 "conversionRate": 7.8,
 "totals": { "conversions": 152, "reversals": 32, "changes": 13 },
 "byLabel": [
 { "toolKey": "mcp__homeservices__book_service", "label": "Book service", "kind": "CONVERSION", "count": 152 },
 { "toolKey": "mcp__homeservices__cancel_booking", "label": "Cancel booking", "kind": "REVERSAL", "count": 32 }
 ],
 "trend": [{ "date": "2026-10-24", "conversions": 6, "reversals": 1 }],
 "recent": [{ "runId": "...", "label": "Book service", "kind": "CONVERSION", "at": "2026-10-24T18:02:11.000Z", "channel": "WHATSAPP" }],
 "costPaise": 288000,
 "costPerConversionPaise": 1895,
 "sampled": false,
 "source": "Successful agent tool calls ..."
}
NoteA result is a successful call of a tool whose name starts with an action (book, order, pay, create, confirm, and so on) or a built-in booking tool. Cancellations and refunds are reversals; reschedules and updates are changes. conversionRate is a percentage, and null when the agent handled no conversations in the window. objective is the goal every agent has, worked out from its tools, and outcomes counts its runs by how they ended: answered without a person, handed to the team, resolved, failed.

List an agent's runs

GET/v1/agents/:id/runs

One run per customer message the agent answered, newest first: how it ended, which version answered, which tools it used, and each failed tool call with the error it returned.

Required scope·agents:read
ParameterTypeDefaultDescription
statusstring RUNNING, COMPLETED or FAILED
outcomestring AUTOMATED, HANDED_OFF, FAILED, ABANDONED or UNCLASSIFIED
channelstring WHATSAPP, INSTAGRAM, SMS, EMAIL, VOICE, WEB or APP
versionIdstring Only runs answered by this version
fromISO 8601 Runs that started at or after this moment
toISO 8601 Runs that started before this moment
pagenumber1Page number
limitnumber20Results per page (max 50)

Get one run

GET/v1/agents/runs/:runId

One run in full: every tool call in order, with its input, a short preview of its result, how long it took and, when it failed, the error the tool returned.

Required scope·agents:read

Tool catalogue

GET/v1/agents/tool-catalog

The tools an agent may hold, by audience: the built-ins and the workspace's own active tools (custom__...). Use it before adding tools to a draft.

Required scope·agents:read

Create, edit and publish

EndpointScopeWhat it does
POST /v1/agentsagents:writeCreates an agent and its draft version 1. A draft reaches no customer.
POST /v1/agents/:id/versionsagents:writeOpens a draft from the live version (one draft at a time).
PATCH /v1/agents/:id/versions/:versionIdagents:writeEdits the draft: instructions, model, tools, knowledge, session settings (for example followUps: false switches follow-ups off).
POST /v1/agents/:id/versions/:versionId/publishagents:publishMakes the draft the live version, after every check the dashboard runs. Optional body { "expectedUpdatedAt" }: when sent, a draft changed since you read it is not published (409 AGENT_VERSION_CHANGED).
POST /v1/agents/:id/versions/:versionId/rollbackagents:publishPublishes a copy of an earlier version as a new version; history is kept.

Automations API

Read, build and run the automations of your workspace: the flows that answer and message your customers on every channel. The same automations as the dashboard's automation builder.

WarningAn automation messages real customers. POST /v1/automations therefore always creates it paused, whatever active you send, and going live is a separate activate call. Activation is refused when a step needs an integration that is not connected.

Node Catalogue

GET/v1/automations/node-catalog

Every trigger type and every step (node) type, with the config fields each one reads. Call it before creating or updating an automation: a step whose type or config is wrong saves without error and then does nothing.

Required scope·automations:read
response.json
{
 "triggers": [ ... ],
 "nodes": [ ... ],
 "unattributedFields": [ ... ],
 "meta": {
 "triggerCount": ...,
 "nodeCount": ...,
 "nodesWithDocumentedConfig": ...,
 "note": "configFields are the persisted columns each node reads. Fields under unattributedFields are settable but the schema does not record which node owns them."
 }
}

The catalogue is generated from the engine's own schema, so it is always what the engine accepts.

List Automations

GET/v1/automations

The workspace's automations, newest first: name, description, whether it is live, its trigger type and how many steps and keywords it has.

Required scope·automations:read
ParameterTypeDefaultDescription
pagenumber1Page number
limitnumber25Results per page (max 100)
response.json
{
 "data": [
 {
 "id": "...",
 "name": "Order shipped",
 "description": null,
 "active": true,
 "trigger": { "type": "SHOPIFY_EVENT" },
 "_count": { "steps": 3, "keywords": 0 },
 "createdAt": "2026-09-13T08:00:00.000Z",
 "updatedAt": "2026-10-24T09:12:00.000Z"
 }
 ],
 "meta": { "total": 8, "page": 1, "limit": 25, "totalPages": 1 }
}

Get an Automation

GET/v1/automations/:id

One automation in full: its trigger with its settings, every step with its config, keywords and linked posts. 404 for an automation of another workspace.

Required scope·automations:read

Create an Automation

POST/v1/automations

Creates an automation, always paused. Counts toward your plan's automation limit (403 when it is reached). Answers 201 with the automation and a _note saying it was created paused.

Required scope·automations:write
FieldTypeRequiredDescription
namestringYesShown in the dashboard.
descriptionstringNoUp to 500 characters.
triggerobjectNo{ "type": "...", ...its config fields }. The type and its fields come from the node catalogue.
stepsarrayNo[{ "order": 0, "type": "...", ...its config fields }], in run order. Types and fields come from the node catalogue.
keywordsarrayNoThe keywords that start a keyword-triggered flow.
postsarrayNoThe posts a comment-triggered flow listens to.
terminal
curl -X POST https://api.slide.cx/v1/automations \
 -H "Authorization: Bearer sk_live_YOUR_API_KEY" \
 -H "Content-Type: application/json" \
 -d '{ "name": "Welcome", "trigger": { "type": "..." }, "steps": [{ "order": 0, "type": "...", "message": "Hi there" }] }'

Change, Switch On and Off, Delete

EndpointScopeWhat it does
PATCH /v1/automations/:idautomations:writeChanges the automation; send the fields to change, in the create shape.
POST /v1/automations/:id/activateautomations:writePuts it live. Refused when a step needs an integration that is not connected.
POST /v1/automations/:id/pauseautomations:writeStops it. Nothing is deleted.
DELETE /v1/automations/:idautomations:writeDeletes it. Answers 204 with no body.
POST /v1/automations/:id/executions/:execId/rerunautomations:writeRuns the automation again with the trigger data of an earlier run. Answers { "ok": true } at once; the new run appears in the run history.

Run History

GET/v1/automations/:id/executions

Every run of one automation, newest first, with the result of each step.

Required scope·automations:executions:read
ParameterTypeDefaultDescription
pagenumber1Page number
limitnumber25Results per page (max 100)
response.json
{
 "data": [
 {
 "id": "...",
 "triggerType": "SHOPIFY_EVENT",
 "status": "COMPLETED",
 "context": { ... },
 "stepResults": [{ "order": 0, "type": "SEND_WHATSAPP", "status": "ok", "durationMs": 412 }],
 "errorMessage": null,
 "startedAt": "2026-10-24T09:12:00.000Z",
 "completedAt": "2026-10-24T09:12:01.000Z",
 "durationMs": 1020
 }
 ],
 "meta": { "total": 214, "page": 1, "limit": 25, "totalPages": 9 }
}

AI Models API

GET/v1/ai/models

The AI models your workspace can choose for its agents, by capability label, priced at what your workspace is charged.

Required scope·ai:models:read
response.json
{
 "data": [
 {
 "slug": "...",
 "label": "...",
 "description": "...",
 "isDefault": true,
 "supportsTools": true,
 "supportsVision": false,
 "pricing": { "inputInrPer1M": ..., "outputInrPer1M": ..., "displayCurrency": "INR", "inputPer1M": ..., "outputPer1M": ... },
 "voice": null
 }
 ]
}

Prices are per million tokens, in rupees (inputInrPer1M, outputInrPer1M) and in your display currency (inputPer1M, outputPer1M). voice is set on models a voice agent can use, with billedRatePaisePerMin. The slug of a model is what an agent takes as its modelRef. There is no write side: which models exist is decided by Slide.

Voice API

The Voice API provides programmatic access to AI voice calling manage agents, initiate outbound calls, retrieve call history with transcripts, and access recordings. Powered by enterprise SIP infrastructure.

NoteVoice endpoints require the Voice module to be enabled for your organization. Contact your admin or visit Dashboard → Admin → Modules to enable it.

List Voice Agents

GET/v1/voice/agents

List all AI voice agents configured for your organization.

Required scope·voice:agents:read

Query Parameters

ParameterTypeDefaultDescription
pagenumber1Page number (1-indexed)
limitnumber20Results per page (max 100)
terminal
curl "https://api.slide.cx/v1/voice/agents?page=1&limit=10" \
 -H "Authorization: Bearer sk_live_YOUR_API_KEY"

Response

response.json
{
 "data": [
 {
 "id": "va_01JXYZ...",
 "name": "Sales Assistant",
 "description": "Handles inbound sales inquiries",
 "language": "en-US",
 "sttProvider": "DEEPGRAM",
 "ttsProvider": "ELEVENLABS",
 "llmProvider": "OPENAI",
 "llmModel": "gpt-4o-mini",
 "isActive": true,
 "createdAt": "2026-03-01T10:00:00.000Z"
 }
 ],
 "meta": { "total": 3, "page": 1, "limit": 10, "totalPages": 1 }
}

Get Voice Agent

GET/v1/voice/agents/:id

Get full agent configuration including persona, language, and provider settings.

Required scope·voice:agents:read
terminal
curl "https://api.slide.cx/v1/voice/agents/va_01JXYZ" \
 -H "Authorization: Bearer sk_live_YOUR_API_KEY"

List Voice Calls

GET/v1/voice/calls

List voice calls with optional filtering by status and direction.

Required scope·voice:calls:read

Query Parameters

ParameterTypeDefaultDescription
pagenumber1Page number
limitnumber20Results per page (max 100)
statusstring Filter: INITIATED, RINGING, ANSWERED, COMPLETED, FAILED, NO_ANSWER, BUSY, CANCELLED
directionstring Filter: INBOUND or OUTBOUND
terminal
curl "https://api.slide.cx/v1/voice/calls?status=COMPLETED&direction=OUTBOUND&limit=10" \
 -H "Authorization: Bearer sk_live_YOUR_API_KEY"

Response

response.json
{
 "data": [
 {
 "id": "vc_01JXYZ...",
 "agentId": "va_01JXYZ...",
 "direction": "OUTBOUND",
 "callerNumber": "+911234567890",
 "calledNumber": "+919876543210",
 "status": "COMPLETED",
 "durationSeconds": 142,
 "sentiment": "POSITIVE",
 "summary": "Customer confirmed appointment for Thursday.",
 "wasConfirmed": true,
 "billedAmountPaise": 3200,
 "startedAt": "2026-03-24T10:00:00.000Z",
 "endedAt": "2026-03-24T10:02:22.000Z"
 }
 ],
 "meta": { "total": 56, "page": 1, "limit": 10, "totalPages": 6 }
}

Initiate Outbound Call

POST/v1/voice/calls/outbound

Initiate an AI-powered outbound voice call to a phone number.

Required scope·voice:calls:write

Request Body

FieldTypeRequiredDescription
agentIdstringYesID of the voice agent to use
toNumberstringYesPhone number to call (E.164 format preferred)
callContextobjectNoKey-value context passed to the AI agent during the call
terminal
curl -X POST "https://api.slide.cx/v1/voice/calls/outbound" \
 -H "Authorization: Bearer sk_live_YOUR_API_KEY" \
 -H "Content-Type: application/json" \
 -d '{
 "agentId": "va_01JXYZ...",
 "toNumber": "+919876543210",
 "callContext": {
 "customerName": "Rahul Sharma",
 "appointmentDate": "2026-04-10",
 "purpose": "appointment_confirmation"
 }
 }'
example.ts
const res = await fetch('https://api.slide.cx/v1/voice/calls/outbound', {
 method: 'POST',
 headers: {
 'Authorization': 'Bearer sk_live_YOUR_API_KEY',
 'Content-Type': 'application/json',
 },
 body: JSON.stringify({
 agentId: 'va_01JXYZ...',
 toNumber: '+919876543210',
 callContext: { customerName: 'Rahul Sharma', purpose: 'appointment_confirmation' },
 }),
});

const call = await res.json();
// { id: 'vc_01J...', status: 'INITIATED', direction: 'OUTBOUND', ... }
example.py
import requests

res = requests.post(
 'https://api.slide.cx/v1/voice/calls/outbound',
 headers={
 'Authorization': 'Bearer sk_live_YOUR_API_KEY',
 'Content-Type': 'application/json',
 },
 json={
 'agentId': 'va_01JXYZ...',
 'toNumber': '+919876543210',
 'callContext': {'customerName': 'Rahul Sharma', 'purpose': 'appointment_confirmation'},
 }
)
call = res.json()
print(call['id'], call['status']) # vc_01J... INITIATED

When no call is placed

StatuscodeWhenWhat to do
409VOICE_NUMBER_BUSYAnother call to this number is in progress, or waiting to be tried again. Always on: one call at a time per number in your workspace.Wait until that call ends, then try again
429VOICE_DAILY_CALL_CAPThis agent already started as many calls to this number as its "Calls to one person" limit allows in 24 hours. A retry of an unanswered call does not count.Do not retry now. Try again tomorrow, or raise the limit on the agent
429VOICE_LINES_BUSYEvery line your workspace may use for outbound calls is busyWait for a call to finish, then retry

Each body carries code and a message sentence. Two different reasons answer 429, so read code, not the status alone.

Get Call Details

GET/v1/voice/calls/:id

Get full call details including transcription, sentiment, and cost breakdown.

Required scope·voice:calls:read
terminal
curl "https://api.slide.cx/v1/voice/calls/vc_01JXYZ" \
 -H "Authorization: Bearer sk_live_YOUR_API_KEY"

Response

response.json
{
 "id": "vc_01JXYZ...",
 "agentId": "va_01JXYZ...",
 "direction": "OUTBOUND",
 "status": "COMPLETED",
 "callerNumber": "+911234567890",
 "calledNumber": "+919876543210",
 "callerName": "Rahul Sharma",
 "durationSeconds": 142,
 "sentiment": "POSITIVE",
 "summary": "Customer confirmed the appointment for Thursday 10 AM.",
 "wasConfirmed": true,
 "wasBooked": false,
 "transcription": [
 { "role": "agent", "content": "Hello! This is a call from Acme Corp regarding your appointment.", "timestamp": "2026-03-24T10:00:03.000Z" },
 { "role": "user", "content": "Yes, I was expecting this call.", "timestamp": "2026-03-24T10:00:08.000Z" }
 ],
 "telephonyPaise": 1500,
 "aiPaise": 1700,
 "billedAmountPaise": 3200,
 "startedAt": "2026-03-24T10:00:00.000Z",
 "endedAt": "2026-03-24T10:02:22.000Z"
}
NoteHow an AI agent call is billed. Every call is charged in two parts: telephony (the call itself plus its transcription), charged in 30-second steps, and AI, charged per second. Ring time and calls nobody answers are free. billedAmountPaise is the total taken from your wallet, and telephonyPaise and aiPaise are its two parts, in paise. Calls billed before the parts were recorded return null for both and show only the total.

Get Call Recording

GET/v1/voice/calls/:id/recording

Get a pre-signed URL for the call recording audio. URL expires in 1 hour.

Required scope·voice:calls:read
terminal
curl "https://api.slide.cx/v1/voice/calls/vc_01JXYZ/recording" \
 -H "Authorization: Bearer sk_live_YOUR_API_KEY"

Response

response.json
{
 "data": {
 "url": "https://s3.ap-south-1.amazonaws.com/recordings/vc_01JXYZ.ogg?X-Amz-Expires=3600&..."
 }
}
WarningRecording URLs are pre-signed and expire after 1 hour. Request a fresh URL before each playback or download. Returns 404 if the call has no recording.

Get Voice Analytics

GET/v1/voice/analytics

Get aggregated voice call metrics for a date range.

Required scope·voice:calls:read

Query Parameters

ParameterTypeDefaultDescription
fromstring Start date (ISO 8601, required)
tostring End date (ISO 8601, required)
terminal
curl "https://api.slide.cx/v1/voice/analytics?from=2026-03-01&to=2026-03-31" \
 -H "Authorization: Bearer sk_live_YOUR_API_KEY"

Response

response.json
{
 "totalCalls": 234,
 "answeredCalls": 198,
 "avgDurationSec": 127,
 "totalCostUsd": null,
 "totalBilledPaise": 748800,
 "callsByDay": [
 { "date": "2026-03-01", "total": 8, "answered": 7 },
 { "date": "2026-03-02", "total": 11, "answered": 9 }
 ],
 "callsByDirection": {
 "INBOUND": 60,
 "OUTBOUND": 174
 },
 "sentimentBreakdown": {
 "POSITIVE": 142,
 "NEUTRAL": 74,
 "NEGATIVE": 18
 }
}
FieldMeaning
answeredCallsCalls that were answered or completed.
avgDurationSecAverage call length in seconds, over calls that have a duration.
totalBilledPaiseWhat the calls in the range cost your wallet, in INR paise (100 paise = ₹1): the sum of each call’s billedAmountPaise.
totalCostUsdAlways null. Kept so existing integrations keep parsing the response; use totalBilledPaise.
callsByDayOne entry per day that had calls (UTC date), with total and answered counts. Two days shown above.
sentimentBreakdownCalls with no sentiment yet are counted as NEUTRAL.

Create, Update and Delete Voice Agents

EndpointScopeWhat it does
POST /v1/voice/agentsvoice:agents:writeCreates a voice agent with the same rules as the dashboard. Answers 201 with the new agent.
PATCH /v1/voice/agents/:idvoice:agents:writeChanges an agent: send only the fields to change.
DELETE /v1/voice/agents/:idvoice:agents:writeDeletes the agent. It cannot be undone. Answers 204 with no body.

Body

FieldTypeRequired on createDescription
namestringYesUp to 120 characters.
personastringYesInstructions for the agent: who it is and how it should handle the call.
descriptionstringNoUp to 500 characters.
firstMessagestringNoWhat the agent says first. Up to 500 characters.
languagestringNoLanguage code, up to 20 characters.
multiLingualbooleanNoThe multilingual switch of the dashboard.
maxDurationSecintegerNoLongest call, 30 to 3600 seconds.
maxTurnsintegerNoMost turns in one call, 1 to 100.
toolsarrayNoTools the agent may use, at most 64.
mcpServerIdsstring[]NoIds of MCP servers registered in Settings whose tools the agent may call.
isActivebooleanNoWhether the agent is active.

The voice, speech and model settings take the values the dashboard offers; read an agent with GET /v1/voice/agents/:id to see them. A field this API does not know is ignored, and a known field with a wrong value is a 400.

Voice External: App to App

Two people talk from inside your own app, for example a pro and a customer. You send two of your own user ids; nothing has to be registered with us first. Calls that start or end on a real phone are in the next section, Voice External: App to Phone and Phone to App.

response.json
POST /v1/voice/app/calls
{ "type": "app", "from": "pro_991", "to": "cust_5521" }
NoteCharged per minute. App-to-app calls are billed as telephony, per started minute of talk time with a 1-minute minimum, and deducted from your wallet. See Pricing.

How the Pieces Connect

terminal
  Your app  <------ your own API, push, socket ------>  Your backend
     |                                                        |    ^
     |                                         API key calls  |    |  signed webhooks
     |                                                        v    |
     |                                                     Slide API
     |
     +------ voice connection (voiceServerUrl + token) ----->  Slide voice server
LinkWhat travels on it
Your backend → Slide APIEvery API call, with your API key. Only your backend holds the key.
Slide → your backendWebhooks: a call is ringing, answered, ended.
Your backend → your appYour own channel: your push notifications, your socket, your API. Slide has no socket or push for your app. When a webhook says a user’s call is ringing, your backend tells that user’s app, the same way it tells it anything else.
Your app → Slide voice serverThe audio. It is the only connection your app makes to us. Your backend gets voiceServerUrl and token from the API and passes both to the app.
NotecallId ties it all together: it is in every webhook, every API response and every API path. Pass it to your app with the token. Always connect to the voiceServerUrl the API returns. Never hard-code it, because it can change. A token lets one person join one call, and is only needed to join: it expires after 5 minutes, the call does not. After a network drop, get a new one from POST /v1/voice/app/calls/:callId/token.

One-Time Setup

StepWhere
1. API key with voice:app:read and voice:app:writeDashboard → API Keys
2. Webhook endpoint subscribed to voice.app.call.incoming, .answered, .completed, .missed and .rejectedDashboard → Webhooks, or the Webhooks API
3. Money in your wallet: calls are charged per minuteDashboard → Billing
WarningCall the API from your backend, server to server. An API key can be pulled out of any mobile app and works for your whole account, so it must never ship inside one. Your app only ever receives a token, which works for one call.

Joining a Call in Your App

The same code runs in the browser and in React Native. On React Native also install @synquic/slide-voice-native and call registerSynquicVoice() at the very top of your entry file, or calls connect with no audio.

terminal
npm install @synquic/slide-voice
# React Native only
npm install @synquic/slide-voice-native
example.ts
import { SynquicVoiceCall } from '@synquic/slide-voice';

// From YOUR backend: { callId, token, voiceServerUrl }
const session = await myApi.post('/calls/start', { to: 'cust_5521' });

const call = new SynquicVoiceCall();
call.on('connected', () => showCallScreen());
call.on('ended', () => {
  // Fires once, however the call ends: this user, the other person, or a phone hanging up.
  closeCallScreen();
  // Your backend calls POST /v1/voice/app/calls/:callId/end. Harmless if it already ended.
  myApi.post('/calls/' + session.callId + '/end');
});
await call.join(session);

// Hang-up button
hangUpButton.onclick = () => call.leave(); // triggers 'ended' above

When the other person hangs up, declines, or never answers, we close the call and ended fires in your app. Start a talk timer when your backend receives voice.app.call.answered, not when the app joins.

The Call, Step by Step

terminal
1. Pro taps Call   Your app asks your backend.
                   Your backend  POST /v1/voice/app/calls
                                 { "type": "app", "from": "pro_991", "to": "cust_5521" }
                                 <-- { callId, token, voiceServerUrl }
                   Pro's app joins with that token and shows "Ringing".

2. Webhook         voice.app.call.incoming  (to.externalId = "cust_5521")  --> your backend
                   Your backend pushes { callId } to cust_5521's app, which rings.

3. Customer taps Answer
                   Your backend  POST /v1/voice/app/calls/:callId/accept  { "asExternalId": "cust_5521" }
                                 <-- { token, voiceServerUrl }
                   Customer's app joins. Both are talking. Recording starts. Billing starts.
                   Webhook       voice.app.call.answered
   (or Decline)    Your backend  POST /v1/voice/app/calls/:callId/reject  { "asExternalId": "cust_5521" }

4. Either taps End Your backend  POST /v1/voice/app/calls/:callId/end  { "asExternalId": "..." }
                   Webhook       voice.app.call.completed  (durationSeconds = billed talk time)

If nobody answers within 45 seconds you get voice.app.call.missed, and the call costs nothing.

Place a Call

POST/v1/voice/app/calls

Start a call from your app. Returns the caller's join token.

Required scope·voice:app:write
FieldTypeRequiredDescription
typestringYesapp to ring another app user. (phone rings a phone number: see App to Phone.)
fromstringYesYour id for the person calling. They talk from the app.
tostringYesYour id for the person being called.
response.json
{
 "callId": "apc_7f31c9a2",
 "roomId": "app-org_1a2b-7f31c9a2",
 "status": "RINGING",
 "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
 "voiceServerUrl": "wss://...",
 "tokenExpiresIn": 300,
 "ringTimeoutSeconds": 45,
 "callee": { "externalId": "cust_5521", "displayName": null }
}
ErrorWhen
400Missing fields, or from and to are the same person.
409These two people already have a live call.

Answer or Decline

POST/v1/voice/app/calls/:callId/accept

The app user picks up and receives their join token. Retrying is safe.

Required scope·voice:app:write
POST/v1/voice/app/calls/:callId/reject

The app user declines a ringing call.

Required scope·voice:app:write
FieldTypeRequiredDescription
asExternalIdstringYesThe user being rung: to.externalId from voice.app.call.incoming.
reasonstringNoDecline only. Returned as endReason.

Hang Up

POST/v1/voice/app/calls/:callId/end

End the call for everyone on it. Safe to call twice.

Required scope·voice:app:write
FieldTypeRequiredDescription
asExternalIdstringNoThe app user hanging up. Leave it out to end the call from your backend.
reasonstringNoReturned as endReason.
POST/v1/voice/app/calls/:callId/token

A new join token after a network drop. Body: { asExternalId }.

Required scope·voice:app:write

An app losing its connection does not end the call, because that is not hanging up. When someone hangs up in your app, call this endpoint: billing stops when the call ends.

StatusMeaning
RINGINGNobody has picked up yet.
ACCEPTEDPicked up. Both people are connected.
COMPLETEDA connected call ended. The only status that is charged.
CANCELLEDEnded while still ringing. Never connected.
REJECTEDDeclined.
MISSEDNobody answered in time.
FAILEDThe call could not be placed.
endReasonMeaning
completedAn app user hung up a connected call.
caller_cancelled, callee_cancelledHung up in the app while it was still ringing.
callee_rejectedDeclined, with no reason given.
ring_timeoutNot answered within 45 seconds.
ended_by_serverYour backend ended it without asExternalId or a reason.
room_closedEveryone left without hanging up.
Your own textThe reason you sent to end or reject.

Calls involving a phone have a few more reasons, listed in Hanging Up a Phone.

Webhooks

EventWhenWhat your backend does
voice.app.call.incomingAn app user is being rung.Tell that user’s app to ring (to.externalId).
voice.app.call.answeredPicked up.Start the talk timer.
voice.app.call.completedThe call ended (COMPLETED, CANCELLED or FAILED).Stop any ringing screen. Save the outcome and talk time.
voice.app.call.missedNobody answered.Stop any ringing screen.
voice.app.call.rejectedDeclined.Stop any ringing screen.
voice.app.session.expiredBookings only: a booking reached its expiry.Nothing required.
response.json
{
 "id": "evt_01JXYZ...",
 "type": "voice.app.call.completed",
 "createdAt": "2026-09-16T10:12:51.000Z",
 "data": {
 "callId": "apc_7f31c9a2",
 "sessionRef": null,
 "roomId": "app-org_1a2b-7f31c9a2",
 "status": "COMPLETED",
 "from": "pro_991",
 "to": "cust_5521",
 "callerLegTransport": "WEBRTC",
 "calleeLegTransport": "WEBRTC",
 "durationSeconds": 92,
 "endReason": "completed",
 "endedAt": "2026-09-16T10:12:51.000Z"
 }
}
NoteOn voice.app.call.incoming, from and to are objects with externalId; on every other event they are plain strings. Reply 200 straight away and do the work after. A failed delivery is retried, so the same event can arrive twice: skip repeats by event id.

Recordings and Transcripts

GET/v1/voice/app/calls/:callId/recording

A link to the call audio. Expires in one hour and is generated fresh each time.

Required scope·voice:app:read
GET/v1/voice/app/calls/:callId/transcript

Who said what, generated from the finished recording.

Required scope·voice:app:read

Every answered call is recorded from the moment it is picked up and transcribed a few seconds after it ends. Every call object carries recordingReady: fetch the recording once it is true. Your call history is at GET /v1/voice/app/calls?identityExternalId=pro_991, and every call is also listed in Dashboard → Voice → Calls → App calls with its recording, transcript and cost.

Transcript stateMeaning
readyturns holds who said what.
processingStill being made. Ask again in a few seconds.
no_speechThe call was answered but nothing was said. Not charged.
not_answeredNever answered, so there is nothing to transcribe.

Pricing

App to app
What is chargedTelephony: talk time, per minute, at your account’s voice rate. The call’s transcript is part of telephony.
When the clock runsFrom pickup (voice.app.call.answered) to hang-up. Ring time is free.
RoundingEach started minute counts as a full minute. A connected call is at least 1 minute.
FreeCalls that are missed, declined, cancelled while ringing, or fail.
TranscriptPart of telephony, per started minute of talk time, on its own billing line: Call telephony (transcript). Not charged when nothing was said.
How you payDeducted from your wallet when the call ends. See Dashboard → Billing.
Where to see itcostPaise on every call object (talk time plus transcript, in paise; null until the call ends, 0 when not charged), the App calls tab in Dashboard → Voice → Calls, and one Billing line per charge.

Example: a call answered and hung up after 92 seconds (durationSeconds: 92) is charged as 2 minutes. Billing lines read like Voice call, app to app: 2 min (call 7f31c9a2) and Call telephony (transcript), app to app: 2 min (call 7f31c9a2).

Bookings (Optional)

You do not need anything in this section, for app calls or phone calls. Calls can also be placed on a booking, with { "externalRef", "fromExternalId" } and no type. That needs the people and the booking registered first, and adds two things: we ring registered devices by push, and match a customer who dials your number back to their open booking. Integrations built on it keep working unchanged.

Create or Update an Identity

POST/v1/voice/app/identities

Register one participant, keyed by your own id. Idempotent: safe to replay for a full roster sync.

Required scope·voice:app:write

Body

FieldTypeRequiredDescription
externalIdstringYesYour id for this person. Stable for the life of the account.
rolestringYesPRO or CUSTOMER
displayNamestringNoShown as the caller name on push notifications.
phonestringNoE.164. Stored encrypted, used only for the PSTN fallback leg. Omit it if you would rather we never hold it.
terminal
curl -X POST https://api.slide.cx/v1/voice/app/identities \
  -H "Authorization: Bearer sk_live_..." \
  -H "Content-Type: application/json" \
  -d '{ "externalId": "pro_991", "role": "PRO", "displayName": "Ramesh K." }' 

Get an Identity

GET/v1/voice/app/identities/:externalId

Includes reachability, so you can hide the call button when a call could not ring.

Required scope·voice:app:read
response.json
{
 "id": "vai_3c81",
 "externalId": "pro_991",
 "role": "PRO",
 "displayName": "Ramesh K.",
 "isActive": true,
 "lastSeenAt": "2026-08-22T10:09:00.000Z",
 "deviceCount": 2,
 "reachable": true
}

Register a Device

POST/v1/voice/app/identities/:externalId/devices

Optional. Register a push target so we can ring the device directly.

Required scope·voice:app:write
WarningOn iOS, register an APNs VoIP token with isVoip: true. A backgrounded app can only raise the native incoming-call screen from a VoIP push. Register a plain notification token and the device can be messaged but not rung.
FieldTypeRequiredDescription
tokenstringYesFCM registration token, or APNs VoIP token.
platformstringYesios or android
isVoipbooleanNoDefaults to true on iOS.

Create a Calling Session

POST/v1/voice/app/sessions

Pair a Pro and a Customer for one booking. This is what authorises them to call each other.

Required scope·voice:app:write
FieldTypeRequiredDescription
externalRefstringYesYour booking / job / order id. Unique per organization.
proExternalIdstringYesMust already exist with role PRO.
customerExternalIdstringYesMust already exist with role CUSTOMER.
expiresAtstringNoISO 8601. Defaults to 24 hours out, capped at 90 days.
metadataobjectNoEchoed back on every webhook for this session.
NoteIdempotent on externalRef. Replaying it extends the booking instead of creating a second session, and reactivates one you previously revoked.

End a Session

DELETE/v1/voice/app/sessions/:externalRef

The booking finished or was cancelled. No new calls can be placed; a call already in progress is left to finish.

Required scope·voice:app:write

Your People, Sessions and Calls

EndpointScopeWhat it does
GET /v1/voice/app/identitiesvoice:app:readYour identities: { data: [{ id, externalId, role, displayName, isActive, lastSeenAt, createdAt }], meta }. Optional role (PRO or CUSTOMER), page and limit (default 20, max 100).
DELETE /v1/voice/app/identities/:externalIdvoice:app:writeOffboards the person: their devices and sessions go with them, and any phone number held for them is destroyed. Answers 204.
DELETE /v1/voice/app/identities/:externalId/devices/:tokenvoice:app:writeRemoves one push target. An unknown token is not an error. Answers 204.
GET /v1/voice/app/sessionsvoice:app:readYour sessions. Optional status (ACTIVE, EXPIRED or REVOKED), page and limit (default 20, max 100).
GET /v1/voice/app/sessions/:externalRefvoice:app:readOne session, with callCount: how many calls were placed on it.
GET /v1/voice/app/callsvoice:app:readYour call history, newest first. Optional sessionRef, identityExternalId and status filters, page and limit (default 20, max 100).
GET /v1/voice/app/calls/:callIdvoice:app:readOne call.
response.json
// A session
{
 "id": "...",
 "externalRef": "BK-8842",
 "status": "ACTIVE",
 "pro": { "externalId": "pro_991", "displayName": "Ramesh K." },
 "customer": { "externalId": "cust_5521", "displayName": "Asha R." },
 "expiresAt": "2026-10-26T07:00:00.000Z",
 "metadata": null,
 "createdAt": "2026-10-25T07:00:00.000Z",
 "callCount": 2
}

// A call
{
 "callId": "...",
 "sessionRef": "BK-8842",
 "roomId": "...",
 "status": "COMPLETED",
 "from": { "externalId": "pro_991", "displayName": "Ramesh K." },
 "to": { "externalId": "cust_5521", "displayName": "Asha R." },
 "callerLegTransport": "WEBRTC",
 "calleeLegTransport": "PSTN",
 "startedAt": "2026-10-25T07:10:00.000Z",
 "answeredAt": "2026-10-25T07:10:09.000Z",
 "endedAt": "2026-10-25T07:13:19.000Z",
 "durationSeconds": 190,
 "endReason": "completed",
 "costPaise": ...,
 "recordingReady": true
}

callerLegTransport and calleeLegTransport are WEBRTC for a leg carried by your app and PSTN for a real phone. costPaise is what the call cost your wallet (talk time plus its transcript), null until it ends and 0 when nobody answered. The status and endReason values are listed under Hang Up, above.

Voice External: App to Phone and Phone to App

Calls where at least one person is on a real phone. They use the same two fields as app to app: type says where the other person picks up (app or phone), and to says who. Phones always see your business number, never the other person’s. Read App to App first: joining from the app, answering and hanging up work the same way here.

The call startsIt ringsYour backend sends
In your appA phonePOST /v1/voice/app/calls
{ "type": "phone", "from": "pro_991", "to": "+919876543210" }
A phone dials your numberYour appPOST /v1/voice/app/calls/:callId/connect
{ "type": "app", "to": "pro_991" }
A phone dials your numberAnother phonePOST /v1/voice/app/calls/:callId/connect
{ "type": "phone", "to": "+919812345678" }
NoteCharged per minute, plus telephony for each phone leg. Talk time is billed per started minute, and each side that is on a phone adds telephony for that leg, per minute. See Pricing.

One-Time Setup

Everything in App to App setup, plus:

StepNeeded forWhere
1. Turn on phone callingAll threeDashboard → Voice → Routing → How calls reach people, or PATCH /v1/voice/app/settings { "pstnEnabled": true }
2. Send your business number to your appCalls to your numberDashboard → Voice → Routing → Your app, or PATCH /v1/voice/app/numbers/:number { "routeTo": "app" }
3. Subscribe your webhook to voice.app.call.inboundCalls to your numberDashboard → Webhooks. Without it, calls to your number ring your team instead.

Until phone calling is on, anything that rings a phone is refused with 403. Reaching the phone network is a licensed activity, so it stays off until you turn it on, and each change is recorded.

App to Phone

terminal
1. Pro taps Call   Your backend  POST /v1/voice/app/calls
                                 { "type": "phone", "from": "pro_991", "to": "+919876543210" }
                                 <-- { callId, token, voiceServerUrl }
                   Pro's app joins and hears it ringing.

2. The customer's phone rings, showing your business number.

3. Picked up       Webhook  voice.app.call.answered.  Recording and billing start.

4. Hang up         Pro taps End: your backend  POST /v1/voice/app/calls/:callId/end  { "asExternalId": "pro_991" }
                   Customer hangs up the phone: the call ends by itself, and the pro's app gets 'ended'.
                   Webhook  voice.app.call.completed
What the phone didEventstatusendReason
Picked upvoice.app.call.answeredACCEPTED
Hung up after talkingvoice.app.call.completedCOMPLETEDcallee_hung_up
Busyvoice.app.call.rejectedREJECTEDbusy
Declinedvoice.app.call.rejectedREJECTEDdeclined
Not answered in 40 secondsvoice.app.call.missedMISSEDno_answer
Could not be rungvoice.app.call.completedFAILEDdial_failed
ErrorWhen
400The number is not valid (use international format), or your account has no active number to call from.
403Phone calling is off. See One-Time Setup, step 1.
409This app user already has a live call with this number.

Phone to App

A customer dials your business number and the pro answers in your app.

terminal
1. Customer dials your number. They stay on the line while you decide.

2. Webhook         voice.app.call.inbound
                   { "callId": "apc_9c02e4b1", "callerNumber": "+919876543210", "calledNumber": "+918031704204" }
                   Your backend looks up who +919876543210 is and which pro they need.

3. Your backend    POST /v1/voice/app/calls/apc_9c02e4b1/connect  { "type": "app", "to": "pro_991" }

4. Webhook         voice.app.call.incoming  (to.externalId = "pro_991")  --> your backend
                   Your backend pushes { callId } to pro_991's app, which rings.

5. Pro taps Answer Your backend  POST /v1/voice/app/calls/:callId/accept  { "asExternalId": "pro_991" }
                                 <-- { token, voiceServerUrl }
                   Pro's app joins. Both are talking. Recording and billing start.

6. Hang up         Customer hangs up: the call ends by itself.
                   Pro taps End: your backend  POST /v1/voice/app/calls/:callId/end  { "asExternalId": "pro_991" }
                   Webhook  voice.app.call.completed

Steps 4 to 6 are exactly the same as app to app. The customer hears no ringing tone while the app rings.

Phone to Phone

Someone dials your business number and you connect them to another phone. It works in either direction: a customer dialling to reach their pro, or a pro dialling to reach their customer.

terminal
1. Pro dials your number from their own phone.

2. Webhook         voice.app.call.inbound  { "callId": "apc_9c02e4b1", "callerNumber": "+919812345678", ... }
                   Your backend sees +919812345678 is pro_991, whose current job is with a customer.

3. Your backend    POST /v1/voice/app/calls/apc_9c02e4b1/connect  { "type": "phone", "to": "+919876543210" }
                   The pro hears it ringing. The customer's phone rings, showing your business number.

4. Picked up       Webhook  voice.app.call.answered.  Recording and billing start.

5. Hang up         Either person hangs up: the call ends for both.
                   Webhook  voice.app.call.completed  (endReason caller_hung_up or callee_hung_up)

One webhook handler covers both kinds of call to your number:

example.ts
import { SlideClient, constructEvent } from '@synquic/slide';

const slide = new SlideClient({ apiKey: process.env.SLIDE_API_KEY! });

app.post('/slide/webhooks', express.raw({ type: 'application/json' }), async (req, res) => {
  const event = constructEvent<any>(req.body, req.get('X-Slide-Signature') || '', process.env.SLIDE_WEBHOOK_SECRET!);
  res.sendStatus(200); // answer first, then act

  if (event.type === 'voice.app.call.inbound') {
    const { callId, callerNumber } = event.data;
    const job = await findOpenJobByPhone(callerNumber); // your own lookup

    if (!job) {
      await slide.voiceApp.endCall(callId, null, 'no_open_job'); // hang up on an unknown caller
    } else if (callerNumber === job.customerPhone) {
      await slide.voiceApp.connectCall(callId, { type: 'app', to: job.proId });        // ring the pro's app
    } else {
      await slide.voiceApp.connectCall(callId, { type: 'phone', to: job.customerPhone }); // ring the customer's phone
    }
  }

  if (event.type === 'voice.app.call.incoming') {
    await sendMyPush(event.data.to.externalId, { callId: event.data.callId }); // ring that user's app
  }
});

Connect a Call to Your Number

POST/v1/voice/app/calls/:callId/connect

Say who picks up a call that arrived on your business number.

Required scope·voice:app:write
FieldTypeRequiredDescription
typestringYesapp to ring an app user, phone to ring a phone number.
tostringYesYour id for the app user, or a phone number in international format.

callId comes from the voice.app.call.inbound webhook. Connect within connectWithinSeconds (45) of the call arriving; after that the caller is disconnected with endReason: not_connected. A phone you connect rings for whatever is left of those 45 seconds. Returns the call.

ErrorWhen
400The call did not come in on your number, it has already ended, or the phone number is not valid.
403type is phone and phone calling is off.
404No such call on your account.
409The call is already connected. Only the first connect counts.

Your Numbers

GET/v1/voice/app/numbers

Your business numbers and where calls to each one go.

Required scope·voice:app:read
PATCH/v1/voice/app/numbers/:number

Change where calls to one number go. Body: { "routeTo": "app" }.

Required scope·voice:app:write
routeToCalls to the number
appYour backend decides who picks up, through voice.app.call.inbound. If you are not subscribed to that webhook, calls ring your team instead.
teamRing your team in the Slide dashboard.
aiAnswered by the voice agent assigned to the number.
terminal
curl -X PATCH https://api.slide.cx/v1/voice/app/numbers/%2B918031704204 \
  -H "Authorization: Bearer sk_live_..." \
  -H "Content-Type: application/json" \
  -d '{ "routeTo": "app" }'

{ "number": "+918031704204", "routeTo": "app" }

Call Delivery Settings

GET/v1/voice/app/settings

How calls reach your people: whether a call may reach a real phone, and whether the app or the phone rings first.

Required scope·voice:app:read
response.json
{
 "pstnEnabled": false,
 "transport": "appFirst",
 "fallbackAfterSeconds": 20,
 "ringTimeoutSeconds": 45,
 "outboundReady": true
}
PATCH/v1/voice/app/settings

Changes either setting and answers the settings as GET does. The same switch as Dashboard, Voice, Routing, How calls reach people.

Required scope·voice:app:write
FieldTypeRequiredDescription
pstnEnabledbooleanNoWhether calls may reach the public phone network. Off until you turn it on; the change is recorded against the key.
transportstringNoappFirst (default) rings the app and tries the phone only if nobody answers within fallbackAfterSeconds. phoneFirst rings the real phone straight away and needs pstnEnabled.

ringTimeoutSeconds is when an unanswered call is given up on. outboundReady is false while the workspace has no active number to dial out from, and turning phone delivery on is then refused with a 400. Turning it off without sending transport also moves phoneFirst back to appFirst.

Hanging Up a Phone

A phone that hangs up ends the call by itself, and the other side is disconnected. Nobody on a phone can use the API, so to cut a call short, end it from your backend: POST /v1/voice/app/calls/:callId/end {}, with no asExternalId. That is the only way to end a phone-to-phone call early. Answer, decline and app hang-up work as in App to App.

endReasonMeaning
caller_hung_upThe person who dialled your number hung up.
callee_hung_upThe phone that was rung hung up.
busy, declined, no_answerThe phone that was rung was busy, declined, or was not answered.
dial_failedThe phone could not be rung.
not_connectedA call to your number was not connected within 45 seconds.

Webhooks

EventWhenWhat your backend does
voice.app.call.inboundSomeone dialled your number and is waiting.Call connect, or end to hang up.
The five call eventsSame as App to App.incoming only fires when an app is rung.
response.json
{
 "id": "evt_01JXYZ...",
 "type": "voice.app.call.inbound",
 "createdAt": "2026-09-16T10:11:12.000Z",
 "data": {
 "callId": "apc_9c02e4b1",
 "roomId": "voice-...",
 "status": "RINGING",
 "callerNumber": "+919876543210",
 "calledNumber": "+918031704204",
 "callerLegTransport": "PSTN",
 "connectWithinSeconds": 45,
 "startedAt": "2026-09-16T10:11:12.000Z"
 }
}
response.json
{
 "id": "evt_01JXYZ...",
 "type": "voice.app.call.answered",
 "createdAt": "2026-09-16T10:11:19.000Z",
 "data": {
 "callId": "apc_9c02e4b1",
 "sessionRef": null,
 "roomId": "voice-...",
 "status": "ACCEPTED",
 "from": "+91******3210",
 "to": "pro_991",
 "callerLegTransport": "PSTN",
 "calleeLegTransport": "WEBRTC",
 "answeredAt": "2026-09-16T10:11:19.000Z"
 }
}
NotecallerLegTransport and calleeLegTransport say how each side is connected: WEBRTC in your app, PSTN on a phone. A person on a phone appears as a masked number such as +91******3210. The full number is sent once, as callerNumber on voice.app.call.inbound, because you need it to decide who picks up. A late voice.app.call.inbound for a caller who already hung up gets 400 from connect, which is safe to ignore.

Pricing

App to phonePhone to appPhone to phone
Telephony for talk time, per minuteYesYesYes
Telephony for each phone leg, per minute1 phone leg1 phone leg2 phone legs

Every kind of call is charged as telephony, per minute of talk time from pickup to hang-up, at your account’s voice rate. On top of that, each side of the call that is on a phone adds telephony for that phone leg, per minute. The same rules as App to App pricing apply: each started minute counts as a full minute, a connected call is at least 1 minute, ring time and calls nobody answers cost nothing, the transcript is part of telephony (its own billing line, Call telephony (transcript)), and the total is deducted from your wallet when the call ends.

Example: a phone-to-phone call that talks for 3 minutes 10 seconds is charged 4 minutes of talk time plus 4 minutes of telephony for each of the two phone legs.

WebSocket Events

Slide provides real-time event streaming via Socket.IO for live call monitoring and transcript streaming. Connect to the WebSocket server to receive voice call lifecycle events and live transcription.

NoteThis stream powers the Slide dashboard and only accepts a signed-in dashboard user. Your backend and your own apps cannot connect to it: they receive events through webhooks. For calls in your app, see How the Pieces Connect.

Connection

Connect using the Socket.IO client library. The WebSocket server runs on the same host as the REST API.

example.ts
import { io } from 'socket.io-client';

const socket = io('https://slide.synquic.com', {
 transports: ['websocket'],
 autoConnect: true,
});

// Join your organization room to receive events
socket.emit('joinOrganization', 'your-org-id');

// Subscribe to a specific call for live transcript
socket.emit('joinCall', 'call-id');

// Unsubscribe when done
socket.emit('leaveCall', 'call-id');
socket.emit('leaveOrganization', 'your-org-id');
example.py
import socketio

sio = socketio.Client()
sio.connect('https://slide.synquic.com', transports=['websocket'])

# Join organization room
sio.emit('joinOrganization', 'your-org-id')

# Listen for events
@sio.on('voice:call.initiated')
def on_call_initiated(data):
 print(f"Call started: {data['callId']}")

@sio.on('voice:transcript.chunk')
def on_transcript(data):
 print(f"[{data['role']}]: {data['content']}")

Voice Call Events

Voice call lifecycle events are emitted to the organization room (org_{orgId}). Subscribe by emitting joinOrganization with your org ID.

EventPayloadDescription
voice:call.initiated{ callId, agentId, direction, calledNumber, status }New call created and dispatched
voice:call.ringing{ callId, status }Call is ringing (voice room started)
voice:call.answered{ callId, status, answeredAt }Call was answered by the recipient
voice:call.ended{ callId, status, durationSeconds, sentiment, summary }Call completed or failed
example.ts
// Listen for call lifecycle events
socket.on('voice:call.initiated', (data) => {
 console.log('Call started:', data.callId, data.direction);
});

socket.on('voice:call.answered', (data) => {
 console.log('Call answered:', data.callId);
 // Join call room for live transcript
 socket.emit('joinCall', data.callId);
});

socket.on('voice:call.ended', (data) => {
 console.log('Call ended:', data.callId, data.sentiment, data.summary);
 socket.emit('leaveCall', data.callId);
});

Live Transcript Streaming

Real-time transcript chunks are emitted to the call room (call_{callId}). Subscribe by emitting joinCall with the call ID. Each chunk contains a single utterance from the AI agent or the human caller.

EventPayloadDescription
voice:transcript.chunk{ role, content, timestamp }Single transcript utterance
FieldTypeDescription
role"agent" | "user"Who spoke the AI agent or the human caller
contentstringTranscribed text of the utterance
timestampstringISO 8601 timestamp of when it was spoken
example.ts
// Subscribe to live transcript for a specific call
socket.emit('joinCall', 'vc_01JXYZ...');

socket.on('voice:transcript.chunk', (chunk) => {
 const speaker = chunk.role === 'agent' ? 'AI' : 'Caller';
 console.log(`[${speaker}] ${chunk.content}`);
 // [AI] Hello! This is a call from Acme Corp regarding your appointment.
 // [Caller] Yes, I was expecting this call.
});

// Clean up when done
socket.emit('leaveCall', 'vc_01JXYZ...');
NoteTranscript chunks are streamed in real time as the conversation happens. After the call ends, the full transcript is available via GET /v1/voice/calls/:id in the transcription field.

Webhooks

Everything above is you calling us. Webhooks are the other direction: we POST a signed JSON payload to a URL you own whenever something happens on your account, so you do not have to poll. Register endpoints at slide.synquic.com/dashboard/webhooks, or over this API with a key holding the webhooks:write scope.

An account can hold up to 50 endpoints, and each one independently chooses which events it wants. That is what makes tools like Zapier work correctly: every Zap subscribes its own URL when you turn it on and removes it when you turn it off, so several can be live at once without interfering with each other.

Zapier, n8n & direct

Three ways to consume these events. All three drive the same endpoints below, so an account can run any combination at once: each connection registers its own webhook endpoint and they never interfere.

IntegrationHow it connectsStatus
n8nInstall n8n-nodes-slide-synquic from Settings → Community Nodes. A trigger node for every event and an action node for sending.Available now
Direct webhooksRegister an endpoint yourself and receive signed POSTs. Works with Make, Pipedream, or your own server.Available now
ZapierTriggers for every event plus actions for sending messages and managing contacts.In review
NoteThe n8n node is published and installable on self-hosted n8n today. Availability inside n8n Cloud requires n8n's own verification review, which is in progress. The Zapier integration is complete and awaiting listing in the Zapier App Directory.

Payload format

Every delivery has the same envelope regardless of event type. The event-specific data is always under data, and type tells you what it is, so you can route on one field rather than sniffing the body shape.

response.json
{
  "id": "evt_9f2c4b1e7a3d5f8c0b2e4a6d8f1c3e5a",
  "type": "contact.created",
  "createdAt": "2026-08-20T10:11:12.000Z",
  "accountId": "org_9f2c4b1e",
  "livemode": true,
  "data": {
    "contact": {
      "id": "ct_9f2c4b1e",
      "name": "Priya Sharma",
      "email": "priya@example.com",
      "phone": "+919876543210",
      "tags": ["newsletter"],
      "lifecycleStageId": "stg_lead",
      "createdAt": "2026-08-20T10:11:12.000Z"
    },
    "source": "widget"
  }
}

Headers

HeaderDescription
X-Slide-Event-IdUnique id for this event. Stable across retries, so use it to make your handler idempotent.
X-Slide-Event-TypeThe event type, identical to the envelope’s type field.
X-Slide-SignatureHMAC signature, described below. Always verify it.
X-Slide-Webhook-IdWhich of your endpoints this delivery was sent to.
X-Slide-Delivery-AttemptAttempt counter, starting at 1.
NoteThe same event delivered to two of your endpoints carries the same X-Slide-Event-Id. Retries of a delivery reuse it too. Storing processed ids and skipping duplicates is the correct way to handle a retry after your server timed out but had already done the work.

Verifying the signature

Your endpoint URL is not a secret, so anyone who learns it could post to it. Every delivery is signed with the secret shown once when you created the endpoint. The header looks like this:

terminal
X-Slide-Signature: t=1755683472,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd

t is the Unix timestamp of the delivery and v1 is HMAC-SHA256 over the string "{t}.{raw request body}", hex encoded, keyed with your signing secret.

WarningVerify against the raw request body bytes, before any JSON parsing. Re-serialising a parsed object reorders keys and changes whitespace, which changes the hash and makes every delivery look forged. This is the most common integration mistake by a wide margin. In Express use express.raw, not express.json, on this route.
example.ts
const crypto = require('crypto');
const express = require('express');

const app = express();
const SECRET = process.env.SLIDE_WEBHOOK_SECRET; // whsec_...

// express.raw, NOT express.json: we need the exact bytes we were sent.
app.post('/webhooks/slide', express.raw({ type: 'application/json' }), (req, res) => {
  const header = req.get('X-Slide-Signature') || '';
  const rawBody = req.body.toString('utf8');

  const parts = Object.fromEntries(
    header.split(',').map((p) => {
      const i = p.indexOf('=');
      return [p.slice(0, i).trim(), p.slice(i + 1).trim()];
    }),
  );

  // Reject anything older than 5 minutes, so a captured delivery cannot be
  // replayed indefinitely. The timestamp is inside the signed string, so it
  // cannot be edited without breaking the signature.
  const drift = Math.abs(Math.floor(Date.now() / 1000) - Number(parts.t));
  if (!parts.t || drift > 300) return res.status(400).send('stale');

  const expected = crypto
    .createHmac('sha256', SECRET)
    .update(`${parts.t}.${rawBody}`, 'utf8')
    .digest();
  const received = Buffer.from(parts.v1 || '', 'hex');

  if (
    received.length !== expected.length ||
    !crypto.timingSafeEqual(received, expected)
  ) {
    return res.status(400).send('bad signature');
  }

  const event = JSON.parse(rawBody);

  // Acknowledge FIRST, then do the work. We time out after 10 seconds.
  res.sendStatus(200);

  handleEvent(event).catch(console.error);
});
example.py
import hashlib, hmac, time

def verify_slide_signature(raw_body: bytes, header: str, secret: str, tolerance: int = 300) -> bool:
    """raw_body must be the exact bytes received, never a re-serialised dict."""
    parts = dict(
        p.strip().split("=", 1) for p in header.split(",") if "=" in p
    )
    timestamp = parts.get("t")
    signature = parts.get("v1")
    if not timestamp or not signature:
        return False

    if abs(int(time.time()) - int(timestamp)) > tolerance:
        return False

    expected = hmac.new(
        secret.encode(),
        f"{timestamp}.".encode() + raw_body,
        hashlib.sha256,
    ).hexdigest()

    return hmac.compare_digest(expected, signature)

Retries and failure handling

Return any 2xx status to acknowledge a delivery. Acknowledge before doing slow work: we time out after 10 seconds and treat that as a failure. Anything that is not a 2xx is retried on this schedule, up to 7 attempts spanning about 8 hours 45 minutes.

AttemptSent after
1Immediately
210 seconds later
31 minute later
45 minutes later
530 minutes later
62 hours later
76 hours later

After the final attempt the delivery is marked exhausted. You can inspect and replay it from the delivery log, in the dashboard or over the API.

WarningAn endpoint that fails 15 deliveries in a row is automatically disabled and you are notified in the dashboard. This protects both sides from an endpoint that has gone away permanently. Fix the receiver, then re-enable it, which also clears the failure count.

Returning 410 Gone is treated as an explicit unsubscribe: we delete the endpoint immediately rather than retrying. This is the REST Hooks convention and is how Zapier cleans up after a Zap is turned off.

Event catalogue

Subscribe to "*" to receive everything, including events added later. Call GET /v1/webhooks/events for this list with a sample payload attached to each entry.

EventFires when
contact.createdA contact is added to the unified contacts hub, from any channel or import.
contact.updatedAn existing contact changes. data.changedFields lists exactly what changed.
contact.lifecycle_changedA contact moves between lifecycle stages, for example Lead to Customer.
form.submittedA visitor submits a form, popup, or landing page.
whatsapp.message.receivedAn inbound WhatsApp message arrives from a customer.
whatsapp.message.statusA message you sent is marked sent, delivered, read, or failed.
instagram.message.receivedAn inbound Instagram direct message arrives.
sms.message.receivedAn inbound SMS arrives on one of your numbers.
voice.call.completedA voice call finishes, with duration, outcome, and summary.
voice.call.analysedThe analysis of an AI voice call is ready, a few seconds after the call ends, with the same data.callId. Carries the summary, the sentiment, data.outcome (key and label, or null), data.criteria and data.fields. Sent only when the agent has analysis switched on and the other person spoke.
email.message.deliveredAn email is accepted by the recipient mail server.
email.message.openedA recipient opens an email.
email.message.clickedA recipient clicks a link. data.url is the destination.
email.message.bouncedAn email bounces. The address is added to your suppression list.
email.message.complainedA recipient marks an email as spam.
shopify.order.createdA new order is placed in your connected store.
shopify.order.fulfilledAn order is fulfilled, with tracking details when available.
otp.sentA one-time code is sent to a phone number. data.channel is sms or whatsapp, and data.isResend is true for a resend.
otp.verifiedA user enters the correct one-time code and the request is verified.
otp.failedA verification is blocked after too many wrong codes. data.reason is max_attempts_exceeded.

The three otp.* events cover every OTP widget in the workspace: data.widgetId says which one fired, and data.identifier is the phone number. The one-time code is never part of a payload, and a demo credential produces no event.

Registering an endpoint

POST/v1/webhooks

Register an endpoint and choose which events it receives. Returns the signing secret once.

Required scope·webhooks:write
terminal
curl -X POST https://api.slide.cx/v1/webhooks \
  -H "Authorization: Bearer sk_live_..." \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://example.com/webhooks/slide",
    "events": ["contact.created", "whatsapp.message.received"],
    "description": "Production CRM sync"
  }'
response.json
{
  "id": "whe_4f8a2c1e",
  "url": "https://example.com/webhooks/slide",
  "description": "Production CRM sync",
  "events": ["contact.created", "whatsapp.message.received"],
  "isActive": true,
  "status": "ACTIVE",
  "createdVia": "api",
  "consecutiveFailures": 0,
  "lastSuccessAt": null,
  "createdAt": "2026-08-20T10:11:12.000Z",
  "secret": "whsec_XQ8vN2pLm4kR7tY1wZ3aB6cD9eF0gH5j"
}
Warningsecret appears in this response and in the rotate response, and nowhere else, ever. Store it before you close the connection. If you lose it, rotate to get a new one and update your receiver, since rotating invalidates the old secret immediately.

The URL must be publicly reachable and use https. Private, loopback, and link-local addresses are rejected, and the destination is re-checked on every delivery, so a hostname that later resolves to an internal address stops being delivered to.

Managing endpoints

GET/v1/webhooks/events

The event catalogue, with a sample payload for each event.

Required scope·webhooks:read
GET/v1/webhooks

List your endpoints, paginated.

Required scope·webhooks:read
GET/v1/webhooks/:id

Retrieve one endpoint, including its current health.

Required scope·webhooks:read
PATCH/v1/webhooks/:id

Change the URL, the subscribed events, or enable and disable the endpoint.

Required scope·webhooks:write
DELETE/v1/webhooks/:id

Unsubscribe. Returns 204 with no body.

Required scope·webhooks:write
POST/v1/webhooks/:id/rotate-secret

Issue a new signing secret. The old one stops working immediately.

Required scope·webhooks:write
POST/v1/webhooks/:id/test

Queue a test delivery. Returns 202. Pass eventType to receive that event's sample payload instead of a generic ping.

Required scope·webhooks:write
GET/v1/webhooks/:id/deliveries

Delivery log for one endpoint, newest first. Filter with ?status=FAILED.

Required scope·webhooks:read
GET/v1/webhooks/deliveries/:deliveryId

One delivery in full, including the exact payload that was sent.

Required scope·webhooks:read
POST/v1/webhooks/deliveries/:deliveryId/replay

Re-send a delivery. Reuses the original event id, so your idempotency check still applies.

Required scope·webhooks:write

Connecting Zapier

Zapier and other REST Hooks tools drive the endpoints above directly. A typical trigger wires up like this:

Zapier stepWhat it calls
AuthenticationAPI key in an Authorization: Bearer header, with webhooks:read and webhooks:write.
Trigger event listGET /v1/webhooks/events
Subscribe (Zap on)POST /v1/webhooks with the Zap’s target URL and the chosen event.
Unsubscribe (Zap off)DELETE /v1/webhooks/:id
Perform list (test)POST /v1/webhooks/:id/test, or read the delivery log for a recent real event.
NoteBecause each Zap subscribes its own URL, turning on a second Zap does not disturb the first. Both endpoints appear under Webhooks in the dashboard alongside anything you registered by hand, each marked with how it was created.

Workspace API

Two small reads about the workspace the key belongs to: its wallet balance, and who it is. Neither route takes a workspace from the request; you always get your own.

Wallet Balance

GET/v1/wallet

The wallet balance, and nothing else: no transactions, no top-ups, no card or payment details.

Required scope·billing:read
response.json
{
 "balance": { "amountMinor": "123450", "currency": "INR", "formatted": "₹1,234.50" },
 "displayCurrency": "INR"
}

amountMinor is a string in paise, so a large balance never loses a digit. The balance is always held in rupees; displayCurrency is the currency your workspace's screens show (INR or USD). A workspace that has never held money answers a balance of zero, not an error.

Organization

GET/v1/organization

The workspace the key belongs to: its id, name, plan name and the modules switched on for it. No tax, address or billing details.

Required scope·organization:read
response.json
{
 "id": "6f387e9a-...",
 "name": "Acme Store",
 "plan": "Pro",
 "enabledModules": ["email", "whatsapp"]
}

plan is null for a workspace with no subscription. enabledModules are the module slugs switched on for the workspace.

Scopes Reference

All available scopes and the operations they unlock. Scopes are chosen per API key at creation time and can be changed later with Edit on the key in the dashboard, without re-issuing it.

NoteThe unified POST /messages/send endpoint (see the Messages API section above) does not have its own scope. It reuses whichever <channel>:send scope below matches the channel(s) you pass in channel / fallbackChannels - there is nothing extra to request here.
ScopeModuleAllowed Operations
contacts:readContactsGET /contacts, GET /contacts/:id, GET /contacts/tags, GET /contacts/segments
contacts:writeContactsPOST /contacts (create or update one contact), PATCH /contacts/:id
contacts:blockContactsPOST /contacts/:id/block, POST /contacts/:id/unblock
contacts:notes:readContactsGET /contacts/:id/notes
contacts:notes:writeContactsPOST /contacts/:id/notes, DELETE /contacts/:id/notes/:noteId
contacts:deleteContactsDELETE /contacts/:id (cannot be undone)
ads:readAdsGET /ads/status, accounts, campaigns, ad sets, ads, insights, insights/summary, leads
ads:writeAdsPOST /ads/accounts/:adAccountId/campaigns (always created paused)
ads:publishAdsPOST /ads/:entity/:id/status, POST /ads/:entity/:id/budget (change live spend)
ads:google:readGoogle AdsGET /google-ads/status, customers, campaigns, ad-groups, ads, conversion-actions, insights, lead-forms
ads:google:writeGoogle AdsPOST /google-ads/lead-forms, DELETE /google-ads/lead-forms/:id (adds or removes a lead form; changes no campaign)
chat:conversations:readLive chatGET /conversations, GET /conversations/:id, GET /conversations/:id/messages
chat:conversations:writeLive chatPOST /conversations/:id/reply (as the key, takes the chat over from an AI agent), POST /conversations/:id/hand-back
billing:readWorkspaceGET /wallet
organization:readWorkspaceGET /organization
automations:readAutomationsGET /automations, GET /automations/:id, GET /automations/node-catalog
automations:writeAutomationsPOST /automations, PATCH /automations/:id, activate, pause, DELETE /automations/:id, re-run
automations:executions:readAutomationsGET /automations/:id/executions
ai:models:readAIGET /ai/models
webhooks:readWebhooksGET /webhooks, GET /webhooks/events, GET /webhooks/:id, GET /webhooks/:id/deliveries
webhooks:writeWebhooksPOST /webhooks, PATCH /webhooks/:id, DELETE /webhooks/:id, rotate-secret, test, replay
email:sendEmailPOST /email/send
email:templates:readEmailGET /email/templates, GET /email/templates/:id
email:contacts:readEmailGET /email/contacts
email:contacts:writeEmailPOST /email/contacts
instagram:messages:readInstagramGET /instagram/profile, GET /instagram/conversations
instagram:messages:sendInstagramPOST /instagram/messages
instagram:insights:readInstagramGET /instagram/insights
whatsapp:sendWhatsAppPOST /whatsapp/send-template, POST /whatsapp/header-media/upload, GET /whatsapp/send-status/:requestId
whatsapp:logs:readWhatsAppGET /whatsapp/logs, GET /whatsapp/messages/:wamid
whatsapp:templates:readWhatsAppGET /whatsapp/templates
whatsapp:templates:writeWhatsAppPOST /whatsapp/templates
whatsapp:conversations:readWhatsAppGET /whatsapp/conversations/by-phone
whatsapp:campaigns:readWhatsAppGET /whatsapp/campaigns, GET /whatsapp/campaigns/:id, campaign analytics
whatsapp:campaigns:writeWhatsAppPOST /whatsapp/campaigns, launch, cancel
sms:sendSMSPOST /sms/send
sms:templates:readSMSGET /sms/templates
sms:senders:readSMSGET /sms/senders
sms:campaigns:readSMSGET /sms/campaigns, GET /sms/campaigns/:id
sms:campaigns:writeSMSPOST /sms/campaigns, launch, cancel
sms:logs:readSMSGET /sms/logs, GET /sms/stats
rcs:sendRCSPOST /rcs/send
rcs:bots:readRCSGET /rcs/bots
rcs:templates:readRCSGET /rcs/templates
rcs:campaigns:readRCSGET /rcs/campaigns
rcs:campaigns:writeRCSPOST /rcs/campaigns
rcs:logs:readRCSGET /rcs/logs, GET /rcs/stats
otp:sendOTPPOST /otp/send, POST /otp/retry
otp:verifyOTPPOST /otp/verify, POST /otp/verify-token
otp:logs:readOTPGET /otp/logs
otp:analytics:readOTPGET /otp/analytics
shopify:products:readShopifyGET /shopify/products/*, GET /shopify/collections/*
shopify:orders:readShopifyGET /shopify/orders/status, GET /shopify/customers/orders
shopify:discounts:readShopifyGET /shopify/discounts/validate
shopify:audit:readShopify store auditGET /shopify/audit, quote, runs, issues, suggested fixes, apply progress
shopify:audit:writeShopify store auditStart an audit (charged after the free one each month), AI fix suggestions (charged), edit, apply and undo fixes, change an issue status
aeo:readAI searchGET /aeo/summary, trend, answers, prompts, settings
aeo:writeAI searchPOST /aeo/run (charged), POST /aeo/prompts, PATCH /aeo/prompts/:id, PUT /aeo/settings
analytics:events:readWebsite eventsGET /analytics/events, GET /analytics/events/:name, GET /analytics/users/:userId/events
analytics:events:writeWebsite eventsPOST /analytics/events (up to 50 events), POST /analytics/identify
agents:readAI AgentsGET /agents, GET /agents/:id, versions, runs, performance, tool catalogue
agents:writeAI AgentsPOST /agents, drafts (POST and PATCH versions)
agents:publishAI AgentsPOST .../publish, POST .../rollback
voice:agents:readVoiceGET /voice/agents, GET /voice/agents/:id
voice:calls:readVoiceGET /voice/calls, GET /voice/calls/:id, recordings, analytics
voice:calls:writeVoicePOST /voice/calls/outbound
voice:agents:writeVoicePOST /voice/agents, PATCH /voice/agents/:id, DELETE /voice/agents/:id
voice:app:readVoice externalGET /voice/app/identities, sessions, calls, recordings, transcripts, numbers, settings
voice:app:writeVoice externalIdentities, devices and sessions; place, connect, answer, decline and end calls; call tokens; number routing; settings
NoteScopes follow the principle of least privilege. Only request the scopes your integration genuinely needs. Contact hello@slide.synquic.com to request new scopes for future API modules.

Ready to integrate?

Create your API key from the dashboard and start building in minutes.