بوابة التوريد، الصفحة الرئيسية

للمطورين وفرق تقنية المعلومات

نظامك يطلب لوحده

اربط نظام المخزون أو الـERP لديكم ببوابة التوريد مرة واحدة: يرسل نظامك أرصدة المخزون، وعندما يصل صنف إلى نقطة إعادة الطلب التي تحددها، يصلنا طلب بالكمية التي اخترتها ليُسعَّر بأسعار منشأتك. ويسمع نظامك كل خطوة عبر الإشعارات: التسعير والتأكيد والشحن والتسليم والفاتورة، ويمكنه قبول العروض المسعّرة بنفسه.

كيف يعمل

  1. 1نظامكيرسل الأرصدة كل ليلة أو مع كل حركة مخزون
  2. 2قواعد إعادة الطلبنقطة إعادة الطلب والكمية لكل صنف
  3. 3الطلبيصل إلى فريقنا كعرض سعر بأسعار منشأتك
  4. 4إشعار لنظامكعند التسعير والتأكيد والشحن والتسليم والفاتورة

البدء في ثلاث خطوات

  1. 1أنشئ مفتاحًامن حسابك: الربط مع أنظمتكم ← المفاتيح. اختر الصلاحيات، وهل تذهب طلبات المفتاح مباشرة إلى بوابة التوريد أو تنتظر موافقة مدير الحساب. المفتاح يظهر مرة واحدة، فاحفظه في خزنة الأسرار لديكم.
  2. 2حدّد قواعد إعادة الطلبلكل صنف: رمزه (sku) ووحدته، ونقطة إعادة الطلب (min_qty)، والكمية التي نطلبها (reorder_qty). من الحساب أو عبر POST /reorder-rules.
  3. 3أرسل الأرصدةاستدعِ PUT /stock-levels بجدول زمني. ما وصل إلى نقطة إعادة الطلب يُجمع في طلب واحد، والرد يخبرك بما قُبل وما طُلب.

إرسال الأرصدة

curl -X PUT https://tawreed-gate-nine.vercel.app/api/v1/stock-levels \
  -H "Authorization: Bearer $TAWREED_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: stock-2026-10-08T03:00" \
  -d '{
    "levels": [
      { "sku": "BUSD039", "unit": "CTN", "qty": 4 },
      { "sku": "BUSH118", "qty": 40 },
      { "sku": "XYZ-404", "qty": 6 }
    ]
  }'

الرد

{
  "accepted": 2,
  "rejected": [ { "index": 2, "error": "unknown_product" } ],
  "triggered": 1,
  "request": { "id": "6b1f0c3e-…", "status": "submitting", "ref": "WEB-2610-4F7Q2K" }
}
acceptedعدد الأرصدة التي سجّلناها
rejectedالأسطر التي لم نتعرف عليها مع رقمها: unknown_product أو unknown_unit أو invalid_qty
triggeredعدد الأصناف التي وصلت إلى نقطة إعادة الطلب وطُلبت
requestالطلب الذي ذهبت فيه (أو null إذا لم يلزم شيء)

منطق إعادة الطلب

إذا كان الرصيد الذي يرسله نظامك عند نقطة إعادة الطلب أو أقل، يدخل الصنف في طلب بالكمية المحددة في قاعدته. كل الأصناف التي يطلقها إرسال واحد تذهب معًا في طلب واحد.

مثال

