Errors
Every error uses one JSON envelope, with a machine-readable code where it helps.
All errors return the same shape:
{
"error": "Human-readable message",
"code": "OPTIONAL_MACHINE_CODE",
"details": { "optional": "structured context" }
}error is always present. code and details appear when there is something structured to say.
Status codes
| Status | Meaning |
|---|---|
400 | The request body, query, or params failed validation. |
401 | Missing or invalid credentials. |
403 | Authenticated, but not allowed — no active organization, or your role lacks the required permission. |
404 | The resource doesn't exist, is archived, or belongs to another organization. |
409 | Conflict with current state — for example archiving a stage that still has active journeys, or sending an attachment that expired or was already used. |
410 | A retained capability or artifact is no longer available, such as an expired CSV export (EXPIRED). |
413 | The request body exceeded its byte ceiling before parsing, including public JSON and authenticated multipart uploads. |
429 | Rate limit or pending-upload quota hit. For request rate limits, see Rate limits; the Retry-After header says how long to wait. |
500 | Something broke on our side. The body is { "error": "Internal server error" } with no details. |
Operational error monitoring
Authenticated platform errors are reported to SmileLine's EU-hosted operational
monitoring so the team can diagnose failures. Server reports contain a
sanitised error class, route family, status and stack location; they do not
include request bodies, query strings, cookies, authorization headers or raw
error messages. Browser diagnostics and session replay travel through the
regional SmileLine /ingest proxy. Inputs and rendered text are masked, URL
queries and fragments are removed, record identifiers in paths are redacted,
and request or response bodies are not recorded.
Validation errors
Failed validation returns 400 with code: "VALIDATION_ERROR" and the individual issues in details:
{
"error": "Validation failed",
"code": "VALIDATION_ERROR",
"details": [
{
"path": ["email"],
"code": "invalid_format",
"message": "Invalid email address"
}
]
}Each issue has a path into the offending field, a machine code, and a message. Fix the listed fields and retry — validation errors are never transient.
Conversation attachment uploads return a private, one-time upload ID rather
than a storage key. Sending an expired, already-used, foreign or duplicate ID
returns 409 with UPLOAD_UNAVAILABLE. Abandoned uploads expire after one
hour. Exceeding the pending-upload quota returns 429 with
UPLOAD_QUOTA_REACHED.
Patient-bearing message metadata has a 1 MiB ceiling. An authenticated send
over that boundary returns 413 with
MESSAGE_CONTENT_METADATA_TOO_LARGE. Provider-originated inbound mail is
bounded and safely omits oversized content metadata instead of asking the
provider to retry an impossible row. Retrying a failed message whose content
has already been removed by retention returns 409 with
MESSAGE_CONTENT_REDACTED.
Some failed messages cannot be retried because sending them now would be
wrong rather than merely late. A reminder whose appointment has already
started returns 409 with REMINDER_APPOINTMENT_ELAPSED_RETRY_UNSAFE. An
ordinary failed message is never refused for its age or for how many times it
has been retried: each retry starts a fresh delivery attempt with its own
48-hour horizon.
An interrupted reminder whose PMS source fact needs review returns
REMINDER_SOURCE_DISPUTED_RETRY_UNSAFE; resolve any pending appointment
correction under Settings → Integrations rather than retrying its frozen
body. The reminder automation decides whether the resolved appointment is
still relevant and safe to re-arm.
REMINDER_PMS_BACKFILL_RETRY_UNSAFE means a historical PMS import resolved the
source dispute without sending that frozen occurrence. Wait for a later live
appointment change to create current reminder content; do not retry the old
message.
DELIVERY_UNKNOWN_RETRY_UNSAFE means the provider may already have accepted
the exact message. A signed callback may still confirm its outcome, but a
manual resend could duplicate it.
MESSAGE_RETRY_STATE_INVALID means the message's stored delivery record is
not readable as a delivery attempt. It is not a limit — send a new message and
report it, because a valid record can always be retried.
Live booking availability returns 503 with PMS_AVAILABILITY_RETRY when the
practice's PMS diary could not be read in full. SmileLine returns this rather
than a partial list of times, so a bookable appointment is never hidden behind
an answer that looks complete; retry the request.
Creating or re-enabling a reminder rule returns
REMINDER_RULE_LIMIT_REACHED when the practice already has 20 active reminder
rules. Switch off another rule, or save the new rule switched off.
PMS appointment-correction commands return
PMS_CORRECTION_ALREADY_RESOLVED when the requested decision conflicts with a
decision already recorded. Reload the correction list before acting again.
PMS_CORRECTION_REMINDER_LIMIT means the practice has more reminder effects
than one correction command is allowed to release; reduce or disable reminder
rules before retrying. The ordinary VERSION_CONFLICT response means the
appointment changed after the review sheet loaded and must be reviewed again.
Practice settings return AUTOMATION_LOCATION_UNAVAILABLE when a selected ID
is missing, archived or belongs to another practice. Clearing the selection is
allowed and pauses business-hours waits. A PMS booking that has neither a
patient location nor a configured automation location returns
AUTOMATION_LOCATION_REQUIRED. Archiving the selected automation location
returns AUTOMATION_LOCATION_IN_USE.
A deposit payment-page timeout returns CHECKOUT_RECONCILIATION_PENDING rather
than minting another payable Stripe session. Retrying is safe and keeps the same
provider idempotency generation until Stripe positively reports expiry.
Stripe Connect returns STRIPE_CONNECT_SWITCH_PENDING when a different account
is already awaiting confirmation, and STRIPE_CONNECT_SWITCH_SUPERSEDED when a
confirm or cancel action lost its exact candidate fence. Reload Settings →
Payments & deposits before acting again. OAuth state failures are deliberately
generic and cannot be retried; start a new authorization from the settings page.
Optimistic updates return 409 with the single code VERSION_CONFLICT. Reload
the latest resource, reapply the intended change, and send its new version.
Subscription Checkout returns CHECKOUT_SESSION_EXPIRED when the durable
command points to an expired Stripe session. The browser then starts a new
command on the next attempt. CHECKOUT_ALREADY_COMPLETED means the original
session completed and the exact command, customer and subscription projection
were durably correlated; it must not be recreated.
BILLING_CHECKOUT_RECONCILIATION_PENDING means Stripe reports that the session
completed but a newer billing generation won while SmileLine was proving that
correlation. Retry the same command while its fixed one-hour ambiguity window
remains open. SmileLine does not create another Checkout; unresolved state moves
to manual review when the window ends.
Webmaster email sends to an address that has never been confirmed are not
errors: they return 202 with status: "awaiting_confirmation", no delivery,
confirmationState set to sent or cooldown, and the next retryAt. Nothing
is rendered or queued until the address confirms.
SEO_WEBMASTER_CONFIRMATION_SEND_FAILED is 503 when the provider does not
accept a confirmation request. The reserved attempt is still consumed and the
Retry-After header gives the remaining cooling-off period.
SEO_WEBMASTER_CONFIRMATION_EXHAUSTED is 409 after all three lifetime
confirmation attempts have been used. Contacts cannot be deleted or reset.
SEO_WEBMASTER_CONTACT_SUPPRESSED is 409 for a generic permanent recipient or
mail-server block. The API does not disclose which provider signal caused it.
SEO_WEBMASTER_DAILY_CAP_REACHED and SEO_WEBMASTER_NEW_CONTACT_CAP_REACHED
are 429 and reset at midnight UTC.
SEO_WEBMASTER_CONTACT_LIMIT_REACHED is 409 when the practice already holds
the maximum of 50 permanent webmaster contacts; unconfirmed and blocked contacts
still count.
A rendered delivery over its per-kind ceiling — 1 MiB for content, 64 KiB for a
developer brief — returns 413 with SEO_DELIVERY_PAYLOAD_TOO_LARGE. Reusing a
clientRef for a materially different delivery returns 409 with
IDEMPOTENCY_KEY_REUSED; an exact repeat of the same delivery is idempotent and
returns the original without consuming another daily delivery.
404 instead of 403 on a specific resource is intentional: the API does not reveal whether an ID exists in another organization.