Website Lead Intake API
Send leads from your website forms and lead providers into your portal with your company posting key.
This guide is for your company's web developer. When a visitor submits a quote form on your website, your server can POST that submission to your company's posting URL, and it appears straight away as a new lead in your portal, with its lead source recorded. Lead providers, Zapier and Make use the same URL and key.
Getting your posting key
Open Settings → Integrations → Lead Intake. The Company posting URL card shows your exact URL, whether intake is Active or Not configured, and when the credentials last changed.
To get the key to your developer, a company admin fills in Send posting instructions: who it is for under Instructions for (for example "Website developer") and their Email address, then clicks Send posting instructions. The email carries the URL, the key and the field list. It is sent from your own connected work email or a company sender, so connect one in Email Integrations first. Anyone else sees the page read-only.
Your company has one posting key, shared by every source. The key is never shown in the portal. Re-sending the instructions sends the same key; only a platform administrator can replace it. The first time instructions are sent, any older provider-specific keys stop working.
Keep the key on your web server only. Never place it in browser JavaScript, page HTML, a public form, or a URL. Anyone who can read your page source can read anything embedded in it, and a leaked key lets strangers inject leads into your portal until it is replaced.
Endpoint
- Method:
POST - URL:
https://movingstorageapp.com/api/leads/intake/your-company-slug
Copy the exact URL from Settings → Integrations → Lead Intake; the final path segment is your company's portal slug. Authenticate with an Authorization: Bearer header carrying the key (keys start with msa_intake_). A key in the query string is rejected. GET requests return 405, and the old generic /api/leads/intake URL without a company slug returns 410.
The body may be application/json or application/x-www-form-urlencoded, up to 64 KB.
Request
Send these on every lead:
source(string) — your own campaign or form name, for exampleWebsite Quote Form. Saved as the lead source in the CRM.label,campaignandpostingare accepted for the same value.phone(string) and/oremail(string) — so your team can reach the customer.
Optional fields (all strings unless noted):
first_name,last_name, orfull_nameinsteadorigin_city,origin_state,origin_zipdestination_city,destination_state,destination_zip(a missing city or state is filled in from the ZIP code)move_date— a date such asYYYY-MM-DDorMM/DD/YYYY; a date that can't be read is ignoredmove_size— for example2 bedroomscubic_feet(string or number)notes— free-text comments from the customersales_agent— the rep to give the lead to (see below)
The portal never refuses a lead for a missing or unusual value. A lead with no source is filed under your company's posting connection, and a missing phone and email, or a phone, email or postal code that doesn't look right, is stored as sent with a note in the lead's history so staff can fix it. A lead never creates an estimate or picks the service type; your sales team does that when they price the move.
Sending a lead to a rep
Put the rep in sales_agent (or agent, sales_rep, salesperson, assigned_to or rep). The value can be their name, company email, Microsoft email, CTM user ID or portal user ID. Underscores, dots and capital letters are ignored, so Jane_Smith and jane smith both reach Jane Smith. A value that matches no active sales rep, or matches two people with the same name, leaves the lead unassigned in Assign Leads rather than guessing. The Sales-agent routing card on the Lead Intake page lists the names that match today.
Example body:
{
"first_name": "Jane",
"last_name": "Doe",
"phone": "5551234567",
"email": "jane@example.com",
"origin_city": "New York",
"origin_state": "NY",
"origin_zip": "10010",
"destination_city": "Orlando",
"destination_state": "FL",
"destination_zip": "32835",
"move_date": "2026-08-10",
"move_size": "2 bedrooms",
"source": "Website Quote Form"
}
Curl example:
curl -X POST "https://movingstorageapp.com/api/leads/intake/your-company-slug" \
-H "Authorization: Bearer msa_intake_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{"first_name":"Jane","phone":"5551234567","source":"Website Quote Form"}'
Server-side fetch example:
const response = await fetch(
"https://movingstorageapp.com/api/leads/intake/your-company-slug",
{
method: "POST",
headers: {
Authorization: `Bearer ${process.env.LEAD_INTAKE_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify(formData),
}
)
const result = await response.json()
Responses
Success is 200 with:
{ "success": true, "lead_id": "uuid", "action": "created" }
action is created for a new lead, or duplicate_skipped when the same phone number already arrived for your company in the last 30 minutes. The existing lead_id is returned and no second lead is created; if the first lead had no rep and this one names a matching rep, the first lead is given to that rep.
Error cases, all shaped { "success": false, "error": "message" }:
400— the body could not be read (not valid JSON or form data), or credentials were put in the URL401— missing, malformed, inactive, or wrong-company key403— your company's plan does not include the Sales CRM404— malformed company slug in the URL413— body over 64 KB415— unsupported content type429— rate limited (per IP, per key, and per company); aRetry-Afterheader says how many seconds to wait503— the endpoint fails closed when request protection, lead routing or lead storage is unavailable; the lead was not saved500— an unexpected error; a lead with a phone number is safe to retry, because the same phone number within 30 minutes is never saved twice
Good practices
- Always go through your own backend: browser to your server, your server to this API. The key never reaches the browser.
- Check the form on your own site first (require a phone or email, check formats) so visitors get instant feedback and staff aren't left fixing bad data.
- Retry conservatively: retry
429after theRetry-Afterinterval, and500or503a small number of times with backoff. Do not retry400or401; fix the request or key instead. A retry within 30 minutes with the same phone number won't create a duplicate. - Log failures on your server (without logging the key) so form submissions are never silently lost.