Skip to main content

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).

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
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 dependency
  • typing_extensions is imported by synquic_slide.types, and normally arrives transitively with httpx. 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.4.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for synquic-slide 0.4.0
File Size Uploaded Provenance Actions
synquic_slide-0.4.0.tar.gz 71.3 kB Without provenance Details for synquic_slide-0.4.0.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for synquic-slide 0.4.0
File Interpreter ABI Platform Actions
synquic_slide-0.4.0-py3-none-any.whl Python 3 none any Details for synquic_slide-0.4.0-py3-none-any.whl

Total release size: 123.6 kB

Release files / synquic_slide-0.4.0.tar.gz

Download URL synquic_slide-0.4.0.tar.gz
Size 71.3 kB
Tags Source
SHA-256 checksum
How to use checksums
6441683d7bd95f6116900f057b2832cc475bd54a720478e9a78a7f7f3a782c77
BLAKE2b-256 checksum
How to use checksums
b3f0a56a46b33e54708ba34b30466689706910ce8b2443b6ca333ac0a3a9732d
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.4.0-py3-none-any.whl

Download URL synquic_slide-0.4.0-py3-none-any.whl
Size 52.3 kB
Tags Python 3
SHA-256 checksum
How to use checksums
601a24b3813c8e4ec3ad66ee5efb2e4cbd59c5d90069006bb6f1496d6692ed9b
BLAKE2b-256 checksum
How to use checksums
c8cfaeaba54d982666bce4112dde642d4ca8506b2dc0d7efea8115573ccdaace
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.4.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.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page