# How to Integrate Replit with Acuity Scheduling

- Tool: Replit
- Difficulty: Intermediate
- Time required: 30 minutes
- Last updated: March 2026

## TL;DR

To integrate Replit with Acuity Scheduling, generate an API key from your Acuity account, store your User ID and API key in Replit Secrets (lock icon 🔒), and call the Acuity Scheduling API from Python or Node.js server-side code to manage appointments, availability, and client records. Deploy with Autoscale for webhook-driven booking workflows.

## Why Connect Replit to Acuity Scheduling?

Acuity Scheduling powers appointment booking for thousands of service businesses — therapists, coaches, salons, tutors, and consultants. Its API gives you programmatic control over availability, bookings, and client data, enabling you to build custom booking flows, sync appointments with your own database, or automate follow-up communications from a Replit backend.

The most common integration patterns are embedding custom booking logic inside a larger web application, syncing Acuity appointments to an external CRM or database, and triggering automated messages (SMS, email) when appointments are booked or cancelled. The Acuity API v1 covers all appointment types, appointment types, calendar availability, and client management with a straightforward REST interface.

Replit's Secrets system (lock icon 🔒 in the sidebar) keeps your Acuity API key encrypted and out of your codebase. Because the API key grants access to all appointments and client data in your Acuity account, treat it like a database password — never commit it to code or expose it on the client side. All Acuity API calls should originate from your server-side backend running in Replit.

## Before you start

- A Replit account with a Python or Node.js project created
- An Acuity Scheduling account (free trial available, paid plan required for API access)
- At least one appointment type created in your Acuity account
- Basic familiarity with REST APIs and HTTP Basic Authentication
- Node.js 18+ or Python 3.10+ (both available on Replit by default)

## Step-by-step guide

### 1. Find Your Acuity User ID and Generate an API Key

Log in to your Acuity Scheduling account and navigate to Integrations in the left sidebar. Scroll down to find the section labeled 'API' or search for 'API Integrations'. Click on 'API Credentials'. On this page you will see your User ID — a numeric value like 12345678 — and an option to generate an API key.

Click 'Generate API Key' if you have not already created one. The API key is a long alphanumeric string. Copy both your User ID and API key immediately — Acuity only shows the full API key once after generation.

Note that Acuity API access requires a paid plan. If you are on the free trial, you may have limited API access. The full API including webhooks and calendar management requires the Powerhouse plan or higher. Check your plan in Acuity under Account > Billing to confirm API access is enabled.

You will also want to note your appointment type IDs. In Acuity, go to Appointment Types and click on any type — the ID appears in the URL as a number. These IDs are used when querying availability for specific appointment types.

**Expected result:** You have your Acuity User ID (numeric) and API key copied and ready to store in Replit Secrets.

### 2. Store Acuity Credentials in Replit Secrets

Open your Replit project and click the lock icon 🔒 in the left sidebar to open the Secrets pane. Add the following secrets using the 'Add a new secret' form:

- Key: ACUITY_USER_ID — Value: your numeric Acuity User ID (e.g., 12345678)
- Key: ACUITY_API_KEY — Value: your full Acuity API key

Click 'Add Secret' after each entry. Replit encrypts these values with AES-256 encryption at rest and injects them as environment variables at runtime. They are never visible in your file tree or committed to Git.

In Python, access these with os.environ['ACUITY_USER_ID'] and os.environ['ACUITY_API_KEY']. In Node.js, use process.env.ACUITY_USER_ID and process.env.ACUITY_API_KEY. Do not use Deno.env.get() — that pattern is specific to Supabase Edge Functions and does not work in Replit's Node.js environment.

Replit's Secret Scanner automatically flags any API keys accidentally written to code files. If you paste an API key directly into your source code by mistake, Replit will warn you and prompt you to move it to Secrets instead.

**Expected result:** Two Secrets (ACUITY_USER_ID and ACUITY_API_KEY) appear in the Replit Secrets pane, each showing a masked value.

### 3. Query Availability and Create Appointments with Python

The Acuity Scheduling API v1 uses HTTP Basic Authentication where the username is your User ID and the password is your API key. The base URL is https://acuityscheduling.com/api/v1/. No official Python SDK is available, so you will use the standard requests library, which comes pre-installed on Replit.

The workflow for booking an appointment has three steps: first fetch available dates for an appointment type, then fetch available times for a specific date, and finally create the appointment with the client's details. The code below implements all three steps along with a function to list upcoming appointments and cancel them.