الرصيد الذي أرسله نظامك
4 كراتين
نقطة إعادة الطلب
30 ← الرصيد أقل منها
كمية إعادة الطلب
36 كرتونة
الطلب
WEB-2610-4F7Q2K · 36 كرتونة
  • لا يتكرر الطلب: ما دام طلب القاعدة مفتوحًا (ينتظر الموافقة أو قيد الإرسال)، أو خلال 24 ساعة من وصوله إلينا، لا يطلق الرصيد المنخفض نفسه طلبًا آخر.
  • المفتاح الذي يرسل مباشرة يوصل الطلب إلى فريقنا فورًا؛ غيره ينتظر موافقة مدير في حسابكم (الحدث request.awaiting_approval).
  • يسعّر فريقنا العرض بأسعار منشأتك المتفق عليها ويرسله (الحدث quotation.priced). اقبله من الحساب، أو من نظامك عبر POST /quotations/{number}/accept بمفتاح يرسل مباشرة.
  • الوحدة: أرسل رمزها (CTN) أو اسمها بالعربية أو الإنجليزية أو uom_id، أو اتركها لتُستخدم وحدة البيع الأساسية للصنف.
curl -X POST https://tawreed-gate-nine.vercel.app/api/v1/reorder-rules \
  -H "Authorization: Bearer $TAWREED_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: rule-BUSD039" \
  -d '{ "sku": "BUSD039", "unit": "CTN", "min_qty": 30, "reorder_qty": 36 }'

المصادقة والصلاحيات

أرسل المفتاح في كل طلب بالترويسة Authorization: Bearer. لكل مفتاح صلاحيات تختارها عند إنشائه، ويعمل كل مفتاح داخل بيانات منشأتك فقط.

Authorization: Bearer tg_live_…

الصلاحيات

catalog:readالبحث في المنتجات ووحداتها
orders:readقراءة الطلبات وعروض الأسعار والطلبات المؤكدة والشحنات والعناوين
requests:writeإرسال الطلبات وقبول العروض المسعّرة
stock:writeإرسال الأرصدة وإدارة قواعد إعادة الطلب
invoices:readقراءة الفواتير وما هو مستحق
webhooks:manageإضافة روابط الإشعارات وحذفها

المسارات

https://tawreed-gate-nine.vercel.app/api/v1

GET/me—المنشأة وصلاحيات المفتاح وأين تذهب طلباته
GET/productscatalog:readالمنتجات بوحداتها (q، page، limit)
GET/products/{sku}catalog:readمنتج واحد برمزه أو رقمه
POST/requestsrequests:writeأرسل طلبًا (أسطر sku أو product_id مع unit وqty)
GET/requests/{id}orders:readحالة طلب وعرض السعر الخاص به
GET/quotationsorders:readالعروض لدى بوابة التوريد والمسعّرة
POST/quotations/{number}/acceptrequests:writeاقبل عرضًا مسعّرًا (للمفاتيح التي ترسل مباشرة)
GET/ordersorders:readالطلبات المؤكدة
GET/orders/{number}orders:readعرض أو طلب واحد بأسطره ومراحله وشحناته
GET/invoicesinvoices:readالفواتير وحالتها والمتبقي منها
GET/addressesorders:readعناوين التوصيل (address_id للطلبات)
PUT/stock-levelsstock:writeأرسل الأرصدة؛ نطلب ما وصل إلى نقطة إعادة الطلب
GET/reorder-rulesstock:writeكل قواعد إعادة الطلب
POST/reorder-rulesstock:writeأنشئ قاعدة صنف أو عدّلها
DELETE/reorder-rules/{id}stock:writeاحذف قاعدة
GET/webhookswebhooks:manageروابط الإشعارات
POST/webhookswebhooks:manageأضف رابطًا (السرّ يظهر مرة واحدة)
DELETE/webhooks/{id}webhooks:manageاحذف رابطًا

إرسال طلب

curl -X POST https://tawreed-gate-nine.vercel.app/api/v1/requests \
  -H "Authorization: Bearer $TAWREED_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: po-77812" \
  -d '{
    "po_ref": "PO-77812",
    "notes": "Deliver to gate 2",
    "lines": [
      { "sku": "BUSH118", "unit": "EA", "qty": 24 },
      { "sku": "BUSD039", "qty": 10 }
    ]
  }'

→ 201 { "id": "…", "status": "submitting", "website_reference": "WEB-2610-4F7Q2K" }

