توسعه‌دهندگان

راهنمای توسعه‌دهندگان API بوکورا

در این صفحه (۹ بخش)

با API بوکورا، سایت یا نرم‌افزار کلینیک رو به نوبت‌ها و مشخصات پایه بیماران متصل کنید. ارتباط بین سرورها انجام می‌شه و مالک کلینیک کلید دسترسی رو در پنل می‌سازه.

English version

API در پلن کلینیک و دوره آزمایشی همین پلن در دسترسه. اگر به پلنی بدون دسترسی API تغییر بدید، همهٔ درخواست‌های API با خطای FEATURE_NOT_IN_PLAN رد می‌شن. همچنان می‌تونید اطلاعات رو در پنل ببینید و خروجی بگیرید.

شروع سریع

  1. مالک کلینیک در بخش «تنظیمات › اتصال‌ها (API)» پنل، یک کلید با سطح دسترسی لازم می‌سازه. کلید فقط یک بار نمایش داده می‌شه.
  2. کلید رو فقط روی سرور خودتون نگه دارید و در کد سایت یا اپ موبایل که کاربر به اون دسترسی داره قرار ندید.
  3. اولین درخواست:
curl https://bookora.net/api/v1/clinic-api/me \
  -H "Authorization: Bearer bk_live_<public-id>_<secret>"

نشانی پایه: https://bookora.net/api/v1/clinic-api. پاسخ‌ها ساختار {"data": …, "meta": …} دارن. پاسخ خطا شامل فیلدهای statusCode، code، message، details و requestId می‌شه؛ متن message فارسیه. مبلغ‌ها به ریال و به‌صورت عدد صحیح، و زمان‌ها با قالب ISO-8601 و به وقت جهانی (UTC) ارسال می‌شن.

احراز هویت

در هر درخواست، هدر Authorization: Bearer bk_live_<public-id>_<secret> رو ارسال کنید. کلید لغوشده یا منقضی‌شده بلافاصله رد می‌شه. هنگام تعویض کلید می‌تونید برای اعتبار کلید قبلی مهلت تعیین کنید.

سطح دسترسی

دسترسی کارهای مجاز دسترسی لازم در پنل
catalog.read خدمات، قیمت‌ها و پزشکان — فهرست خدمات فعال با مدت و قیمت، و پزشکان فعال کلینیک. —
availability.read زمان‌های آزاد — زمان‌های آزاد برای نوبت‌دهی، بر اساس ساعات کاری و نوبت‌های ثبت‌شده. —
appointments.read مشاهده نوبت‌ها — نوبت‌های کلینیک با زمان، وضعیت، پزشک و خدمت؛ بدون یادداشت و مبلغ. —
appointments.write ثبت، لغو و جابه‌جایی نوبت — ثبت نوبت جدید در زمان خالی، لغو و جابه‌جایی نوبت. appointments.readavailability.read
patients.read جست‌وجو و مشاهده مشخصات پایه بیمار — جست‌وجوی بیمار با تطبیق کامل شماره موبایل یا کد ملی و مشاهده نام و شماره تماس؛ بدون سوابق پزشکی. —
patients.write ثبت بیمار جدید — ثبت پرونده جدید با نام و شماره موبایل. patients.read

هیچ‌کس نمی‌تونه کلیدی با سطح دسترسی بالاتر از دسترسی خودش در پنل بسازه.

محدودیت تعداد درخواست‌ها

  • هر کلید: ۱۲۰ درخواست در دقیقه
  • هر کلینیک، مجموع همه کلیدها: ۳۰۰ درخواست در دقیقه
  • ثبت یا تغییر اطلاعات (ثبت نوبت، لغو، جابه‌جایی و ثبت بیمار): ۳۰ درخواست در دقیقه برای هر کلید
  • جست‌وجوی بیمار: ۳۰ در دقیقه برای هر کلید

هر پاسخ هدرهای X-RateLimit-Limit، X-RateLimit-Remaining، X-RateLimit-Reset رو داره. اگر تعداد درخواست‌ها از سقف مجاز بیشتر بشه، پاسخ ۴۲۹ همراه با Retry-After برمی‌گرده.

