Docs / Integrations

Webhooks and API

Signed webhooks, the read-only REST API, API keys and privacy levels, and the event format.

Saient is an AI front desk for clinics and businesses: it answers WhatsApp, SMS and calls, and it books, moves and cancels appointments. This guide covers how to get that data into your own systems: a CRM, a practice-management system, Pabbly Connect, Make, Zapier, or your own code.

There are two ways to connect, and most integrations use both:

What it doesUse it for
WebhooksSaient sends an HTTPS POST to your URL the moment something happensReal-time triggers ("when a booking is created…")
APIYou read from Saient with an API keyLooking things up ("find the contact with this phone"), polling, catching up on missed events

Current version: 2026-10-01. A breaking change ships as a new version; fields may be added within a version.


1. Get an API key

In Saient, go to Settings → Integrations → API keys and click Create key. The key starts with sk_live_ and is shown once, so store it somewhere safe. Revoking a key stops it immediately, and also removes any webhooks it subscribed.

Each key has a privacy level, chosen when it's created. The level decides which personal details the key can read:

LevelIncludes
minimalNo personal details: the contact's Saient ID, what happened, and a link to open it in Saient. Can't look people up by phone or email.
contactAlso includes name, phone and email
fullAlso includes conversation summaries and free-text reasons (for example, why a booking was cancelled)

US clinics: only give contact or full keys to tools covered by your HIPAA business associate agreement.

Send the key on every request in either header:

Authorization: Bearer sk_live_...
X-API-Key: sk_live_...

Base URL: https://<your Saient API host>/api/v1. It's shown on the Integrations page. The full API is also described as OpenAPI at /api/v1/openapi.json, which needs no key.


2. Events

Webhooks and GET /events use the same event types and the same JSON shape.

TypeWhen it happens
booking.createdA booking was confirmed
booking.requestedA booking is waiting for your team to confirm it (clinics in request mode)
booking.rescheduledA booking moved to a new time (previous_start holds the old time)
booking.cancelledA booking was cancelled
lead.capturedSomeone new got in touch for the first time
handoff.requestedA conversation needs a person from the team
conversation.completedA conversation went quiet for 30 minutes after an exchange: how it ended, and a summary at full
no_show.preventedA patient confirmed their visit in reply to a reminder

Example: booking.created (privacy contact)

{
  "id": "evt_1042",
  "type": "booking.created",
  "version": "2026-10-01",
  "occurred_at": "2026-10-20T09:31:12+00:00",
  "business": { "id": 1, "name": "Sunrise Family Clinic" },
  "contact": {
    "id": "pty_32",
    "name": "Aditya Pratap",
    "phone": "+919800000000",
    "email": null,
    "external_ids": {}
  },
  "outcome": {
    "booking_id": "bkg_20",
    "status": "booked",
    "service": "Checkup",
    "provider": "Dr. Sarah Patel",
    "start": "2026-10-22T10:00:00-04:00",
    "end": "2026-10-22T10:30:00-04:00",
    "timezone": "America/New_York",
    "channel": "whatsapp"
  },
  "link": "https://app.saient.ai/business-agents/memory?person=32"
}

Notes:

  • id is the deduplication key. Webhooks are delivered at least once, so a retry repeats the same id. Ignore any id you have already processed.
  • contact.phone is always in E.164 format (+91…, +1…). Match and merge contacts on it.
  • Times are ISO-8601. Booking times carry the clinic's UTC offset, and timezone names the clinic's zone.
  • Other outcomes:
    • lead.captured: {channel}, plus first_message at full
    • handoff.requested: {task_id, kind, priority, channel}, plus reason at full
    • conversation.completed: {channel, messages_in, messages_out, handled_by, resolution, started_at, ended_at, booking_ids}. handled_by is ai, staff or ai+staff; resolution is booked, handed_off or answered. A top-level summary is added at full.
  • Test deliveries carry "test": true and made-up data. Use them to map fields, then ignore them in production.

3. Webhooks

Adding one by hand

In Saient, go to Settings → Integrations → Webhooks → Add webhook. Paste your HTTPS URL, pick the events and a privacy level, then click Send test to deliver a sample of any event type. The delivery log on that page shows every attempt.

Subscribing through the API

A Zap, a Make scenario or a Pabbly workflow can subscribe its own webhook when it's switched on, and remove it when it's switched off:

POST /api/v1/webhooks
{"target_url": "https://hooks.example.com/abc", "events": ["booking.created"]}

-> 201 {"id": "whk_7", "target_url": "...", "events": [...], "privacy": "contact", "signing_secret": "whsec_..."}

GET    /api/v1/webhooks            this key's subscriptions
DELETE /api/v1/webhooks/whk_7      unsubscribe -> 204

A subscription gets its key's privacy level. Only the key that created a subscription can remove it. Each key can have up to 50 subscriptions.

Delivery