الإشعارات (Webhooks)

أضف رابطًا يستقبل الأحداث من حسابك (الربط مع أنظمتكم ← الإشعارات) أو عبر POST /webhooks. نرسل كل حدث كـPOST بصيغة JSON موقّعًا بسرّ الرابط، ونعيد المحاولة عند الفشل حتى 7 مرات أخرى خلال يومين تقريبًا، ونوقف الرابط ونبلغ مديري الحساب بالبريد بعد 15 فشلًا متتاليًا. الرابط يجب أن يكون عنوان https عامًا.

الأحداث

  • request.submitted
  • request.awaiting_approval
  • quotation.priced
  • order.confirmed
  • order.cancelled
  • delivery.out
  • delivery.done
  • invoice.issued
  • reorder.triggered

شكل الإشعار

POST https://erp.example.com/hooks/tawreed
X-Tawreed-Event: quotation.priced
X-Tawreed-Delivery: 3f6c0a3e-…
X-Tawreed-Signature: t=1791450000,v1=5b1f0c…

{
  "id": "evt_8d3c1f9a-…",
  "type": "quotation.priced",
  "created": "2026-10-08T09:12:04.120Z",
  "data": {
    "order": { "id": 4521, "number": "S00142", "website_reference": "WEB-2610-4F7Q2K",
               "amount_total": 880.79, "currency": "SAR" }
  }
}

تحقق من التوقيع

الترويسة X-Tawreed-Signature تحمل t (وقت الإرسال) وv1 (توقيع HMAC-SHA256 للنص "t.جسم_الطلب_الخام" بسرّ الرابط). احسبه بنفسك وقارنه، وارفض الطلبات الأقدم من 5 دقائق. الترويسة X-Tawreed-Delivery ثابتة لكل حدث، فاستخدمها لتجاهل التكرار.

Node.js
import crypto from 'node:crypto';

// rawBody: the request body exactly as received (not re-serialised JSON)
export function verifyTawreed(rawBody, header, secret) {
  const parts = Object.fromEntries(header.split(',').map((p) => p.split('=')));
  const expected = crypto.createHmac('sha256', secret).update(`${parts.t}.${rawBody}`).digest('hex');
  const fresh = Math.abs(Date.now() / 1000 - Number(parts.t)) < 300;
  return fresh && expected.length === parts.v1?.length
    && crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1));
}
Python
import hashlib, hmac, time

def verify_tawreed(raw_body: bytes, header: str, secret: str) -> bool:
    parts = dict(p.split("=", 1) for p in header.split(","))
    signed = parts["t"].encode() + b"." + raw_body
    expected = hmac.new(secret.encode(), signed, hashlib.sha256).hexdigest()
    return abs(time.time() - int(parts["t"])) < 300 and hmac.compare_digest(expected, parts["v1"])
PHP
function tawreed_verify(string $raw, string $header, string $secret): bool {
    parse_str(str_replace(',', '&', $header), $p);
    $expected = hash_hmac('sha256', $p['t'] . '.' . $raw, $secret);
    return abs(time() - (int) $p['t']) < 300 && hash_equals($expected, $p['v1']);
}

وصفات لأنظمة شائعة

كلها تفعل الشيء نفسه: تقرأ أرصدة الأصناف من نظامك وترسلها إلى PUT /stock-levels بجدول زمني. يرسل نظامك رمز صنف بوابة التوريد (sku) لكل صنف، وأغلب الأنظمة تحفظه كرمز المورد على الصنف.

Odoo

وحدة صغيرة مخصصة مع مهمة مجدولة (ir.cron) كل ليلة. تقرأ رمز بوابة التوريد من رمز المورد في صفحة الصنف (المورّدون).

# models/tawreed_sync.py
import requests
from odoo import api, models

TAWREED = "https://tawreed-gate-nine.vercel.app/api/v1"

