API v1

Build on Tatbiq

Connect your own systems to your booking page. Read bookings, customers and services, update bookings, and get a webhook the moment anything changes.

Getting started

  1. In your dashboard, open Settings → API and webhooks and generate an API key. Copy it straight away: it is shown once.
  2. Call the API from your server with that key. Every endpoint lives under:
https://www.tatbiq.store/api/v1

Try it with your own key:

curl https://www.tatbiq.store/api/v1/business \
  -H "Authorization: Bearer tk_your_key_here"

API access is part of your plan. If your plan doesn't include it, calls answer 402 with the code plan_required.

Authentication

Send your key in the Authorization header on every request: Authorization: Bearer tk_.... A key can do everything the endpoints below allow for your business, so keep it on your server. Never put it in a website, an app download or a public repository.

If a key leaks, revoke it in the dashboard and make a new one. A revoked key stops working at once. The dashboard also shows when each key was last used.

Errors, limits, paging

Requests and responses are JSON. Times are ISO 8601 in UTC. Money is a number in your business's currency.

Every error has the same shape, with a status code that matches:

{ "error": { "code": "not_found", "message": "No such booking" } }
400 invalid_request
Something in the query or body is wrong. details lists each field's problem.
401 unauthorized
The key is missing, mistyped or revoked.
402 plan_required
Your plan doesn't include API access.
403 forbidden
The business account is suspended.
404 not_found
No such record in your business.
409 conflict
The change clashes with something, e.g. the new time is taken.
429 rate_limited
Too many requests. Wait for the Retry-After header's seconds.

Rate limit: 120 requests a minute per key.

Paging: lists return the newest first, 50 at a time (up to 100 with ?limit=). When there is more, the response has a nextCursor; pass it back as ?cursor= for the next page. When nextCursor is null, you have everything.

Business

GET/business

The business your key belongs to: id, name, slug, timezone, currency, email and phone. A good first call to check your key.

Services and staff

GET/services

Your services with id, name, nameAr, description, descriptionAr, durationMinutes, price, category and active. Only active ones, unless you add ?includeInactive=true.

GET/staff

The people who can be booked: id, name, position and active. Contact details and HR records are never shared. Add ?includeInactive=true for everyone.

Availability

GET/availability?serviceId=...&date=2026-10-14

The start times your booking page offers for one service on one day, in your business's timezone.

serviceId
Required.
date
Required. YYYY-MM-DD.
staffId
Optional. Only this staff member's times.
branchId
Optional. For businesses with branches.
{ "date": "2026-10-14", "timezone": "Asia/Riyadh", "slots": [{ "time": "2026-10-14T06:00:00.000Z", "available": true }] }

Times that are already booked come back with "available": false.

Customers

GET/customers

Your customers, paged. ?q= searches name, email and phone. Each customer has id, name, email, phone, tags, notes, totalBookings, totalSpent, lastBookingAt, noShowCount, marketingOptOut, createdAt and updatedAt.

GET/customers/{id}

One customer.

POST/customers

Adds a customer. Give at least an email or a phone number.

curl -X POST https://www.tatbiq.store/api/v1/customers \
  -H "Authorization: Bearer tk_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{ "name": "Noura Alharbi", "email": "noura@example.com", "phone": "0500000000", "tags": ["vip"] }'

If that email is already on file, you get 409 conflict with the existing customer's id in details.customerId, so you can link to it instead of making a duplicate.

Bookings

GET/bookings

Your bookings, newest made first, paged.

from, to
Only bookings starting in this range (ISO times, to is exclusive).
status
PENDING, CONFIRMED, COMPLETED, CANCELLED or NO_SHOW.
customerId
One customer's bookings.
updatedSince
Only bookings changed since this time. Ideal for a sync job: save when you last ran and pass it next time.

GET/bookings/{id}

One booking:

{
  "booking": {
    "id": "cmh2x8k1q0001abcd9tajnonm",
    "orderNumber": "BK-9TAJNONM",
    "status": "CONFIRMED",
    "startTime": "2026-10-14T13:00:00.000Z",
    "endTime": "2026-10-14T14:00:00.000Z",
    "services": [{ "id": "cmf...", "name": "Haircut" }],
    "package": null,
    "staff": { "id": "cmg...", "name": "Sara" },
    "branch": { "id": "cmb...", "name": "Olaya" },
    "customer": { "id": "cmc...", "name": "Noura Alharbi", "email": "noura@example.com", "phone": "0500000000" },
    "partySize": null,
    "notes": null,
    "answers": [{ "question": "Any allergies?", "answer": "No" }],
    "recurring": false,
    "rescheduleCount": 0,
    "checkedInAt": null,
    "cancelledAt": null,
    "cancelledBy": null,
    "cancelReason": null,
    "payment": { "currency": "SAR", "total": 120, "paid": 50, "remaining": 70, "status": "DEPOSIT_PAID", "discountCode": null },
    "createdAt": "2026-10-10T09:12:44.000Z",
    "updatedAt": "2026-10-10T09:12:44.000Z"
  }
}

