المتاجر
دليل المطورين

اربط منصتك
بمتاجر عملائك

التاجر بيوافق مرة واحدة، وإنت بتاخد توكن محدود الصلاحيات على متجره هو بس — مع إشعارات فورية موقّعة لكل حدث.

  • OAuth 2.0 + PKCE
  • ويبهوك موقّعة HMAC
  • صلاحيات محدودة لكل متجر

الأسرع · رابط تثبيت بدون تسجيل

مش محتاج تسجّل تطبيقك عندنا ولا تستنى موافقة. ابنِ الرابط ده وحطه في لوحة تحكم عميلك — يوافق، وإحنا بنبعتلك المفتاح على سيرفرك.

التاجر هيشوف «تطبيق غير موثّق»
لأنك وصفت تطبيقك بنفسك، الشاشة بتقول للتاجر كده وبتوريه المضيف اللي المفتاح هيتبعت عليه. ده مقبول تمامًا وبيشتغل من غير أي تدخل مننا — ولو عايز شارة «موثّق» كلّمنا وسجّل تطبيقك.
كل متجر بيديك api_key و webhook_secret خاصين بيه — خزّنهم لكل متجر على حدة، لأن التاجر ممكن يربط أكتر من متجر.
GET https://elmatajer.com/install-app
  ?app_name=Your App
  &app_description=What it does, in one line
  &app_icon=https://your-app.com/logo.png
  &permissions=orders.read,orders.write,webhooks
  &callback_url=https://your-app.com/callback   // we POST the key here
  &webhook_url=https://your-app.com/webhook     // events go here
  &redirect_url=https://your-app.com/connected  // merchant lands here

→ after approval we POST to your callback_url:
{
  "api_key":        "19|xxxxxxxxxxxxxxxx",
  "store_id":       42,
  "scope":          "orders.read orders.write webhooks",
  "webhook_secret": "...",
  "api_base":       "https://elmatajer.com/api/v1/vendor"
}

مهم · أول ربط بيمرّ على مراجعة

أول ما تاجر يضغط رابطك، تطبيقك بيتسجّل عندنا ويدخل طابور المراجعة. لحد ما نوافق، أي محاولة ربط بترجع 403 والتاجر بيشوف صفحة «التطبيق قيد المراجعة».

1 تاجر يضغط رابطك
2 تطبيقك يدخل الطابور
3 إحنا نراجع
4 يشتغل لكل التجار
مرة واحدة بس
المراجعة على التطبيق نفسه مش على كل تاجر. بعد الموافقة، أي تاجر يضغط الرابط بيتربط فورًا من غير أي انتظار. عشان تعدّي المراجعة أسرع، خلّي app_name و app_description واضحين واطلب أقل صلاحيات محتاجها فعلًا.
لو اتغيّر الـ callback_url أو اسم التطبيق، بيتحسب تطبيق جديد ويعدّي على مراجعة تانية — هويّة التطبيق عندنا = مضيف الـ callback + الاسم.

أو · تدفّق OAuth الكامل

لو عايز شارة «تطبيق موثّق» وعناوين إرجاع مقفولة على تطبيقك، سجّل تطبيقك واستخدم التدفّق ده.

تدفّق OAuth 2.0 قياسي (authorization code + PKCE). لو ربطت قبل كده مع أي منصة، ده نفس الكلام.

1 وجّه التاجر
2 هو بيوافق
3 بدّل الكود بتوكن
4 استخدم الـ API

١ · رابط الموافقة

وجّه التاجر للرابط ده. لو مش مسجّل دخول هيسجّل الأول وبعدين يرجع لنفس الصفحة.

الـ redirect_uri لازم يطابق المسجَّل عندنا بالحرف — مفيش مطابقة ببادئة. وارجع دايمًا تأكّد إن الـ state اللي رجع هو نفسه اللي بعته.
GET https://elmatajer.com/oauth/authorize
  ?client_id=YOUR_CLIENT_ID
  &redirect_uri=https://your-app.com/callback
  &scope=orders.read orders.write webhooks
  &state=RANDOM_ANTI_CSRF_VALUE
  &code_challenge=BASE64URL(SHA256(verifier))
  &code_challenge_method=S256