Note that appointment type IDs are numeric integers in Acuity — not strings. The calendarID field in the appointments endpoint refers to the staff member or resource calendar. If you only have one calendar in your account, you can fetch the calendar ID from the /calendars endpoint and use it directly.

```
import os
import requests
from datetime import date, timedelta

USER_ID = os.environ["ACUITY_USER_ID"]
API_KEY = os.environ["ACUITY_API_KEY"]
BASE_URL = "https://acuityscheduling.com/api/v1"

# Basic Auth: User ID as username, API key as password
AUTH = (USER_ID, API_KEY)

def get_appointment_types() -> list:
    """Fetch all active appointment types from the Acuity account."""
    response = requests.get(f"{BASE_URL}/appointment-types", auth=AUTH)
    response.raise_for_status()
    return response.json()

def get_available_dates(appointment_type_id: int, months: int = 1) -> list:
    """Get dates with available slots for the next N months."""
    today = date.today()
    end_date = today + timedelta(days=30 * months)
    params = {
        "appointmentTypeID": appointment_type_id,
        "month": today.strftime("%Y-%m")
    }
    response = requests.get(f"{BASE_URL}/availability/dates", params=params, auth=AUTH)
    response.raise_for_status()
    return response.json()

def get_available_times(appointment_type_id: int, date_str: str) -> list:
    """Get available time slots for a specific date. date_str format: YYYY-MM-DD"""
    params = {
        "appointmentTypeID": appointment_type_id,
        "date": date_str
    }
    response = requests.get(f"{BASE_URL}/availability/times", params=params, auth=AUTH)
    response.raise_for_status()
    return response.json()

def create_appointment(
    appointment_type_id: int,
    datetime_str: str,
    first_name: str,
    last_name: str,
    email: str,
    phone: str = ""
) -> dict:
    """
    Book an appointment.
    datetime_str format: '2026-04-15T14:00:00-0500'
    """
    data = {
        "appointmentTypeID": appointment_type_id,
        "datetime": datetime_str,
        "firstName": first_name,
        "lastName": last_name,
        "email": email,
        "phone": phone
    }
    response = requests.post(f"{BASE_URL}/appointments", json=data, auth=AUTH)
    response.raise_for_status()
    return response.json()

def get_upcoming_appointments(max_results: int = 20) -> list:
    """Fetch upcoming appointments sorted by start time."""
    params = {"max": max_results, "direction": "ASC"}
    response = requests.get(f"{BASE_URL}/appointments", params=params, auth=AUTH)
    response.raise_for_status()
    return response.json()

def cancel_appointment(appointment_id: int) -> bool:
    """Cancel an appointment by ID."""
    response = requests.delete(f"{BASE_URL}/appointments/{appointment_id}", auth=AUTH)
    return response.status_code == 200

# Example usage
if __name__ == "__main__":
    # List appointment types
    types = get_appointment_types()
    print("Appointment types:")
    for t in types:
        print(f"  [{t['id']}] {t['name']} — {t['duration']} min")

    if types:
        type_id = types[0]["id"]
        # Get available dates this month
        dates = get_available_dates(type_id)
        print(f"\nAvailable dates: {[d['date'] for d in dates[:5]]}")

        if dates:
            # Get times on the first available date
            times = get_available_times(type_id, dates[0]["date"])
            print(f"Available times on {dates[0]['date']}: {[t['time'] for t in times[:3]]}")
```

**Expected result:** Running the script prints your appointment types with their IDs, available dates for the first type, and the first few available time slots on the nearest available date.

### 4. Build a Node.js Booking API with Express

For Node.js projects, use the built-in https module or the popular axios package (npm install axios) to call the Acuity API. The Express server below exposes REST endpoints for your frontend to check availability and create bookings, with all Acuity credentials kept server-side in Replit Secrets.

The server implements four endpoints: GET /appointment-types to list available service types, GET /availability to check open slots for a given date, POST /appointments to book an appointment, and DELETE /appointments/:id to cancel. This server-side proxy pattern is important because it prevents your Acuity User ID and API key from being exposed to browser clients.

Install dependencies by running npm install express axios in the Replit Shell, or add them to package.json and let Replit install on startup. The .replit config file should set the run command to node server.js.

