هذه هي الوحدة الرئيسية القابلة للقياس في DZBuild API. كل اشتراك يصل عبرها يُحسب ضمن رقم signups_per_month الخاص بخطتك ويسهم في تسعير المنصة لتكاملات الشركاء. الاحتساب حقيقي؛ أما الفرض فلا — الحد يُبلَّغ عنه في GET /v1/usage، لكن لا شيء يحجب نداءً حاليًا لتجاوزه.
مصممة لحالة استخدام محددة: لديك موقع/تطبيق خارجي يقبل اشتراكات المستخدمين، وتريد أن يُحتسب كل واحد منها لحساب التاجر في 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 | واحد من email/phone/external_user_id | لإزالة التكرار. يُخزّن |
| string | يُخزّن | |
| string ≤ 190 | معرّفك الخاص للمستخدم. مفيد إن لم تجمع email/phone. | |
| string ≤ 64 | تسمية حرة (slug صفحة، حملة…). | |
| string (حرفان) | ISO 3166-1 alpha-2. نحوّله لأحرف كبيرة. | |
| string | يُخزّن مجزَّأً، لا بصيغته الصريحة أبدًا. لا يعيد لك أي فائدة يمكن قراءتها، ومع ذلك يُخرج بيانات شخصية من نظامك — اتركه. | |
| object | أي شيء آخر. يُخزّن JSON. | |
| 32-hex string | ✅ | لاستخدام واحد، إلى الأبد. انظر قواعد إزالة التكرار — نافذة الساعة هي الحارس الخارجي فقط؛ فالـ nonce لا يصلح مرتين أبدًا. |
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.
قواعد إزالة التكرار
قاعدتان، كلتاهما تُطبَّق لكل متجر:
احتساب واحد لكل 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)".
الفائدة العملية: قصة 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 مللي ثانية — تغذية راجعة سريعة للطلبات المشوّهة.
HTTP | الكود / الرسالة | السبب |
400 |
| الجسم ليس JSON |
400 |
| الترويسة مفقودة |
400 |
| أطول من اللازم، أو يستعمل محارف خارج تلك المجموعة (محارف base64 وهي |
401 |
| غاب أحد رؤوس X-DZ-* |
401 |
| انحراف الساعة أكثر من 5 دقائق |
401 |
| الـ nonce ليس 32 محرفًا ست عشريًا بالضبط |
401 |
| نفس الـ nonce مرتين خلال ساعة. وبعد ساعة يُقبل النداء، لكن المكرَّر يُسقَط بصمت |
401 |
| مدخلات HMAC خاطئة |
401 |
| المفتاح ملغى، أو معرّف خاطئ، أو لم يُفعَّل بعد |
403 |
| المفتاح غير مُسجَّل في البرنامج التجريبي |
429 |
| تجاوز اندفاع الدقيقة. تتبع السقوف خطة المتجر: free 60، pro 120، unlimited 300، enterprise 600 طلبًا/دقيقة |
لا يوجد 402 على هذه النقطة. signups_per_month يُحسب لكنه لا يُفرض أبدًا، فتجاوزه لا يُفشل أي نداء.
أفضل الممارسات
وقّع على خادمك، لا في المتصفح. لا تُرسل سر التوقيع لمستخدميك.
ولّد nonces جديدة بـ CSPRNG (
crypto.randomBytes,random.SystemRandom,random_bytesفي PHP). لا تعيد استخدامها أبدًا.أرسل
external_user_idحتى لو كان لديك email — يصمد لتغيير البريد.لا تعالج وتُعد المحاولة على أخطاء 401 تلقائيًا — هي دائمة. افحص مرة وأصلح الخطأ.
أعد المحاولة على 5xx بـ exponential backoff مع nonce جديد في كل مرة.
اشترك في webhook
signup.countedإن احتجت تأكيد دخول الصف.