Authentication
Authenticate with an API key or an OAuth connection over Bearer auth, or ride the browser session. Roles decide what you can write.
The API accepts three credentials:
- API keys —
Authorization: Bearer <key>. Use these for your own integrations, scripts, and the playground on these pages. - OAuth access tokens —
Authorization: Bearer sl_oat_…. Use these when an app connects on a practice's behalf and must be revocable by that practice. - Session cookies — set by the SmileLine app after sign-in. The app uses these; your integration should not.
API keys
API keys are managed through the auth endpoints under /auth/api-key/* while signed in. A key acts as the user who created it: same organizations, same role, same permissions.
curl "$SMILELINE_API_URL/patients?limit=10" \
-H "Authorization: Bearer $SMILELINE_API_KEY" \
-H "X-Organization-ID: $SMILELINE_ORGANIZATION_ID"Set SMILELINE_API_URL to the active organization's EU or US base URL from
the API overview.
Send X-Organization-ID when the key owner belongs to more than one practice.
Without the header, the API auto-selects an organization only when exactly one
current membership exists; an ambiguous organization-scoped request returns
403.
API keys use an explicit limit of 300 requests per minute.
Treat API keys like passwords. Don't commit them, and revoke keys you no longer use.
OAuth connections
SmileLine is its own OAuth 2.0 authorization server. An app that acts on a practice's behalf — the Zapier app is the first — holds an OAuth connection instead of a member's API key, so the practice can see it and cut it off without disturbing anything else.
The endpoints live under /auth/oauth2/* on the practice's regional base URL: authorize, consent, continue, token, revoke and introspect. Only the authorization code grant (with PKCE, which is mandatory) and the refresh grant are supported. There is no client credentials grant, and clients are registered by SmileLine — register, delete-consent and update-consent answer 404.
Tokens
Access tokens are opaque and prefixed sl_oat_; refresh tokens are prefixed sl_ort_. Send the access token exactly as you would an API key:
curl "$SMILELINE_API_URL/patients?limit=10" \
-H "Authorization: Bearer sl_oat_EXAMPLE_TOKEN"An access token lasts one hour. A refresh token lasts 30 days and rotates on every use, so store the replacement each time. Ask for the offline_access scope if you need to refresh at all.
Alongside the standard fields, a token response carries api_base_url — the cell your requests must go to. The authorization code exchange also returns organization_id and organization_name. The refresh grant does not repeat them, so persist them from the first exchange.
One connection, one practice
A connection is bound to exactly one practice when the user consents, and that binding cannot change. Do not send X-Organization-ID — unlike an API key, an OAuth token already names its practice, and the header is ignored on this credential. Connect once per practice.
Only practice owners and managers can authorise a connection. Authorising is also how the practice picks its region: a token minted by one cell is meaningless to the other.
Scopes cap the acting user
The only scopes are read, write and offline_access. This is deliberately not an identity layer: there is no openid, profile or email scope, no id_token, and no user profile to fetch. Use GET /auth/me to check a connection.
A token acts as the person who authorised it, intersected with its scopes. Scopes can only narrow what that person's role already allows — they never widen it.
| Request | Needs |
|---|---|
GET, HEAD, OPTIONS | read |
| Every other method | write |
A request outside its token's scopes returns 403 with code: "INSUFFICIENT_SCOPE". A request the acting user's role forbids returns the usual 403 permission error. Membership and role are re-read on every request, so demoting or removing that member changes what the connection can do immediately.
401 and 403 mean different things here and it matters which one you act on. 401 says the credential is stale — refresh it and retry. 403 says this connection is not allowed to do this, permanently as far as the request is concerned; refreshing will not help, and something has to change on the practice's side. An expired access token is always a 401.
A connection acts on practice data, never on the account behind it. /user, /organization, /agreements, /support and the platform admin area answer 403 with code: "BROWSER_SESSION_REQUIRED" for any machine credential, OAuth connections and API keys alike — a connection is consented to one practice, and those surfaces span all of them.
OAuth connections are limited to 300 requests per minute; see Rate limits.
Revoking
A connection can be ended two ways:
- The client calls
POST /auth/oauth2/revokewith the token. - The practice disconnects the app —
DELETE /connected-apps/{id}, having found it withGET /connected-apps.
Disconnecting takes effect on the connection's very next request rather than at token expiry: consent is verified on every request, so tokens a concurrent refresh had just minted die with it. It also archives the webhook endpoints that connection created — which stops delivery, terminalizes anything still in flight, and releases their share of the practice's 20-endpoint budget. Any member may list connected apps; removing someone else's connection additionally requires settings:update, so owners and managers can clear up after a colleague.
DELETE /connected-apps/{id} is the only way to disconnect. The provider's own consent-deletion endpoints are not exposed and answer 404: they would remove the credential while leaving the connection's webhook endpoints delivering, and leave nothing behind to find them by.
Who am I?
GET /auth/me returns the authenticated user, the active organization, and your member role — useful as a connectivity check and to confirm which organization your requests will hit.
{
"user": { "id": "…", "email": "…", "name": "…" },
"session": { "activeOrganizationId": "…" },
"organization": { "id": "…", "name": "…", "memberRole": "owner" }
}Roles and permissions
Every member of an organization has one role: owner, manager, coordinator, telesales, or analyst. Each route checks its own resource permission—for example creating patients requires patient:create, while configuring native lead providers requires ads:manage. A request without the needed permission returns 403 with the standard error envelope.
Endpoints not listed in the sidebar
The generated reference covers the CRM API. A few surfaces live outside it:
| Surface | What it does |
|---|---|
/auth/* | Sign-in/up/out, email verification, password reset, two-factor, API keys, and organization management (create, invite, switch active org) — powered by better-auth. |
/auth/oauth2/* | The OAuth 2.0 authorization server described above: authorize, consent, continue, token, revoke and introspect. token, revoke and introspect authenticate a registered client rather than a session; register, delete-consent and update-consent are disabled and answer 404. |
/user/two-factor/* | Safe, authenticated authenticator setup, replacement, cancellation, disable and backup-code regeneration. |
GET /user/me, /user/avatar, GET /user/sessions | The authenticated user's profile, avatar upload/stream/remove, and active session list. Avatar uploads accept JPEG, PNG, WebP or GIF up to 5 MB. |
GET /organization/mine, /members, /active, /organization/logo | Your organizations and role, the active org's members, and org logo management. Logo uploads accept JPEG, PNG or WebP up to 5 MB. |
GET /today/ws | WebSocket upgrade for live updates (used by the app; cookie-authenticated). |
Public endpoints
Public routes do not accept a session or API key. Each route has its own narrow authorization boundary:
| Surface | Authorization boundary |
|---|---|
GET /health/runtime | Storage-free Worker liveness. It returns 204 and no practice data. |
GET /health | Coarse-limited app/platform database and production-configuration readiness. It returns no practice data. |
POST /capture/{token} | An unguessable practice routing token plus request and rate limits. Endpoints in signed mode additionally require HMAC headers; see Custom integrations. |
/site/*, /form-page/{token} | The public site key or hosted-form token used by the tracking script and hosted form pages, with coarse and per-site rate limits. They serve per-practice form configuration and accept form-discovery reports; no patient data is readable. |
/l/* | A signed, expiring patient-link token for deposit pages. |
/widget/* | A practice embed key and visitor identity, with per-IP and per-connection rate limits. |
/booking/* | A public booking-page token or opaque appointment-management UID, with rate limits. |
POST /marketing/unsubscribe/{token} | A signed marketing opt-out token. |
/r/* | A Reputation request token sent in a bounded JSON body, with neutral non-enumerating state and per-IP rate limits. |
/referral/* | An opaque advocate share, patient, or status token, with rate limits. |
/webhooks/* | Provider signatures, callback credentials, or signed single-use OAuth state as appropriate; see Webhooks. |
POST /auth/stripe/webhook | A Stripe-signed billing callback recorded in the durable event-receipt ledger before projection. |
The stock two-factor management endpoints are blocked. Authenticator setup and
replacement use the SmileLine safe lifecycle: confirm the account password,
prove the current authenticator when replacing one, prove the new authenticator,
then atomically switch the factor and backup codes. An abandoned or failed
replacement leaves the current authenticator active. Setup expires after ten
minutes, and five incorrect codes cancel that pending setup. Five incorrect
current-authenticator codes pause replacement verification for 15 minutes.
Sensitive two-factor requests are also throttled by both user and IP address;
rate-limited responses include Retry-After. Password confirmation is checked
against the current credential in the same database transaction as the protected
change, so a password changed concurrently cannot leave an old proof valid.
The custom management requests use these JSON bodies:
| Request | Body and result |
|---|---|
POST /user/two-factor/setup | Send password. When replacing an active authenticator, also send its six-digit currentCode. The response contains the pending authenticator URI and its expiry. |
POST /user/two-factor/verify | Send the six-digit code from the new authenticator. Success activates it atomically and returns the new backup codes. |
DELETE /user/two-factor/setup | Cancels only the pending setup. An existing active authenticator remains active. |
POST /user/two-factor/disable | Send password and currentCode. currentCode must be the current six-digit authenticator code or one unused backup code. |
POST /user/two-factor/backup-codes | Send password to replace every existing backup code. |
During password sign-in, a twoFactorRedirect response starts a temporary
verification transaction rather than a normal session. Complete it with an
authenticator code at POST /auth/two-factor/verify-totp, or use one unused
backup code at POST /auth/two-factor/verify-backup-code. The browser app keeps
that temporary cookie through the verification page and creates the normal
session only after the second factor succeeds. Five failed checks exhaust that
temporary verification request. Failures are also counted against the account;
ten consecutive failures lock second-factor verification for 15 minutes.
The generated endpoint reference marks public CRM contracts such as booking, widget, referral, unsubscribe, and Reputation routes. Routes outside these explicit exceptions return 401 without valid credentials.
Public booking, widget and referral JSON writes have a 64 KiB request ceiling that is enforced before JSON parsing. Oversized requests return 413 without reaching their route handler.