Developer API

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 example Website Quote Form. Saved as the lead source in the CRM. label, campaign and posting are accepted for the same value.
  • phone (string) and/or email (string) — so your team can reach the customer.

Optional fields (all strings unless noted):

  • first_name, last_name, or full_name instead
  • origin_city, origin_state, origin_zip
  • destination_city, destination_state, destination_zip (a missing city or state is filled in from the ZIP code)
  • move_date — a date such as YYYY-MM-DD or MM/DD/YYYY; a date that can't be read is ignored
  • move_size — for example 2 bedrooms
  • cubic_feet (string or number)
  • notes — free-text comments from the customer
  • sales_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 URL
  • 401 — missing, malformed, inactive, or wrong-company key
  • 403 — your company's plan does not include the Sales CRM
  • 404 — malformed company slug in the URL
  • 413 — body over 64 KB
  • 415 — unsupported content type
  • 429 — rate limited (per IP, per key, and per company); a Retry-After header says how many seconds to wait
  • 503 — the endpoint fails closed when request protection, lead routing or lead storage is unavailable; the lead was not saved
  • 500 — 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 429 after the Retry-After interval, and 500 or 503 a small number of times with backoff. Do not retry 400 or 401; 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.