استقبال Webhooks
نرسل طلب POST إلى عنوان URL الخاص بك عندما يحدث شيء هنا، موقَّعًا لتعرف أنه منّا. وتتوفر الـ Webhooks وSlack في خطط Pro وTeam.
أو Slack فقط
إذا كان كل ما تريده أن تعرف، لا أن تبني شيئًا، فاربط Slack بدلًا من ذلك: الإعدادات، ثم Slack. تلصق عنوان incoming webhook من Slack (Apps → Incoming Webhooks → Add to Slack، وهناك تختار القناة)، فتصل رسالة إلى تلك القناة عندما يجيب أحدهم عن نموذج، أو يعلّق أحدهم، أو يُنشر موقع.
في حالة النموذج، تذكر الرسالة الحقول التي ملأها الشخص، ولا تذكر أبدًا ما كتبه. وفي حالة التعليق، تذكر ما قيل واسم المعلّق، مع رابط إلى النقاش. فالقناة يراها جمهور أوسع من صندوق بريدك، وليس من حقنا أن نذيع فيها عنوان بريد أحد.
إضافة Webhook
الإعدادات، ثم Webhooks. تعطي عنوان URL يبدأ بـ https وتختار ما تريد أن تُبلَّغ به، ونعطيك سرّ توقيع مرة واحدة. نحتفظ به للتوقيع لا للعرض، فلا سبيل إلى رؤيته مرة أخرى، وكل ما يمكنك هو استبدال الـ Webhook.
ما الذي نرسله
جسم JSON فيه الحقل event، وترويستان:solta-event وsolta-signature. والأحداث هي site.published وversion.created وsubmission.received وsite.deleted وcomment.created وcomment.resolved.
يصل التعليق، أو الرد، على شكل comment.created. والكاتب اسم، وليس عنوان بريد أبدًا، ويفتح url النقاش على النسخة التي تُرك عليها:
{
"event": "comment.created",
"siteId": "6f1c…",
"slug": "quote-builder",
"title": "Quote builder",
"version": 3,
"versionState": "draft",
"comment": {
"id": "a41e…",
"threadId": "a41e…",
"parentId": null,
"author": "Ana",
"body": "The total should include VAT.",
"createdAt": "2026-10-12T09:41:07.000Z"
},
"url": "https://solta.now/s/quote-builder/v/3?panel=comments&thread=a41e…"
}parentId هو التعليق الذي جاء الرد عليه، أو null في نقاش جديد، يكون threadId فيه هو معرّفه نفسه. ولـ comment.resolved حقول الموقع والنسخة نفسها، وcomment فيه id وthreadId وresolvedAt.
التحقق من التوقيع
تبدو الترويسة هكذا: t=1757155200,v1=9f86d0…. والملخّص (digest) هو HMAC-SHA256 للطابع الزمني، ثم نقطة، ثم الجسم نفسه الذي أرسلناه بالضبط. لذلك تحقّق من البايتات الخام، قبل أن يعيد أي تحليل لـ JSON ترتيب أي شيء.
import { createHmac, timingSafeEqual } from "node:crypto";
function verify(rawBody, header, secret) {
const parts = Object.fromEntries(
header.split(",").map((p) => p.trim().split("=")),
);
const age = Math.abs(Date.now() / 1000 - Number(parts.t));
if (!(age < 300)) return false; // أقدم من أن يكون صادقًا
const expected = createHmac("sha256", secret)
.update(`${parts.t}.${rawBody}`)
.digest("hex");
return parts.v1.length === expected.length &&
timingSafeEqual(Buffer.from(parts.v1), Buffer.from(expected));
}الطابع الزمني جزء مما يُوقَّع، لا شيء بجانبه. وهذا ما يمنع أحدًا من أن يعيد اليوم، أمام ساعتك، إرسال تسليم التقطه الأسبوع الماضي.
عندما يفشل التسليم
أي رد ليس 2xx نعيد المحاولة معه خمس مرات، بفاصل يتسع كل مرة. ونعيد المحاولة مع 4xx أيضًا: فمن جهتنا يبدو الرفض الدائم والمسار المضبوط خطأً متطابقين، والتوقف عند أول 404 سيضيّع الأحداث بينما تصلح مسارًا.
لا نقرأ جسم ردّك. ومهلة الانتظار عشر ثوانٍ: أجب أولًا، واعمل بعد ذلك.

