Docs
API guides

Build a tour booking experience

Check live availability against the property's tour provider and reserve a slot, from a public front end.

Who this is for: vendors embedding tour scheduling in a listing page, chatbot, or leasing assistant.

What you'll build: a booking flow that reads live availability from whatever tour provider the property uses and reserves a slot, without holding any credential.

Building a Next.js property or leasing site on the Sites API? Use its tour availability and reservation endpoints from your server instead; they accept an Idempotency-Key. See Forms, tours & analytics. This guide covers the public V1 endpoints.

The shape of the problem

Resi does not own tour inventory. It proxies to the property's configured tour provider, normalizing the interface. Two things follow:

  1. Every availability and booking call is a live third-party round trip. Slower than a normal read, and it can fail because the provider is down (502).
  2. The booking form is provider-specific. Each tour type publishes the fields it requires. You must render the form from that schema: a fixed name/email/phone form will be rejected by providers that need more.

All three endpoints are public V1: no token, safe to call from a browser.

Step 1: find the tour connection and its booking fields

curl "https://v2.getresi.com/api/v1/property/$PROPERTY_ID/integrations"

GET /api/v1/property/{property}/integrations lists the property's enabled connections. Find the one with category: "tour" and keep its id: that is the connection_id for every tour call. Its tour_types[] lists the tour types the property offers, and each one carries the booking form:

{
  "id": "019dd608-2b3c-74d5-9e6f-7a8b9c0d1e2f",
  "type": "rent-cafe-v2-tour",
  "key": "rentCafeV2Tour",
  "name": "RentCafe Tours",
  "category": "tour",
  "enabled": true,
  "settings": [],
  "tour_types": [
    {
      "type": "in_person_tour",
      "label": "In Person Tour",
      "redirect_url": null,
      "schema": {
        "fields": [
          { "type": "email", "name": "email", "data": { "label": "Email", "required": true, "width": "full", "placeholder": "", "minimum_date": "", "multiline": false, "rules": [] } }
        ]
      }
    }
  ]
}

Cache it for an hour; it changes rarely, but not never.

If no tour connection exists, do not render a tour button at all. Falling back to the lead form is a better experience than a button that errors. Only offer the tour types listed in tour_types; a type that is missing is disabled for the property.

Tour types: in_person_tour (agent-led), virtual_tour, self_guided_tour.

Step 2: check availability

POST /api/v1/tour/get-availability, once per tour type you offer:

curl -X POST https://v2.getresi.com/api/v1/tour/get-availability \
  -H "Content-Type: application/json" \
  -H "Accept: application/json" \
  -d '{
    "property_id": "019dd604-8468-7388-9a52-4c31a2e1209a",
    "connection_id": "019dd608-2b3c-74d5-9e6f-7a8b9c0d1e2f",
    "tour_type": "in_person_tour"
  }'
{
  "data": {
    "availability": [
      {
        "date": "2026-10-02",
        "timezone": "America/Chicago",
        "slots": [ { "start": "10:00", "end": "10:30" }, { "start": "14:30", "end": "15:00" } ]
      }
    ]
  }
}

Each day carries its own IANA timezone, and slot times are 24-hour HH:MM in that zone. Show them in the property's time, labelled, not converted to the renter's. The request takes no date range: the provider decides which days come back.

Handle the failure modes in the UI

ResponseWhat the renter should see
422 "No tour connection is configured for this property."Hide tours; show the lead form
422 "This tour type is not available for this property."Hide that tour type; offer the others
502"Scheduling is temporarily unavailable. Request a callback instead," plus the lead form
No days or no slots"No times are available right now," plus the lead form, not an error

Errors come back as { "error": "…" }. Every one of these should route the renter to the lead form rather than dead-ending. A failed tour booking that captures a lead is a partial success; one that shows a red error is a lost renter.

Step 3: render the form from the schema

Base fields are always required:

FieldFormat
property_idUUID
connection_idUUID
tour_typein_person_tour | virtual_tour | self_guided_tour
tour_dateYYYY-MM-DD, strict: the day's date
tour_timeHH:MM, 24-hour, strict: the slot's start

Everything else comes from the chosen tour type's schema.fields in step 1: commonly first_name, last_name, email and phone, sometimes move_in_date or provider-specific fields. Each field is submitted under its name (or its type when name is empty). Validation on reserve is generated from that same schema, so a form built from it will not be surprised.

function fieldsFor(tourType) {
  return tourType.schema.fields.map((f) => ({
    key: f.name || f.type,
    type: f.type,
    label: f.data.label,
    required: f.data.required,
    placeholder: f.data.placeholder,
    minDate: f.data.minimum_date || null,
    multiline: f.data.multiline,
    width: f.data.width,
  }));
}

Step 4: reserve

POST /api/v1/tour/reserve:

curl -X POST https://v2.getresi.com/api/v1/tour/reserve \
  -H "Content-Type: application/json" \
  -H "Accept: application/json" \
  -d '{
    "property_id": "019dd604-8468-7388-9a52-4c31a2e1209a",
    "connection_id": "019dd608-2b3c-74d5-9e6f-7a8b9c0d1e2f",
    "tour_type": "in_person_tour",
    "tour_date": "2026-10-02",
    "tour_time": "14:30",
    "first_name": "Alex",
    "last_name": "Nguyen",
    "email": "renter@example.com",
    "phone": "(765) 555-0134"
  }'
{ "message": "Tour reserved successfully" }

A 201 means Resi booked with the provider and created a lead in Resi, so you do not need to submit a separate lead for a booked tour. If the booking fails, no lead is created: send the renter to the lead form.

ResponseMeaning
201Booked
409The slot is no longer available. Re-fetch availability and re-render
422 with an errors mapA required field was missing or malformed. Map the errors back onto your form fields by key rather than showing a generic message
422 with an error messageThe provider rejected the booking. Show the message; it is written for the renter
502The provider was unreachable. Retry later, not immediately, and offer the lead form

Timing and race conditions

Slots go stale. Between your availability call and the renter's submission, someone else may take the slot.

  • Re-check availability if more than about 2 minutes have passed before submitting.
  • On a 409, re-fetch and re-render rather than erroring out. Put the renter one click from a different time.
  • Set generous client timeouts (15–30s). These calls traverse a third party.
  • Disable the submit button during the request. The endpoint has no idempotency, so a double submission can book two tours.

Attribution

Booking a tour creates a lead, so the same attribution rules apply. Include your lead source key in the reserve payload alongside the provider fields:

{ "resi_source_key": "acme-tours", "resi_source_name": "Acme Tours" }

Extra keys are stored on the resulting lead. See Capture leads from your own site or CRM for how the key resolves.

Demand events for the tour funnel

Send tour_modal_opened to POST /api/v1/demand-events when the renter opens your scheduler:

curl -X POST https://v2.getresi.com/api/v1/demand-events \
  -H "Content-Type: application/json" \
  -d '{
    "event_type": "tour_modal_opened",
    "property_id": "019dd604-8468-7388-9a52-4c31a2e1209a",
    "session_id": "sess_9f8e7d6c",
    "resi_source_key": "acme-tours"
  }'

Do not send tour_availability_checked or tour_reserved. Resi records both server-side when you call the tour endpoints, and the demand events endpoint rejects them with a 422.

Checklist

  • connection_id read from /integrations, not hardcoded
  • Tour button hidden entirely when no tour connection exists
  • Only the tour types in tour_types offered, each checked independently
  • Booking form rendered from tour_types[].schema.fields, not a fixed field list
  • Slot times shown in the day's timezone
  • YYYY-MM-DD and 24-hour HH:MM enforced client-side
  • Every failure path falls back to the lead form
  • 409 handled by re-fetching availability
  • Submit disabled during the request
  • Lead source key included in the reserve payload
  • Only tour_modal_opened sent as a demand event

Last updated on

On this page