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

شبكات الويب تتلقى أحداث التوقيع في الوقت الحقيقي

كوّن عنوان URL HTTPS في لوحة التحكم الخاصة بك في Certyneo واستقبل طلب POST موقّع بـ HMAC-SHA256 عند حدوث أي حدث على مغاليفك: توقيع، رفض، انتهاء الصلاحية. 11 حدثاً مدعوماً، 5 محاولات توصيل مع تراجع أسي، التحقق التشفيري في 8 سطور من الكود.

< 5s

متوسط مدة التسليم بعد الحدث

5x

محاولات التوصيل بالمجموع، موزعة على حوالي ساعة و20 دقيقة

HMAC-SHA256

خوارزمية توقيع كل طلب

قائمة الأحداث

الأحداث الـ 12 أدناه تغطي دورة حياة المغلف كاملة في Certyneo. فعّل ما يهمك منها في الإعدادات → Webhooks، وتجاهل الباقي — الاشتراك دقيق لكل حدث.

الحدثإطلاق
envelope.createdيتم إنشاء غلاف (بواسطة واجهة المستخدم، واجهة برمجة التطبيقات أو قالب) مفيد لمزامنة تسجيل جانب CRM في وقت إنشائه.
envelope.sentيتم إرسال المظروف إلى الموقعين (أول بريد إلكتروني تم إرساله). يمثل بداية دورة التوقيع النشطة.
envelope.completedوقّع جميع الموقّعين وتم حفظ ملف PDF المختوم eIDAS. تحمل الحمولة signedDocumentUrl، وهو رابط موقّع مسبقاً صالح لمدة 7 أيام؛ وإلا، استخدم GET /v1/envelopes/id/signed-document، وأثر التدقيق عبر GET /v1/envelopes/id/audit-trail.
envelope.declinedرفض موقّع واحد المغلف. عنوان الرافض موجود في `data.declinedBy` والسبب، إن تم إدخاله، في `data.reason`.
envelope.voidedتم إلغاء المظروف من قبل المصدر قبل التوقيع الكامل.
envelope.expiredانتهت صلاحية المغلف دون توقيع كامل. استعلم عبر GET /v1/envelopes/id لمعرفة الموقّعين المفقودين.
envelope.returned_to_senderأعاد موقّع المغلف إلى المُصدر للتصحيح، دون رفضه. السبب موجود في `data.reason` وصاحب الإعادة في `data.returnedBy`.
envelope.resubmittedصحّح المُصدر ثم أعاد إرسال مغلف تم إرجاعه سابقاً. يشير إلى استئناف دورة التوقيع.
recipient.signedوقّع موقّع واحد (لكن ليس بالضرورة الجميع). مفيد لتتبع التقدم والانتقال إلى الخطوة التالية من سير عمل متسلسل. تحذير: ملف PDF المختوم غير موجود بعد في هذه المرحلة، حتى للموقّع الأخير — التنزيل المبدوء هنا يعيد HTTP 409. استخدم envelope.completed للمستند.
recipient.viewedأحد الموقعين فتح رابط التوقيع دون أن يوقع بعد، مفيد لإعادة التجارة المستهدفة.
recipient.approvedوافق معتمد على الظرف دون توقيع عليه (سير عمل التحقق الداخلي). العنوان موجود في `data.approvedBy`.
recipient.bouncedخادم البريد الإلكتروني للمستقبل رفض نهائياً الدعوة أو إعادة الإرسال (صندوق غير موجود، نطاق معطل). ينتقل المستقبل إلى حالة BOUNCED وتتوقف محاولات إعادة الإرسال التلقائية. العنوان موجود في `data.recipientEmail`: صححه ثم أعد إرسال المغلف.

لا يعني `recipient.signed` « الوثيقة متاحة »

يتم إصدار `recipient.signed` لكل موقّع، عند انتهائه من التوقيع — بما في ذلك الأخير، قبل تجميع ملف PDF المختوم وتخزينه. يتلقى التحميل المُطلق من معالج هذا الحدث دائماً HTTP 409 « Signed document not available until the envelope is COMPLETED ». هذا ليس خطأ: هو « غير جاهز بعد ». اشترك في `envelope.completed` لاسترجاع الوثيقة، واحتفظ بـ `recipient.signed` لمتابعة التقدم (من وقّع، ومتى).

شكل الحمولة

تشترك جميع التسليمات في نفس مخطط JSON على المستوى الأعلى: `event`، `data` و `timestamp`. يتكرر اسم الحدث أيضاً في رأس `X-Certyneo-Event`، مما يسمح بالتوجيه قبل حتى معالجة الجسم. يختلف محتوى `data` حسب الحدث، لكنه يبقى دائماً كائن مسطح بقيم بسيطة — لا مصفوفة ولا كائن متداخل. إليك تسليم `envelope.completed` كامل.

