Docs
Sites API

Forms, tours & analytics

Render a form from its definition, create leads, book tours with the property's provider, and report what visitors do.

Every endpoint here is called from your server. The pattern is always the same: the browser posts to a route handler on your site; the handler adds the website token and forwards to Resi.

Forms

GET /forms · GET /forms/{form} — enabled forms attached to the website's properties. Tag: forms:{property_id}.

Parameter
propertyA property slug
keyForm keys, comma-separated (for example contact)
typecontact or schedule_tour
sortname
page, per_pageDefault 50, ceiling 100
{
  "id": "01000000-0000-4000-8000-00000000000e",
  "key": "contact",
  "name": "Contact Us",
  "type": "contact",
  "fields": [
    { "key": "full_name", "type": "text", "label": "Full Name", "placeholder": null, "required": false, "width": "full", "options": [], "settings": {} },
    { "key": "email", "type": "email", "label": "Email", "placeholder": null, "required": true, "width": "full", "options": [], "settings": {} },
    { "key": "move_in_date", "type": "move_in_date", "label": "Move In Date", "placeholder": null, "required": false, "width": "half", "options": [], "settings": { "minimum_date": "2026-02-01" } }
  ],
  "tour_types": [],
  "settings": {
    "disclaimer": "We will never share your details.",
    "redirect_url": "https://contract-property.test/thanks",
    "success_message": "Thanks — we will be in touch.",
    "submit_button_label": "Send"
  },
  "properties": [{ "id": "0100…0002", "slug": "contract-property", "name": "Contract Property" }],
  "submit_path": "/api/sites/v1/forms/01000000-0000-4000-8000-00000000000e/submissions"
}

Render the form from fields: a field's type is one of name, email, phone, message, move_in_date, text or select (with options); settings carries anything else the field defines, such as a minimum date. The same reading of the form validates the submission, so a form built from fields is always accepted.

tour_types[] lists the tour types a schedule_tour form offers, each { type, data: { enabled, title, description } }, for labelling the choice. properties[] names the properties the form serves; when there is more than one, a submission must say which.

A form by id that is disabled, malformed, or attached to none of this website's properties is a 404.

Submitting a form

POST /forms/{form}/submissions — creates a lead. Write limit: 60 / minute per website.

curl -X POST "$RESI_DELIVERY_API_URL/forms/01000000-0000-4000-8000-00000000000e/submissions" \
  -H "Authorization: Bearer $RESI_DELIVERY_TOKEN" \
  -H "X-Resi-Website: $RESI_WEBSITE_ID" \
  -H "Idempotency-Key: 6f1c2b9e-3a1d-4c55-9a53-0d6f2f0e7a11" \
  -H "Content-Type: application/json" -H "Accept: application/json" \
  -d '{
    "property": "contract-property",
    "fields": { "full_name": "Alex Nguyen", "email": "renter@example.com" },
    "context": {
      "page_url": "https://www.example-apartments.com/contact",
      "utm_source": "google",
      "gclid": "abc123",
      "resi_source_key": "GOA",
      "resi_source_name": "google_paid",
      "unit": "101"
    },
    "honeypot": ""
  }'
Body key
fieldsRequired. Values keyed by each field's key. email is always required. A key the form does not define is a 422
propertyA property slug. Required only when the form serves more than one of this website's properties
contextAttribution and page context; string values. Allowed keys below; any other key is a 422
honeypotThe value of your hidden trap input

Allowed context keys: page_url, landing_url, referrer, user_agent, ip_address, utm_source, utm_medium, utm_campaign, utm_term, utm_content, gclid, gbraid, wbraid, msclkid, fbclid, ttclid, resi_source_key, resi_source_name, resi_visitor_id, resi_session_id, unit, floor_plan (the last two are slugs).

Because the request comes from your server, Resi cannot see the visitor. Forward their user_agent and ip_address in context.

{
  "data": { "lead_id": "01a0b9f5-b6e9-738b-b3ad-881127ed77cd", "message": "Thanks — we will be in touch.", "redirect_url": "https://contract-property.test/thanks" },
  "meta": { "duplicate": false, "discarded": false }
}
StatusMeaning
201Lead created — or discarded by the honeypot
200This Idempotency-Key was already processed; the original lead is returned with meta.duplicate: true
409A submission with this Idempotency-Key is still being processed
422Validation failed; errors is keyed by parameter, for example fields.email

Show the visitor data.message, and follow data.redirect_url when it is set. Both are the client's own configuration.

Honeypot. Add a hidden input a person will never fill, and pass its value as honeypot. Anything non-empty discards the submission while answering exactly as a success (lead_id: null, meta.discarded: true). Show the visitor the same success; never reveal the difference.

What happens after. The lead is created and recorded as a demand event before the response. Email notifications and the form's CRM connections run on Resi's queue, retried on failure, so a slow CRM never holds the form open.