جلوگیری از ثبت تکراری

برای ثبت نوبت یا بیمار، باید هدر Idempotency-Key رو ارسال کنید؛ مقدار اون باید بین ۸ تا ۱۲۸ کاراکتر باشه. برای لغو یا جابه‌جایی نوبت، ارسال این هدر اختیاریه. اگر تا ۲۴ ساعت همون درخواست رو با همون کلید و بدنه دوباره بفرستید، نتیجه قبلی با هدر Idempotent-Replayed: true برمی‌گرده و نوبت یا پرونده تکراری ثبت نمی‌شه.

curl -X POST https://bookora.net/api/v1/clinic-api/appointments \
  -H "Authorization: Bearer bk_live_<public-id>_<secret>" \
  -H "Idempotency-Key: 6f1c2a9e-booking-4411" \
  -H "Content-Type: application/json" \
  -d '{"patientId":"<patient-id>","serviceId":"<service-id>","startAt":"2026-10-10T07:30:00.000Z"}'

مسیرهای API

GET /me

اطلاعات کلید، کلینیک و محدودیت تعداد درخواست‌ها

سطح دسترسی: بدون نیاز به دسترسی جداگانه

GET /services

خدمات فعال کلینیک با مدت و قیمت (ریال)

سطح دسترسی: catalog.read

GET /doctors

پزشکان فعال کلینیک

سطح دسترسی: catalog.read

GET /availability

زمان‌های آزاد در بازه‌ای حداکثر دوهفته‌ای برای هر درخواست

سطح دسترسی: availability.read

نام محل ارسال الزامی توضیح
from query بله آغاز بازه (ISO-8601)
to query بله پایان بازه (ISO-8601)
serviceId query بله شناسهٔ خدمت
doctorId query نه فقط زمان‌های این پزشک
branchId query نه فقط این شعبه

GET /appointments

فهرست نوبت‌ها؛ بدون یادداشت‌ها و مبلغ‌ها

سطح دسترسی: appointments.read

نام محل ارسال الزامی توضیح
from query نه زمان شروع بازه
to query نه زمان پایان بازه
status query نه وضعیت نوبت
doctorId query نه پزشک
patientId query نه بیمار
branchId query نه شعبه
updatedSince query نه تغییرات پس از این زمان؛ ردیف‌های حذف‌شده با deleted مشخص می‌شن
page query نه شمارهٔ صفحه
pageSize query نه تعداد ردیف‌های هر صفحه، حداکثر ۱۰۰

GET /appointments/{id}

جزئیات یک نوبت

سطح دسترسی: appointments.read

نام محل ارسال الزامی توضیح
id path بله شناسهٔ نوبت

POST /appointments

ثبت نوبت در زمان آزاد، با وضعیت پیش‌فرض «تأییدشده»

سطح دسترسی: appointments.write · Idempotency-Key: لازم

نام محل ارسال الزامی توضیح
patientId body بله شناسهٔ بیمار
serviceId body بله شناسهٔ خدمت
startAt body بله زمان شروع (ISO-8601)
doctorId body نه پزشک دلخواه
branchId body نه شعبه
status body نه CONFIRMED یا REQUESTED
notes body نه یادداشت، حداکثر ۵۰۰ کاراکتر

POST /appointments/{id}/cancel

لغو نوبت

سطح دسترسی: appointments.write · Idempotency-Key: اختیاری

نام محل ارسال الزامی توضیح
id path بله شناسهٔ نوبت
reason body نه دلیل، حداکثر ۳۰۰ کاراکتر

POST /appointments/{id}/reschedule

جابه‌جایی نوبت به زمان آزاد دیگر

سطح دسترسی: appointments.write · Idempotency-Key: اختیاری

نام محل ارسال الزامی توضیح
id path بله شناسهٔ نوبت
startAt body بله زمان شروع جدید (ISO-8601)
doctorId body نه پزشک دلخواه

GET /patients/lookup

جست‌وجوی بیمار با شماره موبایل یا کد ملی کامل و دقیق

سطح دسترسی: patients.read