class TawreedSync(models.AbstractModel):
    _name = "tawreed.sync"
    _description = "Report stock levels to Tawreed Gate"

    @api.model
    def push_stock(self):
        params = self.env["ir.config_parameter"].sudo()
        key = params.get_param("tawreed.api_key")
        vendor = int(params.get_param("tawreed.partner_id"))  # Tawreed Gate as a vendor
        lines = self.env["product.supplierinfo"].search(
            [("partner_id", "=", vendor), ("product_code", "!=", False)])
        levels = []
        for s in lines:
            product = s.product_id or s.product_tmpl_id.product_variant_id
            levels.append({"sku": s.product_code, "qty": max(0, product.qty_available)})
        for i in range(0, len(levels), 2000):
            r = requests.put(f"{TAWREED}/stock-levels",
                             json={"levels": levels[i:i + 2000]},
                             headers={"Authorization": f"Bearer {key}"}, timeout=30)
            r.raise_for_status()
SAP Business One

سكربت يعمل بجدول (Task Scheduler أو cron) يقرأ الأرصدة من Service Layer. احفظ رمز بوابة التوريد في حقل مستخدم على الصنف (U_TawreedSKU).

import os, requests

SL = "https://your-b1-server:50000/b1s/v1"
TAWREED = "https://tawreed-gate-nine.vercel.app/api/v1"

sl = requests.Session()
sl.post(f"{SL}/Login", json={"CompanyDB": os.environ["B1_DB"],
        "UserName": os.environ["B1_USER"], "Password": os.environ["B1_PASS"]}).raise_for_status()

levels = []
url = f"{SL}/Items?$select=ItemCode,QuantityOnStock,U_TawreedSKU&$filter=U_TawreedSKU ne null"
while url:
    page = sl.get(url).json()
    levels += [{"sku": it["U_TawreedSKU"], "qty": max(0, it["QuantityOnStock"])} for it in page["value"]]
    nxt = page.get("odata.nextLink") or page.get("@odata.nextLink")
    url = f"{SL}/{nxt}" if nxt else None

for i in range(0, len(levels), 2000):
    requests.put(f"{TAWREED}/stock-levels", json={"levels": levels[i:i + 2000]},
                 headers={"Authorization": f"Bearer {os.environ['TAWREED_KEY']}"},
                 timeout=30).raise_for_status()
Dynamics 365 Business Central

بلا كود عبر Power Automate (إجراء HTTP يحتاج ترخيصًا مميزًا). استخدم حقلًا يحمل رمز بوابة التوريد للصنف.

1. Trigger      Recurrence: every day at 03:00
2. Action       Business Central → List items (V3)
                Filter: type eq 'Inventory'
3. Action       Data Operation → Select
                From: value
                Map:  sku = <the field holding the Tawreed Gate code>
                      qty = item()?['inventory']
4. Action       HTTP
                Method:  PUT
                URI:     https://tawreed-gate-nine.vercel.app/api/v1/stock-levels
                Headers: Authorization = Bearer <your key>
                         Content-Type  = application/json
                Body:    { "levels": @{body('Select')} }
NetSuite

سكربت SuiteScript 2.1 مجدول؛ يقرأ رمز بوابة التوريد من رمز المورد (Vendor Name/Code)، واحفظ المفتاح في معامل السكربت.

/**
 * @NApiVersion 2.1
 * @NScriptType ScheduledScript
 */
define(['N/https', 'N/search', 'N/runtime', 'N/log'], (https, search, runtime, log) => ({
  execute() {
    const key = runtime.getCurrentScript().getParameter({ name: 'custscript_tawreed_key' });
    const levels = [];
    search.create({
      type: search.Type.INVENTORY_ITEM,
      filters: [['isinactive', 'is', 'F'], 'AND', ['vendorname', 'isnotempty', '']],
      columns: ['vendorname', 'quantityonhand'],
    }).run().each((r) => {
      levels.push({ sku: r.getValue('vendorname'), qty: Math.max(0, Number(r.getValue('quantityonhand')) || 0) });
      return levels.length < 2000;
    });
    const res = https.put({
      url: 'https://tawreed-gate-nine.vercel.app/api/v1/stock-levels',
      body: JSON.stringify({ levels }),
      headers: { Authorization: `Bearer ${key}`, 'Content-Type': 'application/json' },
    });
    log.audit('Tawreed Gate', `${res.code} ${res.body.slice(0, 300)}`);
  },
}));
Zoho Inventory

