Massage Therapy Booking Form: Build Spa Appointment Schedulers
Phone tag with clients is dead time. Every back-and-forth text about "what kind of massage" and "any injuries I should know about" is time you're not getting paid for. A booking form that collects everything upfront lets you show up ready to work, not interview.
The right intake form does more than schedule appointments. It screens for contraindications before you're in the room. It sets expectations on pricing. It captures preferences you'd otherwise forget. And it makes clients feel like you actually care about their session before it starts.
Here's how to build a massage booking form that works for solo practitioners, spas, and wellness centers - with dynamic pricing, health screening, and therapist preferences baked in.
Service Selection
Start with what they want. Different massage types serve different needs, and the type they choose affects everything downstream - pricing, duration options, health questions, even which therapists are available.
const serviceSection = form.addSubform('service', {
title: 'Select Your Service'
});
serviceSection.addRow(row => {
row.addRadioButton('serviceType', {
label: 'Type of Massage',
options: [
{ id: 'swedish', name: 'Swedish Massage' },
{ id: 'deep-tissue', name: 'Deep Tissue Massage' },
{ id: 'hot-stone', name: 'Hot Stone Massage' },
{ id: 'sports', name: 'Sports Massage' },
{ id: 'prenatal', name: 'Prenatal Massage' },
{ id: 'couples', name: 'Couples Massage' }
],
orientation: 'vertical',
isRequired: true
});
});
serviceSection.addRow(row => {
row.addRadioButton('duration', {
label: 'Session Length',
options: [
{ id: '30', name: '30 Minutes' },
{ id: '60', name: '60 Minutes' },
{ id: '90', name: '90 Minutes' },
{ id: '120', name: '120 Minutes' }
],
defaultValue: '60',
isRequired: true
});
});Duration matters for pricing and scheduling. A 30-minute session targets one area. A 90-minute session covers full body with focus areas. Let clients choose based on what they need, not what they think they can afford - they'll see pricing after selection.
Focus Areas and Pressure
Asking where it hurts before the session saves the first five minutes of conversation. It also helps you prepare - if someone's coming in with lower back issues, you're not surprised when they mention it on the table.
const focusSection = form.addSubform('focus', {
title: 'Focus Areas'
});
focusSection.addRow(row => {
row.addCheckboxList('focusAreas', {
label: 'Areas of concern (select all that apply)',
options: [
{ id: 'neck-shoulders', name: 'Neck & Shoulders' },
{ id: 'upper-back', name: 'Upper Back' },
{ id: 'lower-back', name: 'Lower Back' },
{ id: 'legs', name: 'Legs & Calves' },
{ id: 'feet', name: 'Feet' },
{ id: 'arms-hands', name: 'Arms & Hands' },
{ id: 'headache', name: 'Headache/Tension' },
{ id: 'full-body', name: 'Full Body (no specific focus)' }
],
orientation: 'vertical'
});
});
focusSection.addRow(row => {
row.addDropdown('pressurePreference', {
label: 'Pressure Preference',
options: [
{ id: 'light', name: 'Light - Gentle, relaxing touch' },
{ id: 'medium', name: 'Medium - Moderate pressure' },
{ id: 'firm', name: 'Firm - Deep pressure' },
{ id: 'varies', name: 'Varies by area' }
],
defaultValue: 'medium'
});
});Pressure preference is crucial. Nothing derails a session faster than a client who wanted deep work getting featherlight touch, or vice versa. Capturing this upfront sets expectations both ways.
Pro tip
The "Varies by area" option is worth including. Many clients want firm work on their shoulders but lighter touch on their lower back. This flags them for a quick conversation at the start rather than adjustments mid-session.
Health Intake
This is where online intake really shines. Health questions that feel awkward in person become just checkboxes on a form. Clients disclose more when they're not face-to-face.
const healthSection = form.addSubform('health', {
title: 'Health Information'
});
healthSection.addRow(row => {
row.addCheckboxList('conditions', {
label: 'Do you have any of the following?',
options: [
{ id: 'high-blood-pressure', name: 'High blood pressure' },
{ id: 'diabetes', name: 'Diabetes' },
{ id: 'heart-condition', name: 'Heart condition' },
{ id: 'recent-surgery', name: 'Recent surgery (within 6 months)' },
{ id: 'skin-condition', name: 'Skin condition or sensitivity' },
{ id: 'pregnant', name: 'Currently pregnant' },
{ id: 'cancer', name: 'Cancer (current or recent treatment)' },
{ id: 'blood-clots', name: 'History of blood clots' },
{ id: 'none', name: 'None of the above' }
],
orientation: 'vertical'
});
});
healthSection.addRow(row => {
row.addTextarea('injuries', {
label: 'Current injuries or areas to avoid',
placeholder: 'Describe any injuries, surgeries, or sensitive areas...',
rows: 3
});
});
healthSection.addRow(row => {
row.addTextarea('allergies', {
label: 'Allergies (oils, lotions, scents)',
placeholder: 'List any allergies...',
rows: 2,
isVisible: () => serviceType.value() === 'hot-stone' ||
serviceType.value() === 'swedish'
});
});The conditions checklist covers common contraindications. You're not diagnosing anything - you're flagging situations that need modification or clearance. Recent surgery? You'll avoid that area. Blood clots? Deep tissue is off the table.
The allergy field appears conditionally for services that use oils or heated elements. No point asking about aromatherapy allergies for a sports massage that doesn't use them.
Therapist Preferences
For practices with multiple therapists, preference matters. Some clients have a gender preference for comfort reasons. Others want to see the same therapist who helped them last time.
const therapistSection = form.addSubform('therapist', {
title: 'Therapist Preference'
});
therapistSection.addRow(row => {
row.addRadioButton('therapistGender', {
label: 'Therapist Gender Preference',
options: [
{ id: 'no-preference', name: 'No Preference' },
{ id: 'female', name: 'Female' },
{ id: 'male', name: 'Male' }
],
defaultValue: 'no-preference'
});
});
therapistSection.addRow(row => {
row.addDropdown('specificTherapist', {
label: 'Request Specific Therapist (optional)',
options: [
{ id: 'any', name: 'Any available therapist' },
{ id: 'sarah', name: 'Sarah M.' },
{ id: 'mike', name: 'Mike T.' },
{ id: 'lisa', name: 'Lisa K.' },
{ id: 'james', name: 'James R.' }
],
defaultValue: 'any'
});
});The specific therapist dropdown works for established practices. If you're solo, skip this section entirely. The form adapts to your business model.
Dynamic Pricing
Show the price as they build their session. No surprises at checkout, no awkward conversations about cost. They see exactly what they're paying for.
// Pricing calculation
const basePrices: Record<string, Record<string, number>> = {
'swedish': { '30': 45, '60': 75, '90': 105, '120': 135 },
'deep-tissue': { '30': 55, '60': 90, '90': 125, '120': 160 },
'hot-stone': { '30': 60, '60': 100, '90': 140, '120': 180 },
'sports': { '30': 55, '60': 90, '90': 125, '120': 160 },
'prenatal': { '30': 50, '60': 85, '90': 115, '120': null },
'couples': { '30': null, '60': 160, '90': 220, '120': 280 }
};
const sessionPrice = form.computedValue(() => {
const service = serviceType.value();
const dur = duration.value();
if (!service || !dur) return null;
const price = basePrices[service]?.[dur];
return price ?? null;
});
const pricingSection = form.addSubform('pricing', {
title: 'Session Price'
});
pricingSection.addRow(row => {
row.addPriceDisplay('totalPrice', {
label: 'Total',
computedValue: () => sessionPrice(),
currency: '$',
decimals: 0
});
});
pricingSection.addRow(row => {
row.addTextDisplay('durationNote', {
computedValue: () => {
const dur = duration.value();
if (dur === '30') return 'Best for targeting a specific area';
if (dur === '90') return 'Recommended for full body with focus areas';
if (dur === '120') return 'Our most comprehensive session';
return '';
},
isVisible: () => duration.value() !== '60'
});
});The null values handle impossible combinations - no 30-minute couples massage, no 120-minute prenatal. The form just doesn't show a price for these, signaling that the combination isn't available.
See more wellness booking form examples in our gallery.
Session Enhancements
Upsells, done right. Enhancements add value to the session and revenue to your business. Present them as options, not pressure.
const addonsSection = form.addSubform('addons', {
title: 'Enhancements (Optional)'
});
addonsSection.addRow(row => {
row.addCheckboxList('addons', {
label: 'Add to your session',
options: [
{ id: 'aromatherapy', name: 'Aromatherapy (+$10)' },
{ id: 'hot-towels', name: 'Hot Towel Treatment (+$10)' },
{ id: 'scalp-massage', name: 'Extended Scalp Massage (+$15)' },
{ id: 'foot-scrub', name: 'Foot Scrub (+$15)' },
{ id: 'cupping', name: 'Cupping Therapy (+$25)' }
],
orientation: 'vertical'
});
});
const addonPrices: Record<string, number> = {
'aromatherapy': 10,
'hot-towels': 10,
'scalp-massage': 15,
'foot-scrub': 15,
'cupping': 25
};
const addonsTotal = form.computedValue(() => {
const selected = addons.value() ?? [];
return selected.reduce((sum, addon) =>
sum + (addonPrices[addon] ?? 0), 0);
});
const grandTotal = form.computedValue(() => {
const base = sessionPrice() ?? 0;
const extras = addonsTotal() ?? 0;
return base + extras;
});Each addon has a clear price. The total updates in real-time as clients add or remove options. Transparency builds trust - they know exactly what they're getting and what it costs.
Appointment Scheduling
Date and time selection with sensible constraints. No booking yesterday, no booking six months out. Just the window you actually want to fill.
const scheduleSection = form.addSubform('schedule', {
title: 'Appointment Time'
});
scheduleSection.addRow(row => {
row.addDatePicker('appointmentDate', {
label: 'Preferred Date',
minDate: () => new Date(),
maxDate: () => {
const d = new Date();
d.setMonth(d.getMonth() + 2);
return d;
},
isRequired: true
});
});
scheduleSection.addRow(row => {
row.addDropdown('appointmentTime', {
label: 'Preferred Time',
options: [
{ id: '09:00', name: '9:00 AM' },
{ id: '10:00', name: '10:00 AM' },
{ id: '11:00', name: '11:00 AM' },
{ id: '12:00', name: '12:00 PM' },
{ id: '13:00', name: '1:00 PM' },
{ id: '14:00', name: '2:00 PM' },
{ id: '15:00', name: '3:00 PM' },
{ id: '16:00', name: '4:00 PM' },
{ id: '17:00', name: '5:00 PM' },
{ id: '18:00', name: '6:00 PM' }
],
isRequired: true
});
});
scheduleSection.addRow(row => {
row.addCheckbox('flexibleTime', {
label: 'I'm flexible - contact me with available times if this slot is taken'
});
});The flexibility checkbox is a nice touch. Clients who want a specific time get it (if available). Clients who just want an appointment soon signal that you can slot them wherever works. Different needs, same form.
Contact Information
Capture who they are and how to reach them. The first-visit question helps you prepare differently for new clients versus returning ones.
const contactSection = form.addSubform('contact', {
title: 'Your Information'
});
contactSection.addRow(row => {
row.addTextbox('firstName', {
label: 'First Name',
isRequired: true
});
row.addTextbox('lastName', {
label: 'Last Name',
isRequired: true
});
});
contactSection.addRow(row => {
row.addEmail('email', {
label: 'Email',
isRequired: true
});
row.addTextbox('phone', {
label: 'Phone Number',
isRequired: true
});
});
contactSection.addRow(row => {
row.addRadioButton('visitType', {
label: 'Is this your first visit?',
options: [
{ id: 'new', name: 'Yes, first time' },
{ id: 'returning', name: 'No, returning client' }
],
isRequired: true
});
});
contactSection.addRow(row => {
row.addTextarea('specialRequests', {
label: 'Special Requests or Notes',
placeholder: 'Room temperature preference, music preference, anything else...',
rows: 3
});
});The special requests field catches everything else - room temperature, music preferences, mobility issues that weren't covered elsewhere. Give clients a place to tell you what matters to them.
What Happens After Submission
A booking form is just the start. Consider what happens when the form submits:
- Confirmation email with appointment details and health summary
- Calendar invite (Google Calendar, iCal, Outlook)
- Reminder 24 hours before the appointment
- Note in your booking system with focus areas and conditions
The health intake travels with the booking. When the client arrives, you already know they have high blood pressure, want firm pressure on shoulders and light on lower back, and are allergic to lavender. You're prepared.
For Multi-Therapist Practices
If you run a spa or wellness center with multiple therapists, the form can route based on service type. Deep tissue requests go to therapists certified in deep tissue. Prenatal requests go to those with prenatal training.
Gender preference becomes routing logic - female therapist preference only shows female therapists in the specific therapist dropdown. The form respects client preferences while only offering realistic options.
Putting It Together
A complete massage booking form guides clients from service selection through health intake to scheduling. Prices update as they customize their session. Health conditions surface before they're on the table. Preferences are captured once and remembered.
The result: fewer phone calls, better-prepared sessions, and clients who feel cared for before they even walk in the door.
Common Questions
Should I require health intake for every booking?
Yes for new clients, optional for returning. You can auto-fill previous health info for returning clients and just ask if anything has changed. This keeps intake quick for regulars while capturing essential info for first-timers.
How do I handle online payment vs pay-at-visit?
Both work. Many therapists prefer payment at visit to maintain flexibility on tips and adjustments. If you want deposits to reduce no-shows, collect a percentage upfront and the rest at the appointment. FormTs can show different totals for 'due now' vs 'due at visit'.
What about HIPAA compliance for health questions?
Basic intake questions about conditions and injuries don't typically trigger HIPAA - you're not a covered entity unless you bill insurance. That said, use secure form handling and don't store sensitive data longer than needed. Consult a healthcare compliance expert if you're unsure about your specific situation.
How do I integrate with my existing booking system?
FormTs submissions can webhook to most booking systems via API or Zapier. The form captures the data, then pushes it to wherever you manage your calendar - Acuity, Vagaro, Square Appointments, or custom systems. Contact info and preferences transfer automatically.