٢ · بدّل الكود بتوكن

من سيرفرك — مش من المتصفح. الكود صالح ٥ دقايق ولمرة واحدة بس.

POST https://elmatajer.com/oauth/token
Content-Type: application/json

{
  "grant_type":    "authorization_code",
  "client_id":     "YOUR_CLIENT_ID",
  "client_secret": "YOUR_CLIENT_SECRET",
  "code":          "THE_CODE_FROM_THE_REDIRECT",
  "redirect_uri":  "https://your-app.com/callback",
  "code_verifier": "THE_ORIGINAL_VERIFIER"
}

→ 200 OK
{
  "access_token":   "17|xxxxxxxxxxxxxxxx",
  "token_type":     "Bearer",
  "scope":          "orders.read orders.write webhooks",
  "store":          { "vendor_id": 42, "name": "..." },
  "webhook_secret": "store this — it signs your webhooks",
  "api_base":       "https://elmatajer.com/api/v1/vendor"
}

→ 401 { "error": "invalid_client" }   wrong client_id / client_secret, or the app is not approved
→ 400 { "error": "invalid_grant", "error_description": "code_expired" }
       error_description: code_expired · code_already_used · redirect_uri_mismatch · invalid_code_verifier

٣ · الصلاحيات

اطلب أقل صلاحيات ممكنة — كل صلاحية بتظهر للتاجر بالاسم، والقائمة القصيرة بتتوافق عليها أسرع.

الصلاحية بتسمح بإيه
الطلبات
orders.read الوصول إلى الطلبات
orders.write تعديل الطلبات
الكتالوج
products.read الوصول إلى المنتجات
products.write تعديل المنتجات
categories.read الوصول إلى الأقسام
العملاء
customers.read الوصول إلى العملاء
carts.read السلات المتروكة
الشحن
shipping.read مناطق الشحن
التسويق
coupons.read الوصول إلى الكوبونات
التقارير
reports.read الوصول إلى التقارير
الإشعارات
webhooks استقبال إشعارات الأحداث

٤ · استدعاء الـ API

المتجر بيتحدد من التوكن نفسه — مفيش vendor_id بتبعته، وما ينفعش توصل لمتجر تاني.

GET https://elmatajer.com/api/v1/vendor/orders
Authorization: Bearer YOUR_ACCESS_TOKEN
Accept: application/json

→ 200 OK
{
  "status": "ok",
  "data": [ {
    "id": 1042, "order_number": "AB1234",
    "status": 1, "status_type": 1, "status_label": "...",
    "payment_status": 1, "payment_type": "cod",
    "customer": { "name": "...", "email": "...", "mobile": "..." },
    "totals": { "sub_total": 1200, "delivery_charge": 50, "discount_amount": 0,
                "grand_total": 1250, "currency": "...", "currency_code": "EGP" },
    "items": [ ... ], "created_at": "2026-10-08T12:00:00+03:00"
  } ],
  "meta": { "current_page": 1, "last_page": 3, "per_page": 20, "total": 52, "has_more": true }
}

→ 403 if the merchant did not grant that scope
{ "code": "insufficient_scope", "required_scope": "orders.read" }
التقسيم لصفحات والفلترة
القوايم بتتقسّم لصفحات: ‎?page=N&per_page=5–50 (الافتراضي 20)، والرد فيه meta. الطلبات: ‎status (1 قيد المراجعة … 6 مرتجع) و q (رقم الطلب أو اسم العميل أو موبايله). المنتجات: ‎status (all · active · hidden · draft · out · low) و cat (رقم القسم) و q.
PUT بيستبدل المنتج كله
PUT /vendor/products/{id} بيكتب المنتج من جديد: أي حقل اختياري مش موجود في الطلب (زي sku أو description) بيتمسح. ابعت المنتج كامل، ولتغيير المخزون أو الظهور بس استخدم ‎/stock و ‎/availability.

مرجع نقاط النهاية