```
const express = require('express');
const axios = require('axios');

const app = express();
app.use(express.json());

const ACUITY_USER_ID = process.env.ACUITY_USER_ID;
const ACUITY_API_KEY = process.env.ACUITY_API_KEY;
const BASE_URL = 'https://acuityscheduling.com/api/v1';

// Axios instance with Basic Auth pre-configured
const acuity = axios.create({
  baseURL: BASE_URL,
  auth: {
    username: ACUITY_USER_ID,
    password: ACUITY_API_KEY
  }
});

// List all appointment types
app.get('/appointment-types', async (req, res) => {
  try {
    const response = await acuity.get('/appointment-types');
    res.json(response.data);
  } catch (err) {
    console.error('Acuity error:', err.response?.data);
    res.status(err.response?.status || 500).json({ error: err.message });
  }
});

// Check available times for an appointment type on a date
// Query params: appointmentTypeID, date (YYYY-MM-DD)
app.get('/availability', async (req, res) => {
  const { appointmentTypeID, date } = req.query;
  if (!appointmentTypeID || !date) {
    return res.status(400).json({ error: 'appointmentTypeID and date are required' });
  }
  try {
    const response = await acuity.get('/availability/times', {
      params: { appointmentTypeID, date }
    });
    res.json(response.data);
  } catch (err) {
    res.status(err.response?.status || 500).json({ error: err.message });
  }
});

// Book an appointment
app.post('/appointments', async (req, res) => {
  const { appointmentTypeID, datetime, firstName, lastName, email, phone } = req.body;
  if (!appointmentTypeID || !datetime || !firstName || !email) {
    return res.status(400).json({ error: 'appointmentTypeID, datetime, firstName, and email are required' });
  }
  try {
    const response = await acuity.post('/appointments', {
      appointmentTypeID,
      datetime,
      firstName,
      lastName: lastName || '',
      email,
      phone: phone || ''
    });
    res.status(201).json(response.data);
  } catch (err) {
    console.error('Booking error:', err.response?.data);
    res.status(err.response?.status || 500).json({
      error: err.response?.data?.message || err.message
    });
  }
});

// Cancel an appointment
app.delete('/appointments/:id', async (req, res) => {
  try {
    await acuity.delete(`/appointments/${req.params.id}`);
    res.json({ success: true, message: 'Appointment cancelled' });
  } catch (err) {
    res.status(err.response?.status || 500).json({ error: err.message });
  }
});

// Get upcoming appointments
app.get('/appointments', async (req, res) => {
  try {
    const response = await acuity.get('/appointments', {
      params: { max: req.query.max || 20, direction: 'ASC' }
    });
    res.json(response.data);
  } catch (err) {
    res.status(err.response?.status || 500).json({ error: err.message });
  }
});

app.listen(3000, '0.0.0.0', () => {
  console.log('Acuity Scheduling proxy server running on port 3000');
});
```

**Expected result:** The server starts on port 3000, and a GET request to /appointment-types returns a JSON array of your Acuity appointment types.

### 5. Set Up Acuity Webhooks and Deploy

Acuity Scheduling can send webhook notifications to your Replit app when appointments are scheduled, rescheduled, cancelled, or when a client is created or updated. This enables real-time workflows like sending custom confirmations, syncing with a CRM, or triggering downstream processes without polling the API.

