تخط وانتقل إلى المحتوى الرئيسي

الاشتراكات

POST /v1/signups — حسب اشتراك مستخدم نهائي ضمن حصتك الشهرية. الوحدة الرئيسية القابلة للقياس. موقّعة بـ HMAC، تُقبل بشكل غير متزامن، يُزال تكرارها بـ nonce + email.

بقلم: Support

هذه هي الوحدة الرئيسية القابلة للقياس في 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"
}

الحقل

النوع

إلزامي

ملاحظات

email

string

لإزالة التكرار. يُخزّن sha256(lowercase) فقط.

phone

string

يُخزّن sha256(value) فقط.

external_user_id

string ≤ 190

معرّفك الخاص للمستخدم. يُخزَّن لكنه لا يُستعمل لإزالة التكرار، ولا تُرجعه أي نقطة.

source

string ≤ 64

تسمية حرة (slug صفحة، حملة…).

country

string (حرفان)

ISO 3166-1 alpha-2. نحوّله لأحرف كبيرة.

ip

string

يُخزّن مجزَّأً، لا بصيغته الصريحة أبدًا. لا يعيد لك أي فائدة يمكن قراءتها، ومع ذلك يُخرج بيانات شخصية من نظامك — اتركه.

meta

object

أي شيء آخر. يُخزّن JSON.

nonce

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، لا معرّفات المستخدم.

قواعد إزالة التكرار

قاعدتان، كلتاهما تُطبَّق لكل متجر:

  1. احتساب واحد لكل nonce، مدى الحياة — يحمي من الإعادة العرضية لنفس النداء. (يُرفض الـ nonce المعاد استعماله رفضًا صريحًا لمدة ساعة بـ 401 "Nonce reused"؛ وبعدها يُقبل بـ 202 ثم يُسقَط بصمت، لأن الـ nonce يُتذكَّر دائمًا.)

  2. احتساب واحد لكل بريد، مدى الحياة — ولا ينطبق إلا حين ترسل 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

bad_request "Body must be valid JSON"

الجسم ليس JSON

400

bad_request "Idempotency-Key header is required for write requests"

الترويسة مفقودة

400

bad_request "Idempotency-Key must be <=64 chars, [A-Za-z0-9_-:.]"

أطول من اللازم، أو يستعمل محارف خارج تلك المجموعة (محارف base64 وهي + و / و = مرفوضة)

401

unauthorized "Missing signature headers"

غاب أحد رؤوس X-DZ-*

401

unauthorized "Invalid timestamp"

X-DZ-Timestamp ليس ثوانيَ كاملة (من 1 إلى 12 رقمًا). والقيمة بالمللي ثانية مثل Date.now() تفشل هنا

401

unauthorized "Timestamp out of window"

انحراف الساعة أكثر من 5 دقائق

401

unauthorized "Invalid nonce format"

الـ nonce ليس 32 محرفًا ست عشريًا بالضبط

401

unauthorized "Nonce reused"

نفس الـ nonce مرتين خلال ساعة. وبعد ساعة يُقبل النداء، لكن المكرَّر يُسقَط بصمت

401

unauthorized "Signature mismatch"

مدخلات HMAC خاطئة

401

unauthorized "Invalid or revoked public key"

المفتاح ملغى، أو معرّف خاطئ، أو لم يُفعَّل بعد

403

forbidden "API is in pilot mode; key not enrolled"

المفتاح غير مُسجَّل في البرنامج التجريبي

403

forbidden "API access requires an active Enterprise plan"

متجر المفتاح ليس على خطة Enterprise سارية

403

forbidden "Missing scope: signups:write"

ناديت بمفتاح منصة (Bearer)؛ المفاتيح العامة وحدها تحمل هذه الصلاحية (على /v1/events تذكر الرسالة events:write)

429

rate_limited

تجاوز اندفاع الدقيقة: 600 طلب في الدقيقة لكل متجر، مشتركة بين كل مفاتيح المتجر. انتظر عدد ثواني Retry-After ثم أعد المحاولة بـ nonce جديد

لا يوجد 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 إن احتجت تأكيد دخول الصف.

هل أجاب هذا عن سؤالك؟