كل نقطة تقدر توصلها، مرتّبة حسب الصلاحية اللي بتفتحها. كلها تحت https://elmatajer.com/api/v1

الطريقة المسار
orders.read  الوصول إلى الطلبات
GET /vendor/orders
GET /vendor/orders/statuses
GET /vendor/orders/summary
GET /vendor/orders/{id}
orders.write  تعديل الطلبات
POST /vendor/orders/{id}/status
products.read  الوصول إلى المنتجات
GET /vendor/products
GET /vendor/products/brands
GET /vendor/products/counts
GET /vendor/products/variant-options
GET /vendor/products/{id}
products.write  تعديل المنتجات
POST /vendor/categories
DELETE /vendor/categories/{id}
PUT /vendor/categories/{id}
POST /vendor/categories/{id}/status
POST /vendor/products
POST /vendor/products/ai-description
POST /vendor/products/translate
POST /vendor/products/variant-options
DELETE /vendor/products/{id}
PUT /vendor/products/{id}
POST /vendor/products/{id}/availability
POST /vendor/products/{id}/images
DELETE /vendor/products/{id}/images/{imageId}
POST /vendor/products/{id}/stock
categories.read  الوصول إلى الأقسام
GET /vendor/categories
GET /vendor/products/categories
customers.read  الوصول إلى العملاء
GET /vendor/customers
shipping.read  مناطق الشحن
GET /vendor/shipping
coupons.read  الوصول إلى الكوبونات
GET /vendor/coupons
reports.read  الوصول إلى التقارير
GET /vendor/reports

البيانات المرسلة والردود

ابعت JSON (أو multipart/form-data لو فيه ملفات). * = إجباري. أي حقل مش في القايمة بيتجاهل — زي vendor_id أو id.

نقطة النهاية الحقول ملاحظات
POST
/vendor/products
name* · price* · category_ids*[] · name_en · description · description_en · compare_price · sku · video_url · brand_ids[] · is_available · manage_stock · stock_qty · low_qty · variants[]{color, size, price*, qty, sku} · images[] 201 + المنتج. is_available=false يخفي المنتج. images ملفات (jpg/png/webp حتى 4MB، 10 بالكتير) — ساعتها ابعت multipart/form-data.
PUT
/vendor/products/{id}
نفس حقول POST استبدال كامل: الحقول الاختيارية الناقصة بتتمسح، و manage_stock الناقصة بتوقف تتبع المخزون.
POST
/vendor/products/{id}/stock
qty* · low_qty بيشغّل تتبع المخزون. نزول qty تحت low_qty بيبعت product.low_stock.
POST
/vendor/products/{id}/availability
— بدون body: بيقلب الحالة ظاهر ↔ مخفي ويرجّع المنتج.
POST
/vendor/products/{id}/images
image* (file) multipart/form-data. 201 + { id, url }.
DELETE
/vendor/products/{id}
— حذف نهائي — مفيش استرجاع.
POST
/vendor/categories
name* · name_en · image (file) 201 + القسم. لو فيه image ابعت multipart/form-data. PUT /categories/{id} بنفس الحقول.
POST
/vendor/categories/{id}/status
is_available* true يظهر القسم، false يخفيه.
DELETE
/vendor/categories/{id}
— حذف نهائي، وبيتشال من كل منتجاته — product_count في GET /categories بيقولك كام منتج هيتأثر.
POST
/vendor/orders/{id}/status
status* · type* status = id من GET /orders/statuses، و type = نوعها (1–6). بيشغّل نفس آثار لوحة التحكم: رسالة/إيميل للعميل وإرجاع المخزون عند الإلغاء.

حقول الرد

كل رد ناجح: { "status": "ok", "data": … } — والقوايم المقسّمة لصفحات فيها meta كمان.

