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://slide.synquic.com/api/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://slide.synquic.com/api/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://slide.synquic.com/api/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://slide.synquic.com/api/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://slide.synquic.com/api/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. Enable ipWhitelistEnabled and set allowedIps for a key in Admin Settings → API Keys (IP and rate settings). When enabled, 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 whitelist.

response.json
{
 "statusCode": 403,
 "error": "Forbidden",
 "message": "Request from IP 203.0.113.42 is not allowed for this API key"
}
NoteIP whitelisting works behind proxies and load balancers: the client IP is resolved from the x-forwarded-for header, so keys stay locked to the true origin IP.

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 your organization's unified contact database. Contacts are shared across all channels Instagram, email, and future integrations.

NoteAll contact endpoints require the contacts:read scope and are rate-limited to 60 requests/min per API key.

List Contacts

GET/v1/contacts

List contacts with pagination and optional fuzzy search across name, username, email, and phone.

Required scope·contacts:read

Query Parameters

ParameterTypeDefaultDescription
pagenumber1Page number (1-indexed)
limitnumber50Results per page (max 100)
searchstring Optional fuzzy search by name, username, email, or phone
terminal
curl "https://slide.synquic.com/api/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://slide.synquic.com/api/v1/contacts?${params}`, {
 headers: { 'Authorization': 'Bearer sk_live_YOUR_API_KEY' },
});
const { data, meta } = await res.json();

Response

response.json
{
 "data": [
 {
 "id": "cnt_abc123",
 "username": "john_doe",
 "name": "John Doe",
 "email": "john@example.com",
 "phone": "+91987654321",
 "profilePic": "https://...",
 "source": "instagram",
 "tags": [],
 "createdAt": "2026-01-15T10:30:00Z"
 }
 ],
 "meta": {
 "total": 312,
 "page": 1,
 "limit": 50,
 "totalPages": 7
 }
}

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://slide.synquic.com/api/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://slide.synquic.com/api/v1/contacts/cnt_abc123/unblock" \
 -H "Authorization: Bearer sk_live_YOUR_API_KEY"

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://slide.synquic.com/api/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://slide.synquic.com/api/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://slide.synquic.com/api/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://slide.synquic.com/api/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.

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://slide.synquic.com/api/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://slide.synquic.com/api/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://slide.synquic.com/api/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://slide.synquic.com/api/v1/email/templates \
 -H "Authorization: Bearer sk_live_YOUR_API_KEY"
