Post leads from your own code
Point a website form or script at a capture endpoint URL so submissions become leads automatically.
By the end of this page your form will post straight into SmileLine — no backend code on your side.
If the tracking script is installed, your existing forms are detected and captured automatically — you only need the techniques on this page for a form SmileLine cannot see, such as one rendered inside a third-party product.
Before you start
You need a Custom endpoint form in Public browser form mode and its endpoint URL. Create one under Settings → Website and copy the complete regional URL from the form's page — for example, https://api-eu.smileline.io/capture/YOUR_TOKEN. US practices receive an api-us.smileline.io URL. See Custom integrations.
A public-form endpoint accepts application/json, application/x-www-form-urlencoded and multipart/form-data. URL query parameters are merged underneath the body, so pixel-style integrations can ride the query string alone. Files posted on a multipart form are kept: they are stored securely and recorded on the lead's capture record, so an X-ray or referral document sent with the enquiry arrives with it.
This browser guide doesn't apply to forms labelled Signed JSON. Those accept JSON only, reject query parameters, and require a backend to calculate HMAC headers. See Send signed JSON.
Point your form at the endpoint
A classic form post. The browser navigates to the endpoint's raw JSON response, so prefer the fetch variant to keep visitors on your page:
<form action="https://api-eu.smileline.io/capture/YOUR_TOKEN" method="POST">
<input name="first_name" placeholder="First name" />
<input name="last_name" placeholder="Last name" />
<input name="email" type="email" placeholder="Email" required />
<input name="phone" type="tel" placeholder="Mobile" />
<input name="treatment" type="hidden" value="invisalign" />
<textarea name="message" placeholder="How can we help?"></textarea>
<button type="submit">Request a consultation</button>
</form>Submit in the background and show your own thank-you message. The endpoint allows cross-origin requests, so this works from any website:
const form = document.querySelector("#enquiry-form");
form.addEventListener("submit", async (event) => {
event.preventDefault();
const response = await fetch(
"https://api-eu.smileline.io/capture/YOUR_TOKEN",
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
first_name: form.first_name.value,
last_name: form.last_name.value,
email: form.email.value,
phone: form.phone.value,
treatment: "invisalign",
message: form.message.value,
}),
},
);
if (response.ok) showThankYou();
});Test from the command line before wiring up the website:
curl -X POST "https://api-eu.smileline.io/capture/YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{"first_name":"Amelia","email":"amelia@example.co.uk","phone":"+447700900123"}'A successful capture answers 201 with {"ok":true} — and the lead appears on the Today page in real time.
Which fields are understood
Field names are set per form in its Field mapping — the left column below shows the defaults a new custom integration starts with. Rename them in the mapping to match whatever your form already sends.
| Default field name | Fills |
|---|---|
first_name | Patient first name |
last_name | Patient last name |
email | Patient email |
phone | Patient mobile number |
treatment | Matched against a treatment's slug (see Settings → Treatments); unknown values fall back to the form's Default treatment |
location | Matched against a location id, then a case-insensitive location name; unknown values fall back to the form's Location — one form can serve every site |
message | Saved as a note on the patient timeline |
Rules worth knowing:
- Nested payloads are mappable. A box also accepts a dot path such as
data.contact.emailoranswers.0.value, so a webhook that nests its fields needs no middleman to flatten them. Build the mapping from a real submission — see Field mapping. - A valid email or phone is required. A submission with neither (after mapping) is rejected with
400— and the rejected body is kept under the form's Deliveries so you can see exactly what arrived. - Bounded extra fields are retained. Fields that aren't mapped are kept verbatim on the lead's capture record and shown under Additional information on the lead panel. JSON and form posts are limited to 256 KiB; a multipart submission can be up to 10 MiB and carry up to 10 files, no single file larger than 10 MiB. The merged body and query snapshot allows up to 1,000 keys, 512-byte key names, 32 KiB string values, 200 items per array and ten levels of nesting.
- Bad values degrade, they don't fail. An unparseable email or phone is kept in the raw capture record while the other contact detail carries the lead.
- Duplicates attach, they don't multiply. If the email or phone matches an existing patient, the enquiry is attached to them instead of creating a duplicate.
Attribution is automatic
Do not map marketing parameters — they're extracted automatically into the lead's attribution record: utm_* parameters, ad-platform click ids (gclid, fbclid, msclkid, ttclid and many more), landing_url, referrer and user_agent. Just make sure your form forwards the page's query parameters (or posts them alongside the fields). See Attribution.
Rich integrations (e.g. Zapier or a custom tracking script) can also post a structured _sl envelope with visitor context and an event_id idempotency key. Re-posting the same event_id with the same content never creates a duplicate — the original result is returned again. Reusing an event_id with different content is rejected with 409, so a copy-paste mistake can't silently overwrite an earlier enquiry. If a submission was interrupted mid-processing, its event_id becomes usable for a retry after about 15 minutes. The full envelope shape is documented in the API reference under the Capture group at /reference.
Submissions survive outages
Once a submission passes the endpoint's basic checks, it cannot be lost to a problem on SmileLine's side. If an internal component is briefly unavailable when your form posts, the submission is still accepted with a 201 and stored safely, then processed automatically within a few minutes of service recovering — files included. A 201 always means the lead is safe; your website never needs to retry.
Responses
| Status | Meaning |
|---|---|
201 | Captured — body is {"ok":true} |
400 | Invalid body, or no valid email/phone after mapping |
404 | Unknown, paused or archived token |
409 | The _sl event_id was reused with different content, or the same submission is still being processed |
413 | Request body exceeds the intake limit — 256 KiB, or 10 MiB for multipart uploads |
429 | Rate limited — retry after the number of seconds in the Retry-After header |
A 404 from a URL that used to work usually means the form was paused,
archived, or its token was regenerated. Check Settings → Website.
Post leads from a connected integration
A capture endpoint URL is the whole credential, which is right for a browser and
wrong for a server that has already proved who it is. An integration holding an
OAuth connection — the
Zapier app, or your own — should post to POST /leads
instead. It is the same pipeline: same patient matching, same attribution, same
automation enrolment, same Deliveries record.
curl -X POST "$SMILELINE_API_URL/leads" \
-H "Authorization: Bearer sl_oat_EXAMPLE_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"firstName": "Amelia",
"lastName": "Hart",
"email": "amelia@example.co.uk",
"phoneMobile": "+447700900123",
"treatmentSlug": "invisalign",
"message": "Looking for a consultation",
"eventId": "crm-4711",
"landingUrl": "https://example.co.uk/invisalign",
"attribution": { "utm_source": "google", "gclid": "EXAMPLE" }
}'Differences from the browser endpoint:
- The token names the practice, so send no
X-Organization-IDand no capture token. treatmentSlugis required and is a slug, not an id — the value from Settings → Treatments. There is no hook default to fall back on here, and a lead without a treatment would create a patient but no enquiry.locationaccepts a location id or a case-insensitive location name.- Field names are fixed, in the camelCase spelling above. No field mapping applies.
- Attribution rides in one flat
attributionmap —utm_*,gclid,fbclidand friends, at most 40 keys — whilelandingUrl,captureUrlandreferrerare top-level fields. Attribution keys may not reuse a lead field name; a map containingmarketingConsent,emailor any other mapped field is refused, because those carry a declared meaning that a free-form marketing parameter must not be able to set. - An email address or a mobile number is required, as on every capture path.
- Bodies are capped at 128 KiB. Nothing this contract allows comes close.
- Creating a lead needs
writescope pluspatient:createandjourney:createon the acting user's role.
Leads posted this way arrive through a capture form named Zapier that
SmileLine provisions on first use, so they are inspectable under
Settings → Website like any other source. Don't archive it — and if you
switch it off there, this endpoint stops accepting leads too, with 409 and
code: "LEAD_INTAKE_DISABLED". Off means off for integrations as well as for
website forms.
Every request body is kept, whatever the outcome. A lead that succeeds, one refused for an unknown treatment, one that arrived while the database was unreachable — all of them retain the exact bytes you sent. Nothing you post is ever discarded because we could not process it.
Idempotency
eventId is your idempotency key:
- Re-sending the same
eventIdwith the same body returns the original result rather than opening a second enquiry. - Reusing an
eventIdwith a different body is refused with409andcode: "IDEMPOTENCY_KEY_REUSED". - Omit
eventIdand SmileLine keys the request on a digest of the body itself, so a blind retry after a lost response still de-duplicates. The key it used comes back in the response aseventId.
Responses
| Status | Meaning |
|---|---|
201 | Created. The body carries patientId, journeyId (never null), taskId, patientWasCreated, organizationId and eventId. |
202 | Stored durably but not processed yet; SmileLine replays it automatically. Treat it as a success and do not re-send. A treatment archived in the moment between validation and processing lands here rather than being refused. |
400 | Invalid body, or an unknown or archived treatmentSlug. |
401 | The access token is expired or no longer valid. Refresh it and retry. |
403 | The connection lacks write scope, or the acting user's role cannot create patients or enquiries. Unlike 401, retrying will not help. |
409 | IDEMPOTENCY_KEY_REUSED, LEAD_INTAKE_DISABLED, or the same lead is still being processed. |
413 | The body exceeded 128 KiB. |