sdk-python
synquic-slide is the official Python SDK for the Slide by Synquic API, a multi-channel customer engagement platform. One client covers WhatsApp Business messaging, SMS, RCS, OTP verification, transactional email, Instagram DMs, one send call across those channels, AI voice calls, Shopify lookups, unified contacts, automations and outbound webhooks. It targets server-side Python 3.9+ and ships a synchronous client and an identical async one, both built on httpx. It authenticates with a secret API key, so it belongs on your backend, never in a client application.
Installation
pip install synquic-slide
Quick start
Responses are plain dictionaries (typed as TypedDict), so read them with ["key"].
import os
from synquic_slide import SlideClient
slide = SlideClient(api_key=os.environ["SLIDE_API_KEY"])
# WhatsApp template message
msg = slide.whatsapp.send_template(
to="+919876543210",
template_name="order_shipped",
language_code="en",
components=[{"type": "body", "parameters": [{"type": "text", "text": "#1042"}]}],
)
print(msg["wamid"], msg["conversationId"])
# Transactional email from a saved template
slide.email.send(
recipient={"to": "customer@example.com", "firstName": "Priya"},
template_id="tmpl_order_confirm",
from_name="Acme Store",
from_email="orders@acme.com",
variables={"orderId": "#1042"},
)
# Paginated reads always return {"data": [...], "meta": {...}}
page = slide.contacts.list(page=1, limit=50, search="priya")
print(len(page["data"]), page["meta"]["totalPages"])
# Outbound AI voice call. Failures raise SlideValidationError, SlideAuthError, etc.
call = slide.voice.initiate_outbound_call(
agent_id="agent_abc123",
to_number="+919876543210",
call_context={"orderId": "#1042"},
)
print(call["id"], call["status"])
slide.close() # or use `with SlideClient(api_key=...) as slide:`
The async client exposes exactly the same resources and method names:
import asyncio, os
from synquic_slide import AsyncSlideClient
async def main():
async with AsyncSlideClient(api_key=os.environ["SLIDE_API_KEY"]) as slide:
agents = await slide.voice.list_agents(page=1, limit=20)
print([a["name"] for a in agents["data"]])
asyncio.run(main())
Verifying an incoming webhook delivery. Pass the raw body, never a re-serialised dict:
import os
from flask import request
from synquic_slide import construct_event, SlideSignatureVerificationError
@app.post("/webhooks/slide")
def slide_webhook():
try:
event = construct_event(
request.get_data(), # not request.json
request.headers["X-Slide-Signature"],
os.environ["SLIDE_WEBHOOK_SECRET"],
)
except SlideSignatureVerificationError:
return "bad signature", 400
enqueue(event) # event["id"] is stable across retries: dedupe on it
return "", 200
One send call, contacts and automations
# One send call for any channel, with a fallback
sent = slide.messages.send(
to="+919876543210",
channel="whatsapp",
fallback_channels=["sms"],
message="Your order #1042 has shipped.",
template={"name": "order_shipped", "languageCode": "en"},
sms={"senderId": "ACMEIN"},
)
print(sent["channelUsed"], sent["messageId"], sent["attempts"])
# A new automation is always created paused. Going live is its own call.
catalog = slide.automations.node_catalog() # trigger types, node types, their settings
automation = slide.automations.create(
name="Order follow-up",
trigger={"type": "WEBHOOK"},
steps=[{"order": 0, "type": "SMART_DELAY", "delayMinutes": 5}],
)
slide.automations.activate(automation["id"])
# The AI models your workspace can use, with their prices
models = slide.ai.list_models()["data"]
# Create a contact, or update the one with the same phone or email.
# The API key needs the permission contacts:write.
contact = slide.contacts.upsert(
phone="+919876543210",
email="priya@example.com",
name="Priya Sharma",
tags=["Newsletter"],
)
print(contact["id"], contact["created"])
# Find one person: phone and email match exactly, search is a "contains" match
found = slide.contacts.list(email="priya@example.com")
same = slide.contacts.get(contact["id"])
The SDK makes every call once and never retries on its own, except the batches of analytics.track and analytics.track_now, which carry an insertId so a retry cannot count twice. Do not retry a send blindly either: a timeout does not mean the message was not sent, and a second send reaches a real person twice.
contacts.upsert needs an API key with the permission contacts:write (and contacts.list / contacts.get need contacts:read). A key that only has the email list permission cannot create contacts: it is refused with SlideScopeError (403). A key's permissions cannot be edited, so create a new API key that has contacts:write.
Sending the same contacts.upsert again after it has answered, also after a timeout or a server error, gives the same contact and creates nothing twice. Two calls for the same person made at the same moment can be refused with SlideError status 409, and error.body["code"] says what to do: on CONTACT_UPSERT_IN_PROGRESS nothing was written, so wait error.body["retryAfter"] seconds and send it again; on PHONE_IN_USE send it again once.
Website events from your server
create_contact needs a key that also holds contacts:write. A contact that matches is left unchanged. An event carries at most 20 properties.
See your events in the dashboard under Web Analytics, Events, for the website you sent them to. Counts refresh every 5 minutes.
Send session_id when you have it. The browser's own events reach Slide up to 30 seconds after they happen, and a user_id is linked to its browser about a minute after it is first sent, so an event sent straight after a page loads, with only a visitor_id or a user, counts with no visit behind it. It still counts under its name.
Send a named event (pay, view_button_clicked, anything) from your server, with the person it belongs to. Slide ties it to the visit, counts it by name, and shows it in Web Analytics. The API key needs analytics:events:write (and analytics:events:read to read events back). Your website must be verified in Web Analytics first.
# In the browser, read the ids of this visit and send them to your server with your own request:
# const { visitorId, sessionId } = window.slideAnalytics.getIds()
# On your server:
slide = SlideClient(
api_key=os.environ["SLIDE_API_KEY"],
analytics_on_error=lambda error, events: print("Slide events lost:", len(events), error),
)
@app.post("/checkout/paid")
def paid():
slide.analytics.track(
"pay",
visitor_id=request.json["visitorId"], # joins the event to the visit
session_id=request.json["sessionId"],
user_id=current_user.id, # or user={"id": ..., "email": ...}
value_minor=199900, # paise or cents, a whole number
currency="INR",
properties={"plan": "pro", "method": "upi", "order_id": "A-10023456"},
)
return "", 204
track returns at once: the event waits in a queue and a background thread sends up to 50 at a time, when 50 are queued or after 5 seconds (analytics_flush_interval). It has to batch: an API key allows 60 requests a minute by default and every request uses one call of your daily limit. The thread is a daemon: it never keeps your program alive.
Serverless functions (AWS Lambda, Google Cloud Functions, Vercel) must call slide.analytics.flush() before they return. A function can be frozen before the timer fires, and the queued events are lost. A script that ends right after track loses them too: call flush(), or use the client as a context manager (with SlideClient(...) as slide:), which flushes when it closes.
def handler(event, context):
slide.analytics.track("signup", user_id=event["userId"])
slide.analytics.flush() # before returning
return {"statusCode": 200}
Every event needs a visitor_id, or a user_id / user with an id, email or phone; session_id needs visitor_id. user_id="u_1" is shorthand for user={"id": "u_1"}. Without a visit to join (a payment webhook, a cron job) the event still counts under its name; it just does not belong to a visit. Put personal data in user, never in properties: a property that looks like an email, a phone number or a password is left off and named in droppedProperties. value_minor needs a currency. time (epoch seconds or milliseconds, an ISO string or a datetime, naive meaning UTC) can be up to 7 days old and defaults to the moment of the call.
To see what the API did with each event, send a batch yourself:
result = slide.analytics.track_now(
[{"name": "pay", "user_id": "u_123", "value_minor": 199900, "currency": "INR"}],
)
print(result["accepted"], result["rejected"], result["droppedProperties"]) # rejected: [{index, code, message}]
What is retried. A 503 and a dropped connection are tried again, up to 3 tries in all, with the same insertId on every event, so nothing is counted twice; the wait is the retryAfterSeconds the server gives, else 0.5 s then 1 s. A 400, 401, 403, 409 or 429 (error.body["code"] is daily_limit_reached when the website is over its daily limit) is never repeated. In the background nothing raises: a batch that failed, events the API refused, and events dropped from a full queue (5,000 events, oldest first) are reported to analytics_on_error(error, events) and nowhere else. Without it they are lost silently. track_now raises the error instead.
slide.analytics.identify(visitor_id=visitor_id, user_id="u_123") # at login
names = slide.analytics.list_events(from_date="2026-10-01", to_date="2026-10-31")
pay = slide.analytics.get_event("pay") # trend, people, money, properties
mine = slide.analytics.get_user_events("u_123", limit=20)
On AsyncSlideClient the same calls exist; track is still a plain method (no await), and await slide.analytics.flush() sends what is queued. Called with no event loop running, track only queues.
Funnels and cohorts
Read two reports of your website events from your server. Both need an API key with analytics:events:read.
# Of the people who viewed a page, how many added to the cart, then paid, each step within 3 days of the one before:
funnel = slide.analytics.get_funnel(
["page_view", "add_to_cart", "pay"], # 2 to 6; an event name or a built-in action; a list or "a,b,c"
from_date="2026-10-01",
to_date="2026-10-31", # at most 31 days
within_hours=72, # 1 to 720, default 168 (7 days)
)
for step in funnel["steps"]:
print(step["name"], step["people"], step["fromPrevious"], step["fromFirst"])
# page_view 200 None 100 / add_to_cart 50 25 25 / pay 10 20 5 (fromPrevious and fromFirst are percents)
# Retention: people grouped by the week they first signed up, and how many paid in each later week:
cohorts = slide.analytics.get_cohorts("signup", return_event="pay")["cohorts"] # return_event defaults to the start event
# [{"week": "2026-10-05", "people": 40, "returned": [{"weeksLater": 0, "people": 8, "percent": 20}, ...]}]
A step is the name of an event you send (pay) or one Slide records by itself (page_view, product_view, add_to_cart). Steps count in order and each person counts once per step, however often they repeat it. Weeks start on Monday (UTC). A cohort range is at most 91 days (the last 12 weeks when you send none). return_event is sent to the API as return, which is a Python keyword. A request the API refuses (one step, too many days) raises SlideValidationError. Both calls exist on AsyncSlideClient with await.
Name the person in the browser, signed
A web page can call window.slideAnalytics.identify(userId, signature) to tell Slide who is browsing, but only with a signature your server made: the website key is public, so an unsigned name would let anyone attach a stranger's browsing to one of your customers. Sign the user id on your server with your website's identify secret, then render both into the page for the logged-in user:
Find the secret in the dashboard under Web Analytics, Developers. It needs permission to edit the website.
# On your server. Keep the secret in an environment variable, never in a page or a repository.
signature = slide.analytics.sign_user(request.user.id, os.environ["SLIDE_IDENTIFY_SECRET"])
return render(request, "app.html", {"user_id": request.user.id, "signature": signature})
# In the page:
# window.slideAnalytics.identify(userId, signature)
sign_user makes no request and needs no API key: it is HMAC-SHA256 of the user id with the secret, as 64 lower-case hex characters. It is a plain method on AsyncSlideClient too (no await). It signs the id without surrounding spaces, as Slide reads it, and raises ValueError for an empty id, an id over 128 characters or an empty secret. The browser can name a user id this way and nothing else: an email or a phone still has to come from your server (analytics.identify, analytics.track).
Chat inside your own app
Your server makes one call: a short-lived chat token for a user it has already signed in. Your app opens the chat with that token. The API key never leaves your server.
Pilot: slide.chat is not in the version of this package on the registry yet. During the pilot you receive a build that has it with your pilot access; until then the token call is one plain POST /v1/chat/tokens with your key.
import os
from flask import Flask, jsonify, request
from synquic_slide import SlideClient, construct_event, SlideSignatureVerificationError
app = Flask(__name__)
slide = SlideClient(api_key=os.environ["SLIDE_API_KEY"]) # needs the scope chat:tokens:write
# 1. Your app asks YOUR server for a token, behind your own sign-in.
@app.get("/me/chat-token")
@require_sign_in
def chat_token():
result = slide.chat.create_token(
user_id=current_user.id, # your own stable id for the user
name=current_user.name, # optional: how the team sees them
role="patient", # optional
app="patient", # optional: only when you have more than one app
)
return jsonify(token=result["token"]) # to refresh, the app simply asks again
# 2. A reply is waiting for a user who has not seen it: send your own push.
@app.post("/webhooks/slide-chat")
def chat_webhook():
try:
event = construct_event(
request.get_data(),
request.headers.get("X-Slide-Signature", ""),
os.environ["SLIDE_CHAT_WEBHOOK_SECRET"],
)
except SlideSignatureVerificationError:
return "bad signature", 400
if event["type"] == "chat.message.unread":
data = event["data"] # never carries the text; de-duplicate on data["messageId"]
enqueue_push(data["userId"], data["app"], badge=data["unreadCount"])
return "", 200
Optional, when a user signs out or asks to be erased: slide.chat.revoke_user(user_id, app=...) and slide.chat.delete_user(user_id, app=...) (scope chat:users:write).
Ads, live chat conversations and your workspace
# Ads (permission ads:read): the ads account your workspace connected
status = slide.ads.get_status()
accounts = slide.ads.list_accounts()["data"]
campaigns = slide.ads.list_campaigns(accounts[0]["id"])["data"]
totals = slide.ads.get_insights_summary(since="2026-10-01", until="2026-10-30")["data"]
print(totals["spend"], totals["leads"], totals["cpl"])
# A new campaign is ALWAYS created paused (permission ads:write)
draft = slide.ads.create_campaign(accounts[0]["id"], name="Winter launch", objective="OUTCOME_LEADS")
# Starting, pausing and budgets CHANGE LIVE SPEND (permission ads:publish)
slide.ads.set_budget("campaign", draft["id"], daily_minor=50000) # minor units: 500.00
slide.ads.set_status("campaign", draft["id"], "ACTIVE")
# Live chat conversations, read only (permission chat:conversations:read)
open_chats = slide.conversations.list(is_open=True, needs_person=True)["data"]
messages = slide.conversations.list_messages(open_chats[0]["id"])["data"]
# Your workspace
wallet = slide.workspace.get_wallet() # wallet["balance"]["amountMinor"] is a string of paise
org = slide.workspace.get_organization()
Without a connected ads account every ads call but get_status raises SlideError with status 409 (error.body["code"] is ADS_NOT_CONNECTED). ads:publish is separate on purpose: a key that can only make a campaign can never start or resize spending. There is no conversations reply method, because a reply is filed under the teammate who wrote it and a key is not a person.
contacts.update(id, tags=[...]) replaces the contact's tags (the opposite of contacts.upsert, which only adds), and contacts.delete(id) cannot be undone, which is why it needs its own permission, contacts:delete. The same calls exist on AsyncSlideClient with await.
API reference
Every method below exists twice: on SlideClient (blocking) and on AsyncSlideClient (awaitable, same name). Arguments are keyword-only unless shown positionally. List methods also accept page (default 1) and limit (default varies by endpoint, 10 to 50).
| Method | Parameters | Returns |
|---|---|---|
SlideClient(...) / AsyncSlideClient(...) |
api_key: str, base_url: str = "https://slide.synquic.com/api", timeout: float = 30.0 |
client instance, raises ValueError if api_key is empty |
client.close() |
none | None, awaitable on the async client. Both clients are context managers |
contacts.list(...) |
search: str | None ("contains" match), phone: str | None and email: str | None (exact match), include_blocked: bool | None |
PaginatedResponse[Contact], contacts of the contacts hub |
contacts.upsert(...) |
phone=None, email=None (at least one of the two), name=None, first_name=None, last_name=None, company=None, tags: list[str] | None, custom_fields: dict | None |
UpsertContactResponse: the Contact plus created, emailContact, emailUnsubscribed, unmatchedCustomFields, warnings. Matched by phone, then email. Needs contacts:write |
contacts.get(contact_id) |
positional id | Contact |
contacts.update(contact_id, ...) |
name=None, email=None, phone=None, lifecycle_stage_id (a stage id, or None to clear), tags: list[str] | None. At least one. tags REPLACES the contact's tags |
Contact. Needs contacts:write |
contacts.delete(contact_id) |
positional id | None. Deletes for good, cannot be undone. Needs contacts:delete |
contacts.list_tags(...) / contacts.list_segments(...) |
page=1, limit=50 |
PaginatedResponse[ContactTagListItem] / PaginatedResponse[ContactSegment]. Need contacts:read |
conversations.list(...) |
is_open, assigned_to_id, unread, needs_person, contact_id, search, page, limit (default 20) |
PaginatedResponse[Conversation]. Read only, needs chat:conversations:read; there is no reply method |
conversations.get(conversation_id) / conversations.list_messages(conversation_id, ...) |
a UUID / page, limit (default 30), newest first |
ConversationDetail / PaginatedResponse[ConversationMessage] |
workspace.get_wallet() / workspace.get_organization() |
none | Wallet (balance in paise as a string; needs billing:read) / Organization (needs organization:read) |
ads.get_status() |
none | AdsStatus. Needs ads:read; works before an ads account is connected |
ads.list_accounts(...) / ads.list_campaigns(ad_account_id, ...) / ads.list_ad_sets(campaign_id, ...) / ads.list_ads(ad_set_id, ...) |
page=1, limit=50. An ad account id is act_ and digits |
PaginatedResponse[AdAccount] / AdCampaign / AdAdSet / Ad. Budgets are minor units. Need ads:read |
ads.get_insights(...) |
level, since, until, ad_account_id, campaign_id; last 30 days by default |
AdInsightsResponse: data (oldest day first) and meta with since, until, returned, truncated, cap |
ads.get_insights_summary(...) / ads.list_leads(...) |
since, until, ad_account_id / limit |
AdInsightsSummary / ListAdLeadsResponse ({"data": [...]}, no paging) |
ads.create_campaign(ad_account_id, ...) |
name, objective=None, special_ad_categories=None |
{"id", "status": "PAUSED"}. Always created paused. Needs ads:write |
ads.set_status(entity, entity_id, status) |
entity: "campaign", "adset" or "ad"; status: "ACTIVE" or "PAUSED" |
SetAdStatusResponse: id, status, previousStatus, changed. Changes live spend, needs ads:publish |
ads.set_budget(entity, entity_id, ...) |
exactly one of daily_minor or lifetime_minor, minor units |
SetAdBudgetResponse: id, budget, previousMinor, newMinor. Changes live spend, needs ads:publish |
email.send(...) |
recipient: dict, template_id, from_name, from_email, subject=None, reply_to=None, variables: dict | None |
SendEmailResponse |
email.list_templates() / email.get_template(template_id) |
none / positional template_id |
dict / EmailTemplate |
email.list_contacts(...) |
search: str | None |
PaginatedResponse[EmailContact] |
email.upsert_contact(...) |
email, first_name=None, last_name=None, phone=None, company=None, tags: list | None, custom_fields: dict | None |
EmailContact. Email list only: to create or update a contact use contacts.upsert |
whatsapp.send_template(...) |
to, template_name, language_code, components: list | None |
SendWhatsAppTemplateResponse |
whatsapp.upload_header_media(...) |
file: bytes, filename, mime_type, expected_format: "IMAGE" | "VIDEO" | "DOCUMENT" | None |
dict |
whatsapp.list_logs(...) |
direction: "inbound" | "outbound" | None, status=None, from_date=None, to_date=None (ISO dates, not addresses) |
PaginatedResponse[WhatsAppLog] |
whatsapp.list_templates(...) |
status=None, category=None |
PaginatedResponse[WhatsAppTemplate] |
whatsapp.get_conversation_by_phone(...) |
phone, limit=50, oldest message first |
WhatsAppConversationByPhone |
whatsapp.list_campaigns(...) |
status=None |
PaginatedResponse[dict] |
whatsapp.get_campaign(campaign_id) / whatsapp.get_campaign_analytics(campaign_id) |
positional id | dict |
whatsapp.create_campaign(...) |
template_id, phones: list[str], name=None, template_components=None, variable_mapping=None, scheduled_at=None, schedule_timezone=None |
dict |
whatsapp.launch_campaign(campaign_id) / whatsapp.cancel_campaign(campaign_id) |
positional id | dict |
analytics.track(name, ...) |
name, site=None, insert_id=None, time=None, visitor_id=None, session_id=None, user_id=None, user: dict | None (id, email, phone, name, create_contact), value_minor=None, currency=None, path=None, properties: dict | None; needs visitor_id or a user |
None. Queues; sends up to 50 per request from a daemon thread. Never raises. Needs analytics:events:write |
analytics.flush() |
none | None. Sends what is queued; never raises. Serverless functions must call it before returning. Awaitable on the async client |
analytics.track_now(events, site=None) |
events: list[dict] (at most 50, the fields of track), site |
TrackAnalyticsEventsResponse = accepted, capped, rejected (index, code, message), droppedProperties. One request; a 503 or dropped connection is tried again |
analytics.identify(...) |
visitor_id=None, user_id=None, user: dict | None, site=None |
IdentifyAnalyticsVisitorResponse (accepted). Needs analytics:events:write |
analytics.list_events(...) |
site=None, from_date=None, to_date=None (YYYY-MM-DD) |
ListAnalyticsEventsResponse: from, to, events (name, count, sessions, lastDay). Needs analytics:events:read |
analytics.get_event(name, ...) |
positional name, site=None, from_date=None, to_date=None |
AnalyticsEventDetail: count, people, trend, money, properties, recent |
analytics.get_user_events(user_id, ...) |
positional user_id, site=None, from_date=None, to_date=None, page=1, limit=50 |
PaginatedResponse[AnalyticsEventRow], newest first |
analytics.get_funnel(steps, ...) |
positional steps (a list or "a,b,c", 2 to 6), site=None, from_date=None, to_date=None (a range of at most 31 days), within_hours=None (1 to 720) |
AnalyticsFunnel: from, to, withinHours, steps (name, people, fromPrevious, fromFirst). Needs analytics:events:read |
analytics.get_cohorts(start, ...) |
positional start, return_event=None, site=None, from_date=None, to_date=None (a range of at most 91 days) |
AnalyticsCohorts: from, to, start, return, cohorts (week, people, returned: weeksLater, people, percent). Needs analytics:events:read |
analytics.sign_user(user_id, secret) |
the user id and your website's identify secret | str: HMAC-SHA256 as 64 lower-case hex characters, for window.slideAnalytics.identify(userId, signature). No request, no API key, a plain method on both clients; raises ValueError on an empty or over-long id or an empty secret |
instagram.get_profile() |
none | InstagramProfile |
instagram.list_conversations(...) |
pagination only | PaginatedResponse[InstagramConversation] |
instagram.send_message(...) |
recipient_igsid, message |
SendInstagramMessageResponse |
instagram.get_insights(...) |
period=None, since=None, until=None |
dict |
shopify.search_products(...) |
q, limit=10 |
dict |
shopify.get_product_types() / shopify.list_collections() |
none | dict |
shopify.get_product(product_id) |
positional id | ShopifyProduct |
shopify.get_collection_products(collection_id, ...) |
positional id, limit=50 |
dict |
shopify.get_order_status(...) |
identifier (order number or id) |
ShopifyOrder or list[ShopifyOrder] |
shopify.get_customer_orders(...) |
identifier (phone or email) |
dict |
shopify.validate_discount(...) |
code |
DiscountValidation |
agents.list(...) / agents.get(agent_id) |
q=None, audience=None, pagination / positional id |
dict. Needs agents:read |
agents.performance(agent_id, days=None) |
days 1 to 180, default 30 |
dict: results its tools completed (bookings, orders, payments), cancellations apart, conversionRate (a percentage, None over no conversations), trend, cost per result in paise |
agents.list_runs(agent_id, ...) / agents.get_run(run_id) |
status, outcome, channel, version_id, from_, to, pagination |
dict. Never what a customer wrote, what the agent replied or who the customer is |
agents.tool_catalog() |
none | dict |
agents.create(name=...) / agents.create_draft(agent_id) / agents.update_draft(agent_id, version_id, changes) |
keyword name, description, audience / id / dict of changes |
dict. Needs agents:write |
agents.publish(agent_id, version_id) / agents.rollback(agent_id, version_id) |
ids | dict. Needs agents:publish |
voice.list_agents(...) / voice.get_agent(agent_id) |
pagination only / positional id | PaginatedResponse[VoiceAgent] / VoiceAgent |
voice.list_calls(...) |
status=None, direction=None |
PaginatedResponse[VoiceCall] |
voice.initiate_outbound_call(...) |
agent_id, to_number, call_context: dict | None |
InitiateOutboundCallResponse |
voice.get_call(call_id) / voice.get_call_recording(call_id) |
positional id | VoiceCall / dict |
voice.get_analytics(...) |
from_date, to_date |
VoiceAnalytics |
sms.send(...) |
sender_id, to: list[str], message, template_id=None, route=None |
SendSmsResponse |
sms.list_templates(...) / sms.list_senders(...) |
status=None / pagination only |
PaginatedResponse[dict] |
sms.list_campaigns(...) / sms.get_campaign(campaign_id) |
pagination only / positional id | PaginatedResponse[dict] / dict |
sms.create_campaign(...) |
name, message_body, sender_id=None, template_id=None, route=None, audience_type=None, audience_criteria=None, scheduled_at=None |
dict |
sms.launch_campaign(campaign_id) / sms.cancel_campaign(campaign_id) |
positional id | SmsCampaignActionResponse |
sms.list_logs(...) |
direction=None, status=None, start=None, end=None |
PaginatedResponse[dict] |
sms.get_stats(...) |
start=None, end=None |
SmsStats |
rcs.send(...) |
to, template_id, rcs_variables: dict | None, sms_fallback: dict | None, ttl: int | None |
dict |
rcs.list_bots() / rcs.list_templates(...) |
none / pagination only | list[dict] / PaginatedResponse[dict] |
rcs.send_campaign(...) |
bot_id, template_id, campaign_name=None, numbers=None, country=None, remove_duplicate=None, ttl=None, schedule_campaign=None, scheduled_at=None, fallback=None, fallback_message=None |
dict |
rcs.list_campaigns(...) / rcs.list_logs(...) |
pagination only | PaginatedResponse[dict] |
rcs.get_stats(...) |
start=None, end=None |
RcsStats |
otp.send(...) |
widget_id, identifier (a phone number) |
SendOtpResponse (requestId, channel) |
otp.retry(...) |
request_id, channel=None ("sms" or "whatsapp") |
SendOtpResponse (requestId, channel) |
otp.verify(...) / otp.verify_token(...) |
request_id and otp / access_token |
VerifyOtpResponse / VerifyOtpTokenResponse |
otp.list_logs(...) |
widget_id=None, status=None |
PaginatedResponse[dict] |
otp.get_analytics(...) |
widget_id=None |
dict |
messages.send(...) |
to, channel ("whatsapp", "sms", "rcs", "email" or "instagram"), fallback_channels: list | None ("whatsapp", "sms", "rcs"), message=None, template: dict | None (name, id, languageCode, variables, components), sms: dict | None (senderId, route), email: dict | None (subject, fromName, fromEmail, replyTo, firstName, lastName) |
SendMessageResponse (status, channelUsed, messageId, attempts). Made once, never retried |
automations.list(...) / automations.get(automation_id) |
pagination only / positional id | PaginatedResponse[AutomationSummary] / Automation, with its trigger and steps |
automations.node_catalog() |
none | AutomationNodeCatalog, every trigger type and node type with the settings each node reads |
automations.create(...) |
name, description=None, trigger: dict | None, steps: list[dict] | None, keywords: list[dict] | None, posts: list[dict] | None, check_follower=None, node_layout: dict | None |
CreateAutomationResponse, ALWAYS created paused |
automations.update(automation_id, ...) |
positional id, any create argument, plus active=None |
Automation. trigger, steps, keywords and posts replace what is stored |
automations.activate(automation_id) / automations.pause(automation_id) |
positional id | Automation |
automations.delete(automation_id) |
positional id | None |
automations.list_executions(automation_id, ...) |
positional id, pagination only | PaginatedResponse[AutomationExecution], newest run first |
automations.rerun_execution(automation_id, execution_id) |
both positional | dict ({"ok": True}), the steps run again for real |
ai.list_models() |
none | dict ({"data": [AiModel, ...]}), each model with the price your workspace is charged |
webhooks.list_event_types() |
none | dict, the catalogue plus a sample payload each |
webhooks.list(...) / webhooks.get(endpoint_id) |
pagination only / positional id | PaginatedResponse[WebhookEndpoint] / WebhookEndpoint |
webhooks.create(...) |
url, events: list[str] (["*"] for all), description=None, is_active=None |
dict including secret, returned once only |
webhooks.update(endpoint_id, ...) |
positional id, url=None, events=None, description=None, is_active=None |
WebhookEndpoint |
webhooks.delete(endpoint_id) / webhooks.rotate_secret(endpoint_id) |
positional id | None / dict, the previous secret stops working |
webhooks.send_test(endpoint_id, ...) |
positional id, event_type=None |
dict |
webhooks.list_deliveries(endpoint_id, ...) |
positional id, status=None |
PaginatedResponse[WebhookDelivery] |
webhooks.get_delivery(delivery_id) / webhooks.replay_delivery(delivery_id) |
positional id | dict, a replay reuses the original event id |
chat.create_token(...) |
user_id, name=None, role=None, app=None, ttl_seconds=None (300 to 86400, default 3600) |
ChatToken (token, expiresAt, user). Call it again to refresh |
chat.revoke_user(user_id, ...) |
positional id, app=None |
ChatUserRevoked (revoked, notBefore), every earlier token of that user stops working |
chat.delete_user(user_id, ...) |
positional id, app=None |
ChatUserDeleted (deleted, conversations), erases the user's conversations; safe to call twice |
Module-level exports
| Export | Signature |
|---|---|
verify_webhook_signature |
(raw_body: str | bytes, signature_header: str, secret: str, tolerance_seconds: int = 300) -> bool, constant-time, accepts several v1 values during a rotation |
construct_event |
(raw_body, signature_header, secret, tolerance_seconds=300) -> dict, verifies then parses, raises rather than returning unverified data |
DEFAULT_TOLERANCE_SECONDS |
300, the maximum timestamp drift accepted |
SlideSignatureVerificationError |
raised by construct_event |
Failed requests raise SlideValidationError (400), SlideAuthError (401), SlideScopeError (403), SlideNotFoundError (404) or the base SlideError for any other status. All carry .status_code, .body and the API's message. Response TypedDicts such as Contact, VoiceCall and WhatsAppLog are importable from the package root.
Configuration
| Argument | Type | Default | Notes |
|---|---|---|---|
api_key |
str, required, keyword-only |
none | Sent as Authorization: Bearer <key>. Read it from an environment variable such as SLIDE_API_KEY, never hardcode it. To create or update contacts, create a new API key with contacts:write (and contacts:read to list or read them): a key that only has the email list permission cannot create contacts, and a key's permissions cannot be edited |
base_url |
str |
https://slide.synquic.com/api |
Trailing slash is normalised, and every resource path is appended as v1/... |
timeout |
float |
30.0 |
Seconds, applied by the underlying HTTP client to every request |
analytics_flush_interval |
float |
5.0 |
Seconds an event waits in the queue behind analytics.track before it is sent (a batch also goes out at 50 events) |
analytics_on_error |
Callable[[Exception, list[dict]], Any] | None |
None |
The only place a background failure shows: called with the error (SlideError, an httpx error, SlideEventsRejectedError or SlideEventQueueFullError) and the events concerned. On the async client it may be an async def |
analytics_retry_delay |
float |
0.5 |
Seconds before the first retry when the server gave no hint; it doubles |
Webhook signing secrets are separate from the API key: webhooks.create() returns one once, and that is what construct_event and verify_webhook_signature expect (for example from SLIDE_WEBHOOK_SECRET).
Requirements
- Python >= 3.9
httpx >= 0.27.0, the only declared dependencytyping_extensionsis imported bysynquic_slide.types, and normally arrives transitively withhttpx. Install it explicitly if your environment prunes transitive dependencies
Development
The project builds with the hatchling backend declared in pyproject.toml:
pip install -e .
python -m build # produces the wheel and sdist
The chat resource and the chat.message.unread event have tests in tests/test_chat.py, on the standard library's unittest with the HTTP layer replaced, so nothing leaves the machine:
python3 -B -m unittest discover -s tests -v
The messages, automations, ai and contacts resources are covered the same way (tests/test_messages.py, tests/test_automations.py, tests/test_ai.py, tests/test_contacts.py): every method is checked on both clients for its HTTP method, path, query and body. tests/test_analytics.py also holds the queue: batching at 50, the interval, flush(), retries with the same insertIds, no retry on a 400, 401, 403, 409 or 429, analytics_on_error, and the 5,000 event bound.
No linter or formatter configuration is checked in to this repository.
Licence
MIT.
Metadata
Release files for synquic-slide 0.5.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| synquic_slide-0.5.0.tar.gz | 84.9 kB | Without provenance | Details for synquic_slide-0.5.0.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| synquic_slide-0.5.0-py3-none-any.whl | Python 3 | none | any | Details for synquic_slide-0.5.0-py3-none-any.whl |
Total release size: 147.4 kB
Release files / synquic_slide-0.5.0.tar.gz
| Download URL | synquic_slide-0.5.0.tar.gz |
|---|---|
| Size | 84.9 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
652b896a02bdffe6b65f774cb9f72314945cdf4181d2d72f74c3c51914e5f80b
|
|
BLAKE2b-256 checksum How to use checksums |
236f8c109af36713eeec5511d7414f61f6760aec7e243ba9fe43adf5f84eb994
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.14.6
|
Release files / synquic_slide-0.5.0-py3-none-any.whl
| Download URL | synquic_slide-0.5.0-py3-none-any.whl |
|---|---|
| Size | 62.5 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
4667608ce6e3b757d67f70884106d3657aa69778c3285e87ae33645c459dcdaf
|
|
BLAKE2b-256 checksum How to use checksums |
1c2c53ce8aa44cbb55e14042542b5b4f87cd2b7c4fd6c9152f39fe6eade5b63a
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.14.6
|
Security for synquic-slide 0.5.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.