للمطورين وفرق تقنية المعلومات
نظامك يطلب لوحده
اربط نظام المخزون أو الـERP لديكم ببوابة التوريد مرة واحدة: يرسل نظامك أرصدة المخزون، وعندما يصل صنف إلى نقطة إعادة الطلب التي تحددها، يصلنا طلب بالكمية التي اخترتها ليُسعَّر بأسعار منشأتك. ويسمع نظامك كل خطوة عبر الإشعارات: التسعير والتأكيد والشحن والتسليم والفاتورة، ويمكنه قبول العروض المسعّرة بنفسه.
كيف يعمل
- 1نظامكيرسل الأرصدة كل ليلة أو مع كل حركة مخزون
- 2قواعد إعادة الطلبنقطة إعادة الطلب والكمية لكل صنف
- 3الطلبيصل إلى فريقنا كعرض سعر بأسعار منشأتك
- 4إشعار لنظامكعند التسعير والتأكيد والشحن والتسليم والفاتورة
البدء في ثلاث خطوات
- 1أنشئ مفتاحًامن حسابك: الربط مع أنظمتكم ← المفاتيح. اختر الصلاحيات، وهل تذهب طلبات المفتاح مباشرة إلى بوابة التوريد أو تنتظر موافقة مدير الحساب. المفتاح يظهر مرة واحدة، فاحفظه في خزنة الأسرار لديكم.
- 2حدّد قواعد إعادة الطلبلكل صنف: رمزه (sku) ووحدته، ونقطة إعادة الطلب (min_qty)، والكمية التي نطلبها (reorder_qty). من الحساب أو عبر POST /reorder-rules.
- 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 | /products | catalog:read | المنتجات بوحداتها (q، page، limit) |
| GET | /products/{sku} | catalog:read | منتج واحد برمزه أو رقمه |
| POST | /requests | requests:write | أرسل طلبًا (أسطر sku أو product_id مع unit وqty) |
| GET | /requests/{id} | orders:read | حالة طلب وعرض السعر الخاص به |
| GET | /quotations | orders:read | العروض لدى بوابة التوريد والمسعّرة |
| POST | /quotations/{number}/accept | requests:write | اقبل عرضًا مسعّرًا (للمفاتيح التي ترسل مباشرة) |
| GET | /orders | orders:read | الطلبات المؤكدة |
| GET | /orders/{number} | orders:read | عرض أو طلب واحد بأسطره ومراحله وشحناته |
| GET | /invoices | invoices:read | الفواتير وحالتها والمتبقي منها |
| GET | /addresses | orders:read | عناوين التوصيل (address_id للطلبات) |
| PUT | /stock-levels | stock:write | أرسل الأرصدة؛ نطلب ما وصل إلى نقطة إعادة الطلب |
| GET | /reorder-rules | stock:write | كل قواعد إعادة الطلب |
| POST | /reorder-rules | stock:write | أنشئ قاعدة صنف أو عدّلها |
| DELETE | /reorder-rules/{id} | stock:write | احذف قاعدة |
| GET | /webhooks | webhooks:manage | روابط الإشعارات |
| POST | /webhooks | webhooks: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.submittedrequest.awaiting_approvalquotation.pricedorder.confirmedorder.cancelleddelivery.outdelivery.doneinvoice.issuedreorder.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" } }| 400 | bad_json | الجسم ليس JSON صالحًا |
| 400 | idempotency_key_required | طلب POST بدون الترويسة Idempotency-Key |
| 401 | unauthorized | المفتاح مفقود أو غير صالح أو ملغى |
| 403 | forbidden | المفتاح لا يملك الصلاحية (أو لا يقبل العروض) |
| 404 | not_found | المسار أو الطلب أو المنتج غير موجود |
| 409 | not_ready | حساب المنشأة قيد التجهيز، أو العرض لا يقبل في حالته |
| 413 | too_large | الجسم أكبر من 256 كيلوبايت |
| 422 | invalid · invalid_lines | بيانات ناقصة أو غير صالحة (التفاصيل في details) |
| 422 | idempotency_key_reused | استُخدم مفتاح التكرار مع طلب مختلف |
| 429 | rate_limited | تجاوزت 120 طلبًا في الدقيقة لهذا المفتاح |
| 503 | disabled | الربط متوقف مؤقتًا |
| 500 | server_error | خطأ لدينا؛ أعد المحاولة بمفتاح التكرار نفسه |
مفاتيح التكرار
كل طلب POST يحتاج الترويسة Idempotency-Key، ويقبلها PUT /stock-levels أيضًا. إذا أعدت الطلب نفسه بالمفتاح نفسه (بعد انقطاع الشبكة مثلًا) نعيد الرد الأول بالترويسة Idempotent-Replayed: true ولا نرسل شيئًا مرتين. نحتفظ بمفاتيح التكرار 7 أيام.
الحدود
120 طلبًا في الدقيقة لكل مفتاح (الترويستان X-RateLimit-Limit وX-RateLimit-Remaining)، و2000 رصيد في كل إرسال، و300 سطر في الطلب، و256 كيلوبايت لجسم الطلب.
تحتاج مساعدة في الربط؟ فريقنا يرتب معك جلسة تجهيز ويختبر الربط معك. واتساب