Each event is sent as an HTTPS POST with a JSON body and these headers:

X-Saient-Event:     booking.created
X-Saient-Version:   2026-10-01
X-Saient-Delivery:  evt_1042                  (equals the body's "id")
X-Saient-Signature: t=1792488672,v1=5f0c…     (HMAC-SHA256)
  • Reply with any 2xx within 10 seconds. Any other response, or no response, is retried.
  • After repeated failures the event is set aside. You can still fetch it from GET /events.
  • Webhook URLs must use https on a public address. Redirects aren't followed.

Verifying the signature (your own server)

v1 is the hex HMAC-SHA256 of "<t>.<raw body>", keyed with the webhook's signing secret. Reject the request if t is more than 5 minutes old. No-code tools can skip this step.

import hashlib, hmac, time

def verify(secret: str, body: bytes, header: str, tolerance: int = 300) -> bool:
    parts = dict(p.split("=", 1) for p in header.split(","))
    t = int(parts["t"])
    if abs(time.time() - t) > tolerance:
        return False
    expected = hmac.new(secret.encode(), f"{t}.".encode() + body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, parts.get("v1", ""))
const crypto = require("crypto");
function verify(secret, rawBody, header, tolerance = 300) {
  const parts = Object.fromEntries(header.split(",").map((p) => p.split("=")));
  if (Math.abs(Date.now() / 1000 - Number(parts.t)) > tolerance) return false;
  const expected = crypto.createHmac("sha256", secret).update(`${parts.t}.${rawBody}`).digest("hex");
  return parts.v1 && crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1));
}

4. API reference

All list endpoints return the following shape, oldest first (/events can also return newest first):

{ "object": "list", "data": [ ... ], "has_more": true, "next_cursor": "MTA0Mg" }

To get the next page, pass ?cursor=<next_cursor>. limit accepts 1–200 (default 50).

EndpointParametersReturns
GET /menone{business, key, version}. Use it as a "test connection" call
GET /contactsphone (any format), email, updated_since, cursor, limitContacts
GET /contacts/{pty_id}noneOne contact
GET /bookingscontact (pty_id), status, start_from, start_to, cursor, limitBookings (the booking.* outcome, plus contact, created_at and link)
GET /bookings/{bkg_id}noneOne booking
GET /eventstype, since, order (asc oldest first, the default; desc newest first), cursor, limitPast events, in the same shape as webhooks
POST/GET/DELETE /webhookssee section 3Webhook subscriptions

Errors:

StatusMeaning
401Missing, wrong or revoked key
403A minimal key tried to look someone up by phone or email
404Not found, or it belongs to another business
422Bad input
429Over 120 requests a minute for this key. Wait for the time in Retry-After

Each key sees only its own business's data.


5. Recipes for no-code tools

All three tools work today through their built-in webhook and HTTP modules, without a dedicated Saient app.

Pabbly Connect

  1. Trigger: choose Webhook by Pabbly and copy the webhook URL it gives you.
  2. In Saient, open Integrations → Add webhook, paste the URL, pick the events, then click Send test with the event you chose. Back in Pabbly, capture the response: the fields appear for mapping.
  3. Action: map the fields into your CRM. For example, create or update a LeadSquared or Zoho CRM lead, matched on contact.phone.
  4. Looking something up in Saient (optional): use API by Pabbly with a GET request to https://<host>/api/v1/contacts?phone={{phone}} and the header Authorization: Bearer sk_live_….

Make

  1. Trigger: add Webhooks → Custom webhook and copy the address.
  2. In Saient, add the webhook and click Send test. Make detects the data structure from the sample.
  3. Lookups: use HTTP → Make a request with a GET to /api/v1/... and the same Authorization header.

Zapier

  1. Trigger: choose Webhooks by Zapier → Catch Hook and copy the URL.
  2. In Saient, add the webhook and click Send test. Zapier shows the sample to map.
  3. Lookups: use Webhooks by Zapier → GET (or Custom Request) on /api/v1/... with the Authorization header.

6. For integration partners (building a Saient app)

What the app needsEndpoint
AuthenticationAPI key (Authorization: Bearer). Test the connection with GET /api/v1/me
Instant triggersREST hooks: POST /api/v1/webhooks when switched on, DELETE /api/v1/webhooks/{id} when switched off. Each event type is a trigger
Polling triggers (fallback)GET /api/v1/events?type=<type>&order=desc: newest first, deduplicated on id
Sample data for a triggerThe newest item from GET /api/v1/events?type=<type>&limit=1, or ask the user to click Send test
SearchesGET /api/v1/contacts?phone=… and ?email=…, GET /api/v1/bookings?contact=…
Machine-readable specGET /api/v1/openapi.json

The API is read-only apart from webhook subscriptions. Actions that change data in Saient, such as creating a booking, are not available yet.

Something missing or unclear? Ask in the chat at the bottom right, or talk to the Saient team.