مستندات فنی

اتصال به External API و وب‌هوک

راهنمای کامل برای اتصال به داده محصول/موجودی، سفارش، مشتری، خدمات، انبار، حسابداری و خرید هر رستوران در سکه، یا دریافت وب‌هوک تغییرات — به‌همراه ارزیابی امنیت و لاگ‌گیری.

آخرین به‌روزرسانی: ۲ مرداد ۱۴۰۵

۱احراز هویت و کلید API

هر درخواست به مسیرهای /external/* باید هدر X-Api-Key را داشته باشد. کلید فقط یک‌بار — در لحظهٔ ساخت — نمایش داده می‌شود و در پایگاه‌داده تنها هش SHA-256 آن نگه‌داری می‌شود؛ اگر گم شود، باید کلید جدید بسازید.

ساخت کلید (از پنل مدیریت رستوران، نه با X-Api-Key)

POST/api/v1/restaurants/:restaurantId/api-keysAuth: Bearer JWT (مالک رستوران)

Response 201

{
  "id": 4,
  "name": "اتصال پرومال",
  "keyPrefix": "hm_a1b2c3",
  "apiKey": "hm_a1b2c3d4e5f6...   ← فقط همین یک‌بار نمایش داده می‌شود"
}

دامنهٔ دسترسی (Scope)

هر کلید یک یا چند Scope دارد؛ درخواست به Endpointی که Scope لازم را ندارید، خطای ۴۰۳ می‌گیرد.

Scopeدسترسی
products:readخواندن لیست محصولات و جست‌وجوی بارکد
stock:writeثبت کسر/برگشت موجودی
orders:readخواندن لیست/جزئیات سفارش‌ها (فیلتر بر اساس وضعیت و بازهٔ زمانی)
customers:readخواندن اعضای باشگاه مشتریان یک رستوران — فقط نام، موبایل و امتیاز وفاداری
service-jobs:readخواندن پرونده‌های خدمات/نوبت‌دهی
inventory:readخواندن تاریخچهٔ حرکات انبار (خرید، فروش، تعدیل، انتقال بین انبار)
accounting:readخواندن فاکتور فروش، چک و مطالبات مشتری
purchasing:readخواندن فاکتور خرید و مرجوعی خرید

پیش‌فرض کلید تازه‌ساخته‌شده فقط products:read و stock:write را دارد — کلیدهای قدیمی هم همیشه همین دو را داشته‌اند و با اضافه‌شدن Scopeهای جدید، هیچ کلیدی خودبه‌خود دسترسی بیشتری پیدا نمی‌کند.

اضافه‌کردن Scope به کلید موجود

برای دادن دسترسی سفارش/مشتری/خدمات به یک کلید، مالک رستوران باید صریحاً Scopeهای جدید را ثبت کند — خودکار نیست.

PATCH/api/v1/restaurants/:restaurantId/api-keys/:keyId/scopesAuth: Bearer JWT (مالک رستوران)

Request

{ "scopes": ["products:read", "stock:write", "orders:read"] }

لیست کامل Scopeهای جدید کلید را جایگزین می‌کند؛ نام نامعتبر خطای ۴۰۰ می‌دهد.

۲Endpointهای خواندن/نوشتن

همهٔ این مسیرها زیر رستورانی هستند که به کلید API متصل است — نیازی به پاس‌دادن آن در URL نیست.

GET/external/pingبدون Scope خاص

تست اتصال و اعتبار کلید.

Response 200

{ "restaurantId": 12, "restaurantName": "کافه بهار" }
GET/external/products?updatedAfter=&page=products:read

لیست محصولات با قیمت/موجودی/وضعیت فعال. updatedAfter (ISO date) برای Sync افزایشی، page برای صفحه‌بندی (۲۰۰ رکورد در هر صفحه).

Response 200

{
  "items": [
    {
      "productId": 501,
      "name": "قهوه ترک",
      "barcode": "6260011122233",
      "price": 85000,
      "stock": 42,
      "isAvailable": true,
      "updatedAt": "2026-07-20T09:12:00.000Z"
    }
  ],
  "page": 1,
  "limit": 200
}
GET/external/products/by-barcode/:barcodeproducts:read

جست‌وجوی یک محصول با بارکد دقیق. اگر یافت نشود، خطای ۴۰۴.

POST/external/stock-deductionsstock:write

کسر موجودی برای اقلام فروخته‌شده (مثلاً یک سفارش نهایی‌شده در سیستم شما). موجودی هرگز منفی نمی‌شود.

Request

{
  "idempotencyKey": "order-98213",
  "reference": "promal-order-98213",
  "items": [
    { "barcode": "6260011122233", "quantity": 2 }
  ]
}

Response 201

{
  "idempotent": false,
  "results": [
    { "barcode": "6260011122233", "status": "ok", "newStock": 40 }
  ]
}
POST/external/stock-returnsstock:write

برگرداندن موجودی برای اقلام مرجوعی/لغوشده. همان شکل درخواست stock-deductions.

GET/external/orders?status=&from=&to=&page=orders:read

لیست سفارش‌ها. status یکی از pending/confirmed/preparing/ready/delivered/cancelled، from/to بازهٔ ثبت سفارش (ISO date)، ۵۰ رکورد در هر صفحه.

Response 200

{
  "items": [
    {
      "id": 4021,
      "orderNumber": "ORD-4021",
      "status": "confirmed",
      "serviceType": "dine_in",
      "totalAmount": 350000,
      "discountAmount": 0,
      "finalAmount": 350000,
      "paymentMethod": "cash",
      "onlinePaymentStatus": "none",
      "customerName": "علی رضایی",
      "customerPhone": "09120000000",
      "items": [{ "productId": 501, "name": "قهوه ترک", "quantity": 2, "price": 85000 }],
      "createdAt": "2026-07-20T09:12:00.000Z",
      "updatedAt": "2026-07-20T09:15:00.000Z"
    }
  ],
  "page": 1,
  "limit": 50,
  "total": 128
}
GET/external/orders/:idorders:read

جزئیات یک سفارش (همان ساختار بالا). اگر متعلق به رستوران کلید نباشد، ۴۰۴.

GET/external/customers?page=customers:read

اعضای باشگاه مشتریان رستوران — نه هر کاربری که سفارش داده. فقط فیلدهای لازم برای یکپارچه‌سازی: نام، موبایل و مجموع امتیاز وفاداری. ایمیل، تاریخ تولد و نقش‌های کاربری هرگز در این پاسخ نیستند (جزئیات در بخش «ارزیابی امنیت و لاگ»).

Response 200

{
  "items": [
    {
      "userId": 88,
      "firstName": "سارا",
      "lastName": "احمدی",
      "mobile": "09121234567",
      "loyaltyPoints": 340,
      "enrolledAt": "2026-02-10T00:00:00.000Z"
    }
  ],
  "page": 1,
  "limit": 50,
  "total": 62
}
GET/external/service-jobs?status=&from=&to=&page=service-jobs:read

لیست پرونده‌های خدمات/نوبت‌دهی. status بر اساس دستهٔ وضعیت (intake/in_progress/done/cancelledfrom/to روی تاریخ سررسید.

Response 200

{
  "items": [
    {
      "id": 210,
      "jobNumber": "JOB-00210",
      "title": "سرویس دوره‌ای",
      "status": { "id": 3, "label": "در حال انجام", "category": "in_progress", "isFinal": false },
      "priority": "normal",
      "customerName": "سارا احمدی",
      "customerPhone": "09121234567",
      "dueDate": "2026-08-01",
      "estimatedAmount": 500000,
      "finalAmount": 0,
      "createdAt": "2026-07-20T09:12:00.000Z",
      "updatedAt": "2026-07-20T09:12:00.000Z",
      "closedAt": null
    }
  ],
  "page": 1,
  "limit": 50,
  "total": 14
}
GET/external/service-jobs/:idservice-jobs:read

جزئیات یک پروندهٔ خدمت (همان ساختار بالا). اگر متعلق به رستوران کلید نباشد، ۴۰۴.

GET/external/inventory/movements?type=&warehouseId=&from=&to=&page=inventory:read

تاریخچهٔ حرکات انبار. type یکی از purchase/sale_consumption/adjustment/waste/return/warehouse_transfer_out/warehouse_transfer_in، from/to روی زمان ثبت.

Response 200

{
  "items": [
    {
      "id": 5501,
      "movementType": "purchase",
      "quantity": 10,
      "isIncrease": true,
      "rawMaterialName": "آرد",
      "finalProductName": null,
      "warehouseName": "انبار مرکزی",
      "referenceType": "purchase_invoice",
      "referenceId": 220,
      "createdAt": "2026-07-24T10:00:00.000Z"
    }
  ],
  "page": 1,
  "limit": 50,
  "total": 340
}
GET/external/accounting/sales-invoices?status=&from=&to=&page=accounting:read

لیست فاکتورهای فروش. status یکی از draft/issued/paid/cancelled.

Response 200

{
  "items": [
    {
      "id": 340,
      "invoiceNumber": "SI-340",
      "status": "issued",
      "subtotalAmount": 500000,
      "discountAmount": 0,
      "totalAmount": 500000,
      "vatAmount": null,
      "saleDate": "2026-07-24",
      "createdAt": "2026-07-24T10:00:00.000Z"
    }
  ],
  "page": 1,
  "limit": 50,
  "total": 90
}
GET/external/accounting/sales-invoices/:idaccounting:read

جزئیات یک فاکتور فروش (همان ساختار بالا). اگر متعلق به رستوران کلید نباشد، ۴۰۴.

GET/external/accounting/cheques?status=&type=&from=&to=&page=accounting:read

لیست چک‌ها. status یکی از pending/cleared/bounced/cancelled، type یکی از issued/received، from/to روی تاریخ سررسید.

Response 200

{
  "items": [
    {
      "id": 12,
      "chequeType": "received",
      "chequeNumber": "CHQ-12",
      "bankName": "بانک ملت",
      "amount": 2000000,
      "dueDate": "2026-08-01",
      "status": "pending"
    }
  ],
  "page": 1,
  "limit": 50,
  "total": 8
}
GET/external/accounting/receivables?status=&from=&to=&page=accounting:read

لیست مطالبات مشتری. status یکی از open/partial/paid/written_off.

Response 200

{
  "items": [
    {
      "id": 45,
      "customerName": "علی رضایی",
      "customerPhone": "09120000000",
      "totalAmount": 1000000,
      "paidAmount": 400000,
      "dueDate": "2026-08-10",
      "status": "partial"
    }
  ],
  "page": 1,
  "limit": 50,
  "total": 20
}
GET/external/purchasing/invoices?status=&from=&to=&supplierId=&page=purchasing:read

لیست فاکتورهای خرید. status یکی از draft/pending_approval/approved/rejected.

Response 200

{
  "items": [
    {
      "id": 220,
      "invoiceNumber": "PI-220",
      "status": "approved",
      "supplierId": 5,
      "supplierName": "تامین‌کننده تست",
      "purchaseDate": "2026-07-20",
      "totalAmount": 3500000
    }
  ],
  "page": 1,
  "limit": 50,
  "total": 60
}
GET/external/purchasing/invoices/:idpurchasing:read

جزئیات یک فاکتور خرید (همان ساختار بالا). اگر متعلق به رستوران کلید نباشد، ۴۰۴.

GET/external/purchasing/returns?status=&from=&to=&page=purchasing:read

لیست مرجوعی‌های خرید. status یکی از draft/approved/cancelled.

Response 200

{
  "items": [
    {
      "id": 18,
      "returnNumber": "PR-18",
      "status": "approved",
      "purchaseInvoiceId": 220,
      "totalAmount": 200000,
      "returnDate": "2026-07-22"
    }
  ],
  "page": 1,
  "limit": 50,
  "total": 5
}

۳Idempotency

stock-deductions و stock-returns اجباراً یک idempotencyKey یکتا می‌خواهند (مثلاً شناسهٔ سفارش خودتان). اگر همان کلید را دوباره بفرستید — چه به‌خاطر Retry شبکه، چه اشتباه — سیستم موجودی را دوباره کم/زیاد نمی‌کند؛ فقط همان نتیجهٔ قبلی را با idempotent: true برمی‌گرداند.

نکته عملی: از شناسهٔ پایدار خودتان (شمارهٔ سفارش/تراکنش) به‌عنوان idempotencyKey استفاده کنید، نه یک مقدار تصادفی تازه در هر تلاش — وگرنه مزیت Idempotency از بین می‌رود.

۴وب‌هوک خروجی

این جهت برعکس بخش قبل است: Menus_BE به آدرس شما پیام می‌فرستد، نه شما به آن.

ثبت آدرس دریافت‌کننده

POST/api/v1/restaurants/:restaurantId/webhooksAuth: Bearer JWT (مالک رستوران)

Request

{
  "url": "https://your-server.com/hooks/menus",
  "description": "سینک فروشگاه",
  "events": ["product.updated", "order.status_changed"]
}

events اختیاری است — اگر نفرستید، فقط رویداد قیمت/موجودی (product.updated) دریافت می‌کنید، دقیقاً مثل رفتار قبلی. برای رویدادهای جدید باید صریحاً نامشان را در همین آرایه بگنجانید.

Methodمسیرکاربرد
GET.../webhooksلیست آدرس‌های ثبت‌شده
PATCH.../webhooks/:id/toggleفعال/غیرفعال‌کردن موقت
DELETE.../webhooks/:idحذف کامل
GET.../webhooks/:id/deliveriesتاریخچهٔ تحویل (زیر)

رویدادها

Eventچه زمانی ارسال می‌شود
product.updatedروی هر تغییر قیمت یا وضعیت موجودبودن یک محصول/کالای انبار که بارکد دارد (پیش‌فرض، بدون نیاز به فهرست‌کردن در events)
order.status_changedروی هر تغییر وضعیت سفارش
service_job_status_changedروی هر تغییر وضعیت پروندهٔ خدمات/نوبت — همان رویداد داخلی که موتور Workflow هم استفاده می‌کند
inventory_movement.createdروی هر حرکت انبار — خرید، فروش، تعدیل، ضایعات، مرجوعی، انتقال بین انبار
sales_invoice.status_changedروی هر تغییر وضعیت فاکتور فروش
cheque.status_changedروی هر تغییر وضعیت چک (وصول/برگشت/لغو)
receivable.status_changedروی هر پرداخت مطالبهٔ مشتری (وضعیت جزئی/تسویه)
purchase_invoice.status_changedروی هر تغییر وضعیت فاکتور خرید (مثلاً تایید)
purchase_return.status_changedروی تایید یا لغو یک مرجوعی خرید

POST به آدرس شما — product.updated

{
  "items": [
    { "barcode": "6260011122233", "price": 89000, "isAvailable": true }
  ]
}

POST به آدرس شما — order.status_changed

{
  "orderId": 4021,
  "orderNumber": "ORD-4021",
  "previousStatus": "confirmed",
  "status": "ready",
  "updatedAt": "2026-07-24T10:00:00.000Z"
}

POST به آدرس شما — service_job_status_changed

{
  "jobId": 210,
  "jobNumber": "JOB-00210",
  "fromStatusId": 2,
  "toStatus": { "id": 3, "label": "در حال انجام", "category": "in_progress", "isFinal": false },
  "updatedAt": "2026-07-24T10:00:00.000Z"
}

POST به آدرس شما — inventory_movement.created

{
  "movementId": 5501,
  "movementType": "purchase",
  "quantity": 10,
  "isIncrease": true,
  "rawMaterialId": 3,
  "finalProductId": null,
  "warehouseId": 1,
  "referenceType": "purchase_invoice",
  "referenceId": 220,
  "createdAt": "2026-07-24T10:00:00.000Z"
}

POST به آدرس شما — sales_invoice.status_changed

{
  "invoiceId": 340,
  "invoiceNumber": "SI-340",
  "previousStatus": "issued",
  "status": "paid",
  "totalAmount": 500000,
  "updatedAt": "2026-07-24T10:00:00.000Z"
}

POST به آدرس شما — cheque.status_changed

{
  "chequeId": 12,
  "chequeNumber": "CHQ-12",
  "chequeType": "received",
  "previousStatus": "pending",
  "status": "cleared",
  "amount": 2000000,
  "dueDate": "2026-08-01"
}

POST به آدرس شما — receivable.status_changed

{
  "receivableId": 45,
  "previousStatus": "partial",
  "status": "paid",
  "paidAmount": 1000000,
  "totalAmount": 1000000
}

POST به آدرس شما — purchase_invoice.status_changed

{
  "invoiceId": 220,
  "invoiceNumber": "PI-220",
  "previousStatus": "pending_approval",
  "status": "approved",
  "supplierId": 5,
  "totalAmount": 3500000
}

POST به آدرس شما — purchase_return.status_changed

{
  "returnId": 18,
  "returnNumber": "PR-18",
  "previousStatus": "draft",
  "status": "approved",
  "purchaseInvoiceId": 220
}

سیاست تلاش مجدد

پارامترمقدار
Timeout هر تلاش۸ ثانیه
حداکثر تلاش۳ بار
فاصلهٔ بین تلاش‌ها2s → 4s → 6s
بعد از شکست نهاییدر جدول تاریخچهٔ تحویل (زیر) با success: false ثبت می‌شود؛ آدرس شما غیرفعال نمی‌شود، اما همان پیام دوباره فرستاده نمی‌شود.

تاریخچهٔ تحویل

GET/api/v1/restaurants/:restaurantId/webhooks/:id/deliveries?page=Auth: Bearer JWT (مالک رستوران)

هر تلاش ارسال — موفق یا ناموفق — برای این آدرس، جدیدترین اول.

Response 200

{
  "items": [
    {
      "id": 991,
      "eventType": "product.updated",
      "success": true,
      "statusCode": 200,
      "errorMessage": null,
      "attemptCount": 1,
      "createdAt": "2026-07-24T10:00:00.000Z"
    }
  ],
  "page": 1,
  "limit": 50,
  "total": 3400
}
نکته عملی: با این حال سرور خودتان باید هر درخواست دریافتی را idempotent پردازش کند — تاریخچهٔ تحویل برای رفع‌اشکال است، نه صف تحویل تضمین‌شده. اگر ارتباط شما قطعی طولانی داشت، از Endpoint products با updatedAfter، یا از orders/service-jobs/inventory/movements/accounting/*/purchasing/* با from/to، برای همگام‌سازی جبرانی استفاده کنید.

۵خطاها و کدهای وضعیت

کدمعنیعلت رایج
401Unauthorizedهدر X-Api-Key نیست یا کلید نامعتبر/غیرفعال است
403Forbiddenکلید Scope لازم برای این Endpoint را ندارد
404Not Foundبارکد/سفارش/پروندهٔ خدمت یافت نشد
400Bad RequestidempotencyKey ارسال نشده، آیتم‌ها نامعتبرند، یا Scope درخواستی نامعتبر است
429Too Many Requestsبیش از ۱۲۰ درخواست در دقیقه با همین کلید API

۶ارزیابی امنیت و لاگ

پاسخ مستقیم به این سؤال: آیا این لایه امن است و همه‌چیز لاگ می‌شود؟

تأیید‌شده

دسترسی محدود، به‌جا و Scope-based

  • کلید API فقط به Scopeهایی که مالک رستوران صریحاً فعال کرده دسترسی دارد؛ کلیدهای قدیمی با اضافه‌شدن سفارش/مشتری/خدمات/انبار/حسابداری/خرید به این ماژول خودبه‌خود دسترسی جدیدی پیدا نکردند.
  • دادهٔ مشتری (customers:read) عمداً به اعضای باشگاه مشتریان همان رستوران و فقط چهار فیلد محدود است — نام، موبایل، امتیاز وفاداری، تاریخ عضویت. نه ایمیل، نه تاریخ تولد، نه نقش کاربری، نه دادهٔ رستوران‌های دیگر همان کاربر.
  • کلید خام فقط یک‌بار نمایش داده می‌شود؛ در پایگاه‌داده فقط هش SHA-256 آن ذخیره است.
  • مدیریت کلید/وب‌هوک پشت JWT و بررسی مالکیت رستوران است، نه در دسترس عموم.
تأیید‌شده

لاگ نوشتن‌ها (Idempotency Log)

  • هر stock-deductions/stock-returns — موفق یا جزئی — در جدول اختصاصی با payload کامل و نتیجهٔ هر آیتم ذخیره می‌شود.
  • همان درخواست‌ها در Audit Log عمومی سایت هم ثبت می‌شوند.
تأیید‌شده

تاریخچهٔ تحویل وب‌هوک خروجی

  • هر تلاش ارسال — موفق یا ناموفق، با کد وضعیت و پیام خطا — در جدول اختصاصی ذخیره می‌شود؛ از Endpoint .../webhooks/:id/deliveries قابل مشاهده است.
تأیید‌شده

محدودیت نرخ (Rate Limit)

  • روی همهٔ مسیرهای /external/* — ۱۲۰ درخواست در دقیقه، بر اساس خودِ کلید API (نه IP)، چون همهٔ درخواست‌های یک پارتنر معمولاً از یک سرور می‌آید.
تأیید‌شده

شناسهٔ رستوران در Audit Log عمومی

  • Audit Log سراسری سایت حالا علاوه بر body/query/هدر، مسیر URL (/restaurants/:restaurantId/...) و شناسهٔ رستوران متصل به کلید API را هم می‌خواند — دیگر برای این مسیرها خالی نمی‌ماند.

۷چک‌لیست قبل از اتصال

  • کلید API را از پنل رستوران ساختید و در جای امن ذخیره کردید (فقط یک‌بار نمایش داده می‌شود)
  • با GET /external/ping اتصال را تست کردید
  • اگر به سفارش/مشتری/خدمات/انبار/حسابداری/خرید هم نیاز دارید، Scope مربوطه را با PATCH .../api-keys/:keyId/scopes اضافه کردید
  • برای همهٔ درخواست‌های نوشتن، از شناسهٔ پایدار خودتان به‌عنوان idempotencyKey استفاده می‌کنید
  • اگر وب‌هوک خروجی می‌خواهید، آدرستان HTTPS و همیشه در دسترس است و رویدادهای لازم را در events مشخص کرده‌اید
  • یک Job دوره‌ای برای Sync جبرانی دارید (updatedAfter برای محصول، from/to برای سفارش/خدمات)، برای مواقعی که وب‌هوک از دست می‌رود
  • محدودیت ۱۲۰ درخواست در دقیقه به ازای هر کلید API را در نظر گرفته‌اید

سوالی دارید؟

تیم پشتیبانی سکه آماده پاسخ‌گویی به پرسش‌های شماست.

تماس با ما