هذه هي الوحدة الرئيسية القابلة للقياس في DZBuild API. كل اشتراك يصل عبرها يُحتسب، وGET /v1/usage يعرض عدد الشهر بجانب حد signups_per_month الخاص بك. وفي خطة Enterprise يكون هذا الحد -1 افتراضيًا، أي بلا حد (انظر الحصص)، ولا شيء يحجب نداءً لتجاوزه هذا الحد.
مصممة لحالة استخدام محددة: لديك موقع/تطبيق خارجي يقبل اشتراكات المستخدمين، وتريد أن يُحتسب كل واحد منها لحساب التاجر في DZBuild. أمثلة:
موقع WordPress للتسويق → يملأ المستخدم نموذج التسجيل → تنادي
/v1/signups.تطبيق جوال يسجّل المستخدمون فيه → الباك إند يُنادي
/v1/signups.صفحة هبوط على نطاق آخر → الباك إند يُنادي
/v1/signups.
ليست مصممة لطلبات الواجهة (تلك تُنشئ عملاء عبر تدفق الواجهة نفسه) ولا لاشتراكات lead-magnet المعزولة (استخدم /v1/events لها).
المصادقة
مفتاح عام + HMAC. خادمك الخلفي يوقّع كل نداء. انظر المصادقة لمخطط HMAC الكامل.
⚠️ تنبيه — أمران يعطّلان أي مفتاح جديد تمامًا
بوابة البرنامج التجريبي. الـ API مُقيَّد بالبرنامج التجريبي في الإنتاج. المفتاح الذي لم تُسجّله DZBuild يُرجع
403 forbidden"API is in pilot mode; key not enrolled"في كل نداء.تأخير التفعيل. المفتاح العام الجديد ليس صالحًا للاستعمال لحظة إنشائه: وإلى أن يُفعَّل لهذه النقاط ستحصل على
401 unauthorized"Invalid or revoked public key". راسل الدعم مباشرة بعد إنشائه لتفعيله. وإلغاء مفتاح عام لا يوقفه وحده: تظل النداءات الموقّعة مقبولة إلى أن يعطّله الدعم، لذا راسل الدعم كلما ألغيت مفتاحًا.
الجسم
{
"email": "[email protected]",
"phone": "+213555000000",
"external_user_id": "u_42",
"source": "landing-page-1",
"country": "DZ",
"ip": "203.0.113.42",
"meta": { "campaign": "spring-2026" },
"nonce": "32-hex-single-use"
}
الحقل | النوع | إلزامي | ملاحظات |
| string | لإزالة التكرار. يُخزّن | |
| string | يُخزّن | |
| string ≤ 190 | معرّفك الخاص للمستخدم. يُخزَّن لكنه لا يُستعمل لإزالة التكرار، ولا تُرجعه أي نقطة. | |
| string ≤ 64 | تسمية حرة (slug صفحة، حملة…). | |
| string (حرفان) | ISO 3166-1 alpha-2. نحوّله لأحرف كبيرة. | |
| string | يُخزّن مجزَّأً، لا بصيغته الصريحة أبدًا. لا يعيد لك أي فائدة يمكن قراءتها، ومع ذلك يُخرج بيانات شخصية من نظامك — اتركه. | |
| object | أي شيء آخر. يُخزّن JSON. | |
| 32-hex string | ✅ | لاستخدام واحد، إلى الأبد. انظر قواعد إزالة التكرار — نافذة الساعة هي الحارس الخارجي فقط؛ فالـ nonce لا يصلح مرتين أبدًا. |
nonce هو الحقل الوحيد الذي تحتاجه المنصة، وإن خلا الجسم منه تُستعمل قيمة ترويسة X-DZ-Nonce. وحين ترسله في الجسم، استعمل نفس قيمة الترويسة. والجسم الخالي من أي معرّف يبقى محسوبًا قابلًا للفوترة.
أرسل email. فبدونه لا يمكن أن تنطبق إزالة التكرار مدى الحياة لكل بريد الموصوفة أدناه، فتُحسب إعادة الإرسال العرضية لنفس المستخدم مرتين.
كما يجب أن يحمل كل POST ترويسة Idempotency-Key (بحد أقصى 64 حرفًا، من مجموعة المحارف [A-Za-z0-9_-:.]). بدونها تحصل على 400 bad_request ولا يدخل أي شيء الطابور. والـ nonce ذو الـ 32 خانة ست عشرية الذي تولّده أصلًا قيمة صالحة — أعد استعماله.
الاستجابة 202 Accepted
{
"data": { "status": "queued", "kind": "signup", "store_id": 13 },
"meta": { "request_id": "...", "api_version": "v1", "edge": true }
}
202 تعني "قبلناه وأدخلناه طابور الكتابة". ويكتمل التخزين خلال 5 ثوانٍ تقريبًا. لا تنتظرها. إن احتجت تأكيدًا فسجّل webhook لـ signup.counted. وهو ليس فوريًا: يُرسَل بعد تخزين الاشتراك، ولا يُرسَل إلا للاشتراكات المحتسبة (لا المكرَّرة)، ويحمل key_id و source و country، لا معرّفات المستخدم.
قواعد إزالة التكرار
قاعدتان، كلتاهما تُطبَّق لكل متجر:
احتساب واحد لكل nonce، مدى الحياة — يحمي من الإعادة العرضية لنفس النداء. (يُرفض الـ nonce المعاد استعماله رفضًا صريحًا لمدة ساعة بـ
401 "Nonce reused"؛ وبعدها يُقبل بـ202ثم يُسقَط بصمت، لأن الـ nonce يُتذكَّر دائمًا.)احتساب واحد لكل بريد، مدى الحياة — ولا ينطبق إلا حين ترسل
email.
حين يصطدم اشتراك بأي من القاعدتين، لا يُخزَّن شيء. وما تراه بدلًا من ذلك هو usage.signup.total يزيد بمقدار 1 بينما يبقى usage.signup.billable ثابتًا. أي أن المكرَّرات لا تُحاسَب، لكنها كذلك ليست مسجّلة إفراديًا: لا يوجد ما تبحث عن المكرَّر به.
يمكنك استنتاج المكرَّرات من GET /v1/usage/history بحساب count − billable_count لـ endpoint_group = "signup". تُرجع تلك النقطة تجميعات ساعية تحت data.rows (period_hour و endpoint_group و count و billable_count)، وتفترض آخر 7 أيام افتراضيًا، وترفض أي نافذة أوسع من 90 يومًا بـ 400 bad_request "range too large (max 90 days)". استدعِها بمفتاح منصة: فالمفاتيح العامة لا تحمل صلاحية usage:read.
الفائدة العملية: قصة Idempotency لديك تلقائية، ما دمت ترسل email.
مثال عملي: Node.js
import crypto from 'node:crypto';const KEY_ID = process.env.DZ_PUBLIC_KEY;
const SECRET = process.env.DZ_SIGNING_SECRET;export async function trackSignup({ email, phone, external_user_id, source, country, meta }) {
const nonce = crypto.randomBytes(16).toString('hex');
const ts = Math.floor(Date.now() / 1000).toString();
const body = JSON.stringify({ email, phone, external_user_id, source, country, meta, nonce }); const bodyHash = crypto.createHash('sha256').update(body).digest('hex');
const payload = `${KEY_ID}\n${nonce}\n${ts}\n${bodyHash}`;
const sig = crypto.createHmac('sha256', SECRET).update(payload).digest('hex'); const r = await fetch('https://api.dzbuild.app/v1/signups', {
method: 'POST',
headers: {
'Authorization': `DZ-Public ${KEY_ID}`,
'X-DZ-Timestamp': ts,
'X-DZ-Nonce': nonce,
'X-DZ-Signature': sig,
'Idempotency-Key': nonce, // إلزامية في كل POST
'Content-Type': 'application/json',
},
body,
});
if (!r.ok) {
const err = await r.json();
throw new Error(`signup failed: ${err.error?.code} ${err.error?.message}`);
}
return r.json();
}
مثال عملي: PHP (داخل hook لـ WordPress)
<?php
add_action('user_register', function($user_id) {
$user = get_userdata($user_id);
dz_track_signup([
'email' => $user->user_email,
'external_user_id' => "wp_{$user_id}",
'source' => 'wordpress-' . get_bloginfo('name'),
]);
});function dz_track_signup(array $payload): void {
$keyId = getenv('DZ_PUBLIC_KEY');
$secret = getenv('DZ_SIGNING_SECRET');
$nonce = bin2hex(random_bytes(16));
$ts = (string) time();
$payload['nonce'] = $nonce;
$body = json_encode($payload, JSON_UNESCAPED_UNICODE);
$hash = hash('sha256', $body);
$sig = hash_hmac('sha256', "$keyId\n$nonce\n$ts\n$hash", $secret); $ch = curl_init('https://api.dzbuild.app/v1/signups');
curl_setopt_array($ch, [
CURLOPT_POST => true, CURLOPT_POSTFIELDS => $body,
CURLOPT_TIMEOUT => 5,
CURLOPT_HTTPHEADER => [
"Authorization: DZ-Public $keyId",
"X-DZ-Timestamp: $ts",
"X-DZ-Nonce: $nonce",
"X-DZ-Signature: $sig",
"Idempotency-Key: $nonce", // إلزامية في كل POST
'Content-Type: application/json',
],
CURLOPT_RETURNTRANSFER => true,
]);
curl_exec($ch);
curl_close($ch);
}
مثال عملي: Python (Django signal)
import hashlib, hmac, json, os, secrets, time, requests
from django.dispatch import receiver
from django.contrib.auth.models import User
from django.db.models.signals import post_saveKEY_ID = os.environ['DZ_PUBLIC_KEY']
SECRET = os.environ['DZ_SIGNING_SECRET']@receiver(post_save, sender=User)
def track_dz_signup(sender, instance, created, **kw):
if not created: return
payload = {
'email': instance.email, 'external_user_id': str(instance.pk),
'source': 'django-app', 'nonce': secrets.token_hex(16),
}
body = json.dumps(payload)
ts = str(int(time.time()))
h = hashlib.sha256(body.encode()).hexdigest()
sig = hmac.new(SECRET.encode(),
f"{KEY_ID}\n{payload['nonce']}\n{ts}\n{h}".encode(),
hashlib.sha256).hexdigest()
requests.post('https://api.dzbuild.app/v1/signups', data=body, timeout=5,
headers={
'Authorization': f'DZ-Public {KEY_ID}',
'X-DZ-Timestamp': ts, 'X-DZ-Nonce': payload['nonce'],
'X-DZ-Signature': sig, 'Content-Type': 'application/json',
'Idempotency-Key': payload['nonce'], # إلزامية في كل POST
})
الأخطاء
تعود أخطاء التحقق في ~10 مللي ثانية، قبل أن يدخل أي شيء الطابور. وهي لا تشمل إلا الترويسات والتوقيع وصياغة JSON. أما حقول الجسم فتُفحص بعد 202: الجسم الذي لا تستطيع المنصة تخزينه، كـ nonce في الجسم ليس 32 محرفًا ست عشريًا، يُسقَط بلا خطأ.
HTTP | الكود / الرسالة | السبب |
400 |
| الجسم ليس JSON |
400 |
| الترويسة مفقودة |
400 |
| أطول من اللازم، أو يستعمل محارف خارج تلك المجموعة (محارف base64 وهي |
401 |
| غاب أحد رؤوس X-DZ-* |
401 |
|
|
401 |
| انحراف الساعة أكثر من 5 دقائق |
401 |
| الـ nonce ليس 32 محرفًا ست عشريًا بالضبط |
401 |
| نفس الـ nonce مرتين خلال ساعة. وبعد ساعة يُقبل النداء، لكن المكرَّر يُسقَط بصمت |
401 |
| مدخلات HMAC خاطئة |
401 |
| المفتاح ملغى، أو معرّف خاطئ، أو لم يُفعَّل بعد |
403 |
| المفتاح غير مُسجَّل في البرنامج التجريبي |
403 |
| متجر المفتاح ليس على خطة Enterprise سارية |
403 |
| ناديت بمفتاح منصة (Bearer)؛ المفاتيح العامة وحدها تحمل هذه الصلاحية (على |
429 |
| تجاوز اندفاع الدقيقة: 600 طلب في الدقيقة لكل متجر، مشتركة بين كل مفاتيح المتجر. انتظر عدد ثواني |
لا يوجد 402 على هذه النقطة. signups_per_month يُحسب لكنه لا يُفرض أبدًا، فتجاوزه لا يُفشل أي نداء.
أفضل الممارسات
وقّع على خادمك، لا في المتصفح. لا تُرسل سر التوقيع لمستخدميك.
ولّد nonces جديدة بـ CSPRNG (
crypto.randomBytes,random.SystemRandom,random_bytesفي PHP). لا تعيد استخدامها أبدًا.external_user_idلا يُستعمل لإزالة التكرار. من حقول المستخدم لا يُستعمل لذلك إلاemail، فإن أُرسل نفس المستخدم مجددًا بعنوان بريد مختلف يُحتسب مرة أخرى.لا تعالج وتُعد المحاولة على أخطاء 401 تلقائيًا — هي دائمة. افحص مرة وأصلح الخطأ.
أعد المحاولة على
429و 5xx بـ exponential backoff، مع nonce جديد وتوقيع جديد في كل مرة. فبمجرد أن يصحّ التوقيع يُستهلَك الـ nonce، حتى لو رُفض النداء بعدها، لذا فإعادة إرسال نفس الطلب الموقّع تُرجع401 "Nonce reused".اشترك في webhook
signup.countedإن احتجت تأكيد دخول الصف.