To register a webhook in Acuity, go to Integrations in the sidebar, find the API section, and click 'Webhooks'. Enter your deployed Replit server URL plus the webhook path (e.g., https://your-app.replit.app/acuity/webhook). Select the event types you want: 'scheduled' (new booking), 'rescheduled', 'cancelled', and 'changed' cover most use cases.

Webhooks only work against a deployed Replit URL — not the development preview URL. Click the 'Deploy' button in Replit and choose Autoscale deployment for web applications that receive webhooks. Autoscale spins up instances to handle incoming requests and automatically scales back down during quiet periods. If your app must process webhooks 24/7 with no cold-start delay, choose Reserved VM instead.

Acuity does not send a verification challenge when registering webhooks — it simply starts sending POST requests immediately after you save the URL. Make sure your endpoint is deployed and accepting requests before registering.

```
from flask import Flask, request, jsonify
import os
import hmac
import hashlib

app = Flask(__name__)

# Acuity webhook secret (set in Acuity Integrations > API > Webhooks)
WEBHOOK_SECRET = os.environ.get("ACUITY_WEBHOOK_SECRET", "")

@app.route('/acuity/webhook', methods=['POST'])
def acuity_webhook():
    # Verify webhook signature if secret is configured
    if WEBHOOK_SECRET:
        signature = request.headers.get('X-Acuity-Signature', '')
        expected = hmac.new(
            WEBHOOK_SECRET.encode(),
            request.data,
            hashlib.sha256
        ).hexdigest()
        if not hmac.compare_digest(signature, expected):
            return jsonify({'error': 'Invalid signature'}), 401

    payload = request.json
    if not payload:
        return jsonify({'error': 'No payload'}), 400

    action = payload.get('action')  # 'scheduled', 'rescheduled', 'cancelled', 'changed'
    appointment = payload.get('appointment', {})

    appt_id = appointment.get('id')
    client_email = appointment.get('email')
    appt_type = appointment.get('type')
    appt_time = appointment.get('datetime')

    print(f"Acuity webhook: action={action}, id={appt_id}, client={client_email}")

    if action == 'scheduled':
        # New appointment booked — sync to database, send custom confirmation, etc.
        print(f"New booking: {appt_type} at {appt_time} for {client_email}")
    elif action == 'cancelled':
        # Appointment cancelled — update database, trigger refund workflow, etc.
        print(f"Cancelled: appointment {appt_id} for {client_email}")
    elif action == 'rescheduled':
        print(f"Rescheduled: appointment {appt_id} to {appt_time}")

    return jsonify({'received': True}), 200

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=3000)
```

**Expected result:** After deployment, Acuity can reach your webhook endpoint. New appointments trigger a POST to your server and appear in the console output.

## Best practices

- Always store ACUITY_USER_ID and ACUITY_API_KEY in Replit Secrets (lock icon 🔒) — never hardcode them or commit them to Git.
- Use a server-side proxy in Replit so frontend code never directly contacts the Acuity API, keeping your credentials hidden from browser clients.
- Validate appointment type IDs and datetime formats before sending booking requests — Acuity returns unhelpful 400 errors for format mismatches.
- Register a webhook ACUITY_WEBHOOK_SECRET in Acuity and verify the X-Acuity-Signature header on every incoming webhook event to prevent fake booking notifications.
- Deploy with Autoscale for web apps that handle bookings via a frontend; use Reserved VM if your app processes high-volume webhooks and cannot tolerate cold-start latency.
- Always call /availability/times immediately before presenting booking options to users — do not cache availability for more than a few minutes, as slots can fill quickly.
- Handle the 'That time is no longer available' 400 error gracefully in your UI by re-fetching availability and showing updated options rather than displaying a raw error message.
- Use the Acuity sandbox/test environment during development if available on your plan, to avoid creating real test appointments that affect your live calendar.

## Use cases

### Custom Booking Portal with Database Sync

Build a branded booking portal in Replit that shows real-time availability from Acuity, lets users book appointments, and simultaneously writes the booking record to your own PostgreSQL database. This gives you full control over the UI and allows you to store additional data fields that Acuity does not support natively.

Prompt example:

```
Build a Flask web app with a /availability endpoint that fetches open slots from the Acuity API for the next 7 days and a /book endpoint that creates an appointment in Acuity and saves it to a PostgreSQL database, using ACUITY_USER_ID and ACUITY_API_KEY from Replit Secrets.
```

### Automated Appointment Reminders

Use Acuity webhooks to trigger automated SMS or email reminders from your Replit app when an appointment is booked. When Acuity posts a webhook event to your server, your app reads the appointment details and sends a reminder via Twilio or SendGrid 24 hours before the scheduled time.

Prompt example:

```
Create an Express server with a POST /acuity/webhook endpoint that receives Acuity booking confirmation events, reads the appointment datetime and client email, and schedules a reminder email to be sent 24 hours before the appointment using SendGrid.
```

### Availability Analytics Dashboard

Pull appointment history and availability data from Acuity on a schedule to generate utilization reports — showing which time slots are booked most often, average lead time between booking and appointment, and cancellation rates. This data can inform pricing decisions and operating hours adjustments.

Prompt example:

```
Write a Python script that fetches all appointments from Acuity for the past 30 days using the Acuity API, groups them by day of week and time slot, and outputs a CSV report showing booking frequency and cancellation rate per slot.
```

## Troubleshooting

### API returns 401 Unauthorized when making requests

Cause: The User ID or API key stored in Replit Secrets is incorrect, or the Basic Auth credentials are being passed incorrectly. A common mistake is passing the User ID as a string when it must be used as the username in Basic Auth.

Solution: Verify your User ID and API key in Acuity under Integrations > API Credentials. In Replit Secrets, make sure ACUITY_USER_ID contains only the numeric ID (no quotes, no extra spaces). In Python, the Auth tuple is (USER_ID, API_KEY) where both are strings — if USER_ID is stored as an integer, convert it with str(os.environ['ACUITY_USER_ID']).

```
# Python: ensure User ID is a string for Basic Auth
import os
USER_ID = str(os.environ["ACUITY_USER_ID"])  # Convert to string explicitly
API_KEY = os.environ["ACUITY_API_KEY"]
AUTH = (USER_ID, API_KEY)
```

### GET /availability/times returns an empty array even for dates shown as available

Cause: The appointment type ID or date format is incorrect, or the calendar has no hours configured for that day. Acuity availability depends on the calendar's business hours, which must be set in the Acuity calendar settings.

Solution: Verify the appointment type ID is correct by calling /appointment-types first and checking the IDs. Confirm the date format is YYYY-MM-DD. In Acuity, go to Business Hours under your calendar settings and make sure hours are enabled for the day you are querying. Also check that the appointment type is not blocked by a time block on that date.

```
# Python: verify appointment type ID before querying availability
types = get_appointment_types()
print("Valid type IDs:", [t['id'] for t in types])
# Use the numeric ID from this list when calling get_available_times()
```

### Webhook events are never received by the Replit server

Cause: The webhook URL points to the Replit development server (which goes offline when the browser tab is closed) instead of the deployed app URL, or the Replit app is not deployed.

Solution: Click 'Deploy' in Replit and wait for the deployment to complete. Copy the deployed URL (ending in .replit.app) and update the webhook URL in Acuity under Integrations > API Credentials > Webhooks. Development preview URLs that contain 'replit.dev' are temporary and only work while the editor is open.

### POST /appointments returns 400 with 'That time is no longer available'

Cause: Another booking was made for the same time slot between when you fetched available times and when you submitted the booking. This race condition is expected behavior — availability is not held during the selection process.

Solution: Implement retry logic that re-fetches available times and presents updated options when a booking attempt fails with a 400 error. Communicate clearly to users that slot selection does not guarantee availability until the booking is confirmed.

```
# Python: retry booking with fresh availability on 400
import time

def book_with_retry(type_id, date_str, client_data, max_retries=2):
    for attempt in range(max_retries):
        times = get_available_times(type_id, date_str)
        if not times:
            return None, "No times available"
        try:
            return create_appointment(type_id, times[0]['time'], **client_data), None
        except Exception as e:
            if attempt < max_retries - 1:
                time.sleep(1)
            else:
                return None, str(e)
```

## Frequently asked questions

### How do I store my Acuity API key in Replit?

Click the lock icon 🔒 in the left sidebar of your Replit project to open the Secrets pane. Add ACUITY_USER_ID with your numeric Acuity User ID and ACUITY_API_KEY with your API key. Access them in Python with os.environ['ACUITY_API_KEY'] and in Node.js with process.env.ACUITY_API_KEY. Never paste credentials directly into your code files.

### Does Replit work with Acuity Scheduling on the free tier?

Replit's free tier supports outbound API calls, so you can call the Acuity API from a Replit project without a paid Replit plan. However, the Acuity API itself requires a paid Acuity plan — the free Acuity trial has limited API access. You will also need Replit's paid plan (Replit Core) for always-on deployments needed to receive Acuity webhooks reliably.

### How do I find my Acuity appointment type ID?

In Acuity, navigate to Appointment Types in the left sidebar and click on any appointment type. The ID appears as a number in the page URL (e.g., /appointments/types/12345678). Alternatively, call the /appointment-types endpoint via the API and read the 'id' field from each object in the response array.

### Can I book appointments on behalf of clients from Replit?

Yes. The Acuity API's POST /appointments endpoint lets you create appointments programmatically with any client email and details. This is called an 'owner booking' and uses your account credentials. The client receives the standard Acuity confirmation email unless you disable notifications in the appointment type settings.

### Why does Acuity return no available times even though the calendar is open?

Acuity availability depends on three things: business hours must be enabled for that day, the specific appointment type must be assigned to a calendar, and there must not be a blocking time event on that date. Check your calendar's Business Hours settings in Acuity and confirm the appointment type is linked to an active calendar. Also verify the date format in your API request is exactly YYYY-MM-DD.

### How do I handle Acuity webhooks in Replit?

Deploy your Replit app first to get a stable URL (ending in .replit.app), then register that URL plus your webhook path in Acuity under Integrations > API Credentials > Webhooks. Acuity sends JSON POST requests when appointments change. Use Flask's request.json or Express's express.json() middleware to parse the payload. Webhooks do not work against temporary development URLs.

---

Source: https://www.rapidevelopers.com/replit-integration/acuity-scheduling
© RapidDev — https://www.rapidevelopers.com/replit-integration/acuity-scheduling
