Send leads to Moonshot EHR
Post each form submission from your site or CRM to the clinic. The lead lands on the clinic's Leads board with its source, campaign, notes and the service the visitor asked about. With the clinic's secret API key you can also read whether the lead has booked.
The request
POST https://api.moonshotehr.com/api/leads/webhook
X-Tenant-Slug: <clinic>
X-Webhook-Key: <key the clinic gives you>
Content-Type: application/json
- Body: flat JSON.
emailis required. Name (first_name,last_name),phoneandsourceare optional. - Attribution keys:
utm_source,utm_medium,utm_campaign,utm_content,utm_term,gclid,fbclid,msclkid,landing_page,last_page,referrer. Send them inside anattributionobject or as flat top-level keys (many CRMs can only send flat pairs). Other keys are dropped; each value is cut at 256 characters. - Rate limit, per clinic and per sending IP address: the first 10 requests in a minute are accepted at once; after that one request every 30 seconds, then one every 2 minutes, is accepted and the rest get
429withRetry-After. Space a backlog about 6 seconds apart. - Keep the key on your server and treat it like a password. The clinic can rotate it; the old key stops working on the next request.
The response says whether the note landed:
{ "ok": true, "lead_id": "…", "existing_patient": false, "note_saved": true, "note_status": "saved" }
existing_patient is true when the lead matched someone already in the clinic's records and false for a new person. Sending the same lead again within 24 hours does not create a second lead; it answers 200 with "deduplicated": true.
Key names
Keys are matched after lower-casing and turning every run of spaces or punctuation into one underscore, so the labels a CRM sends work as-is: UTM Source, utm-source and utm_source are the same key; Appointment Type is appointment_type; E-mail is email. Fields nested under customData, custom_fields, contact, data, fields or form are read as if they were top level (a top-level key wins on a clash).
| What the form asks | Send as (any of) | Where it shows |
|---|---|---|
email, email_address | contact | |
| Phone | phone, phone_number, mobile | contact |
| Name | name, full_name, or first_name + last_name | contact |
| Source | source, contact_source, lead_source | source badge |
| Event / webinar sign-up | tags (text or list), contact_tags, utm_campaign, form_name, campaign_name, event_name, workflow_name | source badge Event / Webinar (see below) |
| Campaign | utm_source, utm_medium, utm_campaign, utm_content, utm_term, gclid, fbclid, msclkid, landing_page, last_page, referrer | Attribution |
| Appointment type / service | appointment_type, service_interest, service, interest | board card + From the form |
| Patient status (new / existing) | patient_status, patient_type, new_or_existing | From the form |
| Referred by (a person) | referred_by, referral_source, who_referred_you, how_did_you_hear_about_us | From the form |
| Message / health history | see Notes below | timeline, "From the form" |
| Anything else | any other key | From the form, labelled by the key |
Anything else: every other text field you choose to send is kept on the lead and shown in the clinic's lead drawer under its key name (best_time_to_call shows as "Best time to call"). One line each, up to 500 characters, at most 25 such fields per lead; CRM plumbing (contact_id, location_id, workflow_id, tags, dates) is skipped. Adding a question to the form needs no change on the Moonshot side.
The response carries a receipt of what landed:
"captured": {
"source": "meta",
"attribution": ["utm_source", "utm_campaign", "landing_page"],
"service_interest": "TRT consult",
"patient_status": "New patient",
"referral_source": "Dr. Smith",
"note": "saved",
"form_fields": ["best_time_to_call"],
"ignored_keys": []
}
A field missing from captured did not land: check its key name against the table. ignored_keys lists fields past the 25-field cap.
Event and webinar sign-ups
A sign-up for an event or webinar that runs through a Facebook, Google or TikTok funnel is filed under Event or Webinar, not under the ad platform, when any of the fields in the table contains webinar (Webinar) or event, seminar or workshop (Event), or is exactly zoom / online event (Webinar) or open house / in person (Event).
- Tags are read one by one; a comma-separated string works.
- The ad platform is kept on the lead ("Came through: Meta"), so ad reporting is not lost. A scored quiz, a referral and the website chat keep their source.
- In HighLevel, send the contact's tags with the merge field
{{contact.tags}}. A value that arrives as the literal textcontact.tagsmeans the braces are missing and nothing was sent. - The receipt says what was used:
"source": "webinar", "event_signal": "tags", "source_platform": "meta".
Notes from the form
Whatever the visitor typed into the form's free-text box is kept on the lead as a note labelled From the form. Send it as ONE of these keys; the first non-empty one in this order wins: notes (preferred), comments, message, health_history, additional_info.
- Plain text. HTML tags are removed; line breaks are kept.
- Up to 4,000 characters; longer text is cut and ends with
…. - A re-sent lead within 24 hours attaches the note to the existing lead, and the same text is never stored twice. A note that fails to save never fails the lead.
note_statusissaved,duplicateorfailed; it is absent when no notes key was sent.
Service interest
The service the visitor asked about ("Weight loss consult"). Send it as ONE of: service_interest (preferred), service, interest.
- A single line of plain text, up to 200 characters (longer text is cut and ends with
…). Only a text value is read. - Shown on the clinic's Leads board next to the source, and sent back out as
service_interestonlead.converted. - Optional. A re-delivery within 24 hours fills it in when the lead has none yet; a stored value is never overwritten.
When the lead becomes a patient
The clinic can have Moonshot EHR tell your system when its front desk converts a lead to a patient, so your CRM can start its new-patient follow-up. It is sent once, when staff click Convert to patient. A lead that already matched a patient when it arrived does not fire it.
{
"event": "lead.converted",
"tenant_slug": "northside",
"lead_id": "8d1c…",
"patient_id": "f47a…",
"email": "dana@example.com",
"phone": "6125550142",
"first_name": "Dana",
"last_name": "Prospect",
"source": "event",
"attribution": { "utm_source": "facebook", "utm_campaign": "oct-event" },
"service_interest": "TRT consult",
"converted_at": "2026-10-02T15:04:05.000Z"
}
Signature, headers and delivery rules: Receive signed events.
With the clinic's secret API key
For the clinic's own integration, from a server: create the lead, correct its contact details, and read whether the person has booked, finished onboarding or become a member. It uses the clinic's secret API key (see Secret API keys) instead of the lead key above, and lands on the Leads board exactly like a form submission: same source mapping, same 24 hour duplicate rule, same note from the form.
| Route | Permission | Per key, per minute |
|---|---|---|
POST /api/v1/leads | leads:write | 60 |
PATCH /api/v1/leads/{id} | leads:write | 60 |
GET /api/v1/leads/{id}/status | leads:read | 120 |
curl -s https://api.moonshotehr.com/api/v1/leads \
-H 'Authorization: Bearer <key>' \
-H 'Content-Type: application/json' \
-H 'Idempotency-Key: form-submission-81723' \
-d '{ "email": "dana@example.com", "first_name": "Dana", "last_name": "Prospect", "phone": "612-555-0142",
"source": "website", "service_interest": "Weight loss consult", "notes": "Mornings work best.",
"attribution": { "utm_source": "google", "utm_campaign": "fall" } }'
{ "data": { "id": "8d1c2b3a-4e5f-4a6b-9c7d-0e1f2a3b4c5d", "status": "new", "created_at": "2026-11-16T15:04:05.000Z", "duplicate": false, "note_saved": true }, "next_cursor": null }
- Fields:
email(required),first_name,last_name(orname),phone(10 to 15 digits),source,service_interest(up to 200 characters),notes(up to 4,000) and anattributionobject with the attribution keys listed above (strings up to 256). Any other field answers400 invalid_requestand names it; nothing is silently dropped. - A lead with that email address (capitals ignored) from the last 24 hours, sent through this API or the website form, is the same lead: the answer is
200with"duplicate": trueand its id, and no second lead is created. note_savedistruewhen thenotesyou sent are on the lead, andfalsewhen none were sent or they could not be saved. The lead is created either way.Idempotency-Key(optional, create only; it is ignored onPATCH): sending the same key and body again within 24 hours returns the first answer and changes nothing. The same key with a different body, or while the first request is still running, answers409 idempotency_conflict.PATCH /api/v1/leads/{id}correctsfirst_name,last_name,email,phoneand attribution keys only. The lead's status, owner, notes and the rest of what the clinic's staff work with cannot be changed here; sending one answers400naming it.- Correcting a lead and reading its progress work only for leads this API created; a duplicate of a lead that came in another way answers without
statusand stays unreadable. Any other id, including a lead the clinic's staff or website form entered, answers404, the same as an id that does not exist. Responses never contain an email address or a phone number. - Until the clinic's business associate agreement with Moonshot EHR is in place, all three answer
403 phi_access_disabled.
Progress, with the same definitions as the clinic's own conversion report: booked (an appointment booked after the lead came in, even if later cancelled or missed), onboarding_completed (forms completed, none outstanding), member (a membership started after the lead came in). No names, contact details, clinical data or amounts.
{ "data": { "id": "8d1c2b3a-4e5f-4a6b-9c7d-0e1f2a3b4c5d", "booked": true, "onboarding_completed": false, "member": false, "updated_at": "2026-11-18T14:00:00.000Z" }, "next_cursor": null }