Capture leads from your own site or CRM
Send renter inquiries into Resi, get them attributed, and route them to the client's leasing workflow.
Who this is for: CRMs, leasing AI and chatbot vendors, marketing agencies, and any partner running a lead-capture surface outside the property's own website.
What you'll build: a submission path that puts leads into Resi with correct attribution and reaches the client's leasing team through their configured workflow.
Building a Next.js property or leasing site on the Sites API? Submit its forms from your server with POST /api/sites/v1/forms/{form}/submissions instead; see Forms, tours & analytics. This guide covers the public V1 endpoint.
What actually happens when you submit a lead
A lead is bound to a form, and the form is what carries the client's configuration:
POST /api/v1/leads
→ lead stored in Resi, attributed to a lead source
→ a lead_submitted demand event recorded
→ email notifications sent per the form's settings
→ lead pushed through the form's connections (the client's CRM, Zapier, etc.)You are not just recording data; you are triggering the client's leasing workflow. That is why the form_id matters more than anything else in the payload: it determines who gets notified and where the lead is routed.
Step 1: discover the form
Never hardcode a form id or field list. Clients change forms without telling vendors.
curl "https://v2.getresi.com/api/v1/property/$PROPERTY_ID/forms"GET /api/v1/property/{property}/forms returns the property's enabled forms with their fields and the connections each one routes to. Cache it for an hour. Re-fetch immediately after a 404 from lead submission: it means the form no longer exists.
Resi does not check a submission against the form's fields. It validates only form_id, email, and the date format of move_in_date and tour_date if you send them. Keeping your form in step with the client's is on you, which is why the re-fetch matters.
Step 2: get your lead source code
Ask the client to create a lead source for your integration, and get its code. You can confirm what exists on a property with an authenticated V2 call to GET /api/v2/lead-sources:
curl "https://v2.getresi.com/api/v2/lead-sources?property_id=$PROPERTY_ID" \
-H "Authorization: Bearer $RESI_TOKEN"{ "data": [ { "id": "…", "name": "chatbot", "code": "acme-chat", "property_id": "…" } ] }You can also create one yourself with POST /api/v2/lead-sources if your user has permission to create lead sources on the account:
curl -X POST https://v2.getresi.com/api/v2/lead-sources \
-H "Authorization: Bearer $RESI_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"property_id": "'"$PROPERTY_ID"'",
"name": "chatbot",
"code": "acme-chat",
"email": "leads@acme.example"
}'name is a controlled enum, not free text. It is the category of source and must be one of Resi's 54 known values. code is the free-text identifier you actually match on with resi_source_key.
Common values: chatbot, virtual_tour, referral_website, corporate_website, apartments_com, zillow, apartment_list, rentcafe_marketplace, google_paid, google_organic, facebook, instagram, email_campaign, sms, call_in, walk_in, resident_referral, locator_service, event, promotion, other, unknown.
Pick the value that describes what your integration is: a leasing chatbot is chatbot, a listing marketplace is its own value if listed, otherwise other. Sending an unrecognized name returns 422.
Confirm the code with the client first. It lands in their reporting, and a rogue code is worse than no code.
Step 3: submit
Post to POST /api/v1/leads:
curl -X POST https://v2.getresi.com/api/v1/leads \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-d '{
"form_id": "019dd607-1a2b-73c4-8d5e-6f7a8b9c0d1e",
"property_id": "019dd604-8468-7388-9a52-4c31a2e1209a",
"email": "renter@example.com",
"first_name": "Alex",
"last_name": "Nguyen",
"phone": "(765) 555-0134",
"move_in_date": "2026-10-01",
"bedrooms": "2",
"message": "Interested in a 2 bed with a balcony. Two cats.",
"resi_source_key": "acme-chat",
"resi_source_name": "Acme Chat"
}'{ "message": "Thanks! A leasing agent will be in touch shortly." }The response is a 201. Only form_id and email are required. property_id defaults to the form's first property; send it when a form serves several. An id from another account is ignored and the default is used.
Everything else you send is stored on the lead and sent on through the form's connections, so send the full context you collected. A chatbot that captured "two cats, needs parking, moving from out of state" should pass that along; it is exactly what a leasing agent wants and what makes your integration valuable.
Send it as flat top-level keys. Do not nest objects.
Render the returned message to the renter. It is the client's configured success copy, and it may say something specific about response times.
Attribution rules
| Field | How it resolves |
|---|---|
resi_source_key | Exact match against a lead source code on the property. The reliable path. |
resi_source_name / lead_source_name | Normalized to one of the lead source name values (for example chatbot), then matched against the property's lead source with that name. Fallback only. |
Send both. If neither resolves, the lead is stored without a source and your contribution is invisible in the client's reporting.
Verify on your first live lead. Ask the client to confirm the lead appeared with your source attached. It is a five-minute check that prevents a quarter of unattributed volume.
Reliability: the part that matters most
Two obligations sit on your side of this integration.
Submit each lead exactly once. The V1 endpoint has no idempotency: a repeated request creates a second lead. Track what you have sent and check before retrying:
async function submitLead(payload, dedupeKey) {
if (await alreadySubmitted(dedupeKey)) return; // your store, not Resi's
const res = await fetch("https://v2.getresi.com/api/v1/leads", {
method: "POST",
headers: { "Content-Type": "application/json", Accept: "application/json" },
body: JSON.stringify(payload),
});
if (res.status === 201) {
await markSubmitted(dedupeKey);
return (await res.json()).message;
}
if (res.status === 422) throw new PermanentError(await res.json()); // fix the payload; never retry as-is
if (res.status === 404) throw new StaleFormError(); // form or property gone; re-fetch forms
throw new RetryableError(res.status); // 5xx → backoff
}Bot protection is required on any public form. Resi does not rate-limit V1 lead submission. Put a CAPTCHA, honeypot, or equivalent in front of submission, and rate limit at your own edge. Leads land directly in a client's leasing workflow and CRM, so a leasing team fielding junk inquiries will hold the integration that sent them responsible.
Queue, don't fire-and-forget. Persist the lead locally first, then submit from a queue with retry on 5xx and alerting on permanent failure. A dropped lead is lost revenue for the client; that is the failure mode to engineer against.
Enriching your own UI with Resi data
If your surface also shows inventory, such as a chatbot answering "what 2-bedrooms do you have?", read it from GET /api/v2/units and keep it fresh:
curl -G "https://v2.getresi.com/api/v2/units" \
--data-urlencode "property_id=$PROPERTY_ID" \
--data-urlencode "is_enabled=true" \
--data-urlencode "is_available=true" \
--data-urlencode "is_hidden=false" \
--data-urlencode "is_model=false" \
--data-urlencode "bedrooms[gte]=2" \
--data-urlencode "available_at[lte]=2026-10-01" \
-H "Authorization: Bearer $RESI_TOKEN"Cache 5–15 minutes. See Power an AI agent or chatbot for the full pattern, including which price to show.
Closing the loop with demand events
Leads are conversions. To show the client the funnel that produced them, send demand events to POST /api/v1/demand-events as the renter moves through your surface:
| Renter action | Event |
|---|---|
| Views a unit in your UI | unit_viewed |
| Views a floor plan | floor_plan_viewed |
| Changes desired move-in date | move_in_date_changed |
| Opens the lead form | form_started |
| Clicks apply | apply_clicked |
Do not send lead_submitted. Resi records it server-side when you post the lead, and the demand events endpoint rejects it with a 422. The endpoint allows 120 requests a minute per IP address.
Checklist
-
form_iddiscovered from/forms, never hardcoded - Forms re-fetched after a
404 - Lead source code agreed with the client and sent as
resi_source_key - Attribution verified on a real lead
- Full renter context passed through as flat keys
- Returned
messagerendered to the renter - Local dedupe key before submit; retries only on
5xx - Spam controls in front of the form
- Leads queued and persisted before submission
- Demand events sent for the funnel, excluding server-recorded types
Last updated on