تطلق DZBuild عدة ثيمات جاهزة لواجهة المتجر ومخصّصًا بدون كود. لكن إن أردت التحكم الكامل — واجهة React/Vue/Next.js/Flutter خاصة، تصميمك المثالي، التوجيه (routing) الخاص بك — فالواجهة البرمجية مبنية لك.
يمشي هذا الدليل عبر بناء واجهة headless من البداية للنهاية:
عرض الكتالوج (المنتجات، الفئات، المتغيرات)
بناء سلة (من جانب العميل أو الخادم، بحسبك)
إرسال الطلب عبر API
استقبال webhooks عند تغيّر الحالة
في النهاية ستكون واجهتك المخصصة متوافقة 100% مع لوحة تحكم التاجر في DZBuild — تظهر الطلبات، يخصم المخزون، يتكامل الشحن مع شركة التوصيل، بلا تنازلات.
البنية
┌──────────────────────┐ ┌──────────────────────────┐
│ واجهة مخصصة │ HTTPS │ api.dzbuild.app/v1/* │
│ (React / Vue / Flutter)│ ─────► │ Authorization: Bearer … │
│ │ │ │
│ - تقرأ المنتجات │ ◄────── │ استجابات JSON │
│ - تعرض السلة │ │ │
│ - تُرسل الطلبات │ └──────────────────────────┘
└──────────────────────┘ │
▼
لوحة تحكم التاجر في DZBuild
- يؤكد الطلبات
- يدير المخزون
- يتكامل مع شركات التوصيل
أنت تملك الواجهة. DZBuild تملك البيانات والعمليات. التاجر يدخل dzbuild.com/dashboard لإدارة الطلبات المؤكَّدة في واجهتك المخصصة.
المتطلبات المسبقة
حساب تاجر DZBuild. خطة اشتراك المتجر لا علاقة لها بالوصول إلى الـ API — فالترقية لا تمنحه، والخطة المنتهية لا تسحبه.
مفتاح API مُسجَّل في البرنامج التجريبي. هذه هي البوابة الحقيقية: ما دام الـ API في المرحلة التجريبية، يجب أن تُسجّل DZBuild المفتاح. وبدون التسجيل يُرجع كل نداء
403 forbidden"API is in pilot mode; key not enrolled".خادم خلفي (أو دالة serverless / edge worker) يحمل المفتاح. لا تشحن السر إلى المتصفح — انظر الأمان.
وموضع واحد تهمّ فيه خطة التاجر فعلًا: المتجر على الخطة Free يتوقف عن قبول الطلبات بعد 30 طلبًا في الشهر الميلادي، ويُرجع POST /v1/orders عندئذ 400 bad_request "Monthly order limit reached for this store plan". أما Pro و Unlimited و Enterprise فبلا سقف طلبات. عالج هذا الخطأ في صفحة الدفع لديك.
الخطوة 0 — احصل على مفتاح
لا توجد صفحة "Developer → API Keys" في لوحة تحكم التاجر. وخلال البرنامج التجريبي:
اطلب من دعم DZBuild إصدار أول مفتاح للمتجر وتسجيله في البرنامج التجريبي.
أنشئ أي مفاتيح إضافية بنفسك انطلاقًا من ذلك المفتاح:
curl -X POST 'https://api.dzbuild.app/v1/keys' \
-H "Authorization: Bearer $DZ_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"type":"platform","name":"Custom storefront — production"}'
# HTTP 200 → { "data": { "key_id", "bearer_token", "signing_secret", "note" } }
ينسخ المفتاح الجديد فئة تحديد المعدل وعَلَم البرنامج التجريبي من المفتاح المنادي. والصلاحيات غير قابلة للاختيار — فمفتاح المنصة يحمل دائمًا المجموعة الافتراضية الكاملة (products:read و products:write و orders:read و orders:write و store:read و store:write و customers:read و landing_pages:read و landing_pages:write و webhooks:read و webhooks:write و usage:read)، لذا افصل البيئات بمفاتيح منفصلة، لا بصلاحيات أضيق.
انسخ bearer_token — يظهر مرة واحدة عند الإنشاء. فقدته؟ ألغه وأنشئ جديدًا.
اختبر بـ:
curl https://api.dzbuild.app/v1/whoami \
-H "Authorization: Bearer $DZ_KEY"
# المتوقع: { "data": { "key_id": "...", "store_id": 13, "type": "platform", "scopes": [...] } }
الخطوة 1 — عرض الكتالوج
// pages/index.js (Next.js)
export async function getServerSideProps() {
const res = await fetch('https://api.dzbuild.app/v1/products?limit=50&status=active', {
headers: { 'Authorization': `Bearer ${process.env.DZ_KEY}` },
});
const { data } = await res.json();
return { props: { products: data.items } };
}export default function Home({ products }) {
return (
<ul>
{products.map(p => (
<li key={p.id}>
{p.primary_image && (
<img src={`https://cdn.dzbuild.app/${p.primary_image}`} alt={p.name} />
)}
<h2>{p.name}</h2>
<p>{p.price} DZD</p>
<a href={`/product/${p.slug}`}>عرض</a>
</li>
))}
</ul>
);
}
أمران عن الصور:
primary_imageهو مسار تخزين (عادةًuploads/products/<store_id>/<file>)، فرابط الـ CDN هو ببساطةhttps://cdn.dzbuild.app/+ ذلك المسار — لا تُدرج مقطع/{store_id}/products/من عندك. وقد تحمل المنتجات القديمة اسم ملف مجرّدًا بدلًا من ذلك؛ وتلك تُحَل علىhttps://cdn.dzbuild.app/uploads/products/<store_id>/<filename>.عنصر القائمة لا يتضمّن
store_id. اقرأه مرة واحدة منGET /v1/storeأوGET /v1/whoamiواحتفظ به في إعداداتك.primary_imageيكونnullحين لا صورة للمنتج. احترس لذلك، كما في المثال أعلاه.
خزّن الاستجابة — قائمة المنتجات مخزَّنة مؤقتًا لمدة 30 ثانية فالقراءات المتكررة عند الحجم رخيصة.
الخطوة 2 — صفحة تفاصيل منتج بالمتغيرات
const res = await fetch(`https://api.dzbuild.app/v1/products/${id}`, {
headers: { 'Authorization': `Bearer ${process.env.DZ_KEY}` },
});
const { data: product } = await res.json();
تتضمن الاستجابة variants[] — كل إدخال مجموعة متغيرات بخياراتها:
"variants": [
{ "id": 11, "name": "Color", "type": "color",
"options": [
{ "id": 14, "value": "Red", "color_code": "#ff0000", "image_id": 28, "stock": 12 },
{ "id": 15, "value": "Blue", "color_code": "#0000ff", "image_id": 29, "stock": 5 }
]
},
{ "id": 12, "name": "Size", "type": "text",
"options": [
{ "id": 16, "value": "S", "stock": 10 },
{ "id": 17, "value": "M", "stock": 10 },
{ "id": 18, "value": "L", "stock": 5 }
]
}
]
اعرض محدّدًا لكل مجموعة. لـ type: color اعرض swatches بالـ color_code؛ لـ type: text اعرض labels؛ لـ type: image_text اعرض صورًا مصغرة: فـ options[].image_id هو معرّف رقمي يطابق images[].id لأحد المدخلات في نفس استجابة المنتج.
وانتبه لاسم الحقل — images[].url هو مسار تخزين لا رابطًا. ضع أمامه https://cdn.dzbuild.app/ تمامًا كما فعلت مع primary_image في الخطوة 1 قبل وضعه في وسم img.
التعامل مع نفاذ المخزون: options[].stock يكون null إن لم يفعّل التاجر المخزون لكل متغيّر. إن كان رقمًا ≤ 0 ظلّل الخيار. التحقق الفعلي يحدث في الخادم وقت التأكيد فلا بأس بواجهة قديمة قليلًا.
الخطوة 3 — بناء سلة
السلة من جانب العميل (React state, Vuex, Pinia, localStorage…). لا تحتاج نداء API للإضافة. كل سطر:
{
product_id: 26,
product_name: "T-shirt",
base_price: 1500,
quantity: 1,
selected_variants: [
{ group_name: "Color", option_name: "Red", color_code: "#ff0000", price_adjustment: 0 },
{ group_name: "Size", option_name: "L", color_code: null, price_adjustment: 200 }
]
}
احسب إجمالي السطر من جانب العميل: (base_price + sum(price_adjustment)) × quantity. أعرض على العميل إجمالي السلة، لكن عامله كعرض فقط:
أي سعر ترسله داخل العنصر يُتجاهَل (حقل الـ API هو
price). فالخادم يستعمل دائمًا سعر الكتالوج لـproduct_id.price_adjustmentيُعاد حلّه من الكتالوج بحسب الزوج(group_name, option_name). وعند أي عدم تطابق تفوز قيمة الكتالوج. ولا تُستعمل قيمتك أنت إلا حين لا يُحَل الزوج إطلاقًا — وهذا بالضبط ما يحدث بعد أن يُعيد التاجر تسمية خيار متغيّر، فأبقِ سلاسلgroup_name/option_nameلديك متزامنة مع الكتالوج.group_nameوoption_nameيُبتران بصمت إلى 100 حرف، وcolor_codeإلى 7.
الخطوة 4 — جمع بيانات العميل + احتساب الشحن
نموذج checkout قياسي: الاسم، الهاتف، الولاية، البلدية، العنوان. ثبّت الـ 58 ولاية في الواجهة — فهي قائمة ثابتة.
GET /v1/store متاحة فعلًا ويستحق نداؤها مرة واحدة عند الإقلاع: تُرجع اسم المتجر و slug واللغة والوصف والشعار والأيقونة واللافتة وألوان الثيم والخط والنطاق الفرعي والنطاق المخصص (وهل هو موثَّق) و public_url و hide_branding و created_at. وهي إحدى نداءات GET المخزَّنة مؤقتًا (30 ثانية).
وما لا تحمله — وهذا هو بند v1.1 الحقيقي — هو أسعار الشحن لكل ولاية وقائمة مكاتب الاستلام لدى التاجر. لذا لتكلفة الشحن اليوم:
ثبّت الأسعار حسب الولاية + نوع التسليم في واجهتك (الأبسط)، أو
احفظها في إعداداتك الخاصة متزامنة مع التاجر خارج الـ API.
مرّر تكلفة الشحن المحسوبة إلى واجهة الطلبات كـ shipping_cost. الخادم لا يُعيد حساب الشحن لك حاليًا؛ وما تمرّره يصبح جزءًا من إجمالي الطلب.
الخطوة 5 — إرسال الطلب
// على خادمك الخلفي (Next.js API route, Edge function, server…)
async function placeOrder(req, res) {
const cart = req.body; const order = {
customer: {
name: cart.name,
phone: cart.phone,
email: cart.email || null,
wilaya_id: cart.wilaya_id,
commune: cart.commune,
address: cart.address || ''
},
delivery: {
type: cart.delivery_type,
desk_id: cart.desk_id || null,
desk_name: cart.desk_name || null
},
items: cart.items.map(line => ({
product_id: line.product_id,
quantity: line.quantity,
variants: line.selected_variants
})),
shipping_cost: cart.shipping_cost,
discount: 0,
payment_method: 'cod',
notes: cart.notes || null
}; const idempKey = req.headers['x-checkout-id'] || crypto.randomUUID(); const apiRes = await fetch('https://api.dzbuild.app/v1/orders', {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.DZ_KEY}`,
'Content-Type': 'application/json',
'Idempotency-Key': idempKey
},
body: JSON.stringify(order)
}); if (!apiRes.ok) {
const err = await apiRes.json();
return res.status(apiRes.status).json(err);
}
const { data: createdOrder } = await apiRes.json(); // HTTP 200، وليس 201
return res.json({
order_number: createdOrder.order_number,
total: createdOrder.amounts.total
});
}
يُجيب POST /v1/orders بـ HTTP 200 عند النجاح (لا توجد نقطة إنشاء في v1 تُرجع 201)، والجسم هو تفصيل الطلب الكامل المعياري — نفس الشكل الذي يُرجعه GET /v1/orders/{id}. تفرّع على apiRes.ok، لا على status === 201 أبدًا.
يرى العميل order_number في صفحة النجاح. ولوحة التاجر تُظهر الآن الطلب الجديد في قائمة pending جاهزًا للتأكيد.
حدود التحقق
كل قاعدة أدناه ترمي 400 bad_request مع الرسالة مباشرة، فاعرضها في نموذج الدفع لديك بدل اكتشافها في الإنتاج:
الحقل | القاعدة |
| من سطر إلى 50 سطرًا، مصفوفة غير فارغة |
| يجب أن ينتمي لمتجر المفتاح |
| من 1 إلى 9999 |
| من 1 إلى 255 حرفًا |
| يجب أن يطابق |
| من 1 إلى 58 |
| من 1 إلى 100 حرف |
|
|
|
|
| يجب أن تكون |
| تُبتر إلى 1000 حرف (وليس خطأ) |
يُضاف إلى ذلك سقف الخطة: على متجر بالخطة Free، الطلب رقم 31 في الشهر الميلادي يُرجع 400 bad_request "Monthly order limit reached for this store plan".
الخطوة 6 — استقبال webhooks (اختياري لكنه قوي)
سجّل webhook ليتفاعل متجرك مع أحداث الطلبات:
curl -X POST 'https://api.dzbuild.app/v1/webhooks' \
-H "Authorization: Bearer $DZ_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{
"url": "https://yourstorefront.com/api/dz-webhook",
"events": ["order.created", "order.confirmed", "order.shipped", "order.delivered"]
}'
ستحصل على secret في الاستجابة (HTTP 200) — احفظه. لكن لاحظ أن تسليمات API v1 ليست موقَّعة بذلك السر، فلا يمكنك التحقق منها بعد: أبقِ رابط الـ webhook غير قابل للتخمين، وأعد قراءة الطلب بـ GET /v1/orders/{id} قبل التصرّف. انظر التحقق من التواقيع للتفاصيل.
وهناك حدّان يستحقان المعرفة قبل أن تبني على هذا: order.created في API v1 يُطلق فقط للطلبات التي تُنشئها واجهتك عبر الـ API (لا لأي طلب يُوضع في واجهة DZBuild الخاصة بالتاجر)، وأحداث الحالة تُطلق فقط لتغييرات الحالة التي تتم عبر الـ API. انظر كتالوج الأحداث.
استخدم webhooks لـ:
إرسال SMS للعميل عند تأكيد طلبه.
تحديث CRM / Google Sheet / تحليلاتك.
إبطال صفحة "thank you" بعد تأكيد التاجر.
إطلاق تسليم منتج رقمي بعد
order.delivered.
أنماط شائعة
واجهة متعددة اللغات
احفظ ترجماتك في الواجهة. الـ API يُعيد أسماء المنتجات والأوصاف كما أدخلها التاجر تمامًا. إن استعمل التاجر إضافة Multi-language، سيُعيد GET /v1/products/{id} (في v1.1) name_ar و name_fr و description_ar و description_fr إلى جانب الحقول الأساسية. حتى ذلك الحين، الكنوني فقط مكشوف — اختر ما يريده عميلك من جانب العميل.
اختيار مكتب الاستلام (Stop-desk)
لـ delivery.type = "desk":
// 1. اقرأ مكاتب التاجر للولاية المختارة
// (مخطط v1.1: GET /v1/store/desks?wilaya=16)
// حتى ذلك الحين استعلم شركة التوصيل مباشرة (Yalidine, ZR…)// 2. أعرض القائمة، يختار العميل:
selectedDesk = { id: 7842, name: "Yalidine Bab Ezzouar" };// 3. مرّره للطلب:
order.delivery = {
type: "desk",
desk_id: selectedDesk.id,
desk_name: selectedDesk.name
};
الدفع الإلكتروني (SlickPay / Edahabia)
لـ payment_method = "digital_payment":
أرسل الطلب عبر API كالعادة؛ يُنشأ بـ
pendingوpayment_status = pending.وجّه العميل إلى رابط checkout في SlickPay / Edahabia.
عند النجاح يستلم خادمك الخلفي webhook من SlickPay → نادِ
PATCH /v1/orders/{id}لتحويلpayment_statusإلىpaid(مخطط v1.1؛ حتى ذلك الحين يُحدَّث الدفع عبر تكامل SlickPay الموجود أصلًا في المتجر).
الإرجاع والاسترداد
تُدار حاليًا من اللوحة. دعم API لـ POST /v1/orders/{id}/refund ضمن خارطة الطريق.
الأمان
لا تضع أبدًا مفتاح API في كود يصل إليه المتصفح. المفاتيح تنتمي لخادمك / serverless / edge worker. المتصفح ينادي خادمك وخادمك ينادي DZBuild.
HTTPS فقط بين واجهتك و
api.dzbuild.app. طلبات HTTP العادية مرفوضة.Idempotency-Key مطلوب على كل الكتابات. بدونه تحصل على
400بالكودbad_requestوالرسالة"Idempotency-Key header is required for write requests"— فالكود هوbad_request، لاidempotency_key_required. ويجب ألا تتجاوز القيمة 64 حرفًا من[A-Za-z0-9_-:.]؛ فـcrypto.randomUUID()يمرّ، أما base64 ومعظم ترميزات التجزئة فلا (+و/و=مرفوضة).حدود المعدل تُحتسب لكل مفتاح API، ويتبع سقفها خطة المتجر الحالية — فالترقية والتخفيض يسريان فورًا دون الحاجة إلى أي تغيير في المفتاح. ولا شيء بلا حدود في الدقيقة. ولاحظ أن هذه الحدود تخصّ نداءات الواجهة البرمجية وحدها؛ أما واجهة المتجر التي تبنيها فتخدم متسوّقيك دون أن تستهلك منها شيئًا. انظر حدود المعدل للسقوف الحالية.
مفتاح لكل بيئة. لا تشارك مفاتيح dev/prod. أنشئ مفتاحًا منفصلًا للمرحلة (staging) وألغه عند انتهائها. (الصلاحيات متطابقة على كل مفاتيح المنصة — والفصل يأتي من المفتاح نفسه.)
استكشاف الأخطاء
المشكلة | السبب المرجَّح | الإصلاح |
| نسيت رأس | أضفه |
| الرمز بلا نقطة — غالبًا نسخ مبتور احتفظ بمعرّف المفتاح | استعمل رمز bearer الكامل بمحارفه الـ 73 |
| خطأ كتابي، مفتاح ملغى، أو متغير بيئة خاطئ | أنشئ مفتاحًا جديدًا بـ |
| المفتاح موجود لكن DZBuild لم تُسجّله في البرنامج التجريبي | راسل الدعم |
| معرّف من متجر آخر | استعمل المفتاح الصحيح |
| سلة فارغة | لا تُرسل سلال فارغة |
| متجر بالخطة Free بلغ سقف 30 طلبًا في الشهر | يرقّي التاجر إلى Pro أو أعلى |
| استعلام مفرط | استبدل بـ webhooks؛ وتراجع باستعمال ترويسة |
أمثلة الشيفرة
كل ما تحتاجه موجود في هذه الصفحة — عرض الكتالوج، وعرض المتغيرات، وشكل السلة، وإرسال الطلب، وتسجيل webhook. انسخ من الأقسام أعلاه؛ فلا توجد مستودعات بداية منفصلة لاستنساخها.
الإطلاق
اختبر جيدًا بمفتاح
pilotعلى متجر اختبار.أنشئ مفتاحًا منفصلًا للإنتاج (نفس الصلاحيات، اسم مختلف).
انشر واجهتك بمفتاح prod في متغيرات البيئة.
ضع طلب اختبار حقيقي؛ تأكد من ظهوره في اللوحة.
اختبر إرجاعًا إن قدّمت إرجاعات.
سجّل webhooks تشير إلى رابط الإنتاج.
راقب
GET /v1/usageيوميًا أول أسبوع.
خارطة الطريق
الميزة | ETA |
أسعار الشحن حسب الولاية + قائمة مكاتب الاستلام على | v1.1 |
| v1.1 |
| v1.1 |
رفع صور المنتج عبر presigned URL | v1.1 |
حقول متعددة اللغات على استجابات المنتجات | v1.1 |
إن احتجت أيًا منها أبكر، تواصل مع الدعم.