دالة Deluge مخصصة تعمل بجدول، مع حقل مخصص يحمل رمز بوابة التوريد.

items = zoho.inventory.getRecords("items", ORGANIZATION_ID);
levels = List();
for each it in items.get("items")
{
    sku = it.get("cf_tawreed_sku");
    if(sku != null && sku != "")
    {
        levels.add({"sku": sku, "qty": it.get("stock_on_hand")});
    }
}
resp = invokeurl
[
    url: "https://tawreed-gate-nine.vercel.app/api/v1/stock-levels"
    type: PUT
    parameters: {"levels": levels}.toString()
    headers: {"Authorization": "Bearer " + TAWREED_KEY, "Content-Type": "application/json"}
];
info resp;
n8n · Make · Zapier

جدول زمني ثم عقدة HTTP، وعقدة Webhook لاستقبال أحداثنا.

Schedule trigger      every night at 03:00
Your source           Google Sheets / database / ERP node → rows with the Tawreed code and quantity
HTTP Request          PUT https://tawreed-gate-nine.vercel.app/api/v1/stock-levels
                      Header auth: Authorization = Bearer <your key>
                      JSON body:   { "levels": [ { "sku": "{{code}}", "qty": {{qty}} } ] }
Webhook trigger       paste its URL in your account → Integrations → Webhooks
                      (verify X-Tawreed-Signature before acting on it)

الأخطاء والحدود

كل خطأ يعود بصيغة واحدة، مع رمز ثابت تبني عليه منطقك.

{ "error": { "code": "forbidden", "message": "This key lacks the stock:write scope" } }
400bad_jsonالجسم ليس JSON صالحًا
400idempotency_key_requiredطلب POST بدون الترويسة Idempotency-Key
401unauthorizedالمفتاح مفقود أو غير صالح أو ملغى
403forbiddenالمفتاح لا يملك الصلاحية (أو لا يقبل العروض)
404not_foundالمسار أو الطلب أو المنتج غير موجود
409not_readyحساب المنشأة قيد التجهيز، أو العرض لا يقبل في حالته
413too_largeالجسم أكبر من 256 كيلوبايت
422invalid · invalid_linesبيانات ناقصة أو غير صالحة (التفاصيل في details)
422idempotency_key_reusedاستُخدم مفتاح التكرار مع طلب مختلف
429rate_limitedتجاوزت 120 طلبًا في الدقيقة لهذا المفتاح
503disabledالربط متوقف مؤقتًا
500server_errorخطأ لدينا؛ أعد المحاولة بمفتاح التكرار نفسه

مفاتيح التكرار

كل طلب POST يحتاج الترويسة Idempotency-Key، ويقبلها PUT /stock-levels أيضًا. إذا أعدت الطلب نفسه بالمفتاح نفسه (بعد انقطاع الشبكة مثلًا) نعيد الرد الأول بالترويسة Idempotent-Replayed: true ولا نرسل شيئًا مرتين. نحتفظ بمفاتيح التكرار 7 أيام.

الحدود

120 طلبًا في الدقيقة لكل مفتاح (الترويستان X-RateLimit-Limit وX-RateLimit-Remaining)، و2000 رصيد في كل إرسال، و300 سطر في الطلب، و256 كيلوبايت لجسم الطلب.

تحتاج مساعدة في الربط؟ فريقنا يرتب معك جلسة تجهيز ويختبر الربط معك. واتساب