עדכונים חתומים מהפלטפורמה
אמתו את גוף ההודעה המקורי, שמרו אירועים וטפלו בתשלומים בלי עיבוד כפול.
קבלו את העדכון בשרת שמנהל את ההזמנה
בשרת שלכם הקימו נתיב POST ציבורי ל־Daily. שמרו את גוף הבקשה המקורי כבתים לפני JSON parsing. הגדירו את הכתובת באמצעות POST /platform/webhook במפתח הראשי עם platform:manage:
{ "url": "https://merchant.daily.test/webhooks/daily" }הכתובת בדוגמה סינתטית; הפעולה דורשת כתובת HTTPS אמיתית של השרת שלכם, פורט 443, עם DNS ציבורי ו־IPv4, ללא שם משתמש, סיסמה, fragment או הפניה לכתובת אחרת. התשובה מחזירה data.url ו־data.secret; שמרו את הסוד בכספת. הוא מוחזר פעם אחת, אינו מפתח API, ומשמש כמחרוזת המקורית לחישוב HMAC — לא מפענחים אותו מ־hex.
הגדרה חוזרת מסובבת את הסוד מיד. כדי לחדש בלי לאבד הודעות, שמרו סוד קודם לתקופת מעבר מוגבלת בצד שלכם ואמתו מול הסוד החדש, ובמידת הצורך הקודם. אין ב־Daily חלון מעבר מתועד לסוד webhook; קריאות שכבר היו בדרך יכולות להגיע עם הקודם, וניסיונות מסירה חדשים משתמשים בסוד שמוגדר כעת.
אמתו חתימה לפני קריאת השדות
| כותרת | משמעות |
|---|---|
X-Daily-Timestamp | זמן שליחה בשניות Unix, כמחרוזת |
X-Daily-Signature | HMAC SHA-256 ב־hex קטן, 64 תווים, בלי קידומת |
X-Daily-Delivery-ID | UUID קבוע של מסירה; אותו מזהה גם בניסיון חוזר |
Content-Type | application/json |
חתימת ההודעה מחושבת על timestamp + '.' + rawBody. המפריד הוא נקודה אחת, וגוף ההודעה הוא בדיוק הבתים שהתקבלו. אין להשתמש ב־JSON.stringify על גוף שכבר נותח. הדוגמה הבאה דוחה הודעות ישנות או עתידיות ביותר מחמש דקות ומבצעת השוואה בזמן קבוע. סנכרנו את שעון השרת שלכם.
import { createHmac, timingSafeEqual } from 'node:crypto';
export function verifyDailyWebhook(
rawBody: Buffer,
headers: { timestamp?: string; signature?: string; deliveryId?: string },
secret: string,
nowSeconds = Math.floor(Date.now() / 1000),
): boolean {
const { timestamp, signature, deliveryId } = headers;
if (!secret || !timestamp || !/^\d{10}$/.test(timestamp)) return false;
if (!signature || !/^[a-f0-9]{64}$/.test(signature)) return false;
if (!deliveryId || !/^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(deliveryId)) return false;
if (Math.abs(nowSeconds - Number(timestamp)) > 300) return false;
const expected = createHmac('sha256', secret).update(timestamp).update('.').update(rawBody).digest();
const received = Buffer.from(signature, 'hex');
return expected.length === received.length && timingSafeEqual(expected, received);
}ב־Express או שרת דומה, הגדירו קבלת raw body לנתיב הזה לפני JSON middleware. לאחר verifyDailyWebhook=true, נתחו את ה־JSON, בדקו את מבנה האירוע ואת שיוך businessId לעסק אצלכם, ושמרו אותו בתור עמיד. החזירו 2xx רק לאחר שמירה מוצלחת; על כשל זמני בשמירה החזירו non-2xx כדי לאפשר ניסיון חוזר. על חתימה לא תקינה דחו את ההודעה ללא עדכון כספי.
גופי האירועים
אלו דוגמאות סינתטיות. השדה החיצוני id הוא מזהה האירוע, שונה ממזהה המסירה בכותרת. data.id מזהה את המשאב. אין בגוף createdAt, מספר גרסה או מקור הזמנה שלכם.
payment.updated
{
"id": "c1aaf9e0-23b0-4ef4-9cf7-b8690c4bd52a",
"type": "payment.updated",
"businessId": "0b703598-20d1-4eeb-9012-12db41ac8018",
"data": {
"id": "d578fa85-ce50-42ec-94da-939c0ea2eb22",
"status": "CAPTURED",
"amount": "125.50",
"currency": "ILS"
}
}data.id הוא מזהה בקשת התשלום שחזר מיצירת הקישור, ולא מזהה של /payments/{id}. הסכום במטבע הראשי כמחרוזת; אין הבטחה לשתי ספרות אחרי הנקודה בגוף אירוע. השוו באמצעות מספר עשרוני מדויק. המצבים הם PENDING, APPROVED, CAPTURED, FAILED, CANCELED, EXPIRED. רק CAPTURED מאפשר עדכון הזמנה כשולמה, לאחר בדיקת העסק, המיפוי, הסכום והמטבע. כסף וקבלה הם שלבים נפרדים.
integration.updated
{
"id": "7b92672c-c5b0-4ca7-8052-25307e1a6091",
"type": "integration.updated",
"businessId": "0b703598-20d1-4eeb-9012-12db41ac8018",
"data": { "id": "68215d54-6cc6-4786-a9b8-7d374a7d15d5", "status": "CONNECTED" }
}data.id מזהה חיבור עסק, לא את ספק החיבור. כדי לדעת את provider.key, קראו /integrations עם הרשאת business:read של אותו עסק. המצבים השמורים הם COMING_SOON, AVAILABLE, CONNECTED, DISCONNECTED, NEEDS_RECONNECT ו־ERROR. בדקו גם paymentReady בנתיב הסליקה הספציפי; אירוע CONNECTED לבדו אינו מאשר שהעסק יכול לגבות כסף.
document.created
{
"id": "a0d06ba0-1f7d-41e1-9b45-c8f66524a189",
"type": "document.created",
"businessId": "0b703598-20d1-4eeb-9012-12db41ac8018",
"data": { "id": "813d0b3f-ab6c-43d4-9173-8f224566ba2f", "documentType": "TRANSACTION_INVOICE" }
}קראו /documents/{id} עם documents:read לקבלת מספר, סכומים, מצב וקשרים, ו־/documents/{id}/pdf כאשר PDF זמין. ערכי documentType באירוע הם הערכים באותיות גדולות של חוזה יצירת מסמך; תגובת הקריאה הבסיסית משתמשת בערכים הציבוריים הקטנים. יצירת מסמך אינה ראיה לתשלום.
שמרו כפילויות ומצבים מאוחרים באופן בטוח
שמרו X-Daily-Delivery-ID ו־event.id עם unique constraint ובצעו שמירת אירוע ועדכון הזמנה באותה transaction או באמצעות inbox עמיד. אירוע כפול שכבר נקלט יכול לקבל 2xx בלי לבצע שוב פעולה עסקית. אירוע חדש לאותו משאב יכול להיות עדכון אחר ואינו כפילות לפי data.id בלבד.
אין הבטחת סדר, מסירה פעם אחת או אירוע לכל מצב ביניים. האירועים משקפים גרסאות שנשמרו ונאספו, והסריקה פועלת במחזוריות של כ־15 שניות, ללא התחייבות לזמן מסירה. זמן הכותרת הוא זמן השליחה ולא זמן שינוי המצב; אין להשתמש בו לגרסה עסקית. שמרו הצלחת CAPTURED שכבר אומתה, ובדקו מצב נוכחי לפני שכישלון מאוחר משנה הזמנה. על סוג אירוע חדש שמרו את ההודעה, החזירו 2xx והתריעו לצוות שלכם בלי לפרש אותה כתשלום.
Daily מנסה מסירה עד 12 פעמים, עם המתנה עולה ומרבית של יום בין ניסיונות. אין מעקב אחר redirects. retries נחתמים בזמן שליחה חדש, עם אותו מזהה מסירה. גוף אירוע עשוי להגיע לפני שהמיפוי אצלכם הושלם; שמרו אותו בתור ונסו התאמה מאוחר יותר, במקום להשליך אותו.
בדקו מסירה והשלימו מידע חסר
GET /platform/webhook/deliveries במפתח הראשי מחזיר עד 50 מסירות אחרונות. השדות: id, attempts, nextAttemptAt, deliveredAt, createdAt. deliveredAt=null אינו הצלחה; attempts>=12 בלי מסירה מוצלחת דורש בירור. הרשימה אינה כוללת את גוף האירוע, אינה מאפשרת replay ואין לה pagination.
למצב חיבור קראו /integrations; למסמך קראו /documents/{id}. לבקשת תשלום קראו את recentPayments בפרטי עסק דרך המפתח הראשי, בכפוף למגבלת עשר הרשומות. אם המידע אינו מספיק, השלימו בירור עם Daily לפני חיוב נוסף. מדריך התשלום מפרט את ההבדל בין בקשת סליקה לתשלום שנרשם במסמך.

