Developers

Put BarberFlow booking on your own website.

Your services, team, availability and bookings through a simple REST API. A booking made on your site gets the same confirmations and reminders as one made on your hosted booking page, and appears in your calendar, customer list and reports, credited to your website. In this version customers pay in person; when you want card checkout, your site hands off to your BarberFlow booking page.

How it works

Three steps, no developer needed.

If you are building your site with an AI coding tool, this is all it takes.

  1. Create a key

    In BarberFlow, open Settings → Developers and create a publishable key. Only the shop owner can create or revoke keys. Add your website's domain so the key only works from browsers on that domain.

  2. Give your AI tool the docs and the key

    Paste the prompt below into Lovable, v0, Cursor or Claude, with your key in place of bf_pk_your_publishable_key. The docs link hands the tool the whole API in one file.

  3. Publish

    Book a test appointment on your site, check that it shows up in your BarberFlow calendar, then publish. Cancel the test booking from BarberFlow when you are done.

The prompt

Prompt for your AI tool
Build a booking flow for my barbershop using the BarberFlow API.
Docs: https://api.barberflow.com/v1/llms-full.txt
My publishable key is bf_pk_your_publishable_key.
Pay in person only.
Show my locations, services, team and available times, then create the booking and show the confirmation.
If a location requires a verification code, sign the customer in with a one-time code before booking.
Quick start

Four calls to a real booking.

Plain fetch, no library. The base URL is https://api.barberflow.com/v1, everything is JSON, and money is in cents.

1. List your locations

Every request sends your publishable key as a bearer token. Only locations taking online bookings come back, each with its address, timezone, weekly hours, and bookingVerification (NONE, PHONE or EMAIL).

GET /locations
const API = 'https://api.barberflow.com/v1';
const headers = { Authorization: 'Bearer bf_pk_your_publishable_key' };

const locations = await fetch(`${API}/locations`, { headers }).then((res) => res.json());
const locationId = locations[0].id;

2. List the services at a location

Each service has an id to pass in serviceIds, a name, priceCents with currency "CAD", durationMinutes, and the barberIds who offer it. Add-ons are nested under the service they belong to.

GET /locations/{locationId}/services
const services = await fetch(`${API}/locations/${locationId}/services`, { headers }).then((res) =>
  res.json(),
);
const haircut = services.find((service) => !service.addOnOnly && service.allowPayInPerson);

3. Find open times