المورد الحقول
order (GET /orders, /orders/{id}) id, order_number, status, status_type, status_label, payment_status, payment_type, order_type, customer{name, email, mobile}, address{line, building, landmark} (when set), totals{sub_total, delivery_charge, discount_amount, tips, grand_total, currency, currency_code}, items_count, items[], notes, created_at
order status (GET /orders/statuses?order_type=1) id, type, name
product (GET /products, /products/{id}) id, name, name_en, slug, sku, image, images[]{id, url} (single product), price, original_price, discount_pct, stock{state: in|low|out|untracked, qty, tracked}, category_ids, is_available, is_draft, has_variants, variants[]{group, options}, category, views, video_url, description, created_at
GET /products/counts all, active, hidden, draft, out, low
GET /products/brands id, name, image, is_available
GET /products/variant-options colors[], sizes[]
category (GET /categories) id, name, name_en, slug, image, is_available, product_count
GET /products/categories id, name, image, is_available
customer (GET /customers?q=&per_page=) id, name, email, mobile, orders_count, total_spent, currency, currency_code, last_order_at, joined_at
coupon (GET /coupons — not paginated) id, name, code, type: percent|amount, amount, min_amount, usage_limit, start_date, exp_date, is_active, is_expired
GET /shipping zones[]{id, name, charge, is_active}, free_shipping_amount, currency, currency_code
GET /reports?range=week|month|year (month) range, currency, currency_code, revenue, orders, avg_order, series[]{label, total}, top_products[]{name, sold, revenue}

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

كل خطأ بيرجع JSON فيه code ثابت — اتعامل معاه بالـ code مش بنص الرسالة، الرسالة بتتغير باللغة.

HTTP code المعنى وإيه اللي تعمله
401 unauthenticated التوكن ناقص أو مسحوب — التاجر ألغى الربط. اطلب منه يربط تاني.
402 subscription_expired اشتراك التاجر في المنصة انتهى — كل endpoints المتجر بترجع 402 لحد ما يجدد. وقّف المزامنة واعرض له رسالة.
403 insufficient_scope التاجر ما منحش الصلاحية دي (الرد بيقولك required_scope)، أو endpoint مش متاح للتطبيقات أصلًا.
403 app_disabled المنصة أوقفت تطبيقك أو لغت اعتماده — التوكن مش هيشتغل لحد ما يرجع. كلّمنا.
403 — على /install-app: تطبيقك لسه تحت المراجعة أو مرفوض.
404 not_found العنصر أو الـ endpoint مش موجود في متجر التاجر ده (عنصر متجر تاني = 404 برضه).
405 method_not_allowed الـ method غلط للمسار ده.
422 validation_failed بيانات ناقصة أو غير صالحة — الرد فيه errors لكل حقل. تغيير حالة طلب بحالة مش متاحة = invalid_status.
429 rate_limited 60 طلب في الدقيقة لكل متجر (مشتركة بين كل التطبيقات المربوطة بالمتجر وتطبيق التاجر نفسه). تابع X-RateLimit-Remaining، ولو جالك 429 استنى Retry-After ثانية.
اتعامل مع 401 على إنه نهائي
مفيش refresh token. لو التاجر سحب الصلاحيات، التوكن بيموت فورًا — أوقف المزامنة للمتجر ده وبلّغ العميل إنه محتاج يربط تاني، متفضلش تعيد المحاولة.

٥ · الويبهوك

بنبعتلك POST بكل حدث، وبنعيد المحاولة بتدرّج لو سيرفرك مردّش. لازم ترجّع 2xx.

الحدث إمتى محتاج data
order.created طلب جديد اتعمل orders.read + webhooks order{id, order_number, sub_total, tax, discount, grand_total, payment_type, order_type, order_from, status}, customer{id, name, email, phone} + vendor_id
order.paid الطلب اتدفع orders.read + webhooks order_id, order_number, grand_total, payment_type, customer{id, name, email, phone} + vendor_id
order.status_changed حالة الطلب اتغيّرت orders.read + webhooks order_id, order_number, from_status, to_status, customer{id, name, email, phone} + vendor_id
customer.created عميل جديد سجّل customers.read + webhooks customer{id, name, email, phone}, source + vendor_id
product.low_stock مخزون منتج قرب يخلص products.read + webhooks product{id, name, sku, price}, qty_remaining, threshold + vendor_id
cart.abandoned سلة اتسابت من غير شراء (بمرجع سلة مُعمّى cart_ref، مش رقم جلسة) carts.read + webhooks cart{cart_ref, user_id, item_count, subtotal, items[], last_activity}, idle_minutes + vendor_id
POST https://your-app.com/webhook
X-Elmatajer-Event: order.created
X-Elmatajer-Event-Id: 9f1c...            // use it to de-duplicate
X-Elmatajer-Signature: sha256=<hmac>

{ "event": "order.created", "event_id": "9f1c...", "vendor_id": 42,
  "created_at": "2026-10-08T12:00:00+03:00", "data": { ... } }

// Verify before trusting anything — PHP
$expected = hash_hmac('sha256', $rawBody, $webhookSecret);
if (!hash_equals($expected, str_replace('sha256=', '', $header))) abort(401);
اتحقق من التوقيع دايمًا
الـ HMAC محسوب على نص الطلب الخام بالسر اللي أخدته وقت الربط — كل تاجر له سر مختلف. من غير التحقق ده أي حد يعرف رابطك يقدر يبعتلك طلبات مزيّفة.
التوصيل مرة على الأقل
لو سيرفرك ما ردّش بـ 2xx بنعيد المحاولة بفواصل متزايدة (لحد 8 محاولات)، فممكن نفس الحدث يوصلك أكتر من مرة — X-Elmatajer-Event-Id ثابت في كل المحاولات، استخدمه عشان تتجاهل المكرر. الترتيب بين الأحداث مش مضمون.

مثال كامل شغّال

الحتّتين اللي كل شريك لازم يظبّطهم: استقبال المفتاح، والتحقق من الويبهوك قبل ما تصدّقه.

// 1 — receive the merchant's credentials (your callback_url)
POST /elmatajer/callback

$body = json_decode(file_get_contents('php://input'), true);

saveStore([
    'store_id'       => $body['store_id'],        // unique per merchant
    'api_key'        => $body['api_key'],         // use as Bearer token
    'webhook_secret' => $body['webhook_secret'],  // verifies their events
]);

http_response_code(200);  // answer 2xx or we treat it as failed


// 2 — call the API for that store
$res = $http->get('https://elmatajer.com/api/v1/vendor/orders', [
    'headers' => [
        'Authorization' => 'Bearer ' . $store['api_key'],
        'Accept'        => 'application/json',
    ],
]);
// no store_id in the request — the token already identifies the store


// 3 — receive events (your webhook_url)
POST /elmatajer/webhook

$raw = file_get_contents('php://input');
$sig = str_replace('sha256=', '', $_SERVER['HTTP_X_ELMATAJER_SIGNATURE'] ?? '');

$store = findStoreById(json_decode($raw, true)['vendor_id']);

// VERIFY FIRST — never trust the body before this passes
if (!hash_equals(hash_hmac('sha256', $raw, $store['webhook_secret']), $sig)) {
    http_response_code(401); exit;
}

$event = json_decode($raw, true);

// de-duplicate: retries resend the same event_id
if (alreadyHandled($_SERVER['HTTP_X_ELMATAJER_EVENT_ID'])) { http_response_code(200); exit; }

match ($event['event']) {
    'order.created'        => onNewOrder($event['data']),
    'order.status_changed' => onStatusChange($event['data']),
    default                => null,
};

http_response_code(200);
التحقق قبل أي حاجة
الـ HMAC محسوب على نص الطلب الخام زي ما وصل بالظبط — لو فككت الـ JSON وعملته encode تاني، التوقيع مش هيطابق. ومن غير التحقق، أي حد يعرف رابطك يقدر يبعتلك طلبات مزيّفة.

عايز تبدأ؟

اطلب بيانات تطبيقك

ابعتلنا اسم التطبيق، وصف قصير، اللوجو، عناوين الإرجاع، والصلاحيات اللي محتاجها — ونرجعلك client_id و client_secret.

تواصل معنا
التاجر متحكّم دايمًا

أي تاجر يقدر يشوف تطبيقك وصلاحياته وآخر استخدام، ويسحبها في أي وقت من صفحة «التطبيقات المرتبطة». وقتها التوكن بيتلغي فورًا.