Connect Bolt.new to Acuity Scheduling using the Acuity REST API with User ID and API key from the Integrations section. Fetch appointment types, available time slots, and bookings via API routes. Alternatively, embed Acuity's scheduling widget directly using an iframe — no API needed for basic booking. Custom booking flows use the API. Outbound Acuity API calls work in Bolt's WebContainer. Webhook callbacks for real-time appointment notifications require deployment to a public URL.
Add Appointment Booking to Your Bolt.new App with Acuity Scheduling
Acuity Scheduling (acquired by Squarespace in 2019) is one of the most feature-rich appointment booking platforms available, with built-in support for intake forms, package purchases, gift certificates, payment processing, and group classes alongside standard one-on-one appointment booking. For service-based businesses — coaches, consultants, therapists, fitness instructors, medical practitioners — Acuity handles the complexity of scheduling logic (buffer times, recurring availability, multiple calendars, timezone handling) that would take months to build from scratch.
For Bolt-built applications, Acuity offers two distinct integration approaches. The first and simplest is embedding Acuity's hosted scheduling widget as an iframe — you get a fully functional, mobile-responsive booking interface in minutes with zero API calls. This approach works in Bolt's WebContainer preview because it is simply displaying an external URL in an iframe. The second approach uses Acuity's REST API to build a custom booking interface with your own design — this allows complete control over the booking experience, matching it to your app's visual style, prefilling form fields, and integrating booking flows deeply into your app's user journey.
Acuity's API follows REST conventions with basic authentication using your API User ID and key. The core resources are appointment types (the services you offer), calendars (the individuals or locations providing the service), and appointments (the booked slots). Availability checking is a key operation: given an appointment type, date, and calendar, the API returns available time slots that account for existing bookings, buffer times, and calendar hours. Creating a booking reserves a slot, collects client information, and optionally charges through Stripe or PayPal.
Integration method
Bolt generates Next.js API routes that call Acuity's REST API using basic authentication with your User ID and API key. Acuity's API is well-documented JSON REST that works fully from Bolt's WebContainer for reading availability and creating bookings. For the simplest integration, embed Acuity's hosted scheduling widget via iframe — it requires zero API calls and works in the WebContainer preview. Custom booking UIs use the API for availability checking and appointment creation. Webhook callbacks for booking confirmations require a deployed URL.
Prerequisites
- A Bolt.new account with a Next.js project
- An Acuity Scheduling account (free trial or paid plan — Emerging plan at $16/month is the minimum for API access)
- Your Acuity User ID and API key from Business Settings → Integrations → API
- At least one appointment type configured in your Acuity account
Step-by-step guide
Get Acuity API Credentials and Configure Authentication
Get Acuity API Credentials and Configure Authentication
Acuity Scheduling's API uses HTTP Basic Auth with two credentials: your User ID (a number) and your API key (a long alphanumeric string). Both are found in the same location in your Acuity account settings. To get your credentials, log into Acuity Scheduling and navigate to Business Settings (the gear icon in the top right or in your account menu). Scroll down and look for the Integrations section or tab. In the Integrations section, you will see 'API' or 'API access.' Click on it to expand the API credentials section. You will see your User ID (a number like 12345678) and your API Key (a long string of letters and numbers). Copy both values. Add them to your .env file: ACUITY_USER_ID and ACUITY_API_KEY. The API base URL is https://acuityscheduling.com/api/v1. Basic auth is constructed by base64-encoding 'userId:apiKey' — in Node.js: Buffer.from(`${userId}:${apiKey}`).toString('base64'). For the iframe widget embed, you also need your Owner ID — this is the same as your User ID. For the widget URL, use https://app.acuityscheduling.com/schedule.php?owner={YOUR_USER_ID}. Since the Owner ID appears in the client-side iframe src attribute (not in API calls), it is acceptable to expose it as NEXT_PUBLIC_ACUITY_OWNER_ID for client-rendered components. Note: Acuity API access is available on the Emerging plan ($16/month) and above. The free trial includes API access. If you get a 403 response, confirm your plan includes API access and that the credentials are from Business Settings → Integrations → API (not from Acuity's general account settings which show different information). Test the connection immediately with GET /api/v1/me — this returns your Acuity account information and confirms your credentials are working.
Add ACUITY_USER_ID and ACUITY_API_KEY to the .env file. Also add NEXT_PUBLIC_ACUITY_OWNER_ID (same value as ACUITY_USER_ID but with NEXT_PUBLIC_ prefix for iframe embedding). Create a lib/acuity.ts utility that exports an acuityFetch helper. Build the Basic auth header from Buffer.from(userId:apiKey).toString('base64'). Accept a path and optional query params. Build the URL as https://acuityscheduling.com/api/v1{path}. Export acuityPost for creating appointments.
Paste this in Bolt.new chat
1// lib/acuity.ts2const ACUITY_BASE_URL = 'https://acuityscheduling.com/api/v1';34function getAuthHeader(): string {5 const userId = process.env.ACUITY_USER_ID;6 const apiKey = process.env.ACUITY_API_KEY;7 if (!userId || !apiKey) {8 throw new Error('ACUITY_USER_ID and ACUITY_API_KEY must be set in .env');9 }10 return `Basic ${Buffer.from(`${userId}:${apiKey}`).toString('base64')}`;11}1213export async function acuityFetch<T = unknown>(14 path: string,15 params?: Record<string, string | number>16): Promise<T> {17 const url = new URL(`${ACUITY_BASE_URL}${path}`);18 if (params) {19 Object.entries(params).forEach(([k, v]) => url.searchParams.set(k, String(v)));20 }2122 const response = await fetch(url.toString(), {23 headers: {24 Authorization: getAuthHeader(),25 'Content-Type': 'application/json',26 },27 });2829 if (!response.ok) {30 const err = await response.json().catch(() => ({ message: response.statusText })) as { message?: string; error?: string };31 throw new Error(err.message || err.error || `Acuity API error: ${response.status}`);32 }3334 return response.json() as Promise<T>;35}3637export async function acuityPost<T = unknown>(path: string, body: unknown): Promise<T> {38 const response = await fetch(`${ACUITY_BASE_URL}${path}`, {39 method: 'POST',40 headers: {41 Authorization: getAuthHeader(),42 'Content-Type': 'application/json',43 },44 body: JSON.stringify(body),45 });4647 if (!response.ok) {48 const err = await response.json().catch(() => ({ message: response.statusText })) as { message?: string; error?: string };49 throw new Error(err.message || err.error || `Acuity POST error: ${response.status}`);50 }5152 return response.json() as Promise<T>;53}Pro tip: Acuity's User ID is the same value used in the iframe widget URL as the 'owner' parameter. You can verify your User ID is correct by visiting https://app.acuityscheduling.com/schedule.php?owner={YOUR_USER_ID} in a browser — you should see your Acuity scheduling page.
Expected result: The lib/acuity.ts helper is configured. Test by calling acuityFetch('/me') from an API route — you should see your Acuity account details (name, email, timezone) confirming both credentials are correct.
Embed the Scheduling Widget (No-Code Fast Path)
Embed the Scheduling Widget (No-Code Fast Path)
The fastest way to add appointment booking to any Bolt app is the iframe widget embed. Acuity's hosted widget handles everything — appointment type selection, calendar selection, availability display, time slot selection, client information collection, timezone handling, and payment if configured. The entire booking flow happens within the iframe without any custom API code. The widget URL format is: https://app.acuityscheduling.com/schedule.php?owner={USER_ID}. Optional parameters customize the widget experience: appointmentType={typeId} pre-selects a specific service type, calendar={calendarId} filters to a specific provider, and firstName, lastName, email prefill client information if you know it from your app's authentication context. For React, create a BookingWidget component that renders the iframe with appropriate dimensions. The widget is responsive but needs a minimum height to display properly — 800px is a safe default. Add a ref to the iframe and listen for postMessage events that Acuity sends when a booking is completed, allowing your Bolt app to react to successful bookings (show a thank you message, update a local state, trigger other workflows). Acuity sends a 'acuity.scheduling.loaded' message when the widget initializes and 'acuity.scheduled' with appointment details when a booking is completed successfully. Listen for these using window.addEventListener('message', handler) with origin validation (check that the message comes from app.acuityscheduling.com). The iframe widget works in Bolt's WebContainer preview because it simply loads an external URL — no incoming connections required. The entire booking flow, including Stripe payment if configured, happens within Acuity's hosted environment.
Create a BookingWidget React component that embeds Acuity's scheduling widget. Use the NEXT_PUBLIC_ACUITY_OWNER_ID environment variable in the iframe src URL: https://app.acuityscheduling.com/schedule.php?owner={id}. Accept optional props: appointmentTypeId (pre-select service), height (default 800px), prefillEmail, prefillFirstName, prefillLastName. Add a useEffect that listens for window postMessage events from acuityscheduling.com — log 'acuity.scheduled' events which fire when a booking is completed. Show a loading skeleton until the iframe sends the 'acuity.scheduling.loaded' message.
Paste this in Bolt.new chat
1'use client';2// components/BookingWidget.tsx3import { useEffect, useRef, useState } from 'react';45interface BookingWidgetProps {6 appointmentTypeId?: number;7 height?: number;8 prefillEmail?: string;9 prefillFirstName?: string;10 prefillLastName?: string;11 onBooked?: (appointment: Record<string, unknown>) => void;12}1314export function BookingWidget({15 appointmentTypeId,16 height = 800,17 prefillEmail,18 prefillFirstName,19 prefillLastName,20 onBooked,21}: BookingWidgetProps) {22 const [isLoaded, setIsLoaded] = useState(false);23 const ownerId = process.env.NEXT_PUBLIC_ACUITY_OWNER_ID;2425 const src = new URL('https://app.acuityscheduling.com/schedule.php');26 if (ownerId) src.searchParams.set('owner', ownerId);27 if (appointmentTypeId) src.searchParams.set('appointmentType', String(appointmentTypeId));28 if (prefillEmail) src.searchParams.set('email', prefillEmail);29 if (prefillFirstName) src.searchParams.set('firstName', prefillFirstName);30 if (prefillLastName) src.searchParams.set('lastName', prefillLastName);3132 useEffect(() => {33 function handleMessage(event: MessageEvent) {34 // Only accept messages from Acuity's domain35 if (!event.origin.includes('acuityscheduling.com')) return;3637 if (event.data === 'acuity.scheduling.loaded') {38 setIsLoaded(true);39 }4041 if (typeof event.data === 'object' && event.data?.action === 'acuity.scheduled') {42 onBooked?.(event.data.data as Record<string, unknown>);43 }44 }4546 window.addEventListener('message', handleMessage);47 return () => window.removeEventListener('message', handleMessage);48 }, [onBooked]);4950 return (51 <div className="relative w-full" style={{ height }}>52 {!isLoaded && (53 <div className="absolute inset-0 flex items-center justify-center bg-gray-50 rounded-lg animate-pulse">54 <p className="text-gray-400">Loading scheduling widget...</p>55 </div>56 )}57 <iframe58 src={src.toString()}59 title="Schedule an appointment"60 width="100%"61 height={height}62 frameBorder="0"63 className={`rounded-lg transition-opacity duration-300 ${isLoaded ? 'opacity-100' : 'opacity-0'}`}64 />65 </div>66 );67}Pro tip: The iframe widget works immediately in Bolt's WebContainer preview — you can test the full booking flow including form submission and confirmation. This is one of the easiest integrations to validate during development because no API setup is needed beyond having the owner ID.
Expected result: The BookingWidget component renders Acuity's scheduling interface in an iframe. In the Bolt preview, you should see your actual Acuity scheduling page with your appointment types and available times. Test the full booking flow to confirm the widget works end-to-end.
Build a Custom Booking Flow with the Acuity API
Build a Custom Booking Flow with the Acuity API
For a fully custom booking experience that matches your app's design, use the Acuity API to build a multi-step booking flow. The API provides three key operations: listing appointment types, checking availability for a given type and date, and creating an appointment with client details. Appointment types (GET /api/v1/appointment-types) return your services with their id, name, duration (minutes), price, color, description, and calendarIDs (which calendars offer this service). Duration and price are important for displaying service details to users choosing a service. Availability (GET /api/v1/availability/times) returns available time slots for a specific appointment type and date. Required parameters: appointmentTypeID and date (YYYY-MM-DD format). The response is an array of time objects, each with a time field (ISO 8601 datetime string in your account timezone) and slotsAvailable (integer count of remaining slots for group classes). An empty array means no availability on that date. Display available times in the user's local timezone by converting from the ISO string — use JavaScript's Intl.DateTimeFormat for locale-aware time display. Creating an appointment (POST /api/v1/appointments) requires: datetime (ISO 8601 in the appointment timezone), appointmentTypeID, firstName, lastName, and email. Phone is strongly recommended. Optional fields include notes and any intake form field values. The response returns the created appointment object with its id, confirmationPage URL, and full details. For a smooth user experience, validate the selected time slot immediately before submitting — another user may have booked the same slot in the seconds between your availability fetch and the user's form submission. Handle the 'time slot no longer available' error gracefully by refreshing availability and highlighting the conflict.
Build a complete custom booking flow. Create /api/acuity/appointment-types/route.ts (GET, returns types with id, name, duration, price, description). Create /api/acuity/availability/route.ts (GET, accepts appointmentTypeId and date, returns array of available datetime strings). Create /api/acuity/appointments/route.ts (POST, accepts { appointmentTypeId, datetime, firstName, lastName, email, phone, notes }, creates appointment, returns { id, confirmationPage, datetime, appointmentType }). Build a 3-step React form: Step 1 selects service type, Step 2 picks date and time slot, Step 3 enters contact info. Show a success confirmation with calendar add links after booking.
Paste this in Bolt.new chat
1// app/api/acuity/appointments/route.ts2import { NextResponse } from 'next/server';3import { acuityPost } from '@/lib/acuity';45interface BookingRequest {6 appointmentTypeId: number;7 datetime: string;8 firstName: string;9 lastName: string;10 email: string;11 phone?: string;12 notes?: string;13}1415interface AcuityAppointment {16 id: number;17 datetime: string;18 duration: number;19 confirmationPage: string;20 appointmentType: { name: string; duration: number };21 calendar: { name: string };22 firstName: string;23 lastName: string;24 email: string;25}2627export async function POST(request: Request) {28 const body = await request.json() as BookingRequest;2930 if (!body.appointmentTypeId || !body.datetime || !body.firstName || !body.lastName || !body.email) {31 return NextResponse.json(32 { error: 'appointmentTypeId, datetime, firstName, lastName, and email are required' },33 { status: 400 }34 );35 }3637 if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(body.email)) {38 return NextResponse.json({ error: 'Invalid email address' }, { status: 400 });39 }4041 try {42 const appointment = await acuityPost<AcuityAppointment>('/appointments', {43 appointmentTypeID: body.appointmentTypeId,44 datetime: body.datetime,45 firstName: body.firstName,46 lastName: body.lastName,47 email: body.email,48 ...(body.phone ? { phone: body.phone } : {}),49 ...(body.notes ? { notes: body.notes } : {}),50 });5152 return NextResponse.json({53 success: true,54 appointment: {55 id: appointment.id,56 datetime: appointment.datetime,57 duration: appointment.duration,58 type: appointment.appointmentType.name,59 calendarName: appointment.calendar.name,60 clientName: `${appointment.firstName} ${appointment.lastName}`,61 confirmationPage: appointment.confirmationPage,62 },63 });64 } catch (err) {65 const message = err instanceof Error ? err.message : 'Failed to create appointment';66 const isSlotTaken = message.toLowerCase().includes('not available') || message.toLowerCase().includes('time slot');67 return NextResponse.json(68 { error: message, retry: isSlotTaken },69 { status: isSlotTaken ? 409 : 500 }70 );71 }72}Pro tip: Acuity datetime values use the timezone configured in your Acuity account settings, not UTC. When you display times to users in a different timezone, convert from Acuity's timezone to the user's local timezone. The acuityFetch('/me') endpoint returns your account's timezone so you can handle this conversion correctly.
Expected result: The booking API route creates a real appointment in your Acuity account. After submitting the booking form, check your Acuity calendar — the new appointment should appear with the correct time, client details, and appointment type. The confirmation page URL in the response lets you link users to Acuity's native confirmation view.
Handle Acuity Webhooks After Deployment
Handle Acuity Webhooks After Deployment
Acuity Scheduling supports webhook notifications for appointment events: scheduled (new booking), rescheduled, cancelled, and no-show marking. These webhooks allow your Bolt app to react to booking changes in real time — sending Slack notifications, updating a CRM record, triggering automated workflows, or refreshing dashboard data. Acuity webhooks send POST requests to your specified callback URL. The WebContainer preview cannot receive these incoming connections — webhook testing requires deployment. Deploy your Bolt app to Netlify or Bolt Cloud first, then register your webhook endpoint. Register webhooks in Acuity through two methods: the Acuity admin interface (Business Settings → Integrations → Webhooks) or via the API (POST /api/v1/webhooks). Each webhook registration specifies the event type and callback URL. You can register multiple webhooks for different event types pointing to different handlers, or use one endpoint that handles all event types based on the 'action' field in the payload. Webhook payloads contain the appointment ID and action type. For the full appointment details, make a follow-up GET /api/v1/appointments/{id} call to fetch the complete data including client information and form responses. Return 200 quickly from the webhook handler and perform any subsequent API calls asynchronously. For security, Acuity does not sign webhook payloads (unlike Stripe's HMAC verification). If you need to verify that webhook requests genuinely come from Acuity, include a secret token in the callback URL as a query parameter (e.g., /api/acuity/webhook?secret=YOUR_SECRET) and verify it in the handler.
Create an Acuity webhook handler at app/api/acuity/webhook/route.ts. Accept POST requests. Parse the JSON body — it contains action (scheduled/rescheduled/cancelled/no_show), id (appointment ID), and appointmentTypeID. For each action, log the event. For 'scheduled' events, fetch the full appointment details from GET /api/acuity/appointments/{id} using acuityFetch. Return 200 with { received: true }. Add clear comments explaining this endpoint only works after deployment to Netlify or Bolt Cloud.
Paste this in Bolt.new chat
1// app/api/acuity/webhook/route.ts2// IMPORTANT: Acuity webhooks require deployment to Netlify or Bolt Cloud.3// The Bolt WebContainer preview cannot receive incoming POST requests from Acuity.4//5// After deploying:6// 1. Go to Acuity → Business Settings → Integrations → Webhooks7// 2. Add webhook URL: https://yourapp.netlify.app/api/acuity/webhook8// 3. Select events: scheduled, rescheduled, cancelled9import { NextResponse } from 'next/server';10import { acuityFetch } from '@/lib/acuity';1112interface AcuityWebhookBody {13 action: 'scheduled' | 'rescheduled' | 'cancelled' | 'no_show';14 id: number;15 appointmentTypeID: number;16 calendarID?: number;17}1819export async function POST(request: Request) {20 const body = await request.json() as AcuityWebhookBody;21 const { action, id } = body;2223 console.log(`[Acuity Webhook] ${action} — appointment ${id}`);2425 if (action === 'scheduled') {26 // Fetch full appointment details asynchronously27 acuityFetch(`/appointments/${id}`)28 .then((appt) => {29 console.log('[Acuity] New appointment:', appt);30 // Add your logic here: notify via Slack, update CRM, send custom email, etc.31 })32 .catch((err) => console.error('[Acuity] Failed to fetch appointment:', err));33 }3435 if (action === 'cancelled') {36 console.log(`[Acuity] Appointment ${id} cancelled`);37 // Add cancellation logic: free up resources, notify provider, etc.38 }3940 // Return 200 immediately — process async41 return NextResponse.json({ received: true });42}Pro tip: Return 200 from webhook handlers as quickly as possible. If you perform additional API calls (like fetching full appointment details) synchronously before returning, a slow response may cause Acuity to retry the webhook, resulting in duplicate processing. Use fire-and-forget async calls (without await) for post-processing that runs after the 200 response.
Expected result: The webhook handler is deployed and registered in Acuity. Creating a test appointment in Acuity fires the webhook, which logs the event in your Netlify or Bolt Cloud function logs. The handler is ready for custom logic like CRM updates or Slack notifications.
Common use cases
Embedded Scheduling Widget (Fastest Path)
Embed Acuity's hosted scheduling widget directly in your Bolt app using an iframe. The widget handles all booking logic, availability, timezone detection, and payment collection natively. No API calls required. This is the fastest way to add appointment booking to any Bolt app — users complete the entire booking flow within the embedded widget, and Acuity handles confirmation emails and calendar invites automatically.
Add an Acuity scheduling widget to my Bolt app. Create a BookingWidget React component that embeds an Acuity scheduling iframe. Use the URL format: https://app.acuityscheduling.com/schedule.php?owner={ACUITY_OWNER_ID}. The iframe should be full-width and at least 800px tall. Add an optional appointmentTypeId prop that appends &appointmentType={id} to pre-select a service. Style the container with a border and subtle shadow. Store ACUITY_OWNER_ID in process.env and expose it as NEXT_PUBLIC_ACUITY_OWNER_ID since the iframe URL is rendered client-side. Add a loading skeleton that shows while the iframe loads.
Copy this prompt to try it in Bolt.new
Custom Appointment Booking Form
Build a fully custom booking interface that matches your app's design language. Fetch appointment types and available slots from the Acuity API, let users select their service and time, collect required information, and submit the booking — all within your Bolt app's UI without any Acuity branding.
Build a custom appointment booking flow using the Acuity API. Create /api/acuity/appointment-types that fetches appointment types from GET https://acuityscheduling.com/api/v1/appointment-types. Create /api/acuity/availability that accepts appointmentTypeId and date params and calls /availability/times. Create /api/acuity/book that accepts POST with { appointmentTypeId, datetime, firstName, lastName, email, phone } and calls POST /appointments to book. Build a 3-step React booking flow: Step 1 shows service type cards; Step 2 shows a date picker and available time slots; Step 3 is a contact form. Show a confirmation screen with appointment details after booking.
Copy this prompt to try it in Bolt.new
Appointment Management Dashboard
Build an admin dashboard for viewing and managing upcoming appointments. Fetch all upcoming appointments, display them in a calendar or list view, and provide controls to view appointment details, access client intake form responses, and send reminder messages. This gives service providers a custom management interface tailored to their workflow.
Create an appointment management dashboard using the Acuity API. Build /api/acuity/appointments that fetches upcoming appointments using GET /appointments with minDate and maxDate params. Return appointment id, datetime, duration, appointmentType name, client firstName, lastName, email, phone, and forms (intake form responses). Build a React dashboard with a date range selector, a sortable table of appointments showing date/time, service, client name, and status, and a detail panel that shows full appointment info including form responses when a row is clicked.
Copy this prompt to try it in Bolt.new
Troubleshooting
API returns 401 Unauthorized even with User ID and API key in the .env file
Cause: Basic auth requires base64-encoding of 'userId:apiKey' — not just the key alone. A common error is passing only the API key without the User ID prefix, or using the wrong credential pair. The User ID and API key are found in Business Settings → Integrations → API, not in general account settings.
Solution: Verify the Authorization header is 'Basic {base64(userId:apiKey)}'. Confirm you are using the numeric User ID (found in the API section of Business Settings), not your account email. Test the credentials by calling /api/v1/me — a successful response confirms both credentials are correct. Regenerate the API key in Acuity if unsure about the current value.
1// Correct Basic auth for Acuity:2const userId = process.env.ACUITY_USER_ID;3const apiKey = process.env.ACUITY_API_KEY;4const auth = Buffer.from(`${userId}:${apiKey}`).toString('base64');5const headers = { Authorization: `Basic ${auth}` };Availability endpoint returns empty array even though there are open time slots in Acuity
Cause: The appointmentTypeID parameter is wrong or the date format is incorrect. Acuity availability is specific to a combination of appointment type, calendar, and date. Missing the appointmentTypeID returns all types' combined availability which may differ from what you expect.
Solution: Verify the appointmentTypeID by calling GET /api/v1/appointment-types and checking the id fields match your service. The date parameter must be in YYYY-MM-DD format matching your Acuity account's timezone. Confirm the calendar has available hours configured in Acuity's 'Set your availability' settings for the requested date.
1// Correct availability fetch with required params:2const availability = await acuityFetch('/availability/times', {3 appointmentTypeID: 1234567, // Must be exact numeric ID from /appointment-types4 date: '2025-04-22', // YYYY-MM-DD in Acuity account timezone5});Booking creation fails with 'This time is no longer available'
Cause: Another client booked the same time slot between when you fetched availability and when the booking form was submitted. This is a race condition inherent to appointment booking systems.
Solution: Handle the 409 Conflict response gracefully in the UI. Show a message like 'This time slot was just taken — please select a different time' and automatically refresh the availability display. Pre-select the next available slot for the user to reduce friction. Consider re-checking availability immediately before the final submission step.
1// Handle booking conflict in React:2const response = await fetch('/api/acuity/appointments', { method: 'POST', body: JSON.stringify(data) });3if (response.status === 409) {4 setError('This time slot was just taken. Refreshing available times...');5 await refetchAvailability();6 return;7}Acuity webhook events are not arriving at the deployed handler
Cause: The webhook was registered before deployment, using the Bolt WebContainer preview URL which cannot receive incoming connections. Or the webhook was registered in Acuity's test environment while the app is checking the production Acuity account.
Solution: Deploy to Netlify or Bolt Cloud first. Then register the webhook URL in Acuity Business Settings → Integrations → Webhooks using your deployed URL. Test using Acuity's webhook test feature if available, or by creating a test appointment and watching your deployed function logs.
Best practices
- Prefer the iframe widget embed for most use cases — it requires no API code, works in the Bolt preview, handles mobile responsiveness automatically, and lets Acuity manage the UX complexity of availability, timezones, and payments.
- Store ACUITY_USER_ID and ACUITY_API_KEY without the NEXT_PUBLIC_ prefix — API keys grant full write access to your booking calendar and should only be used server-side.
- The Owner ID for the iframe widget can use NEXT_PUBLIC_ since it appears in the HTML source — this is acceptable because the scheduling page URL is public anyway.
- Cache appointment types for several minutes — they change infrequently and fetching on every page load wastes API quota.
- Do not cache availability responses — available time slots change in real time as appointments are booked, and cached availability leads to booking conflicts and poor user experience.
- Validate the time slot immediately before submitting a booking — another user may have taken it between availability fetch and form submission, so a final check reduces failed booking attempts.
- Handle timezone carefully — Acuity stores and returns times in your account's configured timezone, not UTC or the user's local timezone. Convert times to the user's local timezone for display using the browser's Intl API.
- Register webhooks only after deploying to a stable URL — the Bolt WebContainer preview URL changes each session and cannot receive incoming HTTP connections from Acuity's servers.
Alternatives
Calendly has broader name recognition and a simpler setup for meeting scheduling; Acuity (Squarespace) offers more customization options including intake forms, packages, gift certificates, and group classes, making it better for service-based businesses.
Mindbody is purpose-built for fitness, wellness, and beauty businesses with membership and class booking; Acuity is more flexible for general service businesses and has a cleaner API.
Zocdoc is a healthcare-specific appointment marketplace for medical providers; Acuity is a white-label booking tool better for any service business that wants to control their own booking experience.
ScheduleOnce (now Oncehub) specializes in multi-person scheduling and sales qualification workflows; Acuity is better for direct service business booking where clients schedule with specific service providers.
Frequently asked questions
What is the fastest way to add booking to a Bolt app — iframe widget or API?
The iframe widget is fastest by far. Embed Acuity's scheduling widget as an iframe using your Owner ID in the URL — no API setup required, no code for availability or booking logic, and it works in the Bolt WebContainer preview immediately. Use the custom API approach only when you need complete design control or deep integration with your app's data model.
Can I use the Acuity API in Bolt's WebContainer preview without deploying?
Yes — outbound Acuity API calls work in Bolt's WebContainer preview. You can fetch appointment types, check availability, and create bookings during development. The iframe widget also works in preview since it loads an external URL. The only Acuity feature requiring deployment is webhook callbacks, since those are incoming POST requests the WebContainer cannot receive.
Does Bolt.new have a native Acuity Scheduling integration?
No — Bolt.new does not include a built-in Acuity connector. The integration uses Acuity's REST API with basic authentication or the iframe embed approach. Bolt's AI can generate the integration code or the iframe widget component from a description of your booking requirements.
What Acuity plan do I need for API access?
API access is available on the Emerging plan ($16/month) and above. The free trial also includes API access for testing. If you are on the legacy free plan, you may not have API access — check Business Settings → Integrations to see if the API section appears. The iframe widget embed works on all plans since it just links to your public Acuity scheduling page.
Can I pre-fill the booking form with user information from my Bolt app?
Yes — both the iframe widget and the custom booking flow support pre-filling. For the iframe, append firstName, lastName, and email as URL parameters to the widget src. For the custom API booking flow, pre-populate the form fields in your React component using values from your app's authentication context or user profile, then pass them in the POST /appointments request body.
How do I test Acuity webhooks during development?
Webhooks require deployment since the Bolt WebContainer preview cannot receive incoming POST requests from Acuity's servers. Deploy to Netlify or Bolt Cloud first, register the deployed webhook URL in Acuity Business Settings → Integrations → Webhooks, and create a test appointment to trigger the webhook. Alternatively, use a service like ngrok to create a temporary public tunnel to your local development server for webhook testing, though this requires running locally rather than in Bolt's WebContainer.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation