Booking API
REST API for services, free times and reservations. Publishable keys for browsers, secret keys for servers, idempotent booking, readable errors.
The Booking API gives you the same booking engine the widget and Fiko Pages use: services, free times that respect working hours, staff, existing bookings and your booking rules, and reservations that appear in the Fiko calendar, inbox and workflows. Use it to build your own booking screens, book from a mobile app or create bookings from your backend.
If you only need a booking form on a website, the booking widget does all of this with one script tag.
Base URL
https://api.fiko.net/api/v1/public/booking
Bodies are JSON. Send Content-Type: application/json on POST requests.
Keys
Create keys in Settings → API & Developers → Website booking & webhooks. Each key has a publishable and a secret part:
Publishable key pk_live_... | Secret key sk_live_... | |
|---|---|---|
| Send it as | X-Fiko-Key: pk_live_... header (or ?key= in a link) | Authorization: Bearer sk_live_... header |
| Safe in browser or app code | Yes | No. Server only. |
| Works from | The websites on the key's list (checked against the browser's Origin) | Anywhere |
| Read business, services, free times | Yes | Yes |
| Create bookings | Yes | Yes |
| List bookings, read any booking, cancel as the business | No | Yes |
| Customer details in responses | No | Yes |
| Limits | Per website visitor | Per key, higher |
A key also sets how its bookings are confirmed (Request: pending until you approve, or Instant confirmation), whether an email address is required, and whether customers must pay on your website first.
Quick start
Book the first free time of a service in four calls:
KEY=pk_live_...
BASE=https://api.fiko.net/api/v1/public/booking
# 1. Services
curl -s "$BASE/services" -H "X-Fiko-Key: $KEY"
# 2. Days with free times in the next two weeks
curl -s "$BASE/availability/days?service_id=SERVICE_ID&days=14" -H "X-Fiko-Key: $KEY"
# 3. Free times on one day
curl -s "$BASE/availability?service_id=SERVICE_ID&date=2026-12-03" -H "X-Fiko-Key: $KEY"
# 4. Book
curl -s -X POST "$BASE/reservations" \
-H "X-Fiko-Key: $KEY" -H "Content-Type: application/json" \
-H "Idempotency-Key: 6c1f0a9e-checkout-1042" \
-d '{
"service_id": "SERVICE_ID",
"date": "2026-12-03",
"time": "14:30",
"customer": { "name": "Ayşe Yılmaz", "phone": "+905551234567", "email": "ayse@example.com" },
"notes": "First visit"
}'
Business
/businessKey: publishable or secretThe business's name, branding, time zone and booking rules, and the key's settings.
{
"name": "Atelier Hair",
"logo": "https://.../logo.png",
"primary_color": "#0F766E",
"timezone": "Europe/Istanbul",
"phone": "+902121234567",
"address": "Bağdat Cd. 120, Istanbul",
"page_url": "https://fiko.io/atelier",
"booking_window_days": 60,
"same_day_booking": true,
"min_cancel_hours": 12,
"booking_mode": "request",
"require_email": false
}
booking_window_days is how far ahead customers can book, min_cancel_hours how long before the start they can still cancel online.
List services
/servicesKey: publishable or secretBookable services in the order they are set up in Fiko, each with the staff (or rooms, tables…) that can do it.
| Query | |
|---|---|
ids | Optional. Comma-separated service ids to return only those. |
{
"object": "list",
"data": [
{
"id": "3f2a6c1e-...",
"name": "Haircut",
"description": "Wash, cut and style",
"duration_minutes": 45,
"price": "650.00",
"sale_price": null,
"currency": "TRY",
"image": "https://.../haircut.jpg",
"color": "#0F766E",
"resources": [
{ "id": "8d41e0a2-...", "name": "Elif", "type": "staff", "type_label": "Stylist" }
]
}
]
}
Days with free times
/availability/daysKey: publishable or secretFor a month calendar: which days have at least one free time.
| Query | |
|---|---|
service_id | Required. |
from | First day, YYYY-MM-DD. Default today (in the business's time zone). Days in the past are skipped. |
days | How many days, up to 62. Default 31. |
resource_id | Only this staff member. |
{
"timezone": "Europe/Istanbul",
"days": [
{ "date": "2026-12-01", "available": false },
{ "date": "2026-12-02", "available": true }
]
}
Results are cached for up to a minute; a day can fill up in between, so always load the times for the chosen day before booking.
Free times on a day
/availabilityKey: publishable or secret| Query | |
|---|---|
service_id | Required. |
date | Required, YYYY-MM-DD. |
resource_id | Only this staff member. |
{
"date": "2026-12-02",
"timezone": "Europe/Istanbul",
"slots": [
{ "time": "10:00", "start": "2026-12-02T07:00:00+00:00", "resource": { "id": "8d41e0a2-...", "name": "Elif" } },
{ "time": "10:30", "start": "2026-12-02T07:30:00+00:00", "resource": { "id": "b7c90f13-...", "name": "Can" } }
]
}
time is local to the business, start is UTC. Show time to customers and send it back when booking. Without resource_id, each time is offered with whoever is free.
Create a reservation
/reservationsKey: publishable or secret| Field | ||
|---|---|---|
service_id | string | Required. |
date | string | Required. YYYY-MM-DD, local to the business. |
time | string | Required. HH:MM from the free times. |
customer.name | string | Required, 2–120 characters. |
customer.phone | string | Required. 6–18 digits, with or without + and the country code; spaces and dashes are ignored. |
customer.email | string | Optional, or required when the key asks for it. The customer's confirmation and manage link go here. |
resource_id | string | Optional staff member. Without it, Fiko picks someone free at that time. |
notes | string | Optional, up to 1,000 characters. Shown to the business. |
metadata | object | Optional. Up to 20 keys of your own (40-character keys, 500-character string values), returned on the reservation and in webhooks. Handy for your order or user id. |
lang | string | Optional en, tr, ar or fa: language of the customer's emails. |
Send an Idempotency-Key header (any unique string up to 100 characters, such as a UUID or your order id) so a retried request never creates a second booking.
Returns 201 and the reservation. Its status is pending when the key is in Request mode and confirmed with Instant confirmation. The same request with the same Idempotency-Key within 24 hours returns 200 and the booking that was already made.
Bookings made with the publishable key are marked source: "website", with the secret key source: "api". The customer is added to (or matched in) your Fiko contacts by phone number.
Paid bookings
Take payment on your own website before a booking is made. Fiko never handles the money: you use your own payment provider (Stripe, iyzico, PayPal or any other), and Fiko keeps the time free for the customer while they pay.
- The customer picks a service and a time on your website.
- Your page creates a hold. The time is kept for 15 minutes (or what you ask for, 5 to 60), and nobody else can book it. Nothing is booked yet: no email, no webhook, nothing in the calendar.
- You send the customer to your payment page. The hold tells you the price.
- When the payment succeeds, your server confirms the hold with the secret key. That creates the reservation, with the usual emails and webhooks.
- If the payment fails or the customer goes back, release the hold, or just let it expire.
Turn on Customers pay on my website first on the key. Then the publishable key can only create holds: POST /reservations from the browser returns 403 payment_required, so nobody can skip the payment by calling the API directly. Because the confirm step needs the secret key, only your server can turn a hold into a booking.
Create a hold
/holdsKey: publishable or secretTakes the same fields as creating a reservation, plus:
| Field | ||
|---|---|---|
expires_in_minutes | number | Optional. How long to keep the time, 5–60. Default 15. |
Returns 201:
{
"id": "6f0c2b7e-...",
"object": "hold",
"status": "held",
"expires_at": "2026-12-01T11:45:00+00:00",
"service": { "id": "3f2a6c1e-...", "name": "Haircut", "price": "750.00", "currency": "TRY" },
"resource": { "id": "8d41e0a2-...", "name": "Elif" },
"start": "2026-12-02T07:00:00+00:00",
"end": "2026-12-02T07:45:00+00:00",
"local_date": "2026-12-02",
"local_time": "10:00",
"timezone": "Europe/Istanbul",
"business": { "name": "Atelier Hair" }
}
service.price is the sale price when the service has one. Put the hold id in your payment's metadata (for example Stripe's metadata.fiko_hold_id) so your server knows which hold to confirm. A 409 slot_unavailable means someone else got the time first: show the times again.
Confirm a hold
/holds/{id}/confirmKey: secret{ "payment_reference": "pi_3QxY...", "metadata": { "order_id": "1042" } }
| Field | ||
|---|---|---|
payment_reference | string | Optional. Your payment's id, saved on the booking as metadata.payment_reference so the business can match it. |
metadata | object | Optional. Added to the metadata given when the hold was made. |
Returns 201 and the reservation. Its status follows the key's mode; for paid bookings, Instant confirmation is usually what you want.
Confirming is safe to repeat: confirming the same hold again returns 200 and the same reservation, so you can retry after a timeout or when your payment provider sends its notification twice.
If the payment took longer than the hold, Fiko still books the time when it is free. If someone else booked it in the meantime you get 409 hold_expired: refund the customer or offer another time.
// Your Stripe webhook handler, after verifying Stripe's signature
if (event.type === "checkout.session.completed") {
const session = event.data.object;
const res = await fetch(
`https://api.fiko.net/api/v1/public/booking/holds/${session.metadata.fiko_hold_id}/confirm`,
{
method: "POST",
headers: {
Authorization: `Bearer ${process.env.FIKO_SECRET_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ payment_reference: session.payment_intent }),
},
);
if (res.status === 409) await refund(session.payment_intent); // the time was taken after the hold ran out
}
Check or release a hold
/holds/{id}Key: publishable or secretReturns the hold with status held or expired. Once confirmed it returns status: "confirmed" and the reservation, which is handy on your "thank you" page.
/holds/{id}Key: publishable or secretGives the time back right away, for example when the payment fails or the customer cancels checkout. Returns status: "released".
List reservations
/reservationsKey: secretNewest start time first.
| Query | |
|---|---|
status | pending, confirmed, cancelled, completed or no_show. |
from, to | Start date range, YYYY-MM-DD, inclusive. |
source | website, api, fiko_page and the other channels bookings come from. |
limit | 1–100, default 50. |
{ "object": "list", "has_more": false, "data": [ { "id": "...", "object": "reservation" } ] }
When has_more is true, narrow the date range to page through older bookings.
Get a reservation
/reservations/{id or confirmation_code}Key: secretAccepts the reservation id or the confirmation code customers see.
Cancel a reservation
/reservations/{id or confirmation_code}/cancelKey: secret{ "reason": "Customer called to cancel" }
Cancels as the business, so the customer cancellation deadline does not apply. Cancelling an already cancelled booking returns it unchanged. The customer is emailed and a booking.cancelled webhook is sent.
Customers cancel their own bookings with the manage_url link, within your deadline.
The reservation object
{
"id": "a1b2c3d4-...",
"object": "reservation",
"confirmation_code": "K7Q2MD",
"status": "confirmed",
"service": { "id": "3f2a6c1e-...", "name": "Haircut" },
"resource": { "id": "8d41e0a2-...", "name": "Elif" },
"start": "2026-12-02T07:00:00+00:00",
"end": "2026-12-02T07:45:00+00:00",
"local_date": "2026-12-02",
"local_time": "10:00",
"timezone": "Europe/Istanbul",
"business": { "name": "Atelier Hair" },
"source": "website",
"can_cancel": true,
"created_at": "2026-11-20T18:04:11+00:00",
"manage_url": "https://app.fiko.net/booking/manage/...",
"google_calendar_url": "https://calendar.google.com/calendar/render?...",
"customer": { "name": "Ayşe Yılmaz", "phone": "+905551234567", "email": "ayse@example.com" },
"notes": "First visit",
"metadata": { "order_id": "1042" }
}
customer, notes and metadata are only included for secret-key requests and in webhooks.
| Field | |
|---|---|
status | pending (waiting for the business), confirmed, cancelled, completed, no_show. |
local_date, local_time | When it starts, in the business's time zone. |
can_cancel | Whether the customer can still cancel online. |
manage_url | The customer's page to view, cancel and add the booking to their calendar. Private to that customer: don't publish it. |
Errors
Errors use the common format.
| Code | Status | Meaning |
|---|---|---|
invalid_key | 401 | The key is missing, wrong, turned off or revoked. |
origin_not_allowed | 403 | The publishable key was used on a website that is not on its list. |
secret_key_required | 403 | This endpoint needs the secret key. |
missing_service, missing_params | 400 | A required query parameter is missing. |
service_not_found | 404 | No bookable service with this id. |
invalid_date, invalid_time | 400 | Use YYYY-MM-DD and HH:MM. |
invalid_name, invalid_phone, invalid_email, email_required | 400 | Customer details to fix; message is ready to show. |
invalid_metadata | 400 | metadata must be an object. |
slot_unavailable | 409 | The time was just taken or is outside the free times. Load the times again. |
same_day_not_allowed | 400 | The business does not take same-day bookings. |
too_many_bookings | 400 | The customer already has the most upcoming bookings the business allows. |
booking_failed | 400 | Another booking rule stopped it; message says which. |
request_in_progress | 409 | The same Idempotency-Key is still being processed. Retry in a moment. |
rejected | 400 | The request looked automated. |
not_found | 404 | No such reservation for this business. |
cancel_failed | 400 | The booking could not be cancelled; message says why. |
payment_required | 403 | The key takes payment first: create a hold instead of a reservation. |
hold_not_found | 404 | No such hold. It was released, or expired and was cleaned up. |
hold_expired | 409 | The hold ran out and someone else booked the time. Refund or offer another time. |
rate_limited | 429 | Too many requests; see Retry-After. |
Good practice
- Read times from
/availabilityright before booking and handle409 slot_unavailableby showing the times again. - Always send an
Idempotency-KeyonPOST /reservations, and reuse it when you retry. - For paid bookings, confirm holds only from your server after the payment is verified, and keep the hold
idwith the payment. - Treat
pendingas "requested": tell the customer the business will confirm, and listen forbooking.confirmedin a webhook. - Store the reservation
idand keep it in sync with webhooks instead of polling the list. - Show times in the business's time zone (
local_time,timezone), not the visitor's.
Questions or something missing? Write to support@fiko.net.