example.ts
const res = await fetch('https://slide.synquic.com/api/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://slide.synquic.com/api/v1/email/templates/tpl_01JXYZ \
 -H "Authorization: Bearer sk_live_YOUR_API_KEY"
example.ts
const res = await fetch('https://slide.synquic.com/api/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://slide.synquic.com/api/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://slide.synquic.com/api/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://slide.synquic.com/api/v1/instagram/profile \
 -H "Authorization: Bearer sk_live_YOUR_API_KEY"
example.ts
const res = await fetch('https://slide.synquic.com/api/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://slide.synquic.com/api/v1/instagram/conversations?page=1&limit=20" \
 -H "Authorization: Bearer sk_live_YOUR_API_KEY"
example.py
import requests

res = requests.get(
 'https://slide.synquic.com/api/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://slide.synquic.com/api/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://slide.synquic.com/api/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://slide.synquic.com/api/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://slide.synquic.com/api/v1/instagram/insights \
 -H "Authorization: Bearer sk_live_YOUR_API_KEY"
example.ts
const res = await fetch('https://slide.synquic.com/api/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"
}

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)

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://slide.synquic.com/api/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://slide.synquic.com/api/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://slide.synquic.com/api/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 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://slide.synquic.com/api/v1/whatsapp/logs?page=1&limit=20&direction=outbound" \
 -H "Authorization: Bearer sk_live_YOUR_API_KEY"
example.ts
const res = await fetch(
 'https://slide.synquic.com/api/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://slide.synquic.com/api/v1/whatsapp/templates?status=APPROVED" \
 -H "Authorization: Bearer sk_live_YOUR_API_KEY"
example.ts
const res = await fetch(
 'https://slide.synquic.com/api/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 }
}

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://slide.synquic.com/api/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://slide.synquic.com/api/v1/whatsapp/conversations/by-phone?phone=919876543210&limit=50" \
 -H "Authorization: Bearer sk_live_YOUR_API_KEY"
example.ts
const res = await fetch(
 'https://slide.synquic.com/api/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://slide.synquic.com/api/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://slide.synquic.com/api/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://slide.synquic.com/api/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://slide.synquic.com/api/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://slide.synquic.com/api/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://slide.synquic.com/api/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://slide.synquic.com/api/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://slide.synquic.com/api/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://slide.synquic.com/api/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://slide.synquic.com/api/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://slide.synquic.com/api/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://slide.synquic.com/api/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://slide.synquic.com/api/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://slide.synquic.com/api/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://slide.synquic.com/api/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://slide.synquic.com/api/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://slide.synquic.com/api/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://slide.synquic.com/api/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://slide.synquic.com/api/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://slide.synquic.com/api/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://slide.synquic.com/api/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://slide.synquic.com/api/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://slide.synquic.com/api/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://slide.synquic.com/api/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://slide.synquic.com/api/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://slide.synquic.com/api/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://slide.synquic.com/api/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://slide.synquic.com/api/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://slide.synquic.com/api/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://slide.synquic.com/api/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://slide.synquic.com/api/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://slide.synquic.com/api/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://slide.synquic.com/api/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://slide.synquic.com/api/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 over SMS or 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.

NoteCreate and configure OTP widgets in Dashboard → OTP. Each widget defines the OTP length, expiry, resend rules, allowed channels, and per-identifier rate limits used by these endpoints.

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://slide.synquic.com/api/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://slide.synquic.com/api/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://slide.synquic.com/api/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..."
}

Retry OTP

POST/v1/otp/retry

Resend the OTP for an existing request, optionally over a different channel. Respects the widget's resend cooldown and maximum resend count.

Required scope·otp:send

Request Body

FieldTypeRequiredDescription
requestIdstringYesThe requestId returned by POST /otp/send
channelstringNoDelivery channel override (e.g. "sms", "whatsapp"), must be enabled on the widget
terminal
curl -X POST https://slide.synquic.com/api/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..."
}

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://slide.synquic.com/api/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://slide.synquic.com/api/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://slide.synquic.com/api/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://slide.synquic.com/api/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://slide.synquic.com/api/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://slide.synquic.com/api/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://slide.synquic.com/api/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://slide.synquic.com/api/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://slide.synquic.com/api/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://slide.synquic.com/api/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://slide.synquic.com/api/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://slide.synquic.com/api/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://slide.synquic.com/api/v1/shopify/customers/orders?identifier=%2B919876543210" \
 -H "Authorization: Bearer sk_live_YOUR_API_KEY"
example.ts
const res = await fetch(
 'https://slide.synquic.com/api/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://slide.synquic.com/api/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
}

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://slide.synquic.com/api/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://slide.synquic.com/api/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://slide.synquic.com/api/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://slide.synquic.com/api/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://slide.synquic.com/api/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://slide.synquic.com/api/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

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://slide.synquic.com/api/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" }
 ],
 "billedAmountPaise": 3200,
 "startedAt": "2026-03-24T10:00:00.000Z",
 "endedAt": "2026-03-24T10:02:22.000Z"
}

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://slide.synquic.com/api/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://slide.synquic.com/api/v1/voice/analytics?from=2026-03-01&to=2026-03-31" \
 -H "Authorization: Bearer sk_live_YOUR_API_KEY"

Response

response.json
{
 "totalCalls": 234,
 "completedCalls": 198,
 "failedCalls": 12,
 "noAnswerCalls": 24,
 "avgDurationSeconds": 127,
 "totalDurationSeconds": 25146,
 "sentimentBreakdown": {
 "POSITIVE": 142,
 "NEUTRAL": 38,
 "NEGATIVE": 18
 },
 "totalCostPaise": 748800,
 "confirmedCount": 156,
 "bookedCount": 42
}

App-to-App Voice API

Voice calling between two of your own app users, in both directions, with no phone numbers and no AI agent. Both parties connect over WebRTC from your apps into a room we create, so neither side ever sees the others number and there is no per-minute telephony cost on either leg.

WarningCall these endpoints from your backend, server to server. An API key is extractable from any mobile binary and carries organization-wide scope, so it must never ship inside an app. Your backend returns only the short-lived room token to the device: it is scoped to a single call and expires in minutes.

Overview & Call Flow

Three objects. An identity is one person in your marketplace, keyed by your own id. A session pairs two identities for one booking and is the only thing that grants permission to call. A call is one conversation on that session.

NoteNo active session means no call. Two identities with no unexpired session between them cannot reach each other at all, which is what stops this becoming an open dialler if an id leaks.
terminal
SETUP  (once per booking, from your backend)

  POST /v1/voice/app/identities        pro_991   role=PRO
  POST /v1/voice/app/identities        cust_5521 role=CUSTOMER
  POST /v1/voice/app/sessions          externalRef=BK-8842, pairs the two


OUTBOUND  (your Pro taps "Call")

  Pro app --> your backend --> POST /v1/voice/app/calls
                                { externalRef, fromExternalId: "pro_991" }
                          <-- { callId, roomId, token, voiceServerUrl }
  Pro app connects to voiceServerUrl with token (LiveKit client SDK)

  Meanwhile we notify the callee:
     webhook  voice.app.call.incoming  --> your backend  (always)
     push     to registered devices                      (if registered)
     socket   voice:app.call.incoming                    (if app foregrounded)


ANSWER

  Customer app --> your backend --> POST /v1/voice/app/calls/:id/accept
                                     { asExternalId: "cust_5521" }
                               <-- { token, voiceServerUrl }
  Customer app connects. Both are in the room. Connected.


END

  POST /v1/voice/app/calls/:id/end   { asExternalId }
  webhook voice.app.call.completed --> your backend

The direction is symmetric. A Customer calling a Pro is the same request withfromExternalId set to the customer. The callee is always derived from the session, never supplied by the caller.

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.synquic.com/api/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

Place a Call

POST/v1/voice/app/calls

Creates the room, returns the caller's join token, and notifies the callee.

Required scope·voice:app:write
FieldTypeRequiredDescription
externalRefstringYesThe session both parties belong to.
fromExternalIdstringYesWho is placing the call. Must be one of the two identities on the session.
NoteThere is no toExternalId. The callee is derived from the session: letting the caller name the callee would turn a booking-scoped permission into an open dialler.
response.json
{
 "callId": "apc_7f31c9a2",
 "roomId": "app-org_1a2b-7f31c9a2",
 "status": "RINGING",
 "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
 "voiceServerUrl": "wss://livekit.synquic.com",
 "tokenExpiresIn": 300,
 "ringTimeoutSeconds": 45,
 "callee": { "externalId": "cust_5521", "displayName": "Priya S." }
}

Hand token and voiceServerUrl to the callers app and connect with the LiveKit client SDK. If nobody answers withinringTimeoutSeconds the call is markedMISSED automatically. Returns 409 if the session already has a live call.

Accept a Call

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

The callee picks up and receives their join token.

Required scope·voice:app:write
FieldTypeRequiredDescription
asExternalIdstringYesThe callee. Only they can accept.
NoteThis is the authoritative "answered" signal. We do not infer it from a LiveKit join, because a client that connects and immediately drops would otherwise be recorded, and billed, as an answered call. Retrying is safe: it re-issues a token rather than failing.

Tokens are short-lived by design. On network reconnect during a long call, request a fresh one with POST /v1/voice/app/calls/:callId/token.

End or Reject a Call

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

Either party hangs up. Safe to call twice - both apps racing to report the hang-up is normal.

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

The callee declines a ringing call.

Required scope·voice:app:write
StatusMeaning
RINGINGRoom created, callee notified, nobody connected yet.
ACCEPTEDCallee joined. Both parties connected.
COMPLETEDA connected call ended normally.
CANCELLEDHung up while still ringing. Never connected.
REJECTEDThe callee explicitly declined.
MISSEDNobody answered before the ring timeout.
FAILEDThe room could not be created.

Webhooks

Subscribe at Dashboard Settings Webhooks or via the Webhooks API. voice.app.call.incoming is the important one: it is not an after-the-fact notification, it is how you learn to ring your user.

EventWhen
voice.app.call.incomingSomeone is calling one of your users. Deliver it to the callee so their app can ring.
voice.app.call.answeredThe callee accepted. Both parties connected.
voice.app.call.completedA connected call ended. Includes talk time.
voice.app.call.missedNobody answered before the ring timeout.
voice.app.call.rejectedThe callee declined.
voice.app.session.expiredA session reached its expiry. The pair can no longer call.
response.json
{
 "id": "evt_01JXYZ...",
 "type": "voice.app.call.incoming",
 "createdAt": "2026-08-22T10:11:12.000Z",
 "data": {
 "callId": "apc_7f31c9a2",
 "sessionRef": "BK-8842",
 "roomId": "app-org_1a2b-7f31c9a2",
 "direction": "PRO_TO_CUSTOMER",
 "from": { "externalId": "pro_991", "displayName": "Ramesh K.", "role": "PRO" },
 "to": { "externalId": "cust_5521", "displayName": "Priya S.", "role": "CUSTOMER" },
 "ringTimeoutSeconds": 45,
 "startedAt": "2026-08-22T10:11:12.000Z"
 }
}
NotePayloads carry your own ids only. We never put a phone number in an app-call webhook, because in the common case neither party has one on file with us.

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.

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-synquicfrom 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 n8nnode 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.
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.

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.synquic.com/api/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.

Scopes Reference

All available scopes and the operations they unlock. Scopes are assigned per API key at creation time and cannot be changed without re-issuing the key.

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
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:templates:writeEmailPOST /email/templates, PUT /email/templates/:id
email:contacts:readEmailGET /email/contacts
email:contacts:writeEmailPOST /email/contacts
email:campaigns:readEmailGET /email/campaigns
instagram:messages:readInstagramGET /instagram/profile, GET /instagram/conversations
instagram:messages:sendInstagramPOST /instagram/messages
instagram:insights:readInstagramGET /instagram/insights
whatsapp:sendWhatsAppPOST /whatsapp/send-template
whatsapp:logs:readWhatsAppGET /whatsapp/logs
whatsapp:templates:readWhatsAppGET /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
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
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.