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 does | Use it for | |
|---|---|---|
| Webhooks | Saient sends an HTTPS POST to your URL the moment something happens | Real-time triggers ("when a booking is created…") |
| API | You read from Saient with an API key | Looking 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:
| Level | Includes |
|---|---|
minimal | No 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. |
contact | Also includes name, phone and email |
full | Also includes conversation summaries and free-text reasons (for example, why a booking was cancelled) |
US clinics: only give
contactorfullkeys 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.
| Type | When it happens |
|---|---|
booking.created | A booking was confirmed |
booking.requested | A booking is waiting for your team to confirm it (clinics in request mode) |
booking.rescheduled | A booking moved to a new time (previous_start holds the old time) |
booking.cancelled | A booking was cancelled |
lead.captured | Someone new got in touch for the first time |
handoff.requested | A conversation needs a person from the team |
conversation.completed | A conversation went quiet for 30 minutes after an exchange: how it ended, and a summary at full |
no_show.prevented | A 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:
idis the deduplication key. Webhooks are delivered at least once, so a retry repeats the sameid. Ignore anyidyou have already processed.contact.phoneis 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
timezonenames the clinic's zone. - Other outcomes:
lead.captured:{channel}, plusfirst_messageatfullhandoff.requested:{task_id, kind, priority, channel}, plusreasonatfullconversation.completed:{channel, messages_in, messages_out, handled_by, resolution, started_at, ended_at, booking_ids}.handled_byisai,stafforai+staff;resolutionisbooked,handed_offoranswered. A top-levelsummaryis added atfull.
- Test deliveries carry
"test": trueand 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
2xxwithin 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).
| Endpoint | Parameters | Returns |
|---|---|---|
GET /me | none | {business, key, version}. Use it as a "test connection" call |
GET /contacts | phone (any format), email, updated_since, cursor, limit | Contacts |
GET /contacts/{pty_id} | none | One contact |
GET /bookings | contact (pty_id), status, start_from, start_to, cursor, limit | Bookings (the booking.* outcome, plus contact, created_at and link) |
GET /bookings/{bkg_id} | none | One booking |
GET /events | type, since, order (asc oldest first, the default; desc newest first), cursor, limit | Past events, in the same shape as webhooks |
POST/GET/DELETE /webhooks | see section 3 | Webhook subscriptions |
Errors:
| Status | Meaning |
|---|---|
401 | Missing, wrong or revoked key |
403 | A minimal key tried to look someone up by phone or email |
404 | Not found, or it belongs to another business |
422 | Bad input |
429 | Over 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
- Trigger: choose Webhook by Pabbly and copy the webhook URL it gives you.
- 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.
- Action: map the fields into your CRM. For example, create or update a LeadSquared or Zoho CRM lead, matched
on
contact.phone. - Looking something up in Saient (optional): use API by Pabbly with a GET request to
https://<host>/api/v1/contacts?phone={{phone}}and the headerAuthorization: Bearer sk_live_….
Make
- Trigger: add Webhooks → Custom webhook and copy the address.
- In Saient, add the webhook and click Send test. Make detects the data structure from the sample.
- Lookups: use HTTP → Make a request with a GET to
/api/v1/...and the sameAuthorizationheader.
Zapier
- Trigger: choose Webhooks by Zapier → Catch Hook and copy the URL.
- In Saient, add the webhook and click Send test. Zapier shows the sample to map.
- Lookups: use Webhooks by Zapier → GET (or Custom Request) on
/api/v1/...with theAuthorizationheader.
6. For integration partners (building a Saient app)
| What the app needs | Endpoint |
|---|---|
| Authentication | API key (Authorization: Bearer). Test the connection with GET /api/v1/me |
| Instant triggers | REST 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 trigger | The newest item from GET /api/v1/events?type=<type>&limit=1, or ask the user to click Send test |
| Searches | GET /api/v1/contacts?phone=… and ?email=…, GET /api/v1/bookings?contact=… |
| Machine-readable spec | GET /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.