اتصال به External API و وبهوک
راهنمای کامل برای اتصال به داده محصول/موجودی، سفارش، مشتری، خدمات، انبار، حسابداری و خرید هر رستوران در سکه، یا دریافت وبهوک تغییرات — بههمراه ارزیابی امنیت و لاگگیری.
آخرین بهروزرسانی: ۲ مرداد ۱۴۰۵
فهرست مطالب
۱احراز هویت و کلید API
هر درخواست به مسیرهای /external/* باید هدر X-Api-Key را داشته باشد. کلید فقط یکبار — در لحظهٔ ساخت — نمایش داده میشود و در پایگاهداده تنها هش SHA-256 آن نگهداری میشود؛ اگر گم شود، باید کلید جدید بسازید.
ساخت کلید (از پنل مدیریت رستوران، نه با X-Api-Key)
/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های جدید را ثبت کند — خودکار نیست.
/api/v1/restaurants/:restaurantId/api-keys/:keyId/scopesAuth: Bearer JWT (مالک رستوران)Request
{ "scopes": ["products:read", "stock:write", "orders:read"] }لیست کامل Scopeهای جدید کلید را جایگزین میکند؛ نام نامعتبر خطای ۴۰۰ میدهد.
۲Endpointهای خواندن/نوشتن
همهٔ این مسیرها زیر رستورانی هستند که به کلید API متصل است — نیازی به پاسدادن آن در URL نیست.
/external/pingبدون Scope خاصتست اتصال و اعتبار کلید.
Response 200
{ "restaurantId": 12, "restaurantName": "کافه بهار" }/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
}/external/products/by-barcode/:barcodeproducts:readجستوجوی یک محصول با بارکد دقیق. اگر یافت نشود، خطای ۴۰۴.
/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 }
]
}/external/stock-returnsstock:writeبرگرداندن موجودی برای اقلام مرجوعی/لغوشده. همان شکل درخواست stock-deductions.
/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
}/external/orders/:idorders:readجزئیات یک سفارش (همان ساختار بالا). اگر متعلق به رستوران کلید نباشد، ۴۰۴.
/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
}/external/service-jobs?status=&from=&to=&page=service-jobs:readلیست پروندههای خدمات/نوبتدهی. status بر اساس دستهٔ وضعیت (intake/in_progress/done/cancelled)، from/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
}/external/service-jobs/:idservice-jobs:readجزئیات یک پروندهٔ خدمت (همان ساختار بالا). اگر متعلق به رستوران کلید نباشد، ۴۰۴.
/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
}/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
}/external/accounting/sales-invoices/:idaccounting:readجزئیات یک فاکتور فروش (همان ساختار بالا). اگر متعلق به رستوران کلید نباشد، ۴۰۴.
/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
}/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
}/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
}/external/purchasing/invoices/:idpurchasing:readجزئیات یک فاکتور خرید (همان ساختار بالا). اگر متعلق به رستوران کلید نباشد، ۴۰۴.
/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 به آدرس شما پیام میفرستد، نه شما به آن.
ثبت آدرس دریافتکننده
/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 ثبت میشود؛ آدرس شما غیرفعال نمیشود، اما همان پیام دوباره فرستاده نمیشود. |
تاریخچهٔ تحویل
/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
}products با updatedAfter، یا از orders/service-jobs/inventory/movements/accounting/*/purchasing/* با from/to، برای همگامسازی جبرانی استفاده کنید.۵خطاها و کدهای وضعیت
| کد | معنی | علت رایج |
|---|---|---|
401 | Unauthorized | هدر X-Api-Key نیست یا کلید نامعتبر/غیرفعال است |
403 | Forbidden | کلید Scope لازم برای این Endpoint را ندارد |
404 | Not Found | بارکد/سفارش/پروندهٔ خدمت یافت نشد |
400 | Bad Request | idempotencyKey ارسال نشده، آیتمها نامعتبرند، یا Scope درخواستی نامعتبر است |
429 | Too 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 را در نظر گرفتهاید
سوالی دارید؟
تیم پشتیبانی سکه آماده پاسخگویی به پرسشهای شماست.