# Acuity Scheduling

- Tool: Bubble
- Difficulty: Intermediate
- Time required: 2–3 hours
- Last updated: July 2026

## TL;DR

Connect Bubble to Acuity Scheduling using the API Connector with HTTP Basic auth. Base64-encode your numeric User ID and API key together (not your email — this is the #1 mistake) and place the result in a Private Authorization header. API access requires a paid Acuity plan. Build booking flows with appointment types, availability slots, and appointment creation — plus optional webhook notifications via a Backend Workflow.

## Building a Custom Booking UI in Bubble with Acuity's API

Acuity Scheduling is the most popular appointment-booking layer for Bubble service apps. Salons, coaches, and consultants often want a booking experience that matches their Bubble app's branding rather than sending clients to Acuity's generic scheduling page. The Acuity REST API v1 makes this possible — you build the UI in Bubble and Acuity handles the calendar logic, availability calculations, and booking confirmation emails behind the scenes.

The first thing to understand about Acuity's API is what 'User ID' means. It is NOT your email address or login credential. It is a numeric account identifier that Acuity assigns to your business account — typically a 7-9 digit number. Many first-time builders enter their email as the username in Basic auth and get a 401 on every call with no explanation of what went wrong. The User ID is found only in Business Settings → Integrations → API.

The second thing to understand is the plan requirement. Acuity's API access is gated behind the Emerging Entrepreneur plan or higher. If you or your client is on the free plan or the basic 'Growing' plan, every API call returns 403 Forbidden with no error message explaining the plan restriction. Budget for an Acuity upgrade before starting the integration.

Once auth is working, the booking flow has three steps: (1) show the client appointment types to choose from, (2) show available time slots for the chosen type and date, and (3) submit the appointment. Availability responses are timezone-sensitive — Acuity returns times in the calendar's configured timezone, not UTC. Your Bubble app should pass the user's local timezone in the API call parameters to get correctly localized slot times.

## Before you start

- An Acuity Scheduling account on the Emerging Entrepreneur plan or higher — API access is not available on free or lower-tier plans
- Your Acuity User ID (a numeric account ID from Business Settings → Integrations → API) and API key from the same location
- Bubble app on any plan for read-only and booking flows; a paid Bubble plan for Backend Workflow webhook endpoints
- Bubble's API Connector plugin installed (Plugins tab → Add plugins → search 'API Connector' → Install)
- At least one appointment type configured in Acuity — the API returns no useful data without at least one appointment type set up

## Step-by-step guide

### 1. Get Your Acuity User ID and API Key

In Acuity Scheduling, go to Business Settings (the gear icon in the top navigation). In the left sidebar, click 'Integrations'. Scroll down to find the 'API' section — it may be under a 'Developers' or 'Custom Integrations' heading depending on your Acuity plan interface.

In the API section, you will see two values:
- **User ID**: a numeric account identifier (example: 1234567). This is NOT your email address, login name, or any identifier you have created. It is assigned by Acuity and is unique to your account.
- **API Key**: a long alphanumeric string.

Copy both values. Now you need to Base64-encode them together in the format `userId:apiKey`. Open your browser's developer console (right-click anywhere → Inspect → Console tab) and type:

`btoa('1234567:YOUR_API_KEY_HERE')`

Replace `1234567` with your actual numeric User ID and `YOUR_API_KEY_HERE` with your actual key. Copy the resulting Base64 string.

If you cannot find the API section in Business Settings, your Acuity plan does not include API access. Upgrade to the Emerging Entrepreneur plan before continuing.

```
// In browser console — combine userId (number) with apiKey
btoa('1234567:abcdef0123456789abcdef0123456789')
// Returns: 'MTIzNDU2NzphYmNkZWYwMTIzNDU2Nzg5YWJjZGVmMDEyMzQ1Njc4OQ=='

// Authorization header value:
// Basic MTIzNDU2NzphYmNkZWYwMTIzNDU2Nzg5YWJjZGVmMDEyMzQ1Njc4OQ==

// Common mistakes:
// Using email instead of numeric User ID → 401 on every call
// Encoding userId and apiKey separately → 401 on every call
// Leaving out the colon separator → 401 on every call
```

**Expected result:** You have a numeric Acuity User ID, an API key, and a Base64-encoded string combining them. All three values are saved securely.

### 2. Configure the API Connector with a Private Basic Auth Header

In your Bubble app, go to the Plugins tab. Click the API Connector plugin. Click 'Add another API'. Name it 'Acuity Scheduling'.

Click 'Add shared headers / parameters'. Add a header:
- Key: `Authorization`
- Value: `Basic [YOUR_BASE64_STRING]` — replace [YOUR_BASE64_STRING] with the Base64 string from Step 1
- Tick the 'Private' checkbox

The Private checkbox prevents the Authorization header value from being exposed in Bubble's network requests or accessible through client-side workflows.

Set the API root URL to `https://acuityscheduling.com/api/v1`.

Also add an `Accept: application/json` header to ensure Acuity returns JSON responses (this is the default, but being explicit avoids edge cases).

After saving the API group configuration, you are ready to add individual API calls for appointment types, availability, and booking creation.

```
{
  "api_name": "Acuity Scheduling",
  "root_url": "https://acuityscheduling.com/api/v1",
  "shared_headers": [
    {
      "key": "Authorization",
      "value": "Basic <private — Base64(userId:apiKey)>",
      "private": true
    },
    {
      "key": "Accept",
      "value": "application/json"
    }
  ]
}
```

**Expected result:** The Acuity Scheduling API group is created with the Private Authorization header configured. You can now add individual call definitions.

### 3. Fetch Appointment Types and Initialize the API Connection

Add your first call inside the Acuity API group. Click 'Add another call'. Name it 'Get Me' (a simple account info call used to verify credentials). Set Method to GET and path to `/me`. Click 'Initialize call'. If you see account information returned (email, business name, timezone), your auth is working correctly.

Now add the main data call. Click 'Add another call'. Name it 'Get Appointment Types'. Set Method to GET and path to `/appointment-types`. Set 'Use as' to 'Data'. Click 'Initialize call'.

The response is an array of appointment type objects. Each type has:
- `id` (number) — the appointment type ID used when fetching availability
- `name` (string) — human-readable name ('60-Minute Coaching Session')
- `duration` (number) — in minutes
- `price` (string) — as a string with currency symbol
- `description` (string)
- `color` (string)

In your Bubble UI, bind a Dropdown element's data source to the 'Get Appointment Types' API call. Set the label to the `name` field and the value to the `id` field. This gives the user a selector that returns the appointment type ID needed for the next step.

Create a Bubble Data Type called 'AcuityAppointmentType' with fields: `type_id` (number), `type_name` (text), `duration` (number), `price` (text). Populate it on app initialization to enable filtering and display without repeated API calls.

```
// GET /appointment-types — sample response
[
  {
    "id": 1234,
    "name": "60-Minute Coaching Session",
    "duration": 60,
    "price": "$150.00",
    "description": "One-on-one strategy session with your coach.",
    "color": "#d4b8e0",
    "category": "Coaching"
  },
  {
    "id": 5678,
    "name": "30-Minute Consultation",
    "duration": 30,
    "price": "$0.00",
    "description": "Free introductory call.",
    "color": "#a3d4b8",
    "category": "Consultations"
  }
]
```

**Expected result:** The Get Me call returns your account information. The Get Appointment Types call returns your configured appointment types. A Bubble Dropdown populated with this data shows the appointment type names and returns the numeric ID when selected.

### 4. Fetch Available Time Slots for a Date and Appointment Type

Add a new call. Name it 'Get Availability'. Set Method to GET and path to `/availability/times`. Add the following query parameters (these will be dynamically passed from the Bubble workflow):
- `appointmentTypeID` — the numeric ID from the appointment type dropdown (required)
- `date` — the date in YYYY-MM-DD format (required)
- `calendarID` — optional; target a specific staff calendar if you have multiple
- `timezone` — the client's local timezone (e.g., 'America/New_York'); required for correct slot display

Set 'Use as' to 'Data'. For the initialize call, provide real values: a valid `appointmentTypeID` from your account and today's date in YYYY-MM-DD format. If no slots are available today, try a day next week. Click 'Initialize call'.

The response is an array of available time slot objects. Each has:
- `time` — ISO 8601 datetime string in the calendar's timezone
- `slotsAvailable` — number of open slots at this time

In Bubble, display these slots in a Repeating Group. Format the `time` field using Bubble's date formatting options to show it in a user-friendly way ('Monday, July 14 at 2:00 PM'). When a user clicks a slot, store the `time` value in a Custom State — you will pass it to the appointment creation call.

Timezone awareness is critical here. Acuity returns times in the calendar's configured timezone. Pass the user's browser timezone as the `timezone` parameter to align the displayed times with the user's local time. In Bubble, you can access the user's timezone via a 'Run JavaScript' action using `Intl.DateTimeFormat().resolvedOptions().timeZone`.

RapidDev has built multi-timezone booking systems on Bubble with Acuity — if timezone handling for a global audience is a requirement, reach out at rapidevelopers.com/contact for a free scoping call.

```
// GET /availability/times — query parameters
// ?appointmentTypeID=1234&date=2026-07-14&timezone=America%2FNew_York

// Sample response
[
  {
    "time": "2026-07-14T09:00:00-0400",
    "slotsAvailable": 1
  },
  {
    "time": "2026-07-14T10:00:00-0400",
    "slotsAvailable": 1
  },
  {
    "time": "2026-07-14T14:00:00-0400",
    "slotsAvailable": 1
  }
]

// Timezone parameter examples:
// America/New_York
// America/Los_Angeles
// Europe/London
// Asia/Tokyo
```

**Expected result:** The Get Availability call initializes successfully. A Repeating Group bound to this call shows available time slots for the selected appointment type and date. Clicking a slot stores the time value in a Custom State.

### 5. Create an Appointment from the Booking Form

Add a new call. Name it 'Create Appointment'. Set Method to POST and path to `/appointments`. Set 'Use as' to 'Action' — this call is triggered by a button click, not used as a data source. Add `Content-Type: application/json` as a header on this specific call.

In the body section, construct a JSON object with the required fields:
- `appointmentTypeID` (number) — the selected appointment type ID
- `datetime` (string) — the ISO 8601 datetime from the selected availability slot
- `firstName` (string) — from the Bubble form field
- `lastName` (string) — from the Bubble form field
- `email` (string) — from the Bubble form field

Optional but recommended fields:
- `phone` (string)
- `notes` (string) — any intake form data
- `timezone` (string) — the client's timezone

Click 'Initialize call'. For the test, provide real values in all fields — a real datetime from the availability response (this will create a real test appointment in Acuity). After testing, cancel the test appointment in Acuity's calendar.

In the Bubble workflow editor, wire a 'Book Appointment' button to trigger the Create Appointment action. Dynamically pass:
- The appointment type ID from the Custom State holding the selected type
- The datetime from the Custom State holding the selected slot
- The values from the name and email form fields

On successful response (Acuity returns the created appointment object with an `id` field), store the appointment ID in a Bubble Data Type for future reference (viewing, cancelling). Show a confirmation message to the user.

```
// POST /appointments — request body
{
  "appointmentTypeID": 1234,
  "datetime": "2026-07-14T10:00:00-0400",
  "firstName": "<dynamic: first_name_field>",
  "lastName": "<dynamic: last_name_field>",
  "email": "<dynamic: email_field>",
  "phone": "<dynamic: phone_field>",
  "timezone": "<dynamic: user_timezone>",
  "notes": "<dynamic: notes_field>"
}

// Success response (201 Created)
{
  "id": 987654,
  "firstName": "Jane",
  "lastName": "Smith",
  "email": "jane@example.com",
  "date": "July 14, 2026",
  "time": "10:00am",
  "endTime": "11:00am",
  "type": "60-Minute Coaching Session",
  "confirmationPage": "https://acuityscheduling.com/schedule.php?owner=..."
}
```

**Expected result:** Submitting the Bubble booking form triggers the Create Appointment action. Acuity creates the appointment and returns a 201 response with the appointment details. The appointment appears in Acuity's calendar. Acuity sends a confirmation email to the client automatically.

### 6. Optionally Set Up Webhook Notifications for Booking Events

If you want your Bubble app to know about appointments booked through any channel (including Acuity's own scheduling page, not just your Bubble form), configure Acuity webhooks.

In Bubble, go to Settings → API → enable 'This app exposes a Workflow API'. Then go to Backend Workflows → New API Workflow. Name it 'Acuity Webhook'. Under the workflow definition, click 'Detect request data' to auto-detect the fields from an incoming Acuity webhook payload.

Your Backend Workflow endpoint URL will be: `https://YOUR_APP_NAME.bubbleapps.io/api/1.1/wf/acuity-webhook`

In Acuity, go to Business Settings → Integrations → API → Webhooks. Click 'Add new webhook'. Enter your Bubble Backend Workflow URL and select the event types you want to subscribe to:
- `appointment.scheduled` — when any appointment is created
- `appointment.rescheduled` — when an appointment is moved
- `appointment.canceled` — when an appointment is cancelled

Save, then use Acuity's 'Test' button to send a sample payload. In Bubble's Logs tab → Server logs, verify the Backend Workflow received and processed the payload. Add workflow actions to create or update an 'AcuityAppointment' Data Type record based on the incoming data.

Note: Backend Workflows require a paid Bubble plan. On the Free plan, skip this step and rely on polling GET /appointments (with email or date filters) to detect new bookings — but be mindful of Acuity's rate limit recommendation of no more than one request per second.

```
// Acuity webhook payload (appointment.scheduled)
{
  "action": "scheduled",
  "id": 987654,
  "calendarID": 111,
  "appointmentTypeID": 1234,
  "firstName": "Jane",
  "lastName": "Smith",
  "email": "jane@example.com",
  "phone": "+1 555-0100",
  "date": "July 14, 2026",
  "time": "10:00am",
  "endTime": "11:00am",
  "timezone": "America/New_York",
  "type": "60-Minute Coaching Session"
}

// Supported event types for subscriptions:
// appointment.scheduled
// appointment.rescheduled
// appointment.canceled
// appointment.changed
// order.completed
```

**Expected result:** Acuity sends webhook events to the Bubble Backend Workflow endpoint. When a client books through any channel, the Bubble app receives the notification and updates its local appointment data.

## Best practices

- Always use the numeric User ID from Acuity Business Settings → Integrations → API as the username in Basic auth. Never use your email address or any human-readable identifier.
- Mark the Authorization header Private in the API Connector. This prevents the Base64-encoded credentials from being exposed in client-side network requests, even to logged-in users.
- Always pass the `timezone` parameter in GET /availability/times requests. Without it, availability times are returned in the calendar's configured timezone, which will display incorrectly for users in other time zones.
- Cache appointment types in a Bubble Data Type on app initialization rather than calling GET /appointment-types on every page load. Appointment types change rarely and caching saves API calls.
- Do not poll GET /appointments more than once per second. Acuity's documentation recommends this as a courtesy rate limit. For real-time booking notification, use webhooks with a Backend Workflow instead of polling.
- Set up Backend Workflow webhook endpoints using your published app URL, not the Bubble preview URL. Webhooks sent to a preview URL will fail silently after publication.
- Apply Bubble Privacy rules to any Data Type storing appointment data (client names, email addresses, phone numbers). These are PII — restrict read access to the logged-in user or admin roles only.
- Confirm your client's Acuity plan includes API access before building. A 403 error on every call with no explanation is the first signal that a plan upgrade is needed.

## Use cases

### Branded Booking Flow Inside a Bubble App

Replace the generic Acuity scheduling page with a fully branded booking experience built in Bubble. Users select an appointment type, choose a date and available time slot, fill in their name and email, and submit — all without leaving your Bubble app's design.

Prompt example:

```
How do I build a multi-step Bubble booking flow that shows Acuity appointment types, filters by date to show available slots, and submits a booking via POST /appointments?
```

### Client Dashboard with Upcoming Appointments

Display a logged-in client's upcoming appointments in a Bubble dashboard by filtering GET /appointments by email. Let clients see appointment details, cancel, and reschedule — with changes synced back to Acuity via the API.

Prompt example:

```
How do I show a Bubble user their upcoming Acuity appointments filtered by their email address, and let them cancel using the DELETE /appointments/{id} endpoint?
```

### Real-Time Booking Notification via Webhooks

Configure a Backend Workflow endpoint to receive Acuity's appointment.scheduled webhook. When a client books through any channel (Acuity's page or your Bubble UI), the Bubble app is notified instantly and creates a local appointment record, triggers a custom confirmation email, or assigns a staff member.

Prompt example:

```
How do I set up a Bubble Backend Workflow to receive Acuity's appointment.scheduled webhook and create a local Appointment record in Bubble's database?
```

## Troubleshooting

### 401 Unauthorized on every API call, even after setting up the Authorization header

Cause: The most common cause is using your Acuity email address as the 'username' in Basic auth instead of the numeric User ID. A second cause is encoding the User ID and API key separately instead of together.

Solution: Verify you are using the numeric User ID from Business Settings → Integrations → API (a 7-9 digit number like 1234567), not your email address. In the browser console, run `btoa('1234567:YOUR_API_KEY')` with the actual numeric ID and actual API key separated by a colon. Update the Authorization header with `Basic [result]`.

```
// Correct (numeric userId + apiKey together)
btoa('1234567:abcdef0123456789')
// Header: Basic MTIzNDU2NzphYmNkZWYwMTIzNDU2Nzg5

// Incorrect (email as username)
btoa('user@example.com:apikey') // WRONG — returns 401
```

### 403 Forbidden on every API call even with correct credentials

Cause: Your Acuity plan does not include API access. The Acuity API requires the Emerging Entrepreneur plan or higher. Free accounts and lower-tier paid plans receive 403 with no explanation of the plan restriction.

Solution: Log into Acuity and check your current plan under Account Settings → Billing. Upgrade to Emerging Entrepreneur or a higher plan to unlock API access. The 403 error will stop immediately once the plan upgrade takes effect.

### GET /availability/times returns an empty array even for future dates

Cause: Two common causes: (1) the Acuity calendar does not have business hours or availability configured for the requested date and appointment type, or (2) the `appointmentTypeID` parameter is wrong (perhaps a numeric ID was not passed — only a name string).

Solution: Open Acuity's calendar view and verify that your calendar has availability set up for the date you are testing. Confirm the `appointmentTypeID` is the numeric ID (from GET /appointment-types), not the name string. Check that the appointment type is assigned to the calendar you are querying.

### 'There was an issue setting up your call' during API Connector initialization

Cause: The initialize call did not receive a valid successful response. For GET /appointment-types, this means no appointment types exist in Acuity. For GET /availability/times, no slots are available for the provided date and type.

Solution: First, log into Acuity and create at least one appointment type and configure availability in the calendar. Then return to the API Connector and initialize with a date that has confirmed availability. For POST calls, use real valid values (not placeholders) during the initialize step.

### Appointment times are displayed in the wrong timezone for users

Cause: Acuity returns availability times in the calendar's configured timezone, not UTC. If the `timezone` parameter is not passed in the GET /availability/times call, or if the Bubble app displays the raw time string without conversion, users in different timezones see incorrect times.

Solution: Pass the user's local timezone as the `timezone` query parameter in the Get Availability call. In Bubble, capture the user's timezone using a Run JavaScript action with `Intl.DateTimeFormat().resolvedOptions().timeZone` and store it in a Custom State before making the availability call.

```
// JavaScript to get user's local timezone (Toolbox plugin → Run JavaScript)
var timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
bubble_fn_timezone(timezone); // send to Bubble Custom State
```

## Frequently asked questions

### What is the Acuity User ID and where do I find it?

The Acuity User ID is a numeric account identifier — typically a 7-9 digit number like 1234567. It is NOT your email address. You find it in Acuity Business Settings → Integrations → API. This numeric ID is used as the 'username' part of HTTP Basic auth, combined with your API key as `userId:apiKey` before Base64 encoding.

### Why am I getting 403 Forbidden even though my credentials are correct?

A 403 error on valid Acuity credentials almost always means your account is on a plan that does not include API access. Acuity requires the Emerging Entrepreneur plan or higher for REST API access. The 403 response does not include a message about the plan restriction — check your plan under Account Settings → Billing and upgrade if needed.

### Do I need a paid Bubble plan to use this integration?

For read-only and booking flows (fetching appointment types, availability, and creating appointments), any Bubble plan works. You only need a paid Bubble plan if you want to receive Acuity webhook events — those require a Backend Workflow endpoint that is gated behind Bubble's paid plans.

### Why are appointment slots showing in the wrong timezone?

Acuity returns availability times in the timezone configured for your Acuity calendar, not UTC. If you do not pass the `timezone` query parameter to GET /availability/times, or if you display the raw time string without timezone conversion, users in different timezones see incorrect times. Always pass the user's local timezone (detectable via JavaScript: `Intl.DateTimeFormat().resolvedOptions().timeZone`) as the `timezone` parameter.

### Can clients cancel or reschedule appointments through my Bubble app?

Yes. Use DELETE /appointments/{id} to cancel an appointment (triggers Acuity's cancellation email) and PUT /appointments/{id} to reschedule (requires a new datetime from the availability slots). Store the Acuity appointment ID in your Bubble Data Type when an appointment is created, then reference it in cancel/reschedule workflow actions.

### How do I handle multiple staff calendars in Acuity?

If your Acuity account has multiple calendars (for multiple staff members), use the `calendarID` parameter in GET /availability/times to filter availability to a specific calendar. First call GET /calendars to get the list of calendar IDs, then let the user select a staff member before fetching availability for their specific calendar.

---

Source: https://www.rapidevelopers.com/bubble-integrations/acuity-scheduling
© RapidDev — https://www.rapidevelopers.com/bubble-integrations/acuity-scheduling
