Nabaha API · v1

API Reference

Connect your CRM, ERP, or clinic system to Nabaha and keep bookings in sync — read availability and create appointments programmatically. Self-serve: generate a scoped key, read this page, and integrate. No contact required.

https://nabaha.ai/api/v1Create an API key →

Introduction

The Nabaha API is organized around REST. It accepts JSON request bodies, returns JSON responses, and uses standard HTTP verbs, status codes, and authentication.

Quickstart

  1. Create a scoped API key in Dashboard → Settings → API Keys. It's shown once — store it securely.
  2. Send it as a Bearer token on every request.
  3. Make your first call:
curl -G https://nabaha.ai/api/v1/appointments/availability \
  -H "Authorization: Bearer nb_live_your_key" \
  --data-urlencode "service_id=SERVICE_ID" \
  --data-urlencode "branch_id=BRANCH_ID" \
  --data-urlencode "date=2026-07-01" \
  --data-urlencode "time=10:00"

Authentication

Authenticate with an API key in the Authorization header. Keys are prefixed with nb_, scoped to least privilege, and tenant-isolated — a key can never access another organization's data, and only the booking endpoints below accept keys.

Authorization: Bearer nb_live_your_key

Scopes

Grant a key only the scopes it needs. A key can never hold a scope outside this set.

ParameterTypeDescription
availability:readscopeRead bookable time slots. No customer data.
appointments:readscopeRead appointments — includes customer name & phone (PII). Grant only if your integration needs it.
appointments:createscopeCreate appointments (booking).

Get availability

availability:read
GET/api/v1/appointments/availability

Returns the nearest bookable slot at or after the requested date and time for a given service and branch — so your system avoids double-booking.

Query parameters

ParameterTypeDescription
service_idrequiredstring (uuid)The service to check availability for.
branch_idrequiredstring (uuid)The branch where the service is offered.
daterequiredstring (YYYY-MM-DD)Requested date.
timerequiredstring (HH:MM)Requested local time (Asia/Riyadh).

Request

curl -G https://nabaha.ai/api/v1/appointments/availability \
  -H "Authorization: Bearer nb_live_your_key" \
  --data-urlencode "service_id=SERVICE_ID" \
  --data-urlencode "branch_id=BRANCH_ID" \
  --data-urlencode "date=2026-07-01" \
  --data-urlencode "time=10:00"

Response

{
  "available": true,
  "slot": { "date": "2026-07-01", "time": "10:00", "status": "available" }
}

status is one of available, slot_taken, off_grid, out_of_hours, non_working_day. When available is false, slot holds the nearest alternative (or null if none within 14 days).

Resources & slots

availability:read

For services performed by a specific person or asset (a doctor, stylist, room…), you can list the eligible resources and fetch each one's calendar — their free slots computed from their working hours, time-off, and existing appointments — then book the chosen resource. Capacity-based services (group classes) have no per-resource calendar and return 400 here.

List resources for a service

GET/api/v1/appointments/resources
curl -G https://nabaha.ai/api/v1/appointments/resources \
  -H "Authorization: Bearer nb_live_your_key" \
  --data-urlencode "service_id=SERVICE_ID" \
  --data-urlencode "branch_id=BRANCH_ID"
[
  { "resource_id": "8f1...", "name": "Dr. Ahmed", "name_ar": "د. أحمد" },
  { "resource_id": "b27...", "name": "Dr. Sara",  "name_ar": "د. سارة" }
]

Get a resource's free slots

GET/api/v1/appointments/resources/{resource_id}/slots

Returns the resource's bookable times for each day in date_fromdate_to (inclusive, max 14 days). Days with no availability are omitted.

curl -G https://nabaha.ai/api/v1/appointments/resources/RESOURCE_ID/slots \
  -H "Authorization: Bearer nb_live_your_key" \
  --data-urlencode "service_id=SERVICE_ID" \
  --data-urlencode "branch_id=BRANCH_ID" \
  --data-urlencode "date_from=2026-07-01" \
  --data-urlencode "date_to=2026-07-03"
{
  "resource_id": "8f1...",
  "name_ar": "د. أحمد",
  "service_id": "SERVICE_ID",
  "slots_by_date": [
    { "date": "2026-07-01", "available_times": ["09:00", "09:30", "10:00"] },
    { "date": "2026-07-02", "available_times": ["14:00", "14:30"] }
  ]
}

To book a specific resource, pass resource_id in the POST /appointments body below. Omit it to let Nabaha auto-assign the least-busy eligible resource.

Create appointment

appointments:create
POST/api/v1/appointments

Books an appointment. Runs through the same engine the voice agent uses — double-booking prevention, automatic resource selection, and capacity checks. Returns 409 if the slot was just taken, and 400 for a time in the past.