نام محل ارسال الزامی توضیح
phone query نه شمارهٔ موبایل؛ وارد کردن شمارهٔ موبایل یا کد ملی الزامی است
nationalId query نه کد ملی؛ وارد کردن کد ملی یا شمارهٔ موبایل الزامی است

GET /patients/{id}

مشخصات پایه بیمار، بدون سوابق پزشکی

سطح دسترسی: patients.read

نام محل ارسال الزامی توضیح
id path بله شناسهٔ بیمار

POST /patients

ثبت بیمار جدید

سطح دسترسی: patients.write · Idempotency-Key: لازم

نام محل ارسال الزامی توضیح
firstName body بله نام
lastName body بله نام خانوادگی
phone body بله شمارهٔ موبایل
gender body نه جنسیت
birthDate body نه تاریخ تولد
nationalId body نه کد ملی
acquisitionSource body نه نحوهٔ آشنایی بیمار با کلینیک

خطاها

وضعیت code توضیح
۴۰۰ VALIDATION_FAILED اطلاعات ورودی معتبر نیست؛ فیلد مربوط در details مشخص شده
۴۰۰ IDEMPOTENCY_KEY_REQUIRED هدر Idempotency-Key رو ارسال کنید
۴۰۰ APPOINTMENT_NOT_CANCELLABLE نوبت پایان‌یافته رو نمی‌شه لغو کرد
۴۰۱ API_KEY_REQUIRED کلید API ارسال نشده
۴۰۱ API_KEY_INVALID کلید API معتبر نیست
۴۰۱ API_KEY_REVOKED اعتبار کلید لغو شده
۴۰۱ API_KEY_EXPIRED اعتبار کلید تمام شده
۴۰۲ FEATURE_NOT_IN_PLAN پلن کلینیک دسترسی API نداره یا اشتراک تموم شده؛ همه درخواست‌ها رد می‌شن
۴۰۳ API_SCOPE_DENIED کلید این سطح دسترسی رو نداره
۴۰۴ *_NOT_FOUND رکوردی با این شناسه در کلینیک پیدا نشد
۴۰۹ APPOINTMENT_SLOT_TAKEN این زمان دیگه آزاد نیست
۴۰۹ IDEMPOTENCY_IN_PROGRESS درخواست قبلی با همین کلید هنوز در حال پردازش است
۴۲۲ IDEMPOTENCY_KEY_REUSED این کلید قبلاً برای درخواستی با بدنهٔ متفاوت استفاده شده
۴۲۹ RATE_LIMITED تعداد درخواست‌ها از سقف مجاز بیشتر شده؛ زمان انتظار رو در Retry-After ببینید
۵۰۳ CLINIC_API_DISABLED API موقتاً غیرفعال است

وب‌هوک

با وب‌هوک می‌تونید از تغییرات نوبت‌ها باخبر بشید. در پنل برای هر نشانی، رویدادهای موردنیاز رو انتخاب کنید. نشانی باید از HTTPS استفاده کنه و از داخل ایران در دسترس باشه؛ نشانی‌های شبکه داخلی پذیرفته نمی‌شن. پیام وب‌هوک شامل اطلاعات هویتی بیمار نیست.

  • appointment.created — ثبت نوبت
  • appointment.confirmed — تأیید نوبت
  • appointment.rescheduled — جابه‌جایی نوبت
  • appointment.cancelled — لغو نوبت
  • appointment.no_show — عدم مراجعه بیمار
  • appointment_request.created — درخواست نوبت جدید
{
  "id": "evt_…",
  "type": "appointment.cancelled",
  "apiVersion": "2026-10-01",
  "createdAt": "2026-10-10T07:31:02.000Z",
  "clinicId": "…",
  "data": {
    "appointment": {
      "id": "…", "status": "CANCELLED",
      "startAt": "…", "endAt": "…",
      "patientId": "…", "doctorId": "…",
      "serviceIds": ["…"], "source": "API"
    },
    "cancelledByPatient": false,
    "actor": { "type": "api_key", "apiKeyId": "…" }
  }
}

امضا

