Webhooks (קבלה)
כשמתרחשים אירועים בפרויקט SupDesk שלכם, webhooks יוצאים מוסרים בקשות HTTP POST לנקודות הקצה שהגדרתם. העמוד הזה מתעד את מבנה המטען, את מנגנון החתימה ואת התנהגות החזרה על ניסיונות, כדי שתוכלו לבנות מקבלי webhook אמינים.
מבנה המטען
כל מסירת webhook משתמשת במעטפת סטנדרטית:
{
"event": "post.created",
"timestamp": "2026-07-15T12:00:00.000Z",
"project_id": "550e8400-e29b-41d4-a716-446655440000",
"data": { ... }
}| שדה | סוג | תיאור |
|---|---|---|
event | string | האירוע שהפעיל את המסירה |
timestamp | string | חותמת זמן ISO 8601 של מועד האירוע |
project_id | string | ה-UUID של הפרויקט שבו התרחש האירוע |
data | object | מטען ייחודי לאירוע (ראו למטה) |
אירועים
post.created
נשלח פוסט משוב חדש.
{
"event": "post.created",
"data": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"title": "Add dark mode",
"type": "feature",
"status": "open",
"author_type": "end_user",
"body": "It would be great to have a dark mode option.",
"created_at": "2026-07-15T12:00:00Z"
}
}post.updated
פוסט משוב נערך.
{
"event": "post.updated",
"data": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"title": "Add dark mode (updated)",
"type": "feature",
"status": "open",
"author_type": "end_user",
"body": "Updated description.",
"created_at": "2026-07-15T12:00:00Z"
}
}post.deleted
פוסט משוב נמחק.
{
"event": "post.deleted",
"data": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"title": "Add dark mode",
"type": "feature",
"status": "open",
"author_type": "end_user",
"body": "It would be great to have a dark mode option.",
"created_at": "2026-07-15T12:00:00Z"
}
}post.status_changed
הסטטוס של פוסט השתנה.
{
"event": "post.status_changed",
"data": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"title": "Add dark mode",
"type": "feature",
"old_status": "open",
"new_status": "planned",
"author_type": "member",
"body": "We're planning this for Q3.",
"created_at": "2026-07-15T12:00:00Z"
}
}comment.created
נוספה תגובה חדשה לפוסט.
{
"event": "comment.created",
"data": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"post_id": "550e8400-e29b-41d4-a716-446655440001",
"post_title": "Add dark mode",
"author_type": "end_user",
"body": "Any update on this?",
"created_at": "2026-07-15T12:00:00Z"
}
}comment.updated
תגובה נערכה.
{
"event": "comment.updated",
"data": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"post_id": "550e8400-e29b-41d4-a716-446655440001",
"post_title": "Add dark mode",
"author_type": "end_user",
"body": "Updated comment.",
"created_at": "2026-07-15T12:00:00Z"
}
}comment.deleted
תגובה נמחקה.
{
"event": "comment.deleted",
"data": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"post_id": "550e8400-e29b-41d4-a716-446655440001",
"post_title": "Add dark mode",
"author_type": "end_user",
"body": "Any update on this?",
"created_at": "2026-07-15T12:00:00Z"
}
}message.created
נשלחה הודעה פרטית חדשה.
{
"event": "message.created",
"data": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"thread_id": "550e8400-e29b-41d4-a716-446655440001",
"thread_subject": "Help needed",
"sender": "end_user",
"body": "I can't find the settings page.",
"via": "web",
"created_at": "2026-07-15T12:00:00Z"
}
}message.updated
הודעה פרטית נערכה.
{
"event": "message.updated",
"data": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"thread_id": "550e8400-e29b-41d4-a716-446655440001",
"thread_subject": "Help needed",
"sender": "end_user",
"body": "Updated message.",
"via": "web",
"created_at": "2026-07-15T12:00:00Z"
}
}message.deleted
הודעה פרטית נמחקה.
{
"event": "message.deleted",
"data": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"thread_id": "550e8400-e29b-41d4-a716-446655440001",
"thread_subject": "Help needed",
"sender": "end_user",
"body": "I can't find the settings page.",
"via": "web",
"created_at": "2026-07-15T12:00:00Z"
}
}beta_feedback.created
בודק בטא שלח משוב על תוכנית בטא.
{
"event": "beta_feedback.created",
"data": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"program": "Acme 2.0 Beta",
"category": "bug",
"severity": "high",
"title": "Crash on export",
"body": "The app closes when I export a large file.",
"created_at": "2026-07-15T12:00:00Z"
}
}severity עשוי להיות null.
beta_feedback.deleted
פריט משוב בטא נמחק.
{
"event": "beta_feedback.deleted",
"data": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"program": "Acme 2.0 Beta",
"category": "bug",
"severity": "high",
"title": "Crash on export",
"body": "The app closes when I export a large file.",
"created_at": "2026-07-15T12:00:00Z"
}
}waitlist_signup.created
מבקר הצטרף לרשימת ההמתנה.
{
"event": "waitlist_signup.created",
"data": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"email": "jamie@example.com",
"status": "waiting",
"position": 42,
"referral_count": 0,
"referral_code": "a1b2c3",
"source": "portal",
"created_at": "2026-07-15T12:00:00Z",
"invited_at": null,
"joined_at": null
}
}position הוא null עבור רשומות invited ו-joined.
waitlist_signup.invited
רשומה ברשימת ההמתנה הוזמנה מהרשימה.
{
"event": "waitlist_signup.invited",
"data": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"email": "jamie@example.com",
"status": "invited",
"position": null,
"referral_count": 0,
"referral_code": "a1b2c3",
"source": "portal",
"created_at": "2026-07-15T12:00:00Z",
"invited_at": "2026-07-15T12:00:00Z",
"joined_at": null
}
}waitlist_signup.joined
רשומה שהוזמנה אישרה והצטרפה.
{
"event": "waitlist_signup.joined",
"data": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"email": "jamie@example.com",
"status": "joined",
"position": null,
"referral_count": 0,
"referral_code": "a1b2c3",
"source": "portal",
"created_at": "2026-07-15T12:00:00Z",
"invited_at": "2026-07-15T12:00:00Z",
"joined_at": "2026-07-15T12:05:00Z"
}
}csat_survey.completed
סקר שביעות רצון הושלם עם דירוג.
{
"event": "csat_survey.completed",
"data": {
"thread_id": "550e8400-e29b-41d4-a716-446655440000",
"rating": 5,
"comment": "Support was fast and friendly.",
"created_at": "2026-07-15T12:00:00Z"
}
}rating הוא מספר שלם בין 1 ל-5; comment עשוי להיות null.
אימות חתימה
כל מסירה כוללת כותרת X-SupDesk-Signature:
X-SupDesk-Signature: sha256=a1b2c3d4e5f6...הערך שאחרי sha256= הוא ה-HMAC-SHA256 בקידוד הקסדצימלי של גוף הבקשה הגולמי, שמחושב
באמצעות סוד החתימה של ה-webhook שלכם.
שלבי האימות
- קראו את גוף הבקשה הגולמי (אל תפענחו JSON קודם)
- חשבו
HMAC-SHA256(signing_secret, raw_body) - השוו את התקציר ההקסדצימלי לערך בכותרת החתימה
- השתמשו בהשוואה בזמן קבוע כדי למנוע התקפות תזמון
דוגמה ב-Node.js
import { createHmac, timingSafeEqual } from "crypto";
function verifySupDeskWebhook(secret, rawBody, signatureHeader) {
const expected = createHmac("sha256", secret)
.update(rawBody)
.digest("hex");
const received = signatureHeader.replace("sha256=", "");
return timingSafeEqual(Buffer.from(expected), Buffer.from(received));
}
// Express example
app.post("/webhooks/supdesk", express.raw({ type: "application/json" }), (req, res) => {
const sig = req.headers["x-supdesk-signature"];
if (!verifySupDeskWebhook(SECRET, req.body, sig)) {
return res.status(401).json({ error: "Invalid signature" });
}
const event = JSON.parse(req.body);
// Process event...
res.status(200).json({ ok: true });
});דוגמה ב-Python
import hmac
import hashlib
from flask import Flask, request, jsonify
app = Flask(__name__)
WEBHOOK_SECRET = "your-signing-secret"
def verify_signature(secret: str, body: bytes, header: str) -> bool:
expected = hmac.new(secret.encode(), body, hashlib.sha256).hexdigest()
received = header.removeprefix("sha256=")
return hmac.compare_digest(expected, received)
@app.route("/webhooks/supdesk", methods=["POST"])
def handle_webhook():
sig = request.headers.get("X-SupDesk-Signature", "")
if not verify_signature(WEBHOOK_SECRET, request.data, sig):
return jsonify(error="Invalid signature"), 401
event = request.get_json()
# Process event...
return jsonify(ok=True), 200דוגמה ב-Ruby
require "openssl"
require "sinatra"
require "json"
WEBHOOK_SECRET = "your-signing-secret"
def verify_signature(secret, body, header)
expected = OpenSSL::HMAC.hexdigest("SHA256", secret, body)
received = header.sub("sha256=", "")
Rack::Utils.secure_compare(expected, received)
end
post "/webhooks/supdesk" do
body = request.body.read
sig = request.env["HTTP_X_SUPDESK_SIGNATURE"] || ""
unless verify_signature(WEBHOOK_SECRET, body, sig)
halt 401, { error: "Invalid signature" }.to_json
end
event = JSON.parse(body)
# Process event...
status 200
{ ok: true }.to_json
endחזרה על ניסיונות
כשמסירת webhook נכשלת, SupDesk חוזרת עם השהיה מעריכית:
| ניסיון | השהיה |
|---|---|
| 1 | מיידי |
| 2 | שנייה אחת |
| 3 | 2 שניות |
מצבים שמנוסים שוב: תגובות 5xx, שגיאות רשת, פסקי זמן (10 שניות).
לא מנוסים שוב: תגובות 4xx (שגיאות לקוח מעידות על בעיית תצורה).
מענה ל-webhooks
- החזירו קוד סטטוס 2xx כדי לאשר קבלה
- השיבו בתוך 10 שניות כדי להימנע מחזרות עקב פסק זמן
- עבדו את האירוע באופן אסינכרוני אם הלוגיקה שלכם נמשכת יותר
- החזירו את אותה תגובה למסירות כפולות (אידמפוטנטיות)