Body parameters

ParameterTypeDescription
service_idrequiredstring (uuid)The service to book.
branch_idrequiredstring (uuid)The branch for the appointment.
customer_namerequiredstringCustomer's full name.
daterequiredstring (YYYY-MM-DD)Appointment date.
timerequiredstring (HH:MM)Appointment local time.
resource_idstring (uuid)Book a specific resource (from /resources). Omit to auto-assign the least-busy eligible resource.
phonestringCustomer phone in E.164, e.g. +9665XXXXXXXX.
seats_takenintegerDefaults to 1. Multi-seat not yet supported via API.
notesstringBooking note / reason for visit (up to 2000 chars).

Request

curl -X POST https://nabaha.ai/api/v1/appointments \
  -H "Authorization: Bearer nb_live_your_key" \
  -H "Content-Type: application/json" \
  -d '{
    "service_id": "SERVICE_ID",
    "branch_id": "BRANCH_ID",
    "customer_name": "Mohammed Al-Otaibi",
    "date": "2026-07-01",
    "time": "10:00",
    "phone": "+9665XXXXXXXX"
  }'

Response

HTTP/1.1 201 Created

{ "id": "f47ac10b-58cc-4372-a567-0e02b2c3d479" }

Recipe — sync your booking system

  1. Poll GET /appointments/availability for a free slot.
  2. Book it with POST /appointments.
  3. Subscribe to the appointment.created webhook so bookings made elsewhere (e.g. the voice agent) flow back into your system.

Webhooks

Instead of polling, register an HTTPS endpoint from the dashboard to receive events the moment they happen. Every delivery is signed with an X-Nabaha-Signature (HMAC-SHA256) header so you can verify it came from Nabaha. Events: appointment.created, appointment.cancelled, call.completed.

POST  https://your-system.com/nabaha-webhook
X-Nabaha-Signature: sha256=...

{
  "event": "appointment.created",
  "id": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
  "service_id": "SERVICE_ID",
  "branch_id": "BRANCH_ID",
  "date": "2026-07-01",
  "time": "10:00"
}
Full webhooks guide & signature verification →

Errors

Nabaha uses conventional HTTP status codes. Errors return a JSON body { "detail": "..." }.

StatusMeaning
400 Bad RequestInvalid input — e.g. a date/time in the past, or an unsupported capacity/multi-seat booking.
401 UnauthorizedMissing, invalid, or expired key — or a key used on a non-allowlisted endpoint.
403 ForbiddenThe key lacks the required scope for this operation.
404 Not FoundThe resource doesn't exist (or doesn't belong to your organization).
409 ConflictThe slot is no longer available, or no resource is free for the appointment.
429 Too Many RequestsRate limit exceeded. Check Retry-After and back off.

Rate limits

Each API key is limited to 120 requests per minute. Every response includes X-RateLimit-Limit, X-RateLimit-Remaining, and X-RateLimit-Reset (epoch seconds). Exceeding the limit returns 429 with a Retry-After header (seconds until reset).

Connect your system

beta

Sync appointments with any external system — Nabaha pulls your resources' busy times every few minutes and pushes bookings the moment the agent books.

PULL   every N min   Nabaha ← GET /availability   (busy times in)
PUSH   on booking    Nabaha → POST /booking        (booking out)
RECHECK at confirm   Nabaha → GET /availability    (one slot, no double-book)

Integrate in 4 steps

  1. Implement two endpoints in your system (see the reference server below).
  2. Host them at a public HTTPS URL.
  3. In Nabaha → Integrations → Connect your system, paste your URL and a token, then click Test connection.
  4. Map each of your resources to its ID in your system.

The contract

Nabaha authenticates with Authorization: Bearer <your token>.

Availability

GET/availability?resource_ids=doc-1,doc-2&from=…&to=…

Nabaha calls this to check which resources are busy for a given window. from and to are ISO-8601 timestamps. Return the full busy list for each requested resource.

GET /availability?resource_ids=doc-1,doc-2&from=2026-06-25T00:00:00%2B03:00&to=2026-07-09T00:00:00%2B03:00
Authorization: Bearer <your token>
{
  "resources": [
    {
      "resource_id": "staff-42",
      "busy": [
        { "start": "2026-07-01T09:00:00+03:00", "end": "2026-07-01T10:00:00+03:00" }
      ]
    }
  ]
}

Create booking

POST/booking

Nabaha sends this when the agent books an appointment. Return the external system's booking ID as external_id — Nabaha stores it for the cancellation call.

{
  "resource_id": "staff-42",
  "start": "2026-07-01T10:00:00+03:00",
  "end": "2026-07-01T10:30:00+03:00",
  "service": "consultation",
  "customer": {
    "name": "Mohammed Al-Otaibi",
    "phone": "+966501234567"
  },
  "nabaha_appointment_id": "f47ac10b-58cc-4372-a567-0e02b2c3d479"
}

Cancel booking (optional)

DELETE/booking/{external_id}

Called when an appointment is cancelled. Respond with 204 No Content.

DELETE /booking/erp-9001
Authorization: Bearer <your-token>

HTTP/1.1 204 No Content

Resource discovery (optional)

GET/resources

This endpoint is optional. If you implement it, Nabaha will call it once when you connect and automatically pre-map your resources in the dashboard — no manual mapping needed. If you omit it, you map resources manually instead. Return the list of bookable resources (doctors, staff, rooms, etc.) with a stable id and a display name.

{
  "resources": [
    { "id": "dr-7", "name": "Dr. Abdulmajeed" },
    { "id": "dr-3", "name": "Dr. Sarah" }
  ]
}

Rules

  • Times are ISO-8601 with UTC offset, e.g. 2026-07-01T10:00:00+03:00.
  • POST /booking is create-or-update, keyed on nabaha_appointment_id: a first call creates, a repeat with new start/end is a reschedule (update it), and DELETE /booking/{external_id} is a cancel. This also makes retries safe.
  • Availability is pulled every N minutes; bookings are pushed instantly at the moment of booking.
  • If GET /availability errors, Nabaha proceeds and reconciles on the next pull. If POST /booking fails, Nabaha retries with exponential back-off.
  • GET /resources is optional — implement it to let Nabaha auto-discover and pre-map your resources in the dashboard; otherwise resource mapping is done manually.

Reference server (copy & run)

A minimal server that implements both endpoints. Pick a language, adapt the TODOlines to your system, host it on HTTPS, and you're done.

// nabaha-connector.js — reference server (Node / Express)
// Implements the two endpoints Nabaha calls. Run it, host it on public HTTPS,
// then paste its URL into Nabaha → Integrations → Connect your system.
import express from "express";
const app = express();
app.use(express.json());

const TOKEN = process.env.NABAHA_TOKEN; // the token you set in the Nabaha dashboard

const auth = (req, res, next) =>
  req.get("authorization") === `Bearer ${TOKEN}` ? next() : res.sendStatus(401);

// 1) Nabaha pulls your resources' busy times every few minutes.
app.get("/availability", auth, (req, res) => {
  const ids = String(req.query.resource_ids || "").split(",").filter(Boolean);
  // TODO: look up busy times in YOUR system for these ids + the from/to window.
  res.json({
    resources: ids.map((id) => ({
      resource_id: id,
      busy: [{ start: "2026-06-25T14:00:00+03:00", end: "2026-06-25T15:00:00+03:00" }],
    })),
  });
});

// 2) Nabaha pushes a booking. POST /booking is CREATE-OR-UPDATE, keyed on
//    nabaha_appointment_id: a repeat with new start/end is a reschedule.
const bookings = {}; // keyed by nabaha_appointment_id
app.post("/booking", auth, (req, res) => {
  const { nabaha_appointment_id, start, end } = req.body;
  if (bookings[nabaha_appointment_id]) {
    // Known id → reschedule: update this booking's start/end in YOUR system.
    return res.json({ external_id: bookings[nabaha_appointment_id] });
  }
  // TODO: create the booking in YOUR system, return its id.
  const externalId = "appt-" + Date.now();
  bookings[nabaha_appointment_id] = externalId;
  res.json({ external_id: externalId });
});

// 3) Cancel (optional).
app.delete("/booking/:id", auth, (req, res) => {
  // TODO: cancel req.params.id in YOUR system.
  res.sendStatus(204);
});

// 4) OPTIONAL — implement to enable auto-mapping in Nabaha.
//    Nabaha calls this once on connect to discover and pre-map your resources.
app.get("/resources", auth, (req, res) =>
  res.json({ resources: [{ id: "dr-7", name: "Dr. Abdulmajeed" }] })
);

app.listen(3000, () => console.log("Nabaha connector on :3000"));

Odoo (XML-RPC)

Odoo has no Nabaha-shaped endpoints — this thin adapter maps our contract to Odoo's external API (calendar.event / res.users). Set 5 env vars, host it, paste its URL in the wizard.

# Odoo reference adapter — maps the Nabaha contract to Odoo's external API (XML-RPC).
# Odoo has no native Nabaha-shaped endpoints; booked time lives in calendar.event.
# Host this as your service; set the 5 env vars below.
import os, xmlrpc.client
from datetime import datetime, timezone
from fastapi import FastAPI, Header, HTTPException, Query
from fastapi.responses import Response

ODOO_URL, ODOO_DB = os.environ["ODOO_URL"], os.environ["ODOO_DB"]
ODOO_USER, ODOO_KEY = os.environ["ODOO_USER"], os.environ["ODOO_API_KEY"]  # API key = password (Odoo 14+)
NABAHA_TOKEN = os.environ["AUTH_TOKEN"]                                     # the nbh_ token from the wizard

common = xmlrpc.client.ServerProxy(f"{ODOO_URL}/xmlrpc/2/common")
uid = common.authenticate(ODOO_DB, ODOO_USER, ODOO_KEY, {})
obj = xmlrpc.client.ServerProxy(f"{ODOO_URL}/xmlrpc/2/object")
def kw(model, method, *a, **k): return obj.execute_kw(ODOO_DB, uid, ODOO_KEY, model, method, list(a), k)

app = FastAPI()
def auth(a):
    if a != f"Bearer {NABAHA_TOKEN}": raise HTTPException(401)
def to_iso(s):  return datetime.strptime(s, "%Y-%m-%d %H:%M:%S").replace(tzinfo=timezone.utc).isoformat()
def to_odoo(s): return datetime.fromisoformat(s).astimezone(timezone.utc).strftime("%Y-%m-%d %H:%M:%S")

@app.get("/resources")
def resources(authorization: str = Header(None)):
    auth(authorization)
    users = kw("res.users", "search_read", [["share","=",False]], fields=["id","name"])
    return {"resources": [{"id": str(u["id"]), "name": u["name"]} for u in users]}

@app.get("/availability")
def availability(resource_ids: str, to: str = "", from_: str = Query("", alias="from"),
                 authorization: str = Header(None)):
    auth(authorization)
    res = []
    for rid in filter(None, resource_ids.split(",")):
        evs = kw("calendar.event", "search_read",
                 [["user_id","=",int(rid)], ["start","<",to_odoo(to)], ["stop",">",to_odoo(from_)]],
                 fields=["start","stop"])
        res.append({"resource_id": rid,
                    "busy": [{"start": to_iso(e["start"]), "end": to_iso(e["stop"])} for e in evs]})
    return {"resources": res}

@app.post("/booking")
def booking(p: dict, authorization: str = Header(None)):
    auth(authorization)
    nid = p["nabaha_appointment_id"]
    dup = kw("calendar.event", "search", [["x_nabaha_id","=",nid]], limit=1)  # idempotency + reschedule
    phone = p.get("customer", {}).get("phone", "")
    pids = kw("res.partner", "search", [["phone","=",phone]], limit=1) if phone else []
    pid = pids[0] if pids else kw("res.partner", "create",
                                  {"name": p.get("customer",{}).get("name","Customer"), "phone": phone})
    vals = {"name": p.get("service") or "Nabaha booking", "start": to_odoo(p["start"]),
            "stop": to_odoo(p["end"]), "user_id": int(p["resource_id"]),
            "partner_ids": [(6,0,[pid])], "x_nabaha_id": nid}
    if dup: kw("calendar.event","write",[dup[0]],vals); eid = dup[0]
    else:   eid = kw("calendar.event","create",vals)
    return {"external_id": str(eid)}

@app.delete("/booking/{external_id}")
def cancel(external_id: str, authorization: str = Header(None)):
    auth(authorization)
    kw("calendar.event", "write", [int(external_id)], {"active": False})
    return Response(status_code=204)

Endpoint mapping

Nabaha callOdoo operation
GET /resourcesres.users search_read — returns all internal (non-portal) users
GET /availabilitycalendar.event search_read — busy events overlapping the requested window
POST /bookingFind-or-create res.partner by phone, then calendar.event create (or write on reschedule, keyed on x_nabaha_id)
DELETE /booking/{id}calendar.event write {active: false} — soft-archives the event

Before you start

  • Odoo's external API (XML-RPC) requires a Custom plan, Odoo.sh, or on-premise— it is blocked on Odoo Online “Standard”. Check this first.
  • Odoo stores datetimes as UTC-naive (no offset); the adapter adds the offset because our contract requires ISO-8601 with offset.
  • Add a custom field x_nabaha_id on calendar.event (Studio, 1 min) so POST /booking is idempotent and reschedulable.
  • Availability here = booked events. The doctor's bookable hoursstay in Odoo's appointment.type / resource.calendarand Nabaha's own staff working-hours.

Try it against our sandbox

Point a test connector at our sandbox to see the exact request/response live:

https://dev.nabaha.ai/api/v1/demo/mock-erp

The sandbox mock-ERP accepts the same contract as a real connector — use it to confirm your implementation before switching to production.

Ready to integrate?

Generate a scoped key and make your first call in minutes.

Create an API key