هر پیام هدر X-Bookora-Signature: t=<unix>,v1=<hex> داره. برای ساخت امضا، مقدار t، یک نقطه و بدنه خام درخواست کنار هم قرار می‌گیرن و با HMAC-SHA256 و رمز وب‌هوک امضا می‌شن. هنگام تعویض رمز، یک امضای v1 دیگه با رمز قبلی هم فرستاده می‌شه. هدرهای X-Bookora-Event و X-Bookora-Delivery هم همراه پیام ارسال می‌شن.

Node.js

import { createHmac, timingSafeEqual } from 'node:crypto';

// rawBody: the request body exactly as received (a Buffer or string).
export function verifyBookoraSignature(rawBody, header, secret, toleranceSec = 300) {
  const items = header.split(',').map((part) => part.trim().split('='));
  const t = items.find(([key]) => key === 't')?.[1];
  const signatures = items.filter(([key]) => key === 'v1').map(([, value]) => value);
  if (!t || signatures.length === 0) return false;
  if (Math.abs(Date.now() / 1000 - Number(t)) > toleranceSec) return false;
  const expected = createHmac('sha256', secret).update(`${t}.${rawBody}`).digest('hex');
  return signatures.some(
    (sig) => sig.length === expected.length && timingSafeEqual(Buffer.from(sig), Buffer.from(expected)),
  );
}

PHP

function verify_bookora_signature(string $rawBody, string $header, string $secret, int $tolerance = 300): bool
{
    $t = null;
    $signatures = [];
    foreach (explode(',', $header) as $part) {
        [$key, $value] = array_pad(explode('=', trim($part), 2), 2, '');
        if ($key === 't') { $t = $value; }
        if ($key === 'v1') { $signatures[] = $value; }
    }
    if ($t === null || !$signatures || abs(time() - (int) $t) > $tolerance) {
        return false;
    }
    $expected = hash_hmac('sha256', $t . '.' . $rawBody, $secret);
    foreach ($signatures as $signature) {
        if (hash_equals($expected, $signature)) { return true; }
    }
    return false;
}

Python

import hashlib
import hmac
import time


def verify_bookora_signature(raw_body: bytes, header: str, secret: str, tolerance: int = 300) -> bool:
    t, signatures = None, []
    for part in header.split(","):
        key, _, value = part.strip().partition("=")
        if key == "t":
            t = value
        elif key == "v1":
            signatures.append(value)
    if t is None or not signatures or abs(time.time() - int(t)) > tolerance:
        return False
    expected = hmac.new(secret.encode(), f"{t}.".encode() + raw_body, hashlib.sha256).hexdigest()
    return any(hmac.compare_digest(expected, signature) for signature in signatures)

ارسال مجدد و توقف خودکار

پاسخ 2xx یعنی پیام با موفقیت دریافت شده. پاسخ‌های دیگه، تغییر مسیر یا پاسخ ندادن تا ۱۰ ثانیه، ارسال ناموفق حساب می‌شن. تلاش‌های بعدی با فاصله‌های ۱ دقیقه، ۵ دقیقه، ۳۰ دقیقه، ۲ ساعت، ۶ ساعت، ۱۲ ساعت، ۲۴ ساعت انجام می‌شن و در مجموع حدود دو روز ادامه دارن. اگر ارسال به یک نشانی ۵۰ بار پیاپی ناموفق باشه، یا طی ۷۲ ساعت حداقل ۱۰ بار ارسال ناموفق داشته باشه و هیچ ارسال موفقی نداشته باشه، ارسال به اون نشانی خودکار متوقف می‌شه و به مالک کلینیک اطلاع داده می‌شه.

ممکنه پیام‌ها به ترتیب ایجادشون نرسن. اون‌ها رو بر اساس createdAt مرتب کنید و برای دریافت وضعیت فعلی، اطلاعات نوبت رو دوباره از API بخونید.

نسخه‌ها

apiVersion: 2026-10-01 — نسخه اول شامل دوازده مسیر برای نوبت‌ها و بیماران، شش رویداد وب‌هوک و امکان انتخاب شعبه در نوبت‌ها و زمان‌های آزاده.