PATCH/bookings/{id}

Changes a booking exactly as your dashboard would. Your customer gets the same emails, the time is checked for clashes, and webhooks fire. Send only what you want to change:

status
CONFIRMED, COMPLETED, CANCELLED, NO_SHOW or PENDING.
cancelReason
Shown to the customer when you cancel.
startTime
Move it to a new start time (ISO). It keeps its length. 409 if that time is taken.
staffId
Give it to another staff member, or null for anyone.
curl -X PATCH https://www.tatbiq.store/api/v1/bookings/cmh2x8k1q0001abcd9tajnonm \
  -H "Authorization: Bearer tk_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{ "status": "CANCELLED", "cancelReason": "Clinic closed for maintenance" }'

A cancelled booking can't be brought back or moved: its time has been given back. New bookings are made on your booking page.

Webhooks

Instead of asking the API over and over, let us tell you. In Settings → API and webhooks, add an endpoint: an https address on your server. Choose all events or just some. Whenever one happens, we send it a POST with a JSON body.

EventWhen
booking.createdA booking was made, on the booking page or as part of a recurring series. Bookings waiting for online payment start as PENDING.
booking.confirmedA booking became confirmed: approved by the business, or paid online.
booking.rescheduledA booking moved to a new time, by the business or by the customer.
booking.cancelledA booking was cancelled, by the business, the customer, or because online payment was never completed.
booking.completedA booking was marked completed.
booking.no_showA booking was marked as a no-show.
customer.createdA new customer was added, by their first booking or through the API.

Booking events carry the same booking object as the API, in data.booking. customer.created carries data.customer. The Send test button sends a ping event.

POST /webhooks/tatbiq HTTP/1.1
Content-Type: application/json
User-Agent: Tatbiq-Webhooks/1.0
Tatbiq-Event: booking.cancelled
Tatbiq-Delivery: cmh3...
Tatbiq-Signature: t=1760090000,v1=5f2b...

{ "id": "evt_8c1f...", "type": "booking.cancelled", "createdAt": "2026-10-10T09:30:00.000Z", "data": { "booking": { ... } } }

Answer with any 2xx status within 10 seconds. Do slow work after you answer. Anything else counts as a failure and is retried. We don't follow redirects.

Verifying signatures

Each endpoint has its own signing secret (whsec_...), shown when you add it and again under Show secret. The Tatbiq-Signature header is t=<unix seconds>,v1=<hex>, where the hex is an HMAC SHA-256 of <t>.<raw body> using that secret. Check it on every request, and refuse ones older than 5 minutes so a copied request can't be replayed later.

Node.js

import crypto from "node:crypto";
import express from "express";

const app = express();
const SECRET = process.env.TATBIQ_WEBHOOK_SECRET; // whsec_...

// Read the raw body: the signature is over the exact bytes we sent.
app.post("/webhooks/tatbiq", express.raw({ type: "application/json" }), (req, res) => {
  const header = req.get("Tatbiq-Signature") ?? "";
  const parts = Object.fromEntries(header.split(",").map((p) => p.split("=")));
  const t = Number(parts.t);
  const expected = crypto.createHmac("sha256", SECRET).update(`${t}.${req.body}`).digest("hex");

  const fresh = Math.abs(Date.now() / 1000 - t) < 300;
  const valid =
    parts.v1?.length === expected.length &&
    crypto.timingSafeEqual(Buffer.from(parts.v1), Buffer.from(expected));
  if (!fresh || !valid) return res.status(400).send("bad signature");

  const event = JSON.parse(req.body);
  // Skip events you've already handled: the same event can arrive more than once.
  console.log(event.id, event.type, event.data);
  res.sendStatus(200);
});

Python

import hashlib, hmac, json, time

def verify(secret: str, raw_body: bytes, header: str) -> dict:
    parts = dict(p.split("=", 1) for p in header.split(","))
    t = int(parts["t"])
    expected = hmac.new(secret.encode(), f"{t}.".encode() + raw_body, hashlib.sha256).hexdigest()
    if abs(time.time() - t) > 300 or not hmac.compare_digest(expected, parts.get("v1", "")):
        raise ValueError("bad signature")
    return json.loads(raw_body)

Need a new secret? New secret replaces it immediately, so update your server right after.

Retries

If a delivery fails, we try again after 1 min, 5 min, 30 min, 2 h, 6 h, 12 h, 24 h: 8 tries over about two days. After that it is marked failed, and you can send it again from the delivery log in your dashboard. Each event has an id that stays the same across retries and resends. Store the ids you have handled and skip repeats, because the same event can arrive more than once.

Events are sent in the order they happen, but a retry can arrive after a later event. Use the booking's updatedAt to keep the newest version, or fetch the booking from the API when an event arrives.

Turning an endpoint off stops new events and drops any retries still waiting. Delivery history is kept for 30 days.

We add fields and events over time, so ignore ones you don't know. We won't rename or remove anything in v1. Questions? Email us at info@tatbiq.store.