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.
Three steps, no developer needed.
If you are building your site with an AI coding tool, this is all it takes.
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.
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.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
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.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).
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.
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.
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.
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();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
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 };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.
| Method | Path | Credential | Purpose |
|---|---|---|---|
| GET | /business | Key | Shop name, logo, cancellation policy, payment options and the hosted booking link. |
| GET | /locations | Key | Bookable locations with address, time zone, weekly hours, verification setting and booking link. |
| GET | /locations/{locationId}/services | Key | Services 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}/team | Key | Bookable barbers; serviceIds keeps only those who perform every listed service. |
| GET | /locations/{locationId}/availability | Key (+ 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/next | Key (+ customer token when verified) | The first open time for serviceIds and an optional barberId, searched up to the booking horizon, or null. |
| POST | /verifications | Key | Send a customer a one-time code by text or email. |
| POST | /verifications/{id}/confirm | Key | Exchange the code for a 30-day customer token. |
| GET | /me | Key + customer token | The signed-in customer’s profile at your shop. |
| GET | /me/bookings | Key + customer token | Their upcoming or past bookings at your shop, paged with a cursor. |
| POST | /bookings | Key (+ customer token when the location verifies) | Create a pay-in-person booking. Requires an Idempotency-Key header. |
| GET | /bookings/{id} | Key + customer or manage token | The 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}/cancel | Key + customer or manage token | Cancel the booking, with an optional reason. |
| POST | /bookings/{id}/reschedule | Key + customer or manage token | Move the booking to a new startsAt, with an optional reason. A customer can reschedule once. |
| GET | /openapi.json | None | The OpenAPI spec for every endpoint. |
| GET | /llms-full.txt | None | The full reference as one Markdown file, written for AI coding tools. |
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": {
"code": "SLOT_TAKEN",
"message": "That time was just taken. Please choose another time."
}
}| Code | Status | What it means |
|---|---|---|
| VALIDATION_ERROR | 400 | The request is not valid. When a field is the problem, details.issues names it and says what is wrong. |
| RANGE_TOO_LARGE | 400 | The date range is longer than 14 days. |
| IDEMPOTENCY_KEY_REQUIRED | 400 | POST /bookings needs an Idempotency-Key header. |
| VERIFICATION_CODE_INVALID | 400 | The code is wrong. The customer can try again. |
| VERIFICATION_CODE_EXPIRED | 400 | The code has expired. Start a new verification. |
| API_KEY_REQUIRED | 401 | No key was sent in the Authorization header. |
| API_KEY_INVALID | 401 | The key is unknown or has been revoked. |
| CUSTOMER_TOKEN_INVALID | 401 | The customer token is invalid, expired, or was issued under another key. Verify the customer again. |
| ORIGIN_NOT_ALLOWED | 403 | The request came from a domain the key does not allow. |
| VERIFICATION_REQUIRED | 403 | The location needs a verified customer. details.method says whether the code goes by text or email. |
| VERIFICATION_TOO_MANY_ATTEMPTS | 403 | Too many wrong codes for this verification. Start a new one. |
| LOCATION_NOT_BOOKABLE | 404 | The location does not exist or is not open for online booking. |
| BOOKING_NOT_FOUND | 404 | No booking with that id for the credential you sent. |
| SLOT_TAKEN | 409 | Someone booked that time first. Fetch availability again. |
| IDEMPOTENCY_KEY_REUSED | 409 | This Idempotency-Key already made a different booking. Use a new key for each booking attempt. |
| PAY_IN_PERSON_NOT_ALLOWED | 409 | The shop, or one of the services, needs a card for this booking. Send the customer to details.bookingUrl. |
| BARBER_NOT_AT_STORE | 422 | The barber does not work at the location, is not taking online bookings, or does not do every service. |
| SERVICE_NOT_AVAILABLE | 422 | A service id is not an active service offered at this location. Fetch the services again. |
| OUTSIDE_WORKING_HOURS | 422 | The barber is not working at that time. |
| INVALID_SLOT_INTERVAL | 422 | The time is not on the shop’s slot grid. Use a slot from availability. |
| OUTSIDE_BOOKING_HORIZON | 422 | The time is further ahead than the shop takes bookings. |
| DATE_IN_PAST | 422 | The time has already passed. |
| RATE_LIMITED | 429 | Too many requests. Wait for the Retry-After seconds, then retry. |
| SERVICE_UNAVAILABLE | 503 | A 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.
| Requests | Limit |
|---|---|
| Reads (business, locations, services, team, customer) | 120 a minute per visitor, and 600 a minute per key |
| Availability | 60 a minute per visitor, and 240 a minute per key |
| Creating bookings | 10 a minute per visitor, and 120 a minute per key |
| Sending codes | 20 per visitor, and 5 per email address or phone number, every 10 minutes, shared with your BarberFlow booking page |
| Confirming a code | 30 a minute per visitor, and 10 a minute per verification |
| Reading, cancelling or rescheduling a booking | 120 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 key | 30 a minute per visitor; past that they get 429 instead of 401 |