رفتن به محتوای اصلی
Certyneo
مستندات توسعه‌دهنده

Webhooks — دریافت رویدادهای امضا در زمان واقعی

یک آدرس URL HTTPS را در داشبورد Certyneo خود پیکربندی کنید و هر زمان که رویدادی بر روی پاکت‌های شما رخ دهد، یک POST امضا شده HMAC-SHA256 دریافت کنید: امضا، رد، انقضا. 11 رویداد پشتیبانی شده، 5 تلاش برای تحویل در بک‌آف نمایی، تأیید رمزنگاری در 8 خط کد.

< 5s

میانگین تأخیر تحویل پس از رویداد

5x

تلاش‌های تحویل در مجموع، در بازه زمانی تقریباً 1 ساعت و 20 دقیقه

HMAC-SHA256

الگوریتم امضای هر درخواست

کاتالوگ رویدادها

11 رویداد زیر کل چرخه حیات یک پاکت Certyneo را پوشش می‌دهند. آن‌هایی را که به شما علاقه دارند فعال کنید در تنظیمات → وبهوک‌ها، بقیه را نادیده بگیرید — اشتراک به تفکیک رویداد دانه‌ای است.

رویدادراه‌اندازی
envelope.createdیک پاکت ایجاد می‌شود (از طریق UI، API یا الگو) — برای همزمان‌سازی یک رکورد 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پاکت توسط فرستنده قبل از امضای کامل لغو شد. متفاوت از `expired` (انسانی در برابر timeout).
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.signed به معنی «سند دستیاب» نیست

recipient.signed برای هر امضاکننده در لحظه پایان امضای او صادر می‌شود — حتی آخری، پیش از اینکه سند PDF مختوم جمع‌آوری و ذخیره شود. بنابراین دانلود آغاز‌شده از این handler همیشه HTTP 409 «Signed document not available until the envelope is COMPLETED» دریافت می‌کند. این خطا نیست: این «هنوز آماده نیست». برای دریافت سند، recipient.signed را subscribe کنید و برای پیگیری پیشرفت (کی امضا کرد و چه زمانی) recipient.signed را نگاه دارید.

قالب Payload

همه تحویل‌ها دارای یک schema JSON سطح بالا یکسان هستند: `event`، `data` و `timestamp`. نام رویداد نیز در هدر `X-Certyneo-Event` تکرار می‌شود، که امکان مسیریابی را قبل از parse کردن بدنه فراهم می‌کند. محتوای `data` بر اساس رویداد متفاوت است، اما همیشه یک شی تخت از مقادیر ساده باقی می‌ماند — هرگز آرایه یا شی تودرتو نیست. در اینجا یک تحویل کامل `envelope.completed` آمده است.

POST /webhooks/certyneo HTTP/1.1
Host: your.app
Content-Type: application/json
X-Certyneo-Event: envelope.completed
X-Certyneo-Signature: 4f3d1c8b2a9e7f60d5c4b3a2918e7f6d5c4b3a2918e7f6d5c4b3a2918e7f6d5c
{
  "event": "envelope.completed",
  "data": {
    "envelopeId": "cm7x2k9p40001qz8h3f7bn2ld",
    "subject": "Contrat de prestation Acme Corp",
    "status": "COMPLETED",
    "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"
}

signedDocumentUrl یک لینک از قبل امضا‌شده است که 7 روز از رویداد معتبر است: این از یک فراخوانی احراز هویت دوم برای دریافت PDF جلوگیری می‌کند. اگر امضا قبلی ناموفق بود یا روی پاکت QES تکمیل‌شده بدون سند ذخیره‌شده غایب است — null نیست؛ سپس مجدداً از GET /v1/envelopes/id/signed-document بروید، که منبع حقیقت باقی می‌ماند. همچنین به پخش دستی از dead-letter queue بیش از 7 روز بعد از رویداد توجه کنید: لینک payload منقضی است، endpoint API نه.

Payload با کدگذاری UTF-8 بدون BOM است. امضای HMAC بر روی بدنه خام همانطور که ارسال شده محاسبه می‌شود — فضاها را تغییر ندهید، تجزیه مجدد JSON اغلب ترتیب کلیدها را تغییر می‌دهد و تایید را شکست می‌دهد.

تایید امضای HMAC

هر درخواست با secret webhook شما امضا می‌شود (فقط یک‌بار در ایجاد subscription نمایش داده می‌شود). امضا در هدر `X-Certyneo-Signature` منتقل می‌شود: این HMAC-SHA256 بدنه خام درخواست است، به صورت هگزادسیمال کدگذاری‌شده، بدون پیشوند یا timestamp. قبل از پردازش payload، همیشه امضا را تأیید کنید — بدون این مرحله، هرکس می‌تواند یک رویداد جعلی بسازد و 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)

خطای متداول

از `===` یا `==` برای مقایسه امضای مورد انتظار با امضای دریافتی استفاده نکنید. از یک تابع timing-safe استفاده کنید (`crypto.timingSafeEqual` در Node، `hmac.compare_digest` در Python). بدون این، تفاوت زمان مقایسه بین دو امضا به تدریج secret را به یک مهاجم صبور نشان می‌دهد (timing attack).

سیاست تلاش مجدد

اگر endpoint شما خیلی طول بکشد در پاسخ دادن، اتصال را رد کند یا ۵xx (یا ۴۲۹) برگرداند، ما طبق عقبنشینی نمایی دوباره تلاش می‌کنیم: در مجموع ۵ تلاش، در حدود ۱ ساعت و ۲۰ دقیقه. پس از پنجمین تلاش، رویداد به صف dead-letter می‌رود — قابل مشاهده و قابل پخش مجدد از داشبورد شما، اما دیگر به طور خودکار تلاش نمی‌شود. رد صریح (۴۰۱، ۴۰۳، ۴۰۴، ۴۱۰، ۴۲۲…) هرگز دوباره تلاش نمی‌شود: پاسخ تغییر نخواهد کرد، رویداد مستقیماً به صف dead-letter می‌رود.

تلاشتاخیر قبل از تلاشزمان سپری‌شده از رویداد
#100
#2+ 1 min1 min
#3+ 5 min6 min
#4+ 15 min21 min
#5+ 1 h1 h 21

زمان‌های ذکرشده حداقل هستند: اسکن تلاش مجدد توسط یک cron کنترل می‌شود، بنابراین یک تلاش می‌تواند کمی بعد از زمان تئوری آغاز شود. رویدادهای متروک در Webhooks → شکست‌ها درج می‌شوند، با دکمه پخش مجدد دستی و بدون محدودیت مدت نگهداری. یک endpoint که پنج شکست قطعی را پیاپی تجربه کند — یا که ۴۰۴ / ۴۱۰ پاسخ دهد — به طور خودکار غیرفعال می‌شود، و شما از طریق ایمیل مطلع می‌شوید: پس از اصلاح، آن را دوباره فعال کنید، شمارنده از اول شروع می‌شود (هر تحویل موفق آن را نیز به صفر می‌رساند).

تست بدون ارسال پاکت واقعی

ابتدا یک subscription ایجاد کنید — پاسخ شامل `secret` مورد نیاز برای تأیید HMAC است، فقط یک‌بار نمایش داده می‌شود. از Settings → Webhooks، دکمه «Test» سپس یک POST امضا‌شده دقیقاً مانند یک تحویل واقعی ارسال می‌کند و پاسخ خام سرور شما را نمایش می‌دهد: این سریع‌ترین روش برای تأیید handler شما است، به‌طور محلی از طریق ngrok یا در continuous integration.

# 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 در هنگام ایجاد subscription توسط حفاظت SSRF رد می‌شود.

6 شیوه‌ای برای رعایت

  • امضای HMAC را قبل از هر خواندن از body تایید کنید — از مقایسه timing-safe استفاده کنید.
  • هر `data.envelopeId` × `event` را فقط یک‌بار با ذخیره جفت‌های دیده‌شده در پایگاه داده پردازش کنید — یک retry می‌تواند یک رویدادی را مجدداً تحویل دهد که قبلاً پردازش کردید اگر 2xx شما گم شود.
  • در حداکثر 10 ثانیه HTTP 2xx پاسخ دهید، سپس به صورت ناهمزمان (queue) پردازش کنید. بیشتر از آن، تحویل قطع می‌شود و به‌عنوان شکست شمرده می‌شود.
  • در debug بدنه خام + امضای کامل را log کنید — تایید HMAC اغلب بر روی BOM یا whitespace نامرئی شکست می‌خورد.
  • صفحه Webhooks → شکست‌ها را نظارت کنید: اگر endpoint شما غیرفعال شود، از طریق ایمیل مطلع می‌شوید، اما برای هر رویداد نه — رویدادی که تلاش‌های خود را تمام کند، شما باید آن را بازپخش کنید.
  • رویدادها را در subscription به‌جای handler فیلتر کنید و تحت حد 5 subscription در هر حساب بمانید.

شما ملزم نیستید یک نقطه پایانی میزبان کنید تا این رویدادها را دریافت کنید: اتصال‌دهنده برای شما اشتراک را ایجاد می‌کند و جریان را مستقیماً بر روی یک رویداد پاکت شروع می‌کند. نگاه کنید Certyneo برای Power Automate و Microsoft 365.

برای اطلاعات بیشتر

آماده‌اید سیستم‌های خود را متصل کنید؟

webhooks و REST API از پلان Standard گنجانده می‌شوند. حساب خود را ایجاد کنید، کلید `sk_test_` تولید کنید و endpoint خود را در sandbox وصل کنید پیش از حرکت به production.