Smileline

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 keysAuthorization: Bearer <key>. Use these for your own integrations, scripts, and the playground on these pages.
  • OAuth access tokensAuthorization: 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.

RequestNeeds
GET, HEAD, OPTIONSread
Every other methodwrite

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.

There is a third answer, and it applies to every credential — API key, OAuth token and session alike. 503 with code: "AUTH_UNAVAILABLE" and a Retry-After header means SmileLine could not verify the credential at all because its database was unavailable. Nothing is stale and nothing is forbidden: wait the stated seconds and retry the same request. Do not refresh, re-authenticate or treat the token as revoked — a client that reacts to this 503 as if it were a 401 throws away a good token during an outage.

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/revoke with the token.
  • The practice disconnects the app — DELETE /connected-apps/{id}, having found it with GET /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.

Installed apps negotiate a minimum version here: the mobile app sends X-Smileline-Client: mobile/<version> on every request, and /auth/me answers 426 Upgrade Required (with minimumVersion in the body) to a build older than the floor the API still supports, so an old build never receives a role or feature it cannot represent. Web and Desk ship with the API and send no client header.

{
	"user": { "id": "…", "email": "…", "name": "…" },
	"session": { "activeOrganizationId": "…" },
	"organization": {
		"id": "…",
		"name": "…",
		"memberRole": "owner",
		"workspaces": ["crm", "pms"],
		"defaultWorkspace": "crm",
		"pmsAccess": {
			"pmsWorkspaceAvailable": true,
			"nativeWritesEnabled": false,
			"systemOfRecord": "external",
			"authorityGeneration": 1
		},
		"accessGeneration": 3
	}
}

The organization object carries the member's access facts:

  • workspaces — the workspaces the acting member's role opens in this practice: crm (the front-desk product) and pms (the clinical workspace). It is derived on every call, never stored, so a role change shows up immediately. An API key or OAuth token still cannot use the clinical routes, whatever this lists — see below.
  • defaultWorkspace — where the member lands after sign-in. PUT /settings/team/members/{userId}/workspace changes it: a member may set their own, and member:update is needed for anyone else's. A workspace the member's role does not open answers 409 with code: "WORKSPACE_UNAVAILABLE".
  • pmsAccess — whether the practice's clinical workspace is available and whether SmileLine is its system of record.
  • accessGeneration — a counter that moves whenever this member's access changes: their role or default workspace, or a practice-wide entitlement or system-of-record change. API keys and OAuth tokens read 0.

Every organization-scoped response to a signed-in person also carries the X-Smileline-Access-Generation header, whose value is the practice id and the counter joined with a colon, for example 019be2f0-…:3 (machine credentials, which have no member row, get no header). Treat it as an opaque string: a client that caches /auth/me refreshes /auth/me whenever the header differs from the value it last saw, which also covers a request that resolved to a different practice than the cached one.

Roles and permissions

Every member of an organization has one role: owner, manager, front_desk, telesales, analyst, dentist, hygienist, or nurse. 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.

Roles also decide the workspaces a member opens, which GET /auth/me reports in workspaces. owner, manager and front_desk open the CRM and, once the practice's clinical workspace is available, the clinical workspace too; telesales and analyst open the CRM only; the three clinical roles — dentist, hygienist and nurse — open the clinical workspace only and hold no permission on any CRM-only resource.

The clinical routes — GET /day-list, POST /book, GET /patient-record/{id} and GET /pms-inbox/conversations — accept a signed-in member's session only. An API key or OAuth token gets 403 with code: "transport_not_allowed" whatever the acting user's role, and any credential gets 403 with code: "PMS_WORKSPACE_UNAVAILABLE" while the practice has no clinical workspace. A practitioner profile is tied to a member's account through POST /settings/practitioners/{id}/user-link, never through the practitioner lookup itself.

Endpoints not listed in the sidebar

The generated reference covers the CRM API. A few surfaces live outside it:

SurfaceWhat 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. GET /auth/invitation-preview?id= is the one read that needs no session: it tells the holder of an invitation link the invited email, practice, inviter, role and whether the email already has an account, and answers 404 with code INVITATION_NOT_FOUND for anything else.
/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.
POST /user/reauthenticateThe fresh re-authentication challenge behind sensitive clinical acts (signing a note, voiding a sealed row): proves the password and returns a short-lived act-evidence token bound to the session.
GET /user/me, /user/avatar, GET /user/sessionsThe 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/logoYour 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/wsWebSocket 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:

SurfaceAuthorization boundary
GET /health/runtimeStorage-free Worker liveness. It returns 204 and no practice data.
GET /healthCoarse-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/webhookA 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:

RequestBody and result
POST /user/two-factor/setupSend 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/verifySend the six-digit code from the new authenticator. Success activates it atomically and returns the new backup codes.
DELETE /user/two-factor/setupCancels only the pending setup. An existing active authenticator remains active.
POST /user/two-factor/disableSend password and currentCode. currentCode must be the current six-digit authenticator code or one unused backup code.
POST /user/two-factor/backup-codesSend password to replace every existing backup code.

Fresh re-authentication for clinical acts

Some clinical acts are "fresh" acts: they need a credential challenge completed within the last five minutes (signing a clinical note, signing a prescription, voiding a sealed procedure). The browser and mobile apps ask for the password again and call POST /user/reauthenticate with { "password": "…" }. The response is { "token": "…", "expiresAt": "…" }: an HMAC-signed proof bound to the signed-in user and session, valid for five minutes. The act request sends it as the X-Smileline-Act-Evidence header and the act row stamps the challenge time as reauthenticated_at. A missing, expired, tampered or borrowed token answers 403 with code FRESH_AUTHENTICATION_REQUIRED. The challenge shares the two-factor throttle above, and a browser session is required — a machine credential can never mint act evidence.

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 — or 503 with Retry-After while credentials cannot be verified, as described under Scopes cap the acting user.

A public booking, referral or chat-widget write that arrives during an outage answers 503 with Retry-After only after the submission has been stored. Chat-widget messages, referrals and booking completions are recovered automatically; a booking hold made during an outage is kept for staff review instead of being re-created later, because the slot may have gone. Retrying with the same idempotency key is safe, and not retrying loses nothing. A form capture (POST /capture/{token}) that is stored during an outage answers 201; it answers 503, also with Retry-After, only when it could not be stored at all or a multipart post's files could not be — resend it with the same _sl event_id.

Public booking, widget and referral JSON writes have a 256 KiB request ceiling that is enforced before JSON parsing. Oversized requests return 413 without reaching their route handler.

On this page