Skip to content

Integrating Contact Forms

This guide shows how to integrate contact forms and tour requests into your frontend application.

import { useState } from 'react';
interface ContactFormData {
firstName: string;
lastName: string;
email: string;
phone?: string;
subject: string;
message: string;
}
function ContactForm() {
const [formData, setFormData] = useState<ContactFormData>({
firstName: '',
lastName: '',
email: '',
phone: '',
subject: '',
message: '',
});
const [status, setStatus] = useState<'idle' | 'submitting' | 'success' | 'error'>('idle');
const [errorMessage, setErrorMessage] = useState('');
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setStatus('submitting');
setErrorMessage('');
try {
const response = await fetch('/api/contact', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
firstName: formData.firstName,
lastName: formData.lastName,
email: formData.email,
phone: formData.phone || undefined,
subject: formData.subject,
message: formData.message,
source_page: window.location.pathname,
// Include UTM parameters if present
utm_source: new URLSearchParams(window.location.search).get('utm_source') || undefined,
utm_medium: new URLSearchParams(window.location.search).get('utm_medium') || undefined,
utm_campaign: new URLSearchParams(window.location.search).get('utm_campaign') || undefined,
}),
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.detail || 'Failed to submit form');
}
setStatus('success');
setFormData({
firstName: '',
lastName: '',
email: '',
phone: '',
subject: '',
message: '',
});
} catch (err) {
setStatus('error');
setErrorMessage(err instanceof Error ? err.message : 'An error occurred');
}
};
return (
<form onSubmit={handleSubmit}>
<div>
<label htmlFor="firstName">First Name</label>
<input
type="text"
id="firstName"
required
value={formData.firstName}
onChange={(e) => setFormData({ ...formData, firstName: e.target.value })}
/>
</div>
<div>
<label htmlFor="lastName">Last Name</label>
<input
type="text"
id="lastName"
required
value={formData.lastName}
onChange={(e) => setFormData({ ...formData, lastName: e.target.value })}
/>
</div>
<div>
<label htmlFor="email">Email</label>
<input
type="email"
id="email"
required
value={formData.email}
onChange={(e) => setFormData({ ...formData, email: e.target.value })}
/>
</div>
<div>
<label htmlFor="phone">Phone (optional)</label>
<input
type="tel"
id="phone"
value={formData.phone}
onChange={(e) => setFormData({ ...formData, phone: e.target.value })}
/>
</div>
<div>
<label htmlFor="subject">Subject</label>
<input
type="text"
id="subject"
required
value={formData.subject}
onChange={(e) => setFormData({ ...formData, subject: e.target.value })}
/>
</div>
<div>
<label htmlFor="message">Message</label>
<textarea
id="message"
required
rows={5}
value={formData.message}
onChange={(e) => setFormData({ ...formData, message: e.target.value })}
/>
</div>
<button type="submit" disabled={status === 'submitting'}>
{status === 'submitting' ? 'Sending...' : 'Send Message'}
</button>
{status === 'success' && (
<div className="success">
Thank you! We'll get back to you within 24 hours.
</div>
)}
{status === 'error' && (
<div className="error">
{errorMessage}
</div>
)}
</form>
);
}
document.getElementById('contactForm').addEventListener('submit', async (e) => {
e.preventDefault();
const formData = new FormData(e.target);
const data = {
firstName: formData.get('firstName'),
lastName: formData.get('lastName'),
email: formData.get('email'),
phone: formData.get('phone') || undefined,
subject: formData.get('subject'),
message: formData.get('message'),
source_page: window.location.pathname,
};
try {
const response = await fetch('/api/contact', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(data),
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.detail || 'Failed to submit');
}
// Show success message
document.getElementById('successMessage').style.display = 'block';
e.target.reset();
} catch (err) {
// Show error message
document.getElementById('errorMessage').textContent = err.message;
document.getElementById('errorMessage').style.display = 'block';
}
});
import { useState } from 'react';
interface TourRequestData {
listing_id: string;
name: string;
email: string;
phone?: string;
preferred_date?: string;
preferred_time?: string;
message?: string;
}
function TourRequestForm({ listingId }: { listingId: string }) {
const [formData, setFormData] = useState<TourRequestData>({
listing_id: listingId,
name: '',
email: '',
phone: '',
preferred_date: '',
preferred_time: '',
message: '',
});
const [status, setStatus] = useState<'idle' | 'submitting' | 'success' | 'error'>('idle');
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setStatus('submitting');
try {
const response = await fetch('/api/tours', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
listing_id: formData.listing_id,
name: formData.name,
email: formData.email,
phone: formData.phone || undefined,
preferred_date: formData.preferred_date || undefined,
preferred_time: formData.preferred_time || undefined,
message: formData.message || undefined,
}),
});
if (!response.ok) {
throw new Error('Failed to submit tour request');
}
setStatus('success');
} catch (err) {
setStatus('error');
}
};
if (status === 'success') {
return (
<div className="success-message">
<h3>Tour Request Submitted!</h3>
<p>We'll contact you shortly to confirm your tour.</p>
</div>
);
}
return (
<form onSubmit={handleSubmit}>
<h3>Request a Tour</h3>
<div>
<label htmlFor="name">Name</label>
<input
type="text"
id="name"
required
value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
/>
</div>
<div>
<label htmlFor="email">Email</label>
<input
type="email"
id="email"
required
value={formData.email}
onChange={(e) => setFormData({ ...formData, email: e.target.value })}
/>
</div>
<div>
<label htmlFor="phone">Phone</label>
<input
type="tel"
id="phone"
value={formData.phone}
onChange={(e) => setFormData({ ...formData, phone: e.target.value })}
/>
</div>
<div>
<label htmlFor="preferred_date">Preferred Date</label>
<input
type="date"
id="preferred_date"
value={formData.preferred_date}
onChange={(e) => setFormData({ ...formData, preferred_date: e.target.value })}
/>
</div>
<div>
<label htmlFor="preferred_time">Preferred Time</label>
<input
type="time"
id="preferred_time"
value={formData.preferred_time}
onChange={(e) => setFormData({ ...formData, preferred_time: e.target.value })}
/>
</div>
<div>
<label htmlFor="message">Message (optional)</label>
<textarea
id="message"
rows={3}
value={formData.message}
onChange={(e) => setFormData({ ...formData, message: e.target.value })}
/>
</div>
<button type="submit" disabled={status === 'submitting'}>
{status === 'submitting' ? 'Submitting...' : 'Request Tour'}
</button>
{status === 'error' && (
<div className="error">
Failed to submit request. Please try again.
</div>
)}
</form>
);
}

