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:
- 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). - 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
| Response | What 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:
| Field | Format |
|---|---|
property_id | UUID |
connection_id | UUID |
tour_type | in_person_tour | virtual_tour | self_guided_tour |
tour_date | YYYY-MM-DD, strict: the day's date |
tour_time | HH: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
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.
| Response | Meaning |
|---|---|
201 | Booked |
409 | The slot is no longer available. Re-fetch availability and re-render |
422 with an errors map | A 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 message | The provider rejected the booking. Show the message; it is written for the renter |
502 | The 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_idread from/integrations, not hardcoded - Tour button hidden entirely when no tour connection exists
- Only the tour types in
tour_typesoffered, 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-DDand 24-hourHH:MMenforced client-side - Every failure path falls back to the lead form
-
409handled by re-fetching availability - Submit disabled during the request
- Lead source key included in the reserve payload
- Only
tour_modal_openedsent as a demand event
Last updated on