Webhooks — دریافت رویدادهای امضا در زمان واقعی
یک آدرس URL HTTPS را در داشبورد Certyneo خود پیکربندی کنید و هر زمان که رویدادی بر روی پاکتهای شما رخ دهد، یک POST امضا شده HMAC-SHA256 دریافت کنید: امضا، رد، انقضا. 11 رویداد پشتیبانی شده، 5 تلاش برای تحویل در بکآف نمایی، تأیید رمزنگاری در 8 خط کد.
< 5s
میانگین تأخیر تحویل پس از رویداد
5x
تلاشهای تحویل در مجموع، در بازه زمانی تقریباً 1 ساعت و 20 دقیقه
HMAC-SHA256
الگوریتم امضای هر درخواست
کاتالوگ رویدادها
12 رویداد زیر کل چرخه حیات یک پاکت 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.bounced | سرور پیامرسانی یک گیرنده به صورت دائمی دعوت یا تلاش مجدد را رد کرده است (صندوق وجود ندارد، دامنه غیرفعال). گیرنده به وضعیت BOUNCED میرود و تلاشهای خودکار مجدد متوقف میشود. آدرس در `data.recipientEmail` است: آن را اصلاح کنید سپس پاکت را دوباره ارسال کنید. |
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-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" }
],
"fieldValues": [
{ "fieldId": "cm7x2k9p40005qz8h1a2b3c4d", "fieldType": "RADIO_GROUP", "recipientEmail": "alice@acme.com", "value": "Option B" }
],
"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 میرود.
| تلاش | تاخیر قبل از تلاش | زمان سپریشده از رویداد |
|---|---|---|
| #1 | 0 | 0 |
| #2 | + 1 min | 1 min |
| #3 | + 5 min | 6 min |
| #4 | + 15 min | 21 min |
| #5 | + 1 h | 1 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 رد میشود.
اشتراکها مانند کلیدها بر اساس محیط جداشدهاند: وبهوک ایجاد شده با کلید sk_test_ فقط رویدادهای پاکتهای آزمایشی را دریافت میکند، وبهوک ایجاد شده با کلید sk_live_ فقط آنهایی از پاکتهای واقعی را دریافت میکند. بنابراین یک پاکت آزمایشی هرگز به URL تولید شما نمیرسد، و هر بار باراندازی نشان میدهد «sandbox» (true یا false).
6 شیوهای برای رعایت
- امضای HMAC را قبل از هر خواندن از body تایید کنید — از مقایسه timing-safe استفاده کنید.
- رفع تکراری بر روی سرصحفه `X-Certyneo-Delivery-Id` (یکسان با `id` در بدنه) با ذخیره شناسههای دیده شده در پایگاه داده — اگر ۲xx شما گم شود، پخش مجدد میتواند رویدادی را تحویل دهد که قبلاً پردازش کردهاید، با همان شناسه در هر تلاش.
- در حداکثر 10 ثانیه HTTP 2xx پاسخ دهید، سپس به صورت ناهمزمان (queue) پردازش کنید. بیشتر از آن، تحویل قطع میشود و بهعنوان شکست شمرده میشود.
- در debug بدنه خام + امضای کامل را log کنید — تایید HMAC اغلب بر روی BOM یا whitespace نامرئی شکست میخورد.
- صفحه Webhooks → شکستها را نظارت کنید: اگر endpoint شما غیرفعال شود، از طریق ایمیل مطلع میشوید، اما برای هر رویداد نه — رویدادی که تلاشهای خود را تمام کند، شما باید آن را بازپخش کنید.
- رویدادها را در subscription بهجای handler فیلتر کنید و تحت حد 5 subscription در هر حساب بمانید.
شما ملزم نیستید یک نقطه پایانی میزبان کنید تا این رویدادها را دریافت کنید: اتصالدهنده برای شما اشتراک را ایجاد میکند و جریان را مستقیماً بر روی یک رویداد پاکت شروع میکند. نگاه کنید Certyneo برای Power Automate و Microsoft 365.
Power Automate: اشتراک به طور خودکار مدیریت میشود
با استفاده از تریگر «هنگامی که یک رویداد پاکت رخ دهد»، نیازی نیست آدرسی را کپی یا بهروزرسانی کنید. خود Power Automate اشتراک را در Certyneo با اتصالی که ایجاد کردهاید ثبت میکند.
- رویدادها را در نقشه تریگر انتخاب میکنید، سپس جریان را ذخیره و فعال میکنید: اشتراک در آن لحظه ایجاد میشود.
- زمانی که جریان را غیرفعال یا حذف میکنید، Power Automate اشتراک را به طور خودکار حذف میکند.
- اتصال تعیین میکند کدام حساب (و کدام فضای کاری) رویدادها را ارسال میکند. در محیط تولید، از اتصالی استفاده کنید که به حساب صحیح متصل باشد.
- اگر تریگر را تغییر دهید، به عنوان مثال برای تغییر رویدادها، دوباره جریان را ذخیره کنید: اشتراک بهروزرسانی میشود.
- اگر جریان دیگر فعال نشود، آن را غیرفعال کنید و سپس دوباره فعال کنید: اشتراک دوباره ایجاد میشود.
انتقال از توسعه به تولید
- راهحل خود را به محیط تولید وارد کنید.
- جریان را باز کنید و اتصال Certyneo را برای استفاده در تولید، ایجادشده با کلید واقعی (sk_live_)، به آن متصل کنید.
- جریان را فعال کنید. یک اشتراک تولید جدید ایجاد میشود: نیازی نیست چیزی در سمت Certyneo پیکربندی کنید و هیچ آدرسی را تغییر ندهید.
- آزمایشی انجام دهید: پاکت را امضا کنید یا تکمیل کنید، سپس بررسی کنید که یک اجرا در سابقه جریان ظاهر شود.
برای اطلاعات بیشتر
آمادهاید سیستمهای خود را متصل کنید؟
webhooks و REST API از پلان Standard گنجانده میشوند. حساب خود را ایجاد کنید، کلید `sk_test_` تولید کنید و endpoint خود را در sandbox وصل کنید پیش از حرکت به production.