You get days, each with slots. A slot has startsAt (ISO 8601 with the location's offset), time (HH:mm in shop time, ready to display) and the barberIds free then. Leave out barberId to merge every barber. Ask for up to 14 days per call.

GET /locations/{locationId}/availability
const params = new URLSearchParams({
  serviceIds: haircut.id,
  from: '2026-10-20',
  to: '2026-10-26',
});

const availability = await fetch(`${API}/locations/${locationId}/availability?${params}`, {
  headers,
}).then((res) => res.json());

const slot = availability.days.find((day) => day.slots.length > 0)?.slots[0];

4. Create the booking

Mint one Idempotency-Key per booking attempt (letters, digits, - or _, up to 128) and reuse it only to retry that attempt: a retry with the same location, barber, services, time and email returns the same booking instead of a second one, and anything else is IDEMPOTENCY_KEY_REUSED. status is SCHEDULED, or PENDING_APPROVAL when the location approves bookings by hand; a retry reports where the booking stands now, such as CANCELLED. Keep manageToken to let the customer view, cancel or reschedule later, or link them to manageUrl.

POST /bookings
const idempotencyKey = crypto.randomUUID();

const res = await fetch(`${API}/bookings`, {
  method: 'POST',
  headers: {
    ...headers,
    'Content-Type': 'application/json',
    'Idempotency-Key': idempotencyKey,
  },
  body: JSON.stringify({
    locationId,
    barberId: slot.barberIds[0],
    serviceIds: [haircut.id],
    startsAt: slot.startsAt,
    customer: {
      name: 'Sam Lee',
      email: 'sam@example.com',
      phone: '+16475550123',
      smsMarketingConsent: false,
    },
  }),
});

if (!res.ok) {
  const { error } = await res.json();
  throw new Error(`${error.code}: ${error.message}`);
}

const { booking, status, manageToken, manageUrl } = await res.json();
Authentication

One key for your site, one code for your customers.

Your publishable key

Send it on every request as Authorization: Bearer bf_pk_…. The key identifies your shop, so no URL ever carries a shop id.

It is safe to put in browser code. Everything it can read or create is already public on your hosted booking page: your locations, services, team, open times, and pay-in-person bookings. What the key adds is control: it only works from the domains you allow, it has its own rate limits, bookings made with it are credited to your website in your reports, and you can revoke it at any time.

Allowed domains. List your site's domains on the key, such as https://thebarbershop.ca. A browser request from anywhere else is refused with ORIGIN_NOT_ALLOWED. Every key needs at least one domain: add http://localhost:3000 while you build locally, and your real domain before you publish.

Revoking. Revoke a key in Settings → Developers and it stops working at once, along with every customer token signed in through it. Create a new key and update your site.

Customer sign-in

Sign a customer in with a one-time code by text or email, at any of your locations. Some locations also ask customers to confirm their phone number or email before booking; there, use the method the location asks for. Send POST /verifications with the location, the method and the customer's contact (email is always required), then POST /verifications/{id}/confirm with the code they type. The code expires after five minutes, and only the key that sent it can confirm it.

You get back a customer token. Send it as X-Customer-Token. It lasts 30 days, works only for your shop, and only with the key that created it.

With it, a customer can book at locations that require verification, see their profile and bookings at your shop, and cancel or reschedule their own bookings. There is no password, and it is not a sign-in to their BarberFlow account: it never reaches their bookings at other shops or anything else in BarberFlow.

Manage tokens. Every new booking also returns a manageToken. Send it as X-Booking-Token to read, cancel or reschedule that one booking without signing in. It works until 30 days after the appointment.

Signing a customer in

POST /verifications
const verification = await fetch(`${API}/verifications`, {
  method: 'POST',
  headers: { ...headers, 'Content-Type': 'application/json' },
  body: JSON.stringify({
    locationId,
    method: 'PHONE',
    contact: { email: 'sam@example.com', phone: '+16475550123', name: 'Sam Lee' },
  }),
}).then((res) => res.json());

const { customerToken } = await fetch(`${API}/verifications/${verification.id}/confirm`, {
  method: 'POST',
  headers: { ...headers, 'Content-Type': 'application/json' },
  body: JSON.stringify({ code }),
}).then((res) => res.json());

const signedIn = { ...headers, 'X-Customer-Token': customerToken };
Endpoints

Everything in version 1.

Paths are relative to https://api.barberflow.com/v1. Times you send can be shop time (2026-10-20T14:30) or ISO 8601 with an offset.

BarberFlow API version 1 endpoints
MethodPathCredentialPurpose
GET/businessKeyShop name, logo, cancellation policy, payment options and the hosted booking link.
GET/locationsKeyBookable locations with address, time zone, weekly hours, verification setting and booking link.
GET/locations/{locationId}/servicesKeyServices with price in cents, currency, duration and the barbers who offer them; add-ons bookable at the location nested. Book an add-on with a barber listed in both its barberIds and the service’s.
GET/locations/{locationId}/teamKeyBookable barbers; serviceIds keeps only those who perform every listed service.
GET/locations/{locationId}/availabilityKey (+ customer token when verified)Open times by day for serviceIds, an optional barberId, and from–to dates (up to 14 days).
GET/locations/{locationId}/availability/nextKey (+ customer token when verified)The first open time for serviceIds and an optional barberId, searched up to the booking horizon, or null.
POST/verificationsKeySend a customer a one-time code by text or email.
POST/verifications/{id}/confirmKeyExchange the code for a 30-day customer token.
GET/meKey + customer tokenThe signed-in customer’s profile at your shop.
GET/me/bookingsKey + customer tokenTheir upcoming or past bookings at your shop, paged with a cursor.
POST/bookingsKey (+ customer token when the location verifies)Create a pay-in-person booking. Requires an Idempotency-Key header.
GET/bookings/{id}Key + customer or manage tokenThe booking, whether it can be cancelled or rescheduled, any late-cancellation fee, and the manage link. Opened with a manage token, the customer’s email and phone come back masked.
POST/bookings/{id}/cancelKey + customer or manage tokenCancel the booking, with an optional reason.
POST/bookings/{id}/rescheduleKey + customer or manage tokenMove the booking to a new startsAt, with an optional reason. A customer can reschedule once.
GET/openapi.jsonNoneThe OpenAPI spec for every endpoint.
GET/llms-full.txtNoneThe full reference as one Markdown file, written for AI coding tools.
Errors and limits

Every error looks the same.

Any response that is not a 2xx carries an error object with a stable code to branch on, a message you can show, and sometimes details. Branch on the endpoint's own codes first: VALIDATION_ERROR, NOT_FOUND, FORBIDDEN and CONFLICT can come back from any endpoint as fallbacks. The table lists the common codes; the full list is in llms-full.txt.

Error response
{
  "error": {
    "code": "SLOT_TAKEN",
    "message": "That time was just taken. Please choose another time."
  }
}
Error codes
CodeStatusWhat it means
VALIDATION_ERROR400The request is not valid. When a field is the problem, details.issues names it and says what is wrong.
RANGE_TOO_LARGE400The date range is longer than 14 days.
IDEMPOTENCY_KEY_REQUIRED400POST /bookings needs an Idempotency-Key header.
VERIFICATION_CODE_INVALID400The code is wrong. The customer can try again.
VERIFICATION_CODE_EXPIRED400The code has expired. Start a new verification.
API_KEY_REQUIRED401No key was sent in the Authorization header.
API_KEY_INVALID401The key is unknown or has been revoked.
CUSTOMER_TOKEN_INVALID401The customer token is invalid, expired, or was issued under another key. Verify the customer again.
ORIGIN_NOT_ALLOWED403The request came from a domain the key does not allow.
VERIFICATION_REQUIRED403The location needs a verified customer. details.method says whether the code goes by text or email.
VERIFICATION_TOO_MANY_ATTEMPTS403Too many wrong codes for this verification. Start a new one.
LOCATION_NOT_BOOKABLE404The location does not exist or is not open for online booking.
BOOKING_NOT_FOUND404No booking with that id for the credential you sent.
SLOT_TAKEN409Someone booked that time first. Fetch availability again.
IDEMPOTENCY_KEY_REUSED409This Idempotency-Key already made a different booking. Use a new key for each booking attempt.
PAY_IN_PERSON_NOT_ALLOWED409The shop, or one of the services, needs a card for this booking. Send the customer to details.bookingUrl.
BARBER_NOT_AT_STORE422The barber does not work at the location, is not taking online bookings, or does not do every service.
SERVICE_NOT_AVAILABLE422A service id is not an active service offered at this location. Fetch the services again.
OUTSIDE_WORKING_HOURS422The barber is not working at that time.
INVALID_SLOT_INTERVAL422The time is not on the shop’s slot grid. Use a slot from availability.
OUTSIDE_BOOKING_HORIZON422The time is further ahead than the shop takes bookings.
DATE_IN_PAST422The time has already passed.
RATE_LIMITED429Too many requests. Wait for the Retry-After seconds, then retry.
SERVICE_UNAVAILABLE503A safety check is briefly unavailable, so the request was refused. Retry in a moment.

Rate limits

Responses carry RateLimit-Limit, RateLimit-Remaining and RateLimit-Reset for the first limit the request is checked against, which on most endpoints is the per-visitor one. Past a limit you get 429 RATE_LIMITED with a Retry-After header in seconds. Most limits count per key, so one busy shop never slows down another; “per visitor” means per IP address.

Rate limits
RequestsLimit
Reads (business, locations, services, team, customer)120 a minute per visitor, and 600 a minute per key
Availability60 a minute per visitor, and 240 a minute per key
Creating bookings10 a minute per visitor, and 120 a minute per key
Sending codes20 per visitor, and 5 per email address or phone number, every 10 minutes, shared with your BarberFlow booking page
Confirming a code30 a minute per visitor, and 10 a minute per verification
Reading, cancelling or rescheduling a booking120 a minute per visitor (shared with your BarberFlow manage page), and 600 a minute per key
The docs (openapi.json, llms-full.txt)120 a minute per visitor
Requests with a missing or invalid key30 a minute per visitor; past that they get 429 instead of 401