Fiko Developers

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 asX-Fiko-Key: pk_live_... header (or ?key= in a link)Authorization: Bearer sk_live_... header
Safe in browser or app codeYesNo. Server only.
Works fromThe websites on the key's list (checked against the browser's Origin)Anywhere
Read business, services, free timesYesYes
Create bookingsYesYes
List bookings, read any booking, cancel as the businessNoYes
Customer details in responsesNoYes
LimitsPer website visitorPer 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:

Terminal
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

GET/businessKey: publishable or secret

The business's name, branding, time zone and booking rules, and the key's settings.

JSON
{
  "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

GET/servicesKey: publishable or secret

Bookable services in the order they are set up in Fiko, each with the staff (or rooms, tables…) that can do it.

Query
idsOptional. Comma-separated service ids to return only those.
JSON
{
  "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

GET/availability/daysKey: publishable or secret

For a month calendar: which days have at least one free time.

Query
service_idRequired.
fromFirst day, YYYY-MM-DD. Default today (in the business's time zone). Days in the past are skipped.
daysHow many days, up to 62. Default 31.
resource_idOnly this staff member.
JSON
{
  "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

GET/availabilityKey: publishable or secret
Query
service_idRequired.
dateRequired, YYYY-MM-DD.
resource_idOnly this staff member.
JSON
{
  "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

POST/reservationsKey: publishable or secret
Field
service_idstringRequired.
datestringRequired. YYYY-MM-DD, local to the business.
timestringRequired. HH:MM from the free times.
customer.namestringRequired, 2–120 characters.
customer.phonestringRequired. 6–18 digits, with or without + and the country code; spaces and dashes are ignored.
customer.emailstringOptional, or required when the key asks for it. The customer's confirmation and manage link go here.
resource_idstringOptional staff member. Without it, Fiko picks someone free at that time.
notesstringOptional, up to 1,000 characters. Shown to the business.
metadataobjectOptional. 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.
langstringOptional 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.

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.

  1. The customer picks a service and a time on your website.
  2. 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.
  3. You send the customer to your payment page. The hold tells you the price.
  4. When the payment succeeds, your server confirms the hold with the secret key. That creates the reservation, with the usual emails and webhooks.
  5. 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

POST/holdsKey: publishable or secret

Takes the same fields as creating a reservation, plus:

Field
expires_in_minutesnumberOptional. How long to keep the time, 5–60. Default 15.

Returns 201:

JSON
{
  "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

POST/holds/{id}/confirmKey: secret
JSON
{ "payment_reference": "pi_3QxY...", "metadata": { "order_id": "1042" } }
Field
payment_referencestringOptional. Your payment's id, saved on the booking as metadata.payment_reference so the business can match it.
metadataobjectOptional. 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.

JavaScript
// 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

GET/holds/{id}Key: publishable or secret

Returns the hold with status held or expired. Once confirmed it returns status: "confirmed" and the reservation, which is handy on your "thank you" page.

DELETE/holds/{id}Key: publishable or secret

Gives the time back right away, for example when the payment fails or the customer cancels checkout. Returns status: "released".

List reservations

GET/reservationsKey: secret

Newest start time first.

Query
statuspending, confirmed, cancelled, completed or no_show.
from, toStart date range, YYYY-MM-DD, inclusive.
sourcewebsite, api, fiko_page and the other channels bookings come from.
limit1–100, default 50.
JSON
{ "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

GET/reservations/{id or confirmation_code}Key: secret

Accepts the reservation id or the confirmation code customers see.

Cancel a reservation

POST/reservations/{id or confirmation_code}/cancelKey: secret
JSON
{ "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

JSON
{
  "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
statuspending (waiting for the business), confirmed, cancelled, completed, no_show.
local_date, local_timeWhen it starts, in the business's time zone.
can_cancelWhether the customer can still cancel online.
manage_urlThe 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.

CodeStatusMeaning
invalid_key401The key is missing, wrong, turned off or revoked.
origin_not_allowed403The publishable key was used on a website that is not on its list.
secret_key_required403This endpoint needs the secret key.
missing_service, missing_params400A required query parameter is missing.
service_not_found404No bookable service with this id.
invalid_date, invalid_time400Use YYYY-MM-DD and HH:MM.
invalid_name, invalid_phone, invalid_email, email_required400Customer details to fix; message is ready to show.
invalid_metadata400metadata must be an object.
slot_unavailable409The time was just taken or is outside the free times. Load the times again.
same_day_not_allowed400The business does not take same-day bookings.
too_many_bookings400The customer already has the most upcoming bookings the business allows.
booking_failed400Another booking rule stopped it; message says which.
request_in_progress409The same Idempotency-Key is still being processed. Retry in a moment.
rejected400The request looked automated.
not_found404No such reservation for this business.
cancel_failed400The booking could not be cancelled; message says why.
payment_required403The key takes payment first: create a hold instead of a reservation.
hold_not_found404No such hold. It was released, or expired and was cleaned up.
hold_expired409The hold ran out and someone else booked the time. Refund or offer another time.
rate_limited429Too many requests; see Retry-After.

Good practice

  • Read times from /availability right before booking and handle 409 slot_unavailable by showing the times again.
  • Always send an Idempotency-Key on POST /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 id with the payment.
  • Treat pending as "requested": tell the customer the business will confirm, and listen for booking.confirmed in a webhook.
  • Store the reservation id and 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.