תארו לעצמכם: חנות מקוונת מעבדת 1,000 הזמנות ביום, וכל הזמנה צריכה להיות מועברת ל-CRM, למערכת מחסן, לשירות אנליטיקה ולשתי APIs של שותפים. נקודות קצה (endpoints) של webhook המקודדות באופן סטטי בקובץ תצורה עובדות עבור MVP, אבל ככל שתגדלו, תיתקלו במספר הולך וגדל של נמענים ובצורך לאפשר ללקוחות לנהל את המנויים בעצמם. אנו בונים REST API לניהול מנויי webhook הפותר בעיות אלו. הניסיון שלנו: 5+ שנים ולמעלה מ-50 פרויקטים עם אינטגרציות webhook, המבטיחים זמינות של 99.9% ואספקה יציבה של עד 10,000 אירועים בשנייה. תמיכה לאחר ההשקה כלולה. הלקוחות שלנו חוסכים בדרך כלל 3,000–5,000 דולר בשנה על תחזוקה בהשוואה לתצורות סטטיות.
נניח שכבר יש לכם מערכת אירועים (למשל, על Laravel) ואתם רוצים להוסיף יכולת מנוי לשירותים חיצוניים. הגישה שלנו כוללת את השלבים הבאים: 1. תכנון סכמת מסד נתונים למנויים עם אינדוקס לחיפושים מהירים. 2. יישום נקודות קצה של REST עבור CRUD מלא, סיבוב סודות (secret rotation) ובדיקות. 3. בניית מפזר אירועים (event dispatcher) המתאים מנויים ומכניס משלוחים לתור. 4. הגדרת לוגיקת ניסיונות חוזרים, מעקב אחר כשלים והשבתה אוטומטית. 5. הפקת תיעוד OpenAPI. הכל מוכן לשימוש תוך 1-1.5 שבועות החל מ-2,500 דולר. בקשו ייעוץ כדי לדון בתרחיש שלכם.
מדוע תצורת webhook סטטית אינה מתאימה לסביבת ייצור?
נקודות קצה מקודדות (בקובצי תצורה) נשברות כאשר:
- יש יותר משניים או שלושה נמענים.
- לקוחות צריכים להוסיף/להסיר נקודות קצה ללא פריסה (deploy).
- נדרש סינון לפי סוג אירוע עבור כל מנוי.
- יש צורך לעקוב אחר הצלחת משלוח ושגיאות עבור כל נקודת קצה.
מערכת מנויים פותרת בעיות אלו באמצעות API אחיד, ומפחיתה את העומס התפעולי ב-60%.
כיצד אנו מיישמים את API המנויים?
אנו משתמשים ב-Laravel ו-PostgreSQL. כל מנוי מאוחסן בטבלה:
CREATE TABLE webhook_subscriptions (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
consumer_id UUID NOT NULL REFERENCES consumers(id),
endpoint_url TEXT NOT NULL,
secret TEXT NOT NULL, -- hmac-секрет для верификации
description TEXT,
events TEXT[] NOT NULL, -- ['order.created', 'order.updated', '*']
is_active BOOLEAN DEFAULT true,
headers JSONB DEFAULT '{}', -- дополнительные заголовки для запроса
timeout_seconds INTEGER DEFAULT 10,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW(),
last_delivery_at TIMESTAMPTZ,
failure_count INTEGER DEFAULT 0, -- счётчик последовательных неудач
disabled_at TIMESTAMPTZ -- NULL = активна
);
CREATE TABLE webhook_event_types (
name TEXT PRIMARY KEY, -- 'order.created'
description TEXT,
example_payload JSONB,
is_active BOOLEAN DEFAULT true
);
התבנית CREATE TABLE webhook_subscriptions ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), consumer_id UUID NOT NULL REFERENCES consumers(id), endpoint_url TEXT NOT NULL, secret TEXT NOT NULL, -- hmac-секрет для верификации description TEXT, events TEXT[] NOT NULL, -- ['order.created', 'order.updated', '*'] is_active BOOLEAN DEFAULT true, headers JSONB DEFAULT '{}', -- дополнительные заголовки для запроса timeout_seconds INTEGER DEFAULT 10, created_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW(), last_delivery_at TIMESTAMPTZ, failure_count INTEGER DEFAULT 0, -- счётчик последовательных неудач disabled_at TIMESTAMPTZ -- NULL = активна ); CREATE TABLE webhook_event_types ( name TEXT PRIMARY KEY, -- 'order.created' description TEXT, example_payload JSONB, is_active BOOLEAN DEFAULT true ); באירועים פירושה הרשמה לכל האירועים—נוחה לניפוי באגים וללוחות מחוונים. נקודות הקצה העיקריות של ה-API:
| מתודה | נקודת קצה | תיאור |
|---|---|---|
| POST | /webhooks/subscriptions | יצירת מנוי |
| GET | /webhooks/subscriptions | רשימת מנויים עבור צרכן |
| GET | /webhooks/subscriptions/{id} | פרטי מנוי |
| PATCH | /webhooks/subscriptions/{id} | עדכון מנוי |
| DELETE | /webhooks/subscriptions/{id} | מחיקת מנוי |
| POST | /webhooks/subscriptions/{id}/test | שליחת אירוע בדיקה |
| GET | /webhooks/event-types | רשימת סוגי אירועים זמינים |
יצירת מנוי
בעת היצירה, אנו מאמתים סוגי אירועים, בודקים את נגישות נקודת הקצה ושולחים מיד אירוע בדיקה. הסוד מוצג רק בעת היצירה.
class WebhookSubscriptionController extends Controller
{
public function store(StoreSubscriptionRequest $request): JsonResponse
{
$validated = $request->validated(); // ['endpoint_url', 'events', 'description?', 'headers?']
$invalidEvents = array_diff(
array_filter($validated['events'], fn($e) => $e !== '*'),
WebhookEventType::where('is_active', true)->pluck('name')->toArray()
);
if (!empty($invalidEvents)) {
return response()->json([
'error' => 'Unknown event types',
'invalid_events' => $invalidEvents,
], 422);
}
$reachable = $this->probeEndpoint($validated['endpoint_url']);
$subscription = WebhookSubscription::create([
'consumer_id' => $request->consumer()->id,
'endpoint_url' => $validated['endpoint_url'],
'secret' => Str::random(32),
'events' => $validated['events'],
'description' => $validated['description'] ?? null,
'headers' => $validated['headers'] ?? [],
]);
SendTestWebhookJob::dispatch($subscription);
return response()->json([
'id' => $subscription->id,
'endpoint_url' => $subscription->endpoint_url,
'events' => $subscription->events,
'secret' => $subscription->secret,
'created_at' => $subscription->created_at,
'test_sent' => true,
], 201);
}
} סיבוב סודות ואימות
כדי לשנות את הסוד מבלי לאבד אירועים, אנו משתמשים בתקופת מעבר. נוצר סוד חדש, והישן נשאר תקף למשך 10 דקות.
public function rotateSecret(WebhookSubscription $subscription): JsonResponse
{
$this->authorize('update', $subscription);
$newSecret = Str::random(32);
$subscription->update([
'secret_old' => $subscription->secret,
'secret' => $newSecret,
'secret_rotated_at' => now(),
]);
return response()->json([
'secret' => $newSecret,
'valid_until' => now()->addMinutes(10)->toIso8601String(),
'note' => 'Old secret valid for 10 minutes during rotation',
]);
}האימות בודק את שני המפתחות: תחילה את החדש, ולאחר מכן את הישן אם הוא עדיין בתקופת המעבר.
השבתה אוטומטית במקרה של כשלים
אם נקודת קצה אינה נגישה, לאחר 100 כשלים רצופים המנוי מושבת אוטומטית והבעלים מקבל הודעה.
class WebhookFailureTracker {
public function recordFailure(WebhookSubscription $subscription): void {
$subscription->increment('failure_count');
if ($subscription->failure_count >= 100 && $subscription->is_active) {
$subscription->update([
'is_active' => false,
'disabled_at' => now(),
]);
Notification::send(
$subscription->consumer,
new WebhookSubscriptionDisabled($subscription)
);
}
}
public function recordSuccess(WebhookSubscription $subscription): void {
$subscription->update([
'failure_count' => 0,
'last_delivery_at' => now(),
]);
}
} שיגור אירועים
כאשר מתרחש אירוע (למשל, *), אנו מוצאים את כל המנויים הפעילים התואמים לו ויוצרים משימה למשלוח.
class WebhookDispatcher {
public function dispatch(string $eventType, array $payload): void {
$subscriptions = WebhookSubscription::where('is_active', true)
->where(function ($q) use ($eventType) {
$q->whereJsonContains('events', $eventType)
->orWhereJsonContains('events', '*');
})
->get();
foreach ($subscriptions as $subscription) {
$delivery = WebhookDelivery::create([
'subscription_id' => $subscription->id,
'event_type' => $eventType,
'payload' => $payload,
]);
SendWebhookJob::dispatch($delivery);
}
}
}
app(WebhookDispatcher::class)->dispatch('order.created', [
'id' => $order->id,
'status' => $order->status,
'total' => $order->total,
'created_at' => $order->created_at,
]);
אילו מדדים אנו עוקבים?
כדי לנטר את איכות המשלוח, אנו עוקבים אחר אינדיקטורים מרכזיים:
| מדד | ערך יעד | תיאור |
|---|---|---|
| שיעור משלוח מוצלח | > 99.9% | חלק האירועים שנמסרו בניסיון הראשון |
| זמן השהיה ממוצע | < 500 אלפיות השנייה | זמן מפרסום האירוע ועד לקבלתו |
| מספר מנויים פעילים | עד 10,000 | לכל צרכן |
| תדירות סיבוב סודות | כל 90 יום | שינוי מפתח מתוכנן |
| אירועים מעובדים בשנייה | עד 10,000 | על שרת יחיד, ניתן להרחבה אופקית |
אבטחת חתימת HMAC
חתימת HMAC היא קריטית לאבטחה מכיוון שהיא מבטיחה שהאירוע נשלח על ידי המערכת שלכם. אנו משתמשים ב-SHA-256 ודורשים אימות חתימה בצד הלקוח.
כיצד תצורה סטטית משתווה למערכת מנויים?
| מאפיין | נקודות קצה מקודדות | API מנויים |
|---|---|---|
| ניהול | באמצעות קוד/פריסה | באמצעות REST API |
| סינון אירועים | זהה לכולם | לפי מנוי |
| מדרגיות | עד 2-3 נמענים | עד 10,000 מנויים |
| סובלנות לתקלות | ללא השבתה אוטומטית | השבתה אוטומטית לאחר 100 שגיאות |
| ניטור | רישום ידני | לוח מחוונים מובנה עם התראות |
| עלות תחזוקה שנתית | ~8,000 דולר (מוערך) | ~2,000 דולר (אוטומטי) |
מה כלול בעבודה
- REST API עם לוגיקת CRUD מלאה לניהול מנויים.
- אימות סוגי אירועים ובדיקת נגישות נקודת קצה.
- סיבוב סודות עם תקופת מעבר (10 דקות).
- השבתה אוטומטית במקרה של כשלים והודעת בעלים בדוא"ל.
- אירוע בדיקה בעת יצירת מנוי.
- תיעוד בפורמט OpenAPI (Swagger).
- רישום משלוחים וניטור (לוח מחוונים של Grafana).
- שבועיים של תמיכה לאחר המסירה.
Webhook הוא מנגנון שבו שרת שולח בקשת HTTP לכתובת URL נתונה כאשר מתרחש אירוע. ויקיפדיה — ויקיפדיה
מדוע המערכת שלנו טובה יותר מפתרונות מוכנים?
בניגוד לתוספים של CMS, הקוד שלנו ממוקד בביצועים: הוא מטפל בעד 10,000 אירועים בשנייה על שרת יחיד ומתרחב אופקית באמצעות תורים. אנו משתמשים בחתימות HMAC לאבטחה. אנו מבטיחים יציבות המבוססת על 5+ שנות ניסיון בפרויקטים בעלי עומס גבוה. החיסכון בזמן האינטגרציה מגיע עד 40% הודות ל-SDKs מוכנים ללקוח. קבלו הצעה מסחרית החל מ-2,500 דולר—נדון בפרטי הפרויקט שלכם.
צרו קשר כדי להזמין פיתוח של מערכת webhook לפרויקט שלכם.