Tours

Tours are booked with the property's own tour provider, through a tour integration. Read it from GET /integrations?category=tour: its id is the connection_id, and tour_types[] names each type and its fields.

Availability

POST /tours/availability

{ "property": "contract-property", "connection_id": "01000000-0000-4000-8000-000000000020", "tour_type": "in_person_tour" }

Returns the provider's open slots, normalised:

{
  "data": {
    "availability": [
      { "date": "2026-02-03", "timezone": "America/Chicago", "slots": [{ "start": "10:00", "end": "10:30" }, { "start": "10:30", "end": "11:00" }] }
    ]
  },
  "meta": {}
}

Each day carries its IANA timezone; slot times are HH:MM in that timezone, and a slot's start is what tour_time expects. This is a live call to the provider: do not cache it for long, and expect 502 when the provider fails (safe to retry).

Reservation

POST /tours/reservations — books the slot with the provider and creates the lead.

{
  "property": "contract-property",
  "connection_id": "01000000-0000-4000-8000-000000000020",
  "tour_type": "in_person_tour",
  "tour_date": "2026-02-03",
  "tour_time": "10:00",
  "fields": { "first_name": "Alex", "last_name": "Nguyen", "email": "renter@example.com", "phone": "7655550134" },
  "context": { "page_url": "https://www.example-apartments.com/tour", "resi_source_key": "GOA" }
}

tour_date is YYYY-MM-DD; tour_time is HH:MM, 24 hour. fields is validated against the tour type's own schema; a key it does not define is a 422. context takes the same keys as a form submission.

{ "data": { "lead_id": "01a0b9f5-…", "message": "Tour reserved successfully." }, "meta": { "duplicate": false } }
StatusMeaning
201Booked
200This Idempotency-Key was already processed; the original booking is returned, nothing new is booked
404A property this website does not have
409The slot was taken, or the same Idempotency-Key is still being processed
422Validation failed, or the connection is not an enabled tour connection of this property, or does not offer this tour type
502The tour provider failed. Safe to retry with the same Idempotency-Key

Always send an Idempotency-Key when booking. A retry after a timeout then returns the original booking instead of booking the slot twice with the provider.

Analytics events

POST /events — a demand event a page observed, forwarded by your server. Uses the read limit (1,200 / minute), since events arrive in volume.

{ "event_type": "unit_viewed", "property": "contract-property", "unit": "101", "session_id": "s_1", "anonymous_id": "a_1" }
{ "data": { "id": "01a0b9f5-b6e9-738b-b3ad-881127ed77cd", "event_type": "unit_viewed", "status": "recorded" }, "meta": {} }

event_type and property are required. Accepted types are the events a page can observe: unit_viewed, floor_plan_viewed, lease_term_selected, move_in_date_changed, price_matrix_loaded, fee_breakdown_viewed, fee_calculator_opened, apply_clicked, tour_modal_opened, form_started. Leads and tour reservations are recorded by the endpoints that create them; do not send them again.

Optional keys: occurred_at, unit, floor_plan (slugs on this property), form_id, session_id, anonymous_id, move_in_date, lease_term_months (1–36), displayed_base_rent, displayed_tmlp, displayed_fee_total (whole numbers — what the visitor was actually shown), destination_url, the source keys (lead_source_name, resi_source_name, resi_source_key, resi_external_source_field, resi_source_query, resi_source_timestamp, resi_source_method) and a free-form metadata map. Any other key is a 422.

Source observations

POST /source-observations — what the site saw about where a visit came from. Resi resolves it against the property's attribution rules and answers which source matched.

{
  "property": "contract-property",
  "landing_url": "https://www.example-apartments.com/?utm_source=google&gclid=abc",
  "referrer": "https://www.google.com/",
  "query_params": { "utm_source": "google" },
  "click_ids": { "gclid": "abc" },
  "session_id": "s_1"
}
{ "data": { "id": "01a0b9f5-b6ef-70aa-9336-5d6484b49014", "status": "matched", "lead_source_id": 1, "resi_source_key": "GOA", "resi_source_name": "google_paid" }, "meta": {} }

Only property is required. Other keys: occurred_at, anonymous_id, current_url, path, external_source_field, external_source_value, resi_source_key, resi_source_name, match_status, metadata.

What Resi stores is deliberately narrow:

  • URLs are stored with credentials and fragments dropped, and only source-identifying query parameters kept.
  • From query_params: only UTM parameters, click ids, the CRM's source field and parameters the property's rules read.
  • From click_ids: only gclid, gbraid, wbraid, msclkid, fbclid, ttclid, each capped at 512 characters.
  • metadata keys that look personal or credential-shaped are stored as [redacted].

Use the returned resi_source_key and resi_source_name as the context of every later lead from that session.

Last updated on

On this page