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.
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
- Create a scoped API key in Dashboard → Settings → API Keys. It's shown once — store it securely.
- Send it as a Bearer token on every request.
- 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_keyScopes
Grant a key only the scopes it needs. A key can never hold a scope outside this set.
| Parameter | Type | Description |
|---|---|---|
availability:read | scope | Read bookable time slots. No customer data. |
appointments:read | scope | Read appointments — includes customer name & phone (PII). Grant only if your integration needs it. |
appointments:create | scope | Create appointments (booking). |
Get availability
availability:read/api/v1/appointments/availabilityReturns 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
| Parameter | Type | Description |
|---|---|---|
service_idrequired | string (uuid) | The service to check availability for. |
branch_idrequired | string (uuid) | The branch where the service is offered. |
daterequired | string (YYYY-MM-DD) | Requested date. |
timerequired | string (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:readFor 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
/api/v1/appointments/resourcescurl -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
/api/v1/appointments/resources/{resource_id}/slotsReturns the resource's bookable times for each day in date_from…date_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/api/v1/appointmentsBooks 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
| Parameter | Type | Description |
|---|---|---|
service_idrequired | string (uuid) | The service to book. |
branch_idrequired | string (uuid) | The branch for the appointment. |
customer_namerequired | string | Customer's full name. |
daterequired | string (YYYY-MM-DD) | Appointment date. |
timerequired | string (HH:MM) | Appointment local time. |
resource_id | string (uuid) | Book a specific resource (from /resources). Omit to auto-assign the least-busy eligible resource. |
phone | string | Customer phone in E.164, e.g. +9665XXXXXXXX. |
seats_taken | integer | Defaults to 1. Multi-seat not yet supported via API. |
notes | string | Booking 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
- Poll
GET /appointments/availabilityfor a free slot. - Book it with
POST /appointments. - Subscribe to the
appointment.createdwebhook 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"
}Errors
Nabaha uses conventional HTTP status codes. Errors return a JSON body { "detail": "..." }.
| Status | Meaning |
|---|---|
400 Bad Request | Invalid input — e.g. a date/time in the past, or an unsupported capacity/multi-seat booking. |
401 Unauthorized | Missing, invalid, or expired key — or a key used on a non-allowlisted endpoint. |
403 Forbidden | The key lacks the required scope for this operation. |
404 Not Found | The resource doesn't exist (or doesn't belong to your organization). |
409 Conflict | The slot is no longer available, or no resource is free for the appointment. |
429 Too Many Requests | Rate 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
betaSync 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
- Implement two endpoints in your system (see the reference server below).
- Host them at a public HTTPS URL.
- In Nabaha → Integrations → Connect your system, paste your URL and a token, then click Test connection.
- Map each of your resources to its ID in your system.
The contract
Nabaha authenticates with Authorization: Bearer <your token>.
Availability
/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
/bookingNabaha 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)
/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 ContentResource discovery (optional)
/resourcesThis 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 /bookingis create-or-update, keyed onnabaha_appointment_id: a first call creates, a repeat with newstart/endis a reschedule (update it), andDELETE /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 /availabilityerrors, Nabaha proceeds and reconciles on the next pull. IfPOST /bookingfails, Nabaha retries with exponential back-off. GET /resourcesis 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 call | Odoo operation |
|---|---|
| GET /resources | res.users search_read — returns all internal (non-portal) users |
| GET /availability | calendar.event search_read — busy events overlapping the requested window |
| POST /booking | Find-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_idoncalendar.event(Studio, 1 min) soPOST /bookingis 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-erpThe 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