[{"path":"scopes.html","title":"Scopes","description":"The full list of API scopes and which endpoints each one unlocks.","section":"Core concepts","headings":[{"id":"available-scopes","text":"Available scopes"},{"id":"endpoint-mapping","text":"Endpoint mapping"},{"id":"checking-a-keys-scopes","text":"Checking a key's scopes"}],"text":"ScopesScopes are the permission system for API keys. Each key carries a set of scopes, and each endpoint requires exactly one. A request succeeds only when the key holds the endpoint's required scope; otherwise it returns 403 Forbidden.#Available scopesScopeDescriptionpatients:readRead patients, search, and per-patient lookups.appointments:readRead appointments and a patient's appointments.availability:readRead free appointment slots.treatments:readRead treatments performed/recorded for patients.payments:readRead payments and refunds.payments:writeRecord a payment and issue its accounting documents. Granted explicitly — never bundled with reads.leads:writeCapture CRM leads from external channels (website forms, bots, Zapier). Create-only — there is no leads:read.calls:readRead the phone call log, AI summaries, transcripts and recording links.catalog:readRead reference data: branches, types, statuses, treatments, providers.analytics:readRun aggregations and describe queryable metrics (/v1/query, /v1/metrics).webhooks:manageCreate, list, inspect, and delete webhook subscriptions.#Endpoint mappingEndpointRequired scopeGET /v1/me(none - any valid key)GET /v1/patients, /v1/patients/{id}patients:readGET /v1/patients/{id}/appointmentsappointments:readGET /v1/patients/{id}/treatments, /v1/treatments/{id}treatments:readGET /v1/patients/{id}/payments, /v1/payments/{id}payments:readPOST /v1/patients/{id}/paymentspayments:writePOST /v1/leadsleads:writeGET /v1/calls, /v1/calls/{id}, /v1/calls/{id}/recording, /v1/patients/{id}/callscalls:readGET /v1/appointments, /v1/appointments/{id}appointments:readGET /v1/availabilityavailability:readGET /v1/catalog/*catalog:readGET /v1/metrics, POST /v1/queryanalytics:read (+ a measure's own scope, e.g. payments:read, for financial measures)POST/GET/DELETE /v1/webhooks, deliveries, retrywebhooks:managePOST /mcp toolsthe matching *:read scope per toolLeast privilegeMint keys with only the scopes an integration actually needs. A reporting script that reads appointments shouldn't carry payments:read. Narrow keys limit blast radius if one is ever exposed.#Checking a key's scopesCall GET /v1/me to see the clinic and scopes attached to the current key:bashCopycurl https://api.smile-app.co.il/v1/me \\ -H \"Authorization: Bearer sk_live_...\" jsonCopy{ \"data\": { \"clinic_id\": \"demo\", \"scopes\": [\"catalog:read\", \"appointments:read\"] } } For AI agents, smile_whoami exposes the same information so the agent can discover its limits up front instead of probing endpoints and hitting 403s."},{"path":"webhooks.html","title":"Webhooks","description":"Subscribe to clinic events and receive signed, retried, at-least-once deliveries.","section":"Guides","headings":[{"id":"event-types","text":"Event types"},{"id":"subscribe","text":"Subscribe"},{"id":"payload","text":"Payload"},{"id":"headers","text":"Headers"},{"id":"verify-the-signature","text":"Verify the signature"},{"id":"delivery-retries-and-ordering","text":"Delivery, retries, and ordering"},{"id":"inspect-and-replay","text":"Inspect and replay"}],"text":"WebhooksWebhooks 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 typesSubscribe to any combination of these events:EventFires whenappointment.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.#SubscribeCreate 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.bashCopycurl -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.#PayloadEach delivery is a POST with a JSON body shaped like this:jsonCopy{ \"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 resourceThe 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.#HeadersEvery delivery carries these headers:HeaderDescriptionX-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 signatureAlways 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.jsCopyimport 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 bodyCompute 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 orderingAt-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 replayUse the deliveries log to see attempts and outcomes, and retry a specific failed delivery once your endpoint is healthy again."},{"path":"api/leads.html","title":"Leads","description":"Push CRM leads into SmileCloud from any channel - website forms, bots, Facebook Lead Ads via Zapier, and more.","section":"API reference","headings":[{"id":"create-a-lead","text":"Create a lead"},{"id":"attribution","text":"Attribution"},{"id":"connecting-channels-with-zapier","text":"Connecting channels with Zapier"}],"text":"LeadsLeads are potential patients captured before they book - a website form submission, a bot conversation, a Facebook Lead Ad. The API is create-only: external channels push leads in with the leads:write scope, and the clinic qualifies, assigns and converts them inside SmileCloud. There is no leads:read - lead data stays in the clinic.#Create a leadPOSTFieldTypeDescriptionfirst_namestring, requiredMax 120.last_namestringMax 120.phonestringRequired unless email is given.emailstringRequired unless phone is given.sourcestring, requiredfacebook, website, phone, walk_in, referral, bot, or other.notestringFree text (the form message, a bot conversation summary) — lands as a note on the lead's activity timeline. Max 2000.external_idstringIdempotency key, unique per clinic — e.g. the Meta leadgen id or your form submission id. Strongly recommended.attributionobjectMarketing attribution — see below.metaobjectFree-form payload stored on the lead (form answers, custom fields). Max 50 top-level keys.bashCopycurl -X POST https://api.smile-app.co.il/v1/leads \\ -H \"Authorization: Bearer sk_live_...\" \\ -H \"Content-Type: application/json\" \\ -d '{ \"first_name\": \"Dana\", \"last_name\": \"Levi\", \"phone\": \"0501234567\", \"source\": \"website\", \"note\": \"Interested in teeth whitening\", \"external_id\": \"form-8817\", \"attribution\": { \"utm_source\": \"google\", \"utm_campaign\": \"summer-whitening\", \"gclid\": \"Cj0KCQjw...\" } }' jsonCopy{ \"data\": { \"id\": \"112\", \"first_name\": \"Dana\", \"last_name\": \"Levi\", \"phone\": \"0501234567\", \"email\": null, \"source\": \"website\", \"status\": \"new\", \"attribution\": { \"utm_source\": \"google\", \"utm_campaign\": \"summer-whitening\", \"gclid\": \"Cj0KCQjw...\" }, \"created_at\": \"2026-08-02T14:05:00+03:00\" } } 201 means the lead was created. 200 means it already existed - either the same external_id was replayed, or the phone/email matched an open lead (not converted or lost); the submission is then preserved on that lead's activity timeline instead of creating a duplicate, and the matched lead is returned (its status shows where it already got to).#Attributionattribution is a flat key→value map recording where the lead came from. It is deliberately generic - any key is accepted (values max 512 chars, max 30 keys), so every channel a clinic pipes leads through can attach whatever it knows. Conventional keys:KeyMeaningutm_source, utm_medium, utm_campaign, utm_term, utm_contentStandard UTM parameters.gclidGoogle Ads click id.fbclid / ctwa_clidMeta click id / click-to-WhatsApp click id.ttclidTikTok click id.referrer_url, landing_page_urlWhere the visitor came from and landed.campaign_nameHuman-readable campaign label.Send it if you have itAttribution can't be reconstructed later. Pass click ids and UTM tags at capture time even if you don't use them yet - they make conversion reporting and offline-conversion feedback possible down the road.#Connecting channels with ZapierThe recommended way to connect Facebook Lead Ads, Google Ads lead forms, landing-page builders and similar channels is Zapier (or any equivalent automation platform):Create a Zap with the channel as the trigger (e.g. Facebook Lead Ads → New Lead).Add a Webhooks by Zapier → POST action to https://api.smile-app.co.il/v1/leads.Set the Authorization: Bearer sk_live_... header with an API key that carries only the leads:write scope.Map the form fields to first_name, phone, email, set source accordingly, and map the platform's lead/submission id to external_id so retried deliveries never create duplicates.Map ad/campaign fields into attribution (e.g. campaign_name, fbclid).Least privilegeMint a dedicated key holding only leads:write for each automation. A leaked form-integration key then can't read a single patient record."},{"path":"api/webhooks.html","title":"Webhooks API","description":"Create, list, inspect, and delete webhook subscriptions and inspect deliveries.","section":"API reference","headings":[{"id":"the-subscription-object","text":"The subscription object"},{"id":"create-a-subscription","text":"Create a subscription"},{"id":"list-subscriptions","text":"List subscriptions"},{"id":"retrieve-a-subscription","text":"Retrieve a subscription"},{"id":"delete-a-subscription","text":"Delete a subscription"},{"id":"list-deliveries","text":"List deliveries"},{"id":"retry-a-delivery","text":"Retry a delivery"}],"text":"Webhooks APIThese endpoints manage webhook subscriptions - the URLs the API delivers events to. For the delivery model, signature verification, and event payloads, see the Webhooks guide. All endpoints require the webhooks:manage scope.#The subscription objectFieldTypeDescriptionidstringSubscription id.urlstringHTTPS endpoint events are delivered to.event_typesstring[]Event types this subscription receives.enabledbooleanWhether delivery is active.secretstringSigning secret - returned only once, at creation.created_atstringISO 8601 timestamp.#Create a subscriptionPOSTBody fieldTypeDescriptionurlstringHTTPS endpoint to receive events.event_typesstring[]Event types to subscribe to.bashCopycurl -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\"] }' jsonCopy{ \"data\": { \"id\": \"wh_2a9f\", \"url\": \"https://example.com/hooks/smile\", \"event_types\": [\"appointment.created\", \"appointment.updated\"], \"enabled\": true, \"secret\": \"whsec_8f2c...\", \"created_at\": \"2026-06-27T12:00:00+03:00\" } } Save the secret nowsecret (the whsec_... signing key) is shown once. Store it securely - you'll need it to verify delivery signatures, and it can't be retrieved later. If lost, delete the subscription and create a new one.#List subscriptionsGETReturns the clinic's subscriptions with the secret masked.bashCopycurl https://api.smile-app.co.il/v1/webhooks \\ -H \"Authorization: Bearer sk_live_...\" #Retrieve a subscriptionGETbashCopycurl https://api.smile-app.co.il/v1/webhooks/wh_2a9f \\ -H \"Authorization: Bearer sk_live_...\" #Delete a subscriptionDELETEStops all future deliveries to the subscription.bashCopycurl -X DELETE https://api.smile-app.co.il/v1/webhooks/wh_2a9f \\ -H \"Authorization: Bearer sk_live_...\" #List deliveriesGETA paginated log of delivery attempts for a subscription - useful for debugging failures.bashCopycurl https://api.smile-app.co.il/v1/webhooks/wh_2a9f/deliveries \\ -H \"Authorization: Bearer sk_live_...\" #Retry a deliveryPOSTManually re-attempts a failed delivery.bashCopycurl -X POST \\ https://api.smile-app.co.il/v1/webhooks/wh_2a9f/deliveries/dl_771/retry \\ -H \"Authorization: Bearer sk_live_...\""},{"path":"api/catalog.html","title":"Catalog","description":"Reference data - branches, appointment types, statuses, treatments, providers, and billing accounts.","section":"API reference","headings":[{"id":"branches","text":"Branches"},{"id":"appointment-types","text":"Appointment types"},{"id":"appointment-statuses","text":"Appointment statuses"},{"id":"treatments","text":"Treatments"},{"id":"providers","text":"Providers"},{"id":"billing-accounts","text":"Billing accounts"}],"text":"CatalogThe catalog exposes a clinic's reference data: the branches, appointment types, statuses, treatment definitions, and providers that the other resources refer to. It's the data you use to populate filters and resolve embedded ids to names. All endpoints require the catalog:read scope and return a bounded list (no cursor).Cache itCatalog data changes rarely. Fetch it once at startup (or on a slow interval) and cache it - it's the cheapest way to cut request volume and stay well under rate limits.#BranchesGETFieldTypeDescriptionidstringBranch id.namestringBranch name.phonestring | nullContact phone.addressobjectAddress details.colorstring | nullDisplay color.bashCopycurl https://api.smile-app.co.il/v1/catalog/branches \\ -H \"Authorization: Bearer sk_live_...\" #Appointment typesGETFieldTypeDescriptionidstringType id.namestringType name (e.g. \"Cleaning\").duration_minutesintegerDefault duration.colorstring | nullDisplay color.#Appointment statusesGETFieldTypeDescriptionidstringStatus id.namestringStatus name (e.g. \"Confirmed\").colorstringDisplay color.enabledbooleanWhether the status is in use.#TreatmentsGETThe menu of procedures the clinic offers. (For procedures actually performed on a patient, see Treatments.)FieldTypeDescriptionidstringTreatment id.namestringTreatment name.codestring | nullProcedure code.pricenumber | nullDefault price.colorstring | nullDisplay color.category_idstring | nullCategory grouping.#ProvidersGETThe caregivers at the clinic (dentists, hygienists, etc.).FieldTypeDescriptionidstringProvider id.namestringProvider name.typestringProvider type.colorstring | nullDisplay color.bashCopycurl https://api.smile-app.co.il/v1/catalog/providers \\ -H \"Authorization: Bearer sk_live_...\" jsonCopy{ \"data\": [ { \"id\": \"12\", \"name\": \"Dr. Cohen\", \"type\": \"dentist\", \"color\": \"#4f46e5\" }, { \"id\": \"18\", \"name\": \"Noa (Hygienist)\", \"type\": \"hygienist\", \"color\": \"#16a34a\" } ] } #Billing accountsGETThe billing accounts (receipt books) a payment can be recorded into. Pass an id as billing_account_id on POST /v1/patients/{id}/payments; it can be omitted when the clinic has exactly one.FieldTypeDescriptionidstringBilling account id.business_namestringThe business name printed on receipts.business_idstring | nullThe registered business number printed on receipts.bashCopycurl https://api.smile-app.co.il/v1/catalog/billing-accounts \\ -H \"Authorization: Bearer sk_live_...\" jsonCopy{ \"data\": [ { \"id\": \"1\", \"business_name\": \"Smile Dental Ltd\", \"business_id\": \"515123456\" } ] }"},{"path":"api/calls.html","title":"Calls","description":"Read the clinic's phone call log, including AI summaries, full transcripts and recording links.","section":"API reference","headings":[{"id":"the-call-object","text":"The call object"},{"id":"the-transcript-object","text":"The transcript object"},{"id":"list-calls","text":"List calls"},{"id":"syncing-incrementally","text":"Syncing incrementally"},{"id":"retrieve-a-call","text":"Retrieve a call"},{"id":"list-a-patients-calls","text":"List a patient's calls"},{"id":"get-a-recording-link","text":"Get a recording link"},{"id":"related","text":"Related"}],"text":"CallsCalls are the clinic's phone log: who called, when, whether it was answered, and - when the clinic has AI features and call recording enabled - an AI summary and a speaker-by-speaker transcript. All endpoints require the calls:read scope.Calls carry no patient foreign key. The link to a patient is a phone-number match made when you read, so patients may contain more than one person (a shared household number) or none at all.#The call objectFieldTypeDescriptionidstringUnique call id.directionstringinbound or outbound.statusstring | nullanswered or no_answer.extensionstring | nullThe clinic-side line that handled the call.caller_phonestring | nullThe number that placed the call.callee_phonestring | nullThe number that was dialed.patient_phonestring | nullThe non-clinic side, whichever direction the call ran in.started_atstring | nullISO 8601 start time.ended_atstring | nullISO 8601 end time.duration_secondsinteger | nullDerived from the timestamps.notesstring | nullFree-text note written by clinic staff.patientsobject[]Patient summaries matched on patient_phone.recordingobject{ \"available\": boolean } - fetch the audio link separately.transcriptobjectSee below.created_atstring | nullISO 8601 timestamp.updated_atstring | nullISO 8601 timestamp. Bumps when a transcript lands.#The transcript objectFieldTypeDescriptionstatusstring | nullprocessing, done, failed, or null when transcription was never attempted.summarystring | nullOne-line AI summary of the conversation.segmentsarray | nullSpeaker turns, each { speaker, text, start_seconds }. Populated only when you pass response_format=detailed; the key is always present.Transcripts and recordings are patient conversationsA transcript is the textual equivalent of listening to the call. Both it and the recording link are covered by the single calls:read scope - only grant that scope to integrations that genuinely need to read what patients said.#List callsGETReturns a paginated list, newest call first.Query paramDescriptionfrom, toCall start time, inclusive (ISO 8601 date or timestamp).updated_sinceRows changed at or after this instant, oldest change first. See below.directioninbound or outbound.statusanswered or no_answer.extensionThe clinic-side line.phoneMatches either side of the call; separators are ignored.patient_idCalls to or from that patient's number.has_recordingtrue or false.has_transcripttrue or false (a completed transcript).response_formatconcise (default) or detailed to include transcript.segments.bashCopycurl \"https://api.smile-app.co.il/v1/calls?from=2026-07-01&status=no_answer\" \\ -H \"Authorization: Bearer sk_live_...\" jsonCopy{ \"data\": [ { \"id\": \"8421\", \"direction\": \"inbound\", \"status\": \"no_answer\", \"extension\": \"036477878\", \"caller_phone\": \"0522859234\", \"callee_phone\": \"036477878\", \"patient_phone\": \"0522859234\", \"started_at\": \"2026-07-23T14:49:00+03:00\", \"ended_at\": \"2026-07-23T14:49:20+03:00\", \"duration_seconds\": 20, \"notes\": null, \"patients\": [{ \"id\": \"8842\", \"first_name\": \"Dana\", \"last_name\": \"Levi\" }], \"recording\": { \"available\": false }, \"transcript\": { \"status\": null, \"summary\": null, \"segments\": null }, \"created_at\": \"2026-07-23T14:49:25+03:00\", \"updated_at\": \"2026-07-23T14:49:25+03:00\" } ], \"next_cursor\": null } #Syncing incrementallyTranscription finishes minutes or hours after a call ends, so a row can change long after its started_at. Poll with updated_since - it orders by the change time, oldest first - and store the highest updated_at you have seen as your next watermark. Ordering by call time would miss those late updates.bashCopycurl \"https://api.smile-app.co.il/v1/calls?updated_since=2026-07-23T14:00:00%2B03:00\" \\ -H \"Authorization: Bearer sk_live_...\" #Retrieve a callGETPass response_format=detailed for the full transcript.bashCopycurl \"https://api.smile-app.co.il/v1/calls/8421?response_format=detailed\" \\ -H \"Authorization: Bearer sk_live_...\" jsonCopy{ \"data\": { \"id\": \"8421\", \"direction\": \"outbound\", \"status\": \"answered\", \"duration_seconds\": 80"},{"path":"api/availability.html","title":"Availability","description":"Find free appointment slots for a branch over a date range.","section":"API reference","headings":[{"id":"the-slot-object","text":"The slot object"},{"id":"find-availability","text":"Find availability"}],"text":"AvailabilityThe availability endpoint returns free appointment slots for a branch over a date range, optionally narrowed to a single provider. Use it to answer \"when is the next opening?\" or to render a booking calendar. It requires the availability:read scope.#The slot objectFieldTypeDescriptionprovider_idstringThe provider this slot is free for.branch_idstringThe branch the slot is at.startstring | nullISO 8601 earliest start of the slot.endstring | nullISO 8601 end of the slot window.latest_startstring | nullISO 8601 latest a visit can start and still fit.#Find availabilityGETReturns a bounded list (no cursor) of slots in the window.ParameterInRequiredDescriptionfromqueryyesStart date, ISO YYYY-MM-DD.toqueryyesEnd date, ISO YYYY-MM-DD.branch_idqueryyesThe branch to search.provider_idquerynoRestrict to one provider.cURLCopycurl -G https://api.smile-app.co.il/v1/availability \\ -H \"Authorization: Bearer sk_live_...\" \\ --data-urlencode \"from=2026-07-01\" \\ --data-urlencode \"to=2026-07-07\" \\ --data-urlencode \"branch_id=1\" sc CLICopysc availability --branch 1 --from 2026-07-01 --to 2026-07-07 jsonCopy{ \"data\": [ { \"provider_id\": \"12\", \"branch_id\": \"1\", \"start\": \"2026-07-01T09:00:00+03:00\", \"end\": \"2026-07-01T17:00:00+03:00\", \"latest_start\": \"2026-07-01T16:30:00+03:00\" } ] } from, to, and branch_id are requiredUnlike most list endpoints, availability requires from, to, and branch_id. Omitting any of them returns 400 Bad Request."},{"path":"api/patients.html","title":"Patients","description":"List, search, and retrieve patients and their appointments, treatments, and payments.","section":"API reference","headings":[{"id":"the-patient-object","text":"The patient object"},{"id":"list-search-patients","text":"List / search patients"},{"id":"retrieve-a-patient","text":"Retrieve a patient"},{"id":"a-patients-appointments","text":"A patient's appointments"},{"id":"a-patients-treatments","text":"A patient's treatments"},{"id":"a-patients-payments","text":"A patient's payments"}],"text":"PatientsPatients are the people treated at a clinic. You can search the patient list, fetch a single patient, and read a patient's appointments, treatments, and payments.All patient endpoints require the patients:read scope (sub-resources additionally require their own scope, noted below).#The patient objectFieldTypeDescriptionidstringUnique patient id (clinic-scoped).first_namestring | nullGiven name.last_namestring | nullFamily name.phonestring | nullPrimary phone.emailstring | nullPrimary email.genderstring | nullmale, female, or null.birth_datestring | nullISO YYYY-MM-DD.statusobject | nullEmbedded status reference (id, name, color).created_atstring | nullISO 8601 timestamp.updated_atstring | nullISO 8601 timestamp.With response_format=detailed, additional fields such as address, contacts, balance, and id number are included where available.#List / search patientsGETReturns a paginated list of patients. Combine filters to narrow the search.ParameterInDescriptionphonequeryMatch by phone number.emailqueryMatch by email.queryqueryName search.limit, cursorqueryPagination.response_formatqueryconcise (default) or detailed.bashCopycurl -G https://api.smile-app.co.il/v1/patients \\ -H \"Authorization: Bearer sk_live_...\" \\ --data-urlencode \"query=levi\" \\ --data-urlencode \"limit=25\" jsonCopy{ \"data\": [ { \"id\": \"8842\", \"first_name\": \"Dana\", \"last_name\": \"Levi\", \"phone\": \"0521234567\", \"email\": \"dana@example.com\" } ], \"next_cursor\": null } #Retrieve a patientGETParameterInDescriptionidpathThe patient id.response_formatqueryconcise or detailed.bashCopycurl -G https://api.smile-app.co.il/v1/patients/8842 \\ -H \"Authorization: Bearer sk_live_...\" \\ --data-urlencode \"response_format=detailed\" jsonCopy{ \"data\": { \"id\": \"8842\", \"first_name\": \"Dana\", \"last_name\": \"Levi\", \"phone\": \"0521234567\", \"email\": \"dana@example.com\", \"gender\": \"female\", \"birth_date\": \"1990-04-12\", \"status\": { \"id\": \"1\", \"name\": \"Active\", \"color\": \"#16a34a\" }, \"created_at\": \"2024-01-08T10:22:00+02:00\", \"updated_at\": \"2026-05-30T14:01:00+03:00\" } } #A patient's appointmentsGETPaginated list of the patient's appointments. Requires appointments:read. The items use the appointment object.bashCopycurl https://api.smile-app.co.il/v1/patients/8842/appointments \\ -H \"Authorization: Bearer sk_live_...\" #A patient's treatmentsGETPaginated list of treatments performed/recorded for the patient. Requires treatments:read.ParameterInDescriptionappointment_idqueryOnly treatments for one appointment.limit, cursorqueryPagination.See the treatment object.#A patient's paymentsGETPaginated list of the patient's payments and refunds. Requires payments:read. See the payment object.Card details are never returnedPayment responses include the amount, currency, method type, and whether it's a refund - but never card numbers, check numbers, or bank details."},{"path":"api/payments.html","title":"Payments","description":"Read patient payments and refunds, and record new payments. Sensitive instrument details are never exposed.","section":"API reference","headings":[{"id":"the-payment-object","text":"The payment object"},{"id":"list-a-patients-payments","text":"List a patient's payments"},{"id":"retrieve-a-payment","text":"Retrieve a payment"},{"id":"record-a-payment","text":"Record a payment"},{"id":"related","text":"Related"}],"text":"PaymentsPayments are money recorded against a patient - charges and refunds. Reads require the payments:read scope; recording a payment requires the separate payments:write scope (granted explicitly, never bundled with reads).#The payment objectFieldTypeDescriptionidstringUnique payment id.patientobject | nullEmbedded patient summary.recorded_byobject | nullThe user who recorded it (id, name).amountnumber | nullAmount; positive for charges.currencystringISO 4217 currency code (e.g. ILS).is_refundbooleanWhether this is a refund.methodsstring[]Method types used: cash, credit_card, check, bank_transfer, payment_app.paid_atstring | nullISO 8601 time of payment.created_atstring | nullISO 8601 timestamp.updated_atstring | nullISO 8601 timestamp.No instrument details, everOnly the method type is returned (credit_card, check, …). Card numbers, expiry, check numbers, and bank account details are never exposed through the API.#List a patient's paymentsGETReturns a paginated list of the patient's payments and refunds.bashCopycurl https://api.smile-app.co.il/v1/patients/8842/payments \\ -H \"Authorization: Bearer sk_live_...\" jsonCopy{ \"data\": [ { \"id\": \"33010\", \"patient\": { \"id\": \"8842\", \"first_name\": \"Dana\", \"last_name\": \"Levi\" }, \"recorded_by\": { \"id\": \"5\", \"name\": \"Front Desk\" }, \"amount\": 480.0, \"currency\": \"ILS\", \"is_refund\": false, \"methods\": [\"credit_card\"], \"paid_at\": \"2026-07-01T09:35:00+03:00\" } ], \"next_cursor\": null } #Retrieve a paymentGETbashCopycurl https://api.smile-app.co.il/v1/payments/33010 \\ -H \"Authorization: Bearer sk_live_...\" #Record a paymentPOSTRecords a simplified payment and issues the accounting documents (receipt / tax invoice) in the same call: exactly one payment method, no multi-method split, no family split. The response embeds the issued and scheduled documents, including short-lived temp_url PDF links — so the receipt arrives in one round-trip.FieldTypeDescriptionamountnumber, requiredGross (VAT-inclusive) amount actually collected, in ILS. Positive, max 2 decimals.methodstring, requiredcash, credit_card, check, bank_transfer, or payment_app.descriptionstringPrinted on the receipt (max 255).notesstringFree-text notes printed on the receipt (max 1000).billing_account_idintegerTarget billing account. Optional when the clinic has exactly one, or the patient has a default.external_idstringIdempotency key, unique per clinic — e.g. your transaction id. Strongly recommended for machine callers.zero_vatbooleanRecord with 0% VAT (default false).credit_cardobjectOnly when method=credit_card; all fields optional: brand (visa/mastercard/isracard/amex/diners/other), last_4 (exactly 4 digits — never a full card number), expiry (MM/YY), mode (regular/installments/credit), num_payments (required unless regular), first_payment_amount + next_payment_amount (computed when omitted), transaction_ref.checkobjectRequired when method=check: date (value date, YYYY-MM-DD) plus optional bank, branch, account, number.bank_transferobjectRequired when method=bank_transfer: date plus optional bank, branch, account.payment_appobjectRequired when method=payment_app (e.g. Bit): date plus optional app, reference.bashCopycurl -X POST https://api.smile-app.co.il/v1/patients/8842/payments \\ -H \"Authorization: Bearer sk_live_...\" \\ -H \"Content-Type: application/json\" \\ -d '{ \"amount\": 1180.00, \"method\": \"credit_card\", \"description\": \"תוכנה לניהול מרפאה\", \"notes\": \"מנוי עד 14/08/2026\", \"external_id\": \"payplus:abc-123\", \"credit_card\": { \"brand\": \"visa\", \"last_4\": \"1234\", \"transaction_ref\": \"abc-123\" } }' jsonCopy{ \"data\": { \"id\": \"33011\", \"patient\": { \"id\": \"8842\", \"first_name\": \"Dana\", \"last_name\": \"Levi\" }, \"recorded_by\": null, \"amount\": 1180.0, \"currency\": \"ILS\", \"is_refund\": false, \"methods\": [\"credit_card\"], \"paid_at\": \"2026-07-30T10:12:00+03:00\", \"documents\": [ { \"type\": \"kabala\", \"status\": \"issued\", \"number\": \"2042\", \"amount\": 1180.0, \"amount_without_vat\": 1000.0, \"vat_amount\": 180.0, \"vat_rate\": 0.18, \"issued_at\": \"2026-07-30T10:12:01+0"},{"path":"api/account.html","title":"Account","description":"Introspect the calling API key - its clinic and scopes.","section":"API reference","headings":[{"id":"the-account-object","text":"The account object"},{"id":"introspect-the-current-key","text":"Introspect the current key"}],"text":"AccountThe account endpoint reports which clinic the calling key belongs to and which scopes it carries. It's the simplest way to verify a key works and to discover its permissions before making other calls.#The account objectFieldTypeDescriptionclinic_idstringThe clinic this key belongs to.scopesstring[]The scopes this key can use.#Introspect the current keyGETRequires any valid key - no specific scope.cURLCopycurl https://api.smile-app.co.il/v1/me \\ -H \"Authorization: Bearer sk_live_...\" sc CLICopysc whoami jsonCopy{ \"data\": { \"clinic_id\": \"demo\", \"scopes\": [\"patients:read\", \"appointments:read\", \"catalog:read\"] } } Use this to fail fastCall /v1/me at startup to confirm the key is valid and holds the scopes your integration needs - surface a clear configuration error rather than discovering missing scopes through scattered 403s later."},{"path":"api/treatments.html","title":"Treatments","description":"Read treatments performed and recorded for patients.","section":"API reference","headings":[{"id":"the-treatment-object","text":"The treatment object"},{"id":"list-a-patients-treatments","text":"List a patient's treatments"},{"id":"retrieve-a-treatment","text":"Retrieve a treatment"},{"id":"catalog-vs-recorded-treatments","text":"Catalog vs. recorded treatments"}],"text":"TreatmentsTreatments are the procedures performed or recorded for a patient, with the treatment definition, provider, appointment, and plan embedded. All endpoints require the treatments:read scope.#The treatment objectFieldTypeDescriptionidstringUnique treatment record id.patientobject | nullEmbedded patient summary.treatmentobject | nullThe treatment definition (id, name, code).providerobject | nullProvider who performed it (id, name).appointmentobject | nullLinked appointment (id, start, end).planobject | nullTreatment plan it belongs to (id, name).quantityintegerNumber of units.pricenumber | nullRecorded price.performed_atstring | nullISO 8601 time performed.created_atstring | nullISO 8601 timestamp.updated_atstring | nullISO 8601 timestamp.#List a patient's treatmentsGETReturns a paginated list of treatments for the patient.ParameterInDescriptionidpathThe patient id.appointment_idqueryOnly treatments for one appointment.limit, cursorqueryPagination.bashCopycurl https://api.smile-app.co.il/v1/patients/8842/treatments \\ -H \"Authorization: Bearer sk_live_...\" jsonCopy{ \"data\": [ { \"id\": \"9001\", \"patient\": { \"id\": \"8842\", \"first_name\": \"Dana\", \"last_name\": \"Levi\" }, \"treatment\": { \"id\": \"44\", \"name\": \"Composite filling\", \"code\": \"D2391\" }, \"provider\": { \"id\": \"12\", \"name\": \"Dr. Cohen\" }, \"appointment\": { \"id\": \"55021\", \"start\": \"2026-07-01T09:00:00+03:00\", \"end\": \"2026-07-01T09:30:00+03:00\" }, \"plan\": { \"id\": \"70\", \"name\": \"Restorative 2026\" }, \"quantity\": 1, \"price\": 480.0, \"performed_at\": \"2026-07-01T09:20:00+03:00\" } ], \"next_cursor\": null } #Retrieve a treatmentGETbashCopycurl https://api.smile-app.co.il/v1/treatments/9001 \\ -H \"Authorization: Bearer sk_live_...\" #Catalog vs. recorded treatmentsTwo related things named \"treatment\"The catalog treatment (/v1/catalog/treatments) is the menu of procedures a clinic offers - name, code, default price. A recorded treatment (here) is one actually performed for a patient, referencing a catalog treatment via its embedded treatment object."},{"path":"api/appointments.html","title":"Appointments","description":"List and retrieve appointments, filtered by date, branch, provider, patient, or status.","section":"API reference","headings":[{"id":"the-appointment-object","text":"The appointment object"},{"id":"list-appointments","text":"List appointments"},{"id":"retrieve-an-appointment","text":"Retrieve an appointment"},{"id":"related","text":"Related"}],"text":"AppointmentsAppointments are scheduled visits. List them across the clinic with rich filters, or fetch a single appointment by id. All endpoints require the appointments:read scope.#The appointment objectFieldTypeDescriptionidstringUnique appointment id.patientobject | nullEmbedded patient summary (id, first_name, last_name).branchobject | nullEmbedded branch reference (id, name).typeobject | nullAppointment type (id, name, duration_minutes).statusobject | nullStatus (id, name, color).providersarrayEmbedded provider references (id, name).startstring | nullISO 8601 start time.endstring | nullISO 8601 end time.created_atstring | nullISO 8601 timestamp.updated_atstring | nullISO 8601 timestamp.#List appointmentsGETReturns a paginated list. Filters combine with AND.ParameterInDescriptionfromqueryISO 8601 - only appointments starting at/after this time.toqueryISO 8601 - only appointments starting at/before this time.branch_idqueryFilter by branch.provider_idqueryFilter by provider.patient_idqueryFilter by patient.status_idqueryFilter by status.limit, cursorqueryPagination.response_formatqueryconcise (default) or detailed.bashCopycurl -G https://api.smile-app.co.il/v1/appointments \\ -H \"Authorization: Bearer sk_live_...\" \\ --data-urlencode \"from=2026-07-01T00:00:00+03:00\" \\ --data-urlencode \"to=2026-07-07T23:59:59+03:00\" \\ --data-urlencode \"branch_id=1\" jsonCopy{ \"data\": [ { \"id\": \"55021\", \"patient\": { \"id\": \"8842\", \"first_name\": \"Dana\", \"last_name\": \"Levi\" }, \"branch\": { \"id\": \"1\", \"name\": \"Downtown Branch\" }, \"type\": { \"id\": \"3\", \"name\": \"Cleaning\", \"duration_minutes\": 30 }, \"status\": { \"id\": \"2\", \"name\": \"Confirmed\", \"color\": \"#16a34a\" }, \"providers\": [{ \"id\": \"12\", \"name\": \"Dr. Cohen\" }], \"start\": \"2026-07-01T09:00:00+03:00\", \"end\": \"2026-07-01T09:30:00+03:00\" } ], \"next_cursor\": \"eyJpZCI6IjU1MDIxIn0\" } #Retrieve an appointmentGETbashCopycurl https://api.smile-app.co.il/v1/appointments/55021 \\ -H \"Authorization: Bearer sk_live_...\" Times are timezone-awarestart and end are full ISO 8601 timestamps including the clinic's UTC offset. Parse them as instants - don't assume a fixed timezone.#RelatedA patient's appointments - scope a list to one patient.Availability - find free slots to book into.Webhooks - get appointment.created/updated/cancelled events pushed to you."},{"path":"api/analytics.html","title":"Analytics","description":"Run server-side aggregations (GROUP BY) over clinic data instead of fetching and summing rows yourself.","section":"","headings":[{"id":"discover-whats-queryable","text":"Discover what's queryable"},{"id":"run-an-aggregation","text":"Run an aggregation"},{"id":"cohorts-and-gaps","text":"Cohorts and gaps"},{"id":"datasets","text":"Datasets"}],"text":"AnalyticsThe analytics surface answers \"total / count / average per X\" questions with a single server-side aggregation, instead of paging every record and summing it client-side. You describe what to aggregate as a small JSON query against a registry of datasets; the API runs one GROUP BY in the database and returns grouped rows plus grand totals. It requires the analytics:read scope (financial measures additionally require their own scope, e.g. payments:read).New questions need no new endpoint — anything the registry exposes is queryable by composing a request.#Discover what's queryableGETReturns the datasets and, for each, its measures (what you can aggregate), dimensions (what you can group by), and filterable fields with their allowed operators. Call this first to compose a valid query.cURLCopycurl https://api.smile-app.co.il/v1/metrics \\ -H \"Authorization: Bearer sk_live_...\" sc CLICopysc metrics sc metrics treatments # one dataset jsonCopy{ \"data\": { \"datasets\": [ { \"name\": \"treatments\", \"description\": \"Performed treatments: revenue (price − discount), quantity and counts…\", \"measures\": [ { \"name\": \"net_revenue\", \"kind\": \"sum\", \"unit\": \"currency\", \"requires_scope\": \"payments:read\" }, { \"name\": \"treatment_count\", \"kind\": \"count\", \"unit\": \"count\" } ], \"dimensions\": [ { \"name\": \"provider\", \"kind\": \"column\" }, { \"name\": \"effective_date\", \"kind\": \"time\", \"grains\": [\"day\", \"week\", \"month\", \"quarter\", \"year\"] } ], \"filters\": [ { \"name\": \"effective_date\", \"type\": \"date\", \"operators\": [\"between\", \"gte\", \"lte\"] }, { \"name\": \"provider\", \"type\": \"id_list\", \"operators\": [\"in\", \"not_in\"] } ], \"requires_date_range\": true, \"date_filter\": \"effective_date\" } ] } } A measure or dimension that needs a scope the key lacks is simply omitted from the response.#Run an aggregationPOSTThe request body is the query contract:FieldTypeRequiredDescriptiondatasetstringyesDataset name from /v1/metrics.measuresstring[]yesMeasure names to aggregate.dimensions(string | {field, grain})[]noGroup-by axes. Use {field, grain} for time dimensions.filters{field, op, value}[]noop is one of eq, in, not_in, between, gte, lte.order{by, dir}[]noby references a selected measure or dimension; dir is asc/desc.limitintegernoMax groups returned (capped per dataset).Datasets that aggregate over time require a bounded date range (a between, or gte + lte) on their date field.cURLCopycurl https://api.smile-app.co.il/v1/query \\ -H \"Authorization: Bearer sk_live_...\" \\ -H \"Content-Type: application/json\" \\ -d '{ \"dataset\": \"treatments\", \"measures\": [\"net_revenue\", \"treatment_count\"], \"dimensions\": [\"provider\"], \"filters\": [ { \"field\": \"effective_date\", \"op\": \"between\", \"value\": [\"2026-01-01\", \"2026-06-30\"] } ], \"order\": [{ \"by\": \"net_revenue\", \"dir\": \"desc\" }], \"limit\": 50 }' sc CLICopysc query treatments \\ -m net_revenue -m treatment_count \\ -b provider \\ -f effective_date:between:2026-01-01..2026-06-30 \\ -o net_revenue:desc --limit 50 jsonCopy{ \"data\": { \"dataset\": \"treatments\", \"rows\": [ { \"net_revenue\": 153204.5, \"treatment_count\": 412, \"provider\": \"12\", \"provider_label\": \"Dr. Cohen\" } ], \"totals\": { \"net_revenue\": 1512333.0, \"treatment_count\": 4120 }, \"measures\": [\"net_revenue\", \"treatment_count\"], \"dimensions\": [\"provider\"], \"truncated\": false } } Each row carries the dimension values (with a *_label for named entities) and the measure values.totals are the grand totals over the whole filtered set.truncated is true when the group count hit the cap — narrow the filters.Why this existsAsking an agent for \"revenue per provider\" the slow way means listing every patient, paging their treatments, and summing in the model — hundreds of calls, and easy to get wrong (no discount netting, wrong dates). One /v1/query returns the exact figure straight from the database.#Cohorts and gapsA query runs one GROUP BY over a single dataset — there are no cross-dataset joins or NOT EXISTS. You don't need them: answer \"in A but not in B\" questions by running two aggregations grouped by patien"},{"path":"api/overview.html","title":"API reference","description":"Conventions, base URL, resources, and response shapes for the v1 REST API.","section":"API reference","headings":[{"id":"base-url","text":"Base URL"},{"id":"conventions","text":"Conventions"},{"id":"resources","text":"Resources"},{"id":"embedded-references","text":"Embedded references"}],"text":"API referenceThe v1 REST API is a small, read-only surface over your clinic's data. This page covers the conventions that apply everywhere; the resource pages document each endpoint in detail.#Base URLtextCopyhttps://api.smile-app.co.il All endpoints are versioned under /v1. An OpenAPI 3.1 description of the entire surface is served unauthenticated at /openapi.json.#ConventionsAuth - every request needs Authorization: Bearer sk_live_.... See Authentication.Format - requests and responses are JSON. Successful responses wrap the payload in a data field.Single resource - { \"data\": { ... } }.Collection - { \"data\": [ ... ], \"next_cursor\": \"...\" | null } for paginated lists, or { \"data\": [ ... ] } for bounded lists.Pagination - ?limit= and ?cursor=. See Pagination.Detail level - ?response_format=concise|detailed where supported.Errors - application/problem+json. See Errors.Ids are strings - always treat ids as opaque strings, scoped to one clinic.#ResourcesResourceEndpointsScopeAccountGET /v1/me(none)PatientsGET /v1/patients, /v1/patients/{id} and sub-resourcespatients:readAppointmentsGET /v1/appointments, /v1/appointments/{id}appointments:readAvailabilityGET /v1/availabilityavailability:readTreatmentsGET /v1/treatments/{id}, patient treatmentstreatments:readPaymentsGET /v1/payments/{id}, patient payments, record a paymentpayments:read, payments:writeLeadsPOST /v1/leads — capture CRM leads from external channelsleads:writeCallsGET /v1/calls, summaries, transcripts, recording linkscalls:readCatalogGET /v1/catalog/*catalog:readAnalyticsGET /v1/metrics, POST /v1/queryanalytics:readWebhooks APIPOST/GET/DELETE /v1/webhookswebhooks:manage#Embedded referencesTo save you from chasing ids, list and detail responses embed compact references to related objects rather than returning bare ids. An appointment, for example, includes a small patient, branch, type, and status object inline:jsonCopy{ \"data\": { \"id\": \"55021\", \"patient\": { \"id\": \"8842\", \"first_name\": \"Dana\", \"last_name\": \"Levi\" }, \"branch\": { \"id\": \"1\", \"name\": \"Downtown Branch\" }, \"type\": { \"id\": \"3\", \"name\": \"Cleaning\", \"duration_minutes\": 30 }, \"status\": { \"id\": \"2\", \"name\": \"Confirmed\", \"color\": \"#16a34a\" }, \"providers\": [{ \"id\": \"12\", \"name\": \"Dr. Cohen\" }], \"start\": \"2026-07-01T09:00:00+03:00\", \"end\": \"2026-07-01T09:30:00+03:00\" } } These embedded shapes (PatientSummary, BranchRef, TypeRef, StatusRef, ProviderRef) are intentionally minimal - enough to display without a second request."},{"path":"authentication.html","title":"Authentication","description":"How API keys, bearer tokens, and scopes authorize requests to the SmileCloud Public API.","section":"Get started","headings":[{"id":"api-keys","text":"API keys"},{"id":"authorizing-a-request","text":"Authorizing a request"},{"id":"scopes","text":"Scopes"},{"id":"errors","text":"Errors"},{"id":"rotating-and-revoking-keys","text":"Rotating and revoking keys"},{"id":"next-steps","text":"Next steps"}],"text":"AuthenticationThe SmileCloud Public API authenticates every request with a per-clinic secret key, presented as an HTTP bearer token. There are no sessions, cookies, or OAuth flows for v1 - a single key both identifies the clinic and carries the scopes that gate access.#API keysKeys are opaque, randomly generated strings prefixed with sk_live_:textCopysk_live_4f8a2c9e1b7d6a3f0e5c8b2a9d4f7e1c Each key belongs to exactly one clinic. SmileCloud stores only a SHA-256 hash of the key - the plaintext is shown once at creation and can never be retrieved again. If a key is lost, revoke it and mint a new one.#Authorizing a requestSend the key in the Authorization header on every request:bashCopycurl https://api.smile-app.co.il/v1/me \\ -H \"Authorization: Bearer sk_live_...\" The same bearer key works across all three surfaces - REST, the MCP server at /mcp, and the sc CLI.Keep keys server-sideBecause a key carries read access to clinic data, it must only ever live in a trusted backend or a secured operator environment. Never ship a key in a browser bundle, a mobile app, or any client you don't fully control.#ScopesEvery key carries a set of scopes - fine-grained permissions that decide which resources it can read. A request is allowed only if the key holds the scope that the endpoint requires.ScopeGrants access topatients:readPatients and patient lookupsappointments:readAppointmentsavailability:readFree appointment slotstreatments:readTreatments performed/recordedpayments:readPayments and refundscatalog:readBranches, types, statuses, treatments, providerswebhooks:manageCreate, list, and delete webhook subscriptionsYou can inspect the scopes on the current key at any time with GET /v1/me. See Scopes for a deeper reference and a per-endpoint mapping.#ErrorsAuthentication and authorization failures use standard status codes and the application/problem+json body described in Errors:StatusMeaning401 UnauthorizedThe Authorization header is missing, malformed, or the key is invalid or revoked.403 ForbiddenThe key is valid but lacks the scope the endpoint requires.jsonCopy{ \"type\": \"https://api.smile-app.co.il/problems/forbidden\", \"title\": \"Missing required scope\", \"status\": 403, \"detail\": \"This key needs the 'patients:read' scope.\" } #Rotating and revoking keysRotate by minting a new key, deploying it, then revoking the old one - keys are independent, so there's no downtime.Revoke immediately if a key is exposed. Revocation takes effect on the next request.Because keys are scoped per clinic, a compromised key can never reach another clinic's data.#Next steps🛡️Scopes referenceThe full scope list and which endpoints require each one.⚠️ErrorsThe problem+json error format and status codes."},{"path":"mcp.html","title":"MCP for AI agents","description":"Connect AI agents to clinic data over the Model Context Protocol, using the same key and contract.","section":"Guides","headings":[{"id":"endpoint","text":"Endpoint"},{"id":"tools","text":"Tools"},{"id":"connect-from-claude-code","text":"Connect from Claude Code"},{"id":"connect-from-claude-desktop-custom-connector","text":"Connect from Claude Desktop (custom connector)"},{"id":"connect-from-the-messages-api","text":"Connect from the Messages API"}],"text":"MCP for AI agentsThe API ships a Model Context Protocol server at /mcp, so AI agents can read clinic data through well-described tools instead of raw HTTP. It's built on the official MCP SDK, is stateless (JSON responses, no SSE), and is backed by the same endpoints and scopes as REST - so an agent can never do anything a REST client with the same key couldn't.#EndpointtextCopyPOST https://api.smile-app.co.il/mcp Authorization: Bearer sk_live_... GET and DELETE on /mcp return 405 - there's no standalone SSE stream. Authentication is the same sk_live_... bearer key; the agent's identity and scopes are derived per request.#ToolsEach tool maps to a REST endpoint and requires the matching *:read scope. Tool descriptions guide the agent on when to call each one.ToolDoesScopesmile_whoamiReturns the clinic and the scopes this key holds. Call first to learn what's possible.(none)smile_get_catalogLists reference data: branches, types, statuses, treatments, providers.catalog:readsmile_search_patientsSearches patients by phone, email, or name.patients:readsmile_get_patientFetches one patient (with detailed for full identity).patients:readsmile_list_appointmentsLists appointments by date and branch/provider/patient/status.appointments:readsmile_find_availabilityFinds free slots for a branch over a date range.availability:readsmile_list_treatmentsLists treatments recorded for a patient.treatments:readsmile_list_paymentsLists a patient's payments (amount, method type, refund flag).payments:readsmile_describe_metricsLists what can be aggregated: datasets, measures, dimensions, filters. Call before smile_aggregate.analytics:readsmile_aggregateRuns one server-side aggregation (GROUP BY) and returns grouped rows + grand totals.analytics:readSame contract, agent-friendly shapesTools return the same compact, embedded-reference objects as REST and default to the concise format - keeping token usage low. Agents resolve names from ids without extra calls.Aggregate, don't enumerateFor any \"total / count / average per X\" question, reach for smile_describe_metrics then smile_aggregate — one call returns the figure straight from the database. Listing every patient and summing rows in the model is slow, expensive, and error-prone. See Analytics.#Connect from Claude CodeRun the gateway, mint a key with read scopes, then register the remote MCP server with that key:bashCopyclaude mcp add --transport http smile-public-api \\ https://api.smile-app.co.il/mcp \\ --header \"Authorization: Bearer sk_live_...\" Then in a Claude Code session, run /mcp to confirm the tools are connected and ask in natural language:textCopyList the clinic's branches. Find the next free slot at branch 1 this week. What treatments were recorded for patient 8842? What was the net revenue per provider in the first half of 2026? Manage the connection with claude mcp list, claude mcp get smile-public-api, and claude mcp remove smile-public-api.#Connect from Claude Desktop (custom connector)Claude Desktop adds remote MCP servers by URL and authenticates them over OAuth 2.1 — there's no place to paste a raw header. The gateway is its own OAuth Authorization Server for exactly this flow, so no extra service is needed.In Claude Desktop, go to Settings → Connectors → Add custom connector and enter:textCopyhttps://api.smile-app.co.il/mcp Claude then runs the standard connector handshake automatically:Discovery — a 401 from /mcp advertises the authorization server via WWW-Authenticate and the protected-resource metadata (RFC 9728).Registration — Claude registers itself dynamically (RFC 7591); nothing to configure by hand.Consent — a browser window opens asking you to paste a SmileCloud API key (sk_live_…). The key is the login: the connector inherits that key's clinic and scopes, and can never exceed them.Done — Claude exchanges the grant (with PKCE) for a connector token and the tools above appear in the desktop app.Revoking a connectorA connector grant is a derived, scoped child of the key you pas"},{"path":"cli.html","title":"Command-line (sc)","description":"The sc CLI wraps the REST API for the terminal - for clinic IT, scripts, and agents.","section":"Guides","headings":[{"id":"install","text":"Install"},{"id":"authenticate","text":"Authenticate"},{"id":"common-commands","text":"Common commands"},{"id":"analytics","text":"Analytics"},{"id":"output-formats","text":"Output formats"},{"id":"self-discovery-for-agents","text":"Self-discovery for agents"}],"text":"Command-line (sc)sc is a small command-line client over the REST API, for clinic IT, automation scripts, and terminal-based AI agents. It speaks the same contract as REST and uses the same sk_live_... keys.#InstallbashCopycd cli bun install bun link # exposes the `sc` command #AuthenticateLog in once to store a key locally, or pass it via environment variables per invocation.Interactive loginCopysc login # prompts for a key, stores it in ~/.sc/config.json EnvironmentCopyexport SC_API_KEY=sk_live_... export SC_BASE_URL=https://api.smile-app.co.il sc whoami #Common commandsbashCopysc whoami # clinic id + scopes sc patients list --phone 0521234567 # search patients sc patients get 8842 # one patient sc appointments list --branch 1 --from 2026-07-01 --to 2026-07-07 sc availability --branch 1 --from 2026-07-01 --to 2026-07-07 sc catalog branches # reference data #AnalyticsRun server-side aggregations instead of fetching and summing rows. sc metrics lists what's queryable; sc query runs one GROUP BY. See Analytics.bashCopysc metrics # all datasets, measures, dimensions, filters sc metrics treatments # one dataset # net revenue + count per provider, H1 2026, top 50 sc query treatments \\ -m net_revenue -m treatment_count \\ -b provider \\ -f effective_date:between:2026-01-01..2026-06-30 \\ -o net_revenue:desc --limit 50 -m/--measure and -b/--by are repeatable; -f/--filter is field:op:value (a comma list for in/not_in, an a..b range for between).#Output formatsA global --format flag controls output:FormatDescriptiontable (default)Human-friendly aligned tables.jsonPretty-printed JSON.agentsMinified JSON; large payloads spill to a temp file path, ideal for AI agents.bashCopysc appointments list --branch 1 --format json #Self-discovery for agentssc schema emits the full command tree as JSON, so an AI agent can discover available commands and flags without guessing:bashCopysc schema Pick the right tool for the jobUse sc for terminal workflows and scripts, REST for application integrations, and MCP when an AI agent should call tools directly. They all share one key and one contract."},{"path":"quickstart.html","title":"Quickstart","description":"Make your first authenticated request to the SmileCloud Public API in a few minutes.","section":"Get started","headings":[{"id":"1-get-an-api-key","text":"1. Get an API key"},{"id":"2-make-your-first-request","text":"2. Make your first request"},{"id":"3-read-some-data","text":"3. Read some data"},{"id":"4-search-and-paginate","text":"4. Search and paginate"},{"id":"whats-next","text":"What's next"}],"text":"QuickstartThis guide takes you from zero to a working API call. You'll get a key, make an authenticated request, and read your first page of data.#1. Get an API keyKeys are issued per clinic. Clinic administrators mint and revoke keys from the SmileCloud app (Settings → Integrations → API keys). Each key is shown once at creation - copy it immediately and store it in your secret manager.A key looks like this:textCopysk_live_4f8a2c9e1b7d6a3f0e5c8b2a9d4f7e1c Treat keys like passwordsA key grants read access to clinic data for every scope it carries. Never commit keys to source control, embed them in browser or mobile apps, or paste them into logs.#2. Make your first requestEvery request is a standard HTTPS call with your key in the Authorization header. Start with /v1/me - it introspects the calling key and needs no scopes.cURLCopycurl https://api.smile-app.co.il/v1/me \\ -H \"Authorization: Bearer sk_live_...\" sc CLICopysc whoami jsonCopy{ \"data\": { \"clinic_id\": \"demo\", \"scopes\": [\"patients:read\", \"appointments:read\", \"catalog:read\"] } } The scopes array tells you exactly what this key can read. If a call later returns 403, it's because the key is missing the required scope.#3. Read some dataList the clinic's branches from the catalog. This needs the catalog:read scope.bashCopycurl https://api.smile-app.co.il/v1/catalog/branches \\ -H \"Authorization: Bearer sk_live_...\" jsonCopy{ \"data\": [ { \"id\": \"1\", \"name\": \"Downtown Branch\", \"phone\": \"+972-3-555-0100\", \"address\": {}, \"color\": \"#4f46e5\" } ] } #4. Search and paginateList endpoints accept filters and return an opaque cursor for the next page. Here we search patients by phone number.bashCopycurl -G https://api.smile-app.co.il/v1/patients \\ -H \"Authorization: Bearer sk_live_...\" \\ --data-urlencode \"phone=0521234567\" \\ --data-urlencode \"limit=25\" jsonCopy{ \"data\": [ { \"id\": \"8842\", \"first_name\": \"Dana\", \"last_name\": \"Levi\", \"phone\": \"0521234567\" } ], \"next_cursor\": null } When next_cursor is non-null, pass it back as ?cursor=... to fetch the next page. See Pagination for the full pattern.#What's next🔑AuthenticationKeys, scopes, and how requests are authorized.📚API referenceBrowse every resource and endpoint.📡WebhooksGet pushed events instead of polling."},{"path":"errors.html","title":"Errors","description":"The problem+json error format, status codes, and how to handle failures.","section":"Core concepts","headings":[{"id":"error-format","text":"Error format"},{"id":"status-codes","text":"Status codes"},{"id":"handling-errors","text":"Handling errors"}],"text":"ErrorsThe API uses conventional HTTP status codes and a single, predictable error body. Any non-2xx response carries a machine-readable problem document.#Error formatErrors are returned as RFC 9457 application/problem+json:jsonCopy{ \"type\": \"https://api.smile-app.co.il/problems/forbidden\", \"title\": \"Missing required scope\", \"status\": 403, \"detail\": \"This key needs the 'patients:read' scope.\" } FieldTypeDescriptiontypestring (URI)Stable identifier for the error category.titlestringShort, human-readable summary.statusintegerThe HTTP status code, repeated for convenience.detailstringContext specific to this occurrence (optional).Always branch on status (or type) rather than matching on title or detail, which are meant for humans and may be reworded.#Status codesStatusMeaningWhat to do400 Bad RequestInvalid parameters (bad cursor, malformed date, unknown filter).Fix the request; don't retry unchanged.401 UnauthorizedMissing, malformed, invalid, or revoked key.Check the Authorization header and the key.403 ForbiddenValid key without the required scope.Mint a key with the needed scope.404 Not FoundThe resource doesn't exist for this clinic.Verify the id; ids are clinic-scoped.429 Too Many RequestsRate limit exceeded.Back off and retry after the Retry-After window.500 Internal Server ErrorUnexpected server error.Retry with backoff; if it persists, contact support.502 / 503Upstream/backend temporarily unavailable.Retry with exponential backoff.#Handling errorsA small, robust client checks the status, parses the problem body, and only retries on transient failures.bashCopycurl -sS -w '\\n%{http_code}' https://api.smile-app.co.il/v1/patients/does-not-exist \\ -H \"Authorization: Bearer sk_live_...\" jsonCopy{ \"type\": \"https://api.smile-app.co.il/problems/not-found\", \"title\": \"Patient not found\", \"status\": 404, \"detail\": \"No patient with id 'does-not-exist'.\" } Retry only transient errorsRetry 429, 500, 502, and 503 with exponential backoff. Never auto-retry 400, 401, 403, or 404 - the request will keep failing until you change it."},{"path":"index.html","title":"SmileCloud Public API","description":"The REST, webhooks, and MCP API for building on top of your SmileCloud clinic data.","section":"Get started","headings":[{"id":"choose-your-surface","text":"Choose your surface"},{"id":"how-it-works","text":"How it works"},{"id":"base-url","text":"Base URL"},{"id":"next-steps","text":"Next steps"}],"text":"SmileCloud Public APIThe SmileCloud Public API is the single internet-facing surface for clinic integrations. It exposes three coordinated surfaces over one curated, versioned read contract - REST for scripts, webhooks for push, and MCP for AI agents - all authenticated with a per-clinic key.v1 is read-firstEvery endpoint reads clinic data - patients, appointments, availability, treatments, payments, and catalog - with three deliberate exceptions: managing your own webhook subscriptions, recording a payment (scope payments:write, granted explicitly), and capturing a lead (scope leads:write).#Choose your surface🔌REST APIPlain HTTPS + JSON for clinic IT and scripts. Cursor pagination, predictable errors, an OpenAPI spec.📡WebhooksSubscribe to appointment, patient, treatment, and payment events. Signed, retried, at-least-once delivery.🤖MCP for AIA Model Context Protocol server so AI agents can read clinic data over the same contract, with the same key.⌨️Command lineThe sc CLI wraps the REST surface for the terminal - for clinic IT, scripts, and agents.#How it worksAuthenticate every request with a per-clinic secret key as a bearer token. The key identifies your clinic and carries a set of scopes that gate which data it can read.bashCopycurl https://api.smile-app.co.il/v1/catalog/branches \\ -H \"Authorization: Bearer sk_live_...\" jsonCopy{ \"data\": [ { \"id\": \"1\", \"name\": \"Downtown Branch\", \"phone\": \"+972-3-555-0100\", \"color\": \"#4f46e5\" }, { \"id\": \"2\", \"name\": \"North Branch\", \"phone\": \"+972-4-555-0200\", \"color\": \"#16a34a\" } ] } The API holds no domain logic and no direct database access. Every read is a thin, authenticated forward to SmileCloud's backend, which returns a small, stable, versioned schema. That means the shapes you see here are intentionally compact and safe to depend on.#Base URLAll requests go to the production host over HTTPS:textCopyhttps://api.smile-app.co.il Endpoints are versioned under /v1. We will never make breaking changes to /v1 - new fields may be added, but existing fields will not be removed or repurposed.#Next steps🚀QuickstartMint a key and make your first authenticated call in under five minutes.🔑AuthenticationHow keys, scopes, and bearer auth work - and how to keep them safe.📚API referenceEvery endpoint, parameter, and response shape, resource by resource."},{"path":"rate-limits.html","title":"Rate limits","description":"How request rate limiting works and how to handle 429 responses gracefully.","section":"Core concepts","headings":[{"id":"how-limits-work","text":"How limits work"},{"id":"staying-under-the-limit","text":"Staying under the limit"},{"id":"handling-429-responses","text":"Handling 429 responses"}],"text":"Rate limitsTo keep the API fast and fair for every clinic, requests are rate limited per API key. Well-behaved clients rarely hit the limit; when they do, the response tells them exactly how long to wait.#How limits workLimits are applied per key using a rolling window. When you exceed the limit, the API responds with 429 Too Many Requests and a Retry-After header indicating how many seconds to wait before retrying.textCopyHTTP/1.1 429 Too Many Requests Retry-After: 2 Content-Type: application/problem+json jsonCopy{ \"type\": \"https://api.smile-app.co.il/problems/rate-limited\", \"title\": \"Rate limit exceeded\", \"status\": 429, \"detail\": \"Too many requests. Retry after 2 seconds.\" } #Staying under the limitPage wide, not deepUse the maximum limit=100 on list endpoints and the concise response format to fetch more per request. Cache catalog data (branches, providers, types) - it changes rarely.Batch and cache. Catalog and reference data is stable; fetch it once and reuse it.Avoid tight polling. Prefer webhooks over polling for changes - you'll get events pushed to you instead of hammering the API.Spread bulk work. When backfilling, add a small delay between pages rather than firing requests in parallel bursts.#Handling 429 responsesRespect Retry-After and use exponential backoff with jitter for repeated failures:bashCopyattempt=0 until resp=$(curl -fsS https://api.smile-app.co.il/v1/appointments \\ -H \"Authorization: Bearer sk_live_...\"); do attempt=$((attempt + 1)) [ \"$attempt\" -ge 5 ] && break sleep $((2 ** attempt)) done If you consistently need higher throughput for a legitimate integration, contact SmileCloud support to discuss your use case."},{"path":"pagination.html","title":"Pagination","description":"How cursor-based pagination and response formats work across list endpoints.","section":"Core concepts","headings":[{"id":"request-parameters","text":"Request parameters"},{"id":"response-shape","text":"Response shape"},{"id":"paging-through-every-result","text":"Paging through every result"},{"id":"response-formats","text":"Response formats"},{"id":"non-paginated-lists","text":"Non-paginated lists"}],"text":"PaginationList endpoints return results in pages using opaque cursors. Cursors are stable across inserts and deletes, so you never miss or double-count a record while paging.#Request parametersParameterTypeDefaultDescriptionlimitinteger25Items per page, 1–100.cursorstring-Opaque cursor from a previous response's next_cursor.#Response shapePaginated responses wrap results in a data array alongside a next_cursor:jsonCopy{ \"data\": [ { \"id\": \"8842\", \"first_name\": \"Dana\", \"last_name\": \"Levi\" }, { \"id\": \"8843\", \"first_name\": \"Yossi\", \"last_name\": \"Cohen\" } ], \"next_cursor\": \"eyJpZCI6Ijg4NDMifQ\" } When next_cursor is a string, there are more results - pass it back as ?cursor=....When next_cursor is null, you've reached the last page.Cursors are opaqueNever parse, construct, or store assumptions about a cursor's contents. Treat it as a black box: take the next_cursor you're given and send it back verbatim.#Paging through every resultbashCopycursor=\"\" while : ; do resp=$(curl -sG https://api.smile-app.co.il/v1/patients \\ -H \"Authorization: Bearer sk_live_...\" \\ --data-urlencode \"limit=100\" \\ --data-urlencode \"cursor=$cursor\") echo \"$resp\" | jq '.data[]' cursor=$(echo \"$resp\" | jq -r '.next_cursor // empty') [ -z \"$cursor\" ] && break done #Response formatsMany endpoints support a response_format query parameter to trade detail for payload size:ValueDescriptionconcise (default)Core identity and reference fields only. Smaller payloads, ideal for lists and agents.detailedAdds extra fields such as address, contacts, and balances where available.bashCopycurl -G https://api.smile-app.co.il/v1/patients/8842 \\ -H \"Authorization: Bearer sk_live_...\" \\ --data-urlencode \"response_format=detailed\" Keep payloads small for agentsFor AI agents and bulk scripts, prefer concise and a high limit. Fetch detailed only for the specific records you need to act on.#Non-paginated listsA few small reference endpoints (such as catalog lists and availability) return all results in a single data array with no cursor, since the result set is naturally bounded.jsonCopy{ \"data\": [ /* ... */ ] }"}]