Capture marketing campaign data:

// Extract UTM parameters from URL
function getUTMParams() {
const params = new URLSearchParams(window.location.search);
return {
utm_source: params.get('utm_source') || undefined,
utm_medium: params.get('utm_medium') || undefined,
utm_campaign: params.get('utm_campaign') || undefined,
};
}
// Include in form submission
const submitData = {
...formData,
...getUTMParams(),
source_page: window.location.pathname,
};

Handle 422 Unprocessable Entity responses:

try {
const response = await fetch('/api/contact', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(formData),
});
if (response.status === 422) {
const error = await response.json();
// Extract field-specific errors
const fieldErrors: Record<string, string> = {};
error.detail.forEach((err: any) => {
const field = err.loc[err.loc.length - 1]; // Last element is field name
fieldErrors[field] = err.msg;
});
// Display errors next to form fields
setErrors(fieldErrors);
return;
}
if (!response.ok) {
throw new Error('Submission failed');
}
// Success
} catch (err) {
setGeneralError('An unexpected error occurred');
}

If testing on an unmapped domain:

{
"detail": "Domain 'localhost' is not configured. Unable to process lead."
}

Solution: Use X-Forwarded-Host header in development, or configure a test domain mapping.

Terminal window
curl -X POST \
-H "Content-Type: application/json" \
-d '{
"firstName": "John",
"lastName": "Smith",
"email": "john@example.com",
"phone": "208-555-0123",
"subject": "Interested in 123 Main St",
"message": "I would like more information about this property."
}' \
https://www.your-domain.com/api/contact
Terminal window
curl -X POST \
-H "Content-Type: application/json" \
-d '{
"listing_id": "202412345",
"name": "Jane Doe",
"email": "jane@example.com",
"preferred_date": "2024-12-30",
"preferred_time": "10:00 AM"
}' \
https://www.your-domain.com/api/tours