POST /webhooks/certyneo HTTP/1.1
Host: your.app
Content-Type: application/json
X-Certyneo-Event: envelope.completed
X-Certyneo-Delivery-Id: cm8f2h6a10004qr9k5p2wm3xt
X-Certyneo-Signature: 4f3d1c8b2a9e7f60d5c4b3a2918e7f6d5c4b3a2918e7f6d5c4b3a2918e7f6d5c
{
  "id": "cm8f2h6a10004qr9k5p2wm3xt",
  "event": "envelope.completed",
  "data": {
    "envelopeId": "cm7x2k9p40001qz8h3f7bn2ld",
    "sandbox": false,
    "subject": "Contrat de prestation Acme Corp",
    "status": "COMPLETED",
    "signatureLevel": "ADVANCED",
    "aesSignerCount": 2,
    "completedAt": "2026-05-27T08:42:13.000Z",
    "recipientCount": 2,
    "recipients": [
      { "email": "alice@acme.com", "name": "Alice Martin", "role": "SIGNER", "status": "SIGNED" },
      { "email": "bob@acme.com", "name": "Bob Durand", "role": "SIGNER", "status": "SIGNED" }
    ],
    "signedDocumentUrl": "https://storage.certyneo.com/signed/cm7x2k9p4.../contrat.pdf?X-Amz-Expires=604800&X-Amz-Signature=..."
  },
  "timestamp": "2026-05-27T08:42:13.521Z"
}

هو رابط موقّع مسبقاً صالح لمدة 7 أيام من الحدث: يتجنب استدعاء مصرح ثانياً لاسترجاع ملف PDF. يكون غائباً — وليس فارغاً — إذا فشل التوقيع المسبق، أو على ظرف QES مكتمل بدون وثيقة مخزنة؛ عد إذاً إلى GET /v1/envelopes/id/signed-document، الذي يبقى مصدر الحقيقة. احذر أيضاً من إعادة التشغيل اليدوي من antitail queue أكثر من 7 أيام بعد الحدث: الرابط في الحمولة منتهي الصلاحية، endpoint API ليس كذلك.

يتم تشفير الحمولة بالرمز UTF-8 ، بدون BOM. يتم حساب توقيع HMAC على الجسم الخام كما تم إرساله لا تغير المساحات ، غالبًا ما يغير إعادة تحليل JSON ترتيب المفاتيح ويكسر التحقق.

تحقق من توقيع HMAC

يتم توقيع كل طلب برسك webhook (معروض مرة واحدة فقط عند إنشاء الاشتراك). يتم نقل التوقيع في رأس `X-Certyneo-Signature`: هو HMAC-SHA256 لجسم الطلب الخام، مشفر بصيغة hex، بدون بادئة أو طابع زمني. تحقق دائماً من التوقيع قبل معالجة الحمولة — بدون هذه الخطوة، يمكن لأي شخص أن يزيف حدثاً ويستدعي endpoint.

Node.js / TypeScript

import crypto from "node:crypto";

export function verifyCertyneoSignature(
  rawBody: string,
  signatureHeader: string,
  secret: string,
): boolean {
  // X-Certyneo-Signature = HMAC-SHA256(secret, rawBody), hex-encoded.
  const expected = crypto
    .createHmac("sha256", secret)
    .update(rawBody)
    .digest("hex");

  const received = Buffer.from(signatureHeader, "hex");
  const computed = Buffer.from(expected, "hex");

  // timingSafeEqual throws when the two buffers differ in length —
  // a malformed header must return false, not crash the handler.
  if (received.length !== computed.length) return false;

  // timingSafeEqual to mitigate timing attacks.
  return crypto.timingSafeEqual(computed, received);
}

Python

import hashlib
import hmac


def verify_certyneo_signature(
    raw_body: bytes, signature_header: str, secret: str
) -> bool:
    """Verify a Certyneo webhook signature.

    `X-Certyneo-Signature` holds the hex-encoded HMAC-SHA256 of the
    raw request body, computed with your webhook secret.
    """
    expected = hmac.new(
        secret.encode("utf-8"),
        raw_body,
        hashlib.sha256,
    ).hexdigest()

    return hmac.compare_digest(expected, signature_header)

خطأ شائع

لا تستخدم `===` أو `==` لمقارنة التوقيع المتوقع مع التوقيع المستلم. استخدم وظيفة مؤقتة (`crypto.timingSafeEqual` في Node ، `hmac.compare_digest` في Python). خلاف ذلك ، فإن فاصل وقت المقارنة بين اثنين من التوقيعات يكشف السر تدريجياً لمهاجم مريض (هجوم توقيت).

سياسة إعادة التجربة

إذا استغرق endpoint الخاص بك وقتاً طويلاً للرد أو رفض الاتصال أو أرسل 5xx (أو 429)، فسنحاول مرة أخرى وفقاً لتراجع أسي: 5 محاولات إجمالية، على مدى حوالي 1 ساعة و 20 دقيقة. بعد الخمسة، ينتقل الحدث إلى قائمة الرسائل المرفوضة — يمكن الاطلاع عليها وإعادة تشغيلها من لوحة التحكم الخاصة بك، لكن لن تتم إعادة محاولة تلقائية. الرفض الصريح (401 أو 403 أو 404 أو 410 أو 422 إلخ) لن تتم إعادة محاولة له على الإطلاق: الرد لن يتغير، ينتقل الحدث مباشرة إلى قائمة الرسائل المرفوضة.

محاولةمدة قبل المحاولةالزمن المضى منذ الحدث
#100
#2+ 1 min1 min
#3+ 5 min6 min
#4+ 15 min21 min
#5+ 1 h1 h 21

المهل الزمنية المذكورة هي الحد الأدنى: يتم مراقبة إعادة المحاولة بواسطة cron، لذا قد تنطلق محاولة بعد قليل من الوقت النظري. الأحداث المتخلى عنها مدرجة في Webhooks → الأخطاء، مع زر إعادة تشغيل يدوي بدون حد زمني للاحتفاظ. يتم تعطيل endpoint الذي يتعرض لخمسة أخطاء نهائية — أو يستجيب بـ 404 / 410 — تلقائياً، وستتلقى إشعاراً عبر البريد الإلكتروني: أعد تفعيله بعد الإصلاح، سيبدأ العداد من الصفر (أي تسليم ناجح يعيده أيضاً إلى الصفر).

اختبار دون إرسال مظروف حقيقي

أنشئ اشتراكاً أولاً — الرد يحتوي على `secret` الضروري للتحقق من HMAC، معروض مرة واحدة فقط. من Paramètres → Webhooks، يرسل زر « Tester » بعد ذلك POST موقّع تماماً مثل التسليم الفعلي ويعرض لك الرد الخام من خادمك: هذه أسرع طريقة للتحقق من معالج، محلياً عبر ngrok أو في التكامل المستمر.

# Create a subscription — "secret" is returned once, store it now.
curl -X POST https://api.certyneo.com/v1/webhooks \
  -H "Authorization: Bearer $CERTYNEO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://your.app/webhooks/certyneo",
    "events": ["envelope.completed", "recipient.signed"]
  }'

يجب أن يكون عنوان URL قابل للوصول علناً في HTTPS: يتم رفض عنوان خاص أو localhost بواسطة حماية SSRF عند إنشاء الاشتراك.

الاشتراكات مقسمة حسب البيئة، مثل المفاتيح: webhook تم إنشاؤه باستخدام مفتاح sk_test_ يتلقى أحداث المغلفات الاختبارية فقط، webhook تم إنشاؤه باستخدام مفتاح sk_live_ يتلقى أحداث المغلفات الفعلية فقط. لذا فإن مغلف الاختبار لا يصل أبداً إلى عنوان الويب الخاص بك في الإنتاج، وتشير كل حمولة إلى "sandbox" (true أو false).

6 أساليب يجب اتباعها

  • تحقق من توقيع HMAC قبل أي قراءة للجسم استخدم مقارنة آمنة للوقت.
  • إزالة التكرارات بناءً على رأس الطلب `X-Certyneo-Delivery-Id` (مطابق لـ `id` في الجسم) بتخزين المعرفات التي تمت رؤيتها بالفعل في قاعدة البيانات — قد تؤدي إعادة التشغيل إلى إعادة تسليم حدث تمت معالجته بالفعل إذا فُقد رمز 2xx الخاص بك، بنفس المعرف في كل محاولة.
  • الرد HTTP 2xx في أقصى 10 ثوانٍ، ثم معالجة غير متزامنة (queue). بعد ذلك، يتم قطع التسليم وعدّه كفشل.
  • تسجيل الجسم الخام + التوقيع الكامل في التحليل الاختياري التحقق من HMAC غالبا ما يفشل على BOM أو مساحة بيضاء غير مرئية.
  • راقب صفحة Webhooks → الأخطاء: ستتلقى إشعاراً عبر البريد الإلكتروني إذا تم تعطيل endpoint الخاص بك، لكن ليس حدثاً تلو الآخر — حدث يستنزف محاولاته، عليك أن تذهب وتعيد تشغيله.
  • تصفية الأحداث عند الاشتراك بدلاً من معالج، والبقاء تحت حد 5 اشتراكات لكل حساب.

أنت غير ملزم باستضافة نقطة نهاية لتلقي هذه الأحداث: يضع الموصل الاشتراك لك ويبدأ التدفق مباشرة عند حدث ظرف. انظر Certyneo لـ Power Automate و Microsoft 365.

للذهاب إلى أبعد من ذلك

هل أنتم مستعدون لربط أنظمتكم؟

تضمين Webhooks و REST API ابتداءً من خطة Standard. أنشئ حسابك، أنشئ مفتاح `sk_test_` وأوصل endpoint الخاص بك في Sandbox قبل الانتقال إلى الإنتاج.