SmileCloudDocs

Webhooks

Webhooks push clinic events to your server as they happen, so you don't have to poll. When something changes - an appointment is booked, a patient is updated, a payment is recorded - the API sends a signed POST to your endpoint with the event payload.

#Event types

Subscribe to any combination of these events:

EventFires when
appointment.createdA new appointment is booked.
appointment.updatedAn appointment changes (time, status, provider…).
appointment.cancelledAn appointment is cancelled.
patient.createdA new patient is added.
patient.updatedA patient's details change.
treatment.createdA treatment is recorded.
treatment.updatedA recorded treatment changes.
treatment.cancelledA recorded treatment is removed.
payment.recordedA payment is recorded.
payment.cancelledA payment is cancelled/refunded.
call.completedA phone call ended. This is the first event for a call - there is no start-of-call event.
call.updatedA staff note on the call was added or edited.
call.recording.availableThe recording finished downloading from the PBX and can be fetched.
call.transcript.completedTranscription succeeded; the payload carries the summary and full segments.
call.transcript.failedTranscription was attempted and failed. No transcript will arrive for this call.
whatsapp.message.receivedA patient sent the clinic a WhatsApp message.
whatsapp.message.sentAn outbound message was accepted by WhatsApp.
whatsapp.message.deliveredAn outbound message reached the patient's handset.
whatsapp.message.readThe patient opened an outbound message.
whatsapp.message.failedAn outbound message failed.

#Call event ordering

A call arrives in pieces, so expect several events for the same call, minutes apart:

  1. call.completed fires the moment the call ends. At this point recording.available is false and transcript.status is usually null.
  2. call.recording.available follows once the PBX has finalized the audio - typically 2-15 minutes later, and never for calls the PBX didn't record.
  3. call.transcript.completed (or .failed) follows the recording, and only for answered calls with a non-zero duration on clinics with AI transcription enabled. Calls that were never answered are never transcribed, so most calls will never emit a transcript event - don't block on one.

Every one of these carries the complete current call object, so you can treat each delivery as an upsert on the call id rather than a patch.

#WhatsApp message ordering

Inbound is a single event: whatsapp.message.received fires as the patient's message lands, and nothing else happens to it afterwards.

Outbound is a chain, and how much of it you see is up to the patient's phone:

  1. There is no event for queued. POST /v1/whatsapp/messages already returns that state, and a message staff sent from the CRM shows up a second later as sent.
  2. whatsapp.message.sent fires when WhatsApp accepts the message - that is acceptance, not delivery.
  3. whatsapp.message.delivered follows when it reaches the handset. A phone that is off or out of coverage can hold this up for hours.
  4. whatsapp.message.read only ever arrives from patients who left read receipts on, so plenty of messages that were read will never emit it. Don't treat its absence as unread.
  5. whatsapp.message.failed can arrive instead of, or after, sent - a message WhatsApp accepted can still be rejected downstream. Its failure_reason is null on the event: the reason is recorded a moment after the failure itself, so re-read the message with GET /v1/whatsapp/messages/{id} if you need it.

Each delivery carries the whole message object with its current status, so treat them as upserts on the message id. Ordering is not guaranteed: compare the timestamps in the payload rather than assuming the deliveries arrive in lifecycle order.

Subscribing to any whatsapp.* event needs both webhooks:manage and whatsapp:read on the key - the payload carries the message body, which is whatever the patient chose to type.

#Subscribe

Create a subscription with the URL to deliver to and the events you care about. The response includes a signing secret - save it, it's shown only once.

bash
curl -X POST https://api.smile-app.co.il/v1/webhooks \
  -H "Authorization: Bearer sk_live_..." \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://example.com/hooks/smile",
    "event_types": ["appointment.created", "appointment.updated", "appointment.cancelled"]
  }'

See the Webhooks API reference for the full set of management endpoints.

#Payload

Each delivery is a POST with a JSON body shaped like this:

json
{
  "id": "evt_9f2a7c",
  "type": "appointment.created",
  "created_at": "2026-07-01T09:00:05+03:00",
  "data": {
    "id": "55021",
    "patient": { "id": "8842", "first_name": "Dana", "last_name": "Levi" },
    "branch": { "id": "1", "name": "Downtown Branch" },
    "start": "2026-07-01T09:00:00+03:00",
    "end": "2026-07-01T09:30:00+03:00"
  }
}

data mirrors the REST resource

The data object is byte-identical to the matching REST resource (an appointment, patient, treatment, or payment). Whatever you'd get from a GET, you get in the event - no second lookup needed.

#Headers

Every delivery carries these headers:

HeaderDescription
X-Smile-Signaturesha256=<hex hmac> of the raw body, keyed by your subscription secret.
X-Smile-TimestampUnix time the delivery was signed.
X-Smile-Event-IdThe event id (same as id in the body).
X-Smile-Event-TypeThe event type.

#Verify the signature

Always verify the signature before trusting a payload. Compute the HMAC-SHA256 of the raw request body using your subscription secret, and compare it to X-Smile-Signature with a constant-time comparison.

Node.js
import crypto from "node:crypto";

function verify(rawBody, signatureHeader, secret) {
  const expected =
    "sha256=" + crypto.createHmac("sha256", secret).update(rawBody).digest("hex");
  const a = Buffer.from(signatureHeader);
  const b = Buffer.from(expected);
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}

Verify on the raw body

Compute the HMAC over the exact bytes you received, before any JSON parsing or re-serialization. Re-encoding the body will change the bytes and break the signature.

#Delivery, retries, and ordering

  • At-least-once. A delivery may arrive more than once. Deduplicate on X-Smile-Event-Id and make your handler idempotent.
  • Retries. Failed deliveries (non-2xx or timeout) are retried with exponential backoff for up to ~24 hours.
  • Auto-disable. A subscription that keeps failing is automatically disabled; re-enable it by fixing your endpoint and creating a new subscription.
  • Respond fast. Return 2xx quickly (ideally after just enqueuing the event). Do heavy work asynchronously so you don't time out.

#Inspect and replay

Use the deliveries log to see attempts and outcomes, and retry a specific failed delivery once your endpoint is healthy again.