Webhooks

קבלו אירועים מ־Zestt בשרת שלכם כבקשות HTTPS חתומות. צרו endpoint, אמתו כל חתימה וטפלו בניסיונות חוזרים ובמשלוחים כפולים.

יצירת endpoint

לפני שמתחילים, צריך:

  • מפתח API עם ה־scope webhooks:manage.
  • חבילה שכוללת webhooks. ודאו שהשדה features בתשובה של GET /me כולל את webhooks. מספר ה־endpoints שאפשר ליצור תלוי בחבילה.
  • כתובת בשרת שלכם שמתחילה ב־https://. כתובת http:// נדחית.

שלחו POST /webhooks עם הכתובת ועם האירועים שה־endpoint יקבל:

cURL
curl "https://sandbox-api.zester.co.il/v2https://api.zester.co.il/v2/webhooks" \
  -H "Authorization: Bearer zk_test_XXXXXXXX_…zk_live_XXXXXXXX_…" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://erp.example.com/zestt/webhooks",
    "events": ["order.created", "order.updated", "document.received"],
    "description": "ERP פריוריטי: הזמנות ומסמכים"
  }'
201 Created
{
  "id": "wh_01J9KR2B7N",
  "url": "https://erp.example.com/zestt/webhooks",
  "events": ["order.created", "order.updated", "document.received"],
  "status": "active",
  "description": "ERP פריוריטי: הזמנות ומסמכים",
  "last_delivery_at": null,
  "consecutive_failures": 0,
  "created_at": "2026-09-11T08:00:00+03:00",
  "secret": "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"
}

ה־secret מופיע רק בתשובה הזו. שמרו אותו בכספת הסודות שלכם, ליד מפתח ה־API. בלעדיו אי אפשר לאמת חתימות. אם הוא אבד או נחשף, צרו endpoint חדש ומחקו את הישן.

שליחת אירוע בדיקה

POST /webhooks/{webhook_id}/test שולחת ל־endpoint אירוע ping, חתום כמו כל אירוע אחר, ומחזירה את ניסיון המשלוח עם סטטוס ה־HTTP שה־endpoint שלכם החזיר. סטטוס 2xx מאשר שהכתובת נגישה ושבדיקת החתימה שלכם מקבלת את האירוע.

cURL
curl -X POST "https://sandbox-api.zester.co.il/v2https://api.zester.co.il/v2/webhooks/wh_01J9KR2B7N/test" \
  -H "Authorization: Bearer zk_test_XXXXXXXX_…zk_live_XXXXXXXX_…"

מבנה הבקשה

Zestt שולח כל אירוע כבקשת POST עם גוף JSON ועם כותרת חתימה:

כותרות
Content-Type: application/json
Zestt-Signature: t=1789103702,v1=…

הגוף תמיד בנוי מאותה מעטפת:

שדהתיאור
idמזהה ייחודי של האירוע. לפיו מזהים משלוח כפול.
typeסוג האירוע, מתוך קטלוג האירועים.
api_versionגרסת ה־API שקובעת את המבנה של data. כרגע "2".
occurred_atמתי האירוע קרה, כתאריך ושעה עם הפרש מ־UTC.
account_idמזהה החשבון שהאירוע שייך לו.
dataהמשאב כפי שהיה ברגע האירוע. תמיד כולל את object (סוג המשאב) ואת id.

אירוע order.created על הזמנה חדשה שנשלחה לספק:

order.created
{
  "id": "evt_01J9KQ7X4M9C",
  "type": "order.created",
  "api_version": "2",
  "occurred_at": "2026-09-11T08:15:00+03:00",
  "account_id": "72223",
  "data": {
    "object": "order",
    "id": "2335619",
    "number": "71148-80",
    "status": "pending_approval",
    "buyer": { "id": "71148", "name": "שניצי קפה", "customer_number": "777077070" },
    "supplier": { "id": "72223", "name": "מאפייה אחת עשרה" },
    "branch": { "id": "71148-1", "name": "שניצי קפה – ראשי" },
    "sent_at": "2026-09-11T08:15:00+03:00",
    "delivery_date": "2026-09-14",
    "currency": "ILS",
    "totals": { "before_vat": "138.00", "vat": "24.84", "with_vat": "162.84" },
    "lines": [
      { "id": "l1", "sku": "300", "name": "בייבי ג'בטה לבן", "quantity": "1", "unit": "carton", "unit_price": "80.00", "total": "80.00" },
      { "id": "l2", "sku": "412", "name": "לחמניית חיטה מלאה", "quantity": "2", "unit": "carton", "unit_price": "29.00", "total": "58.00" }
    ],
    "notes": null,
    "created_at": "2026-09-11T08:15:00+03:00",
    "updated_at": "2026-09-11T08:15:00+03:00"
  }
}

data הוא תמונת מצב, ולא בהכרח המצב הנוכחי של המשאב. לפני פעולה שתלויה במצב העדכני, למשל אישור הזמנה, קראו את המשאב מחדש עם GET /supplier/orders/{order_id}.

סוגי אירועים ושדות חדשים יכולים להתווסף. התעלמו מסוג או משדה שאתם לא מכירים, והחזירו 2xx כרגיל.

אימות חתימות

כל אחד יכול לשלוח בקשה לכתובת שלכם. החתימה מוכיחה שהבקשה נשלחה מ־Zestt ושהגוף לא שונה בדרך. אמתו אותה בכל בקשה, לפני שאתם משתמשים בתוכן.

בכותרת Zestt-Signature יש שני ערכים: t, זמן החתימה כ־Unix timestamp בשניות, ו־v1, חתימת HMAC-SHA256 בקידוד hex.

  1. קראו את גוף הבקשה כ־bytes גולמיים, בדיוק כפי שהגיע, לפני כל פענוח JSON.
  2. פצלו את הכותרת לפי ,, פצלו כל חלק לפי ה־= הראשון, וקחו את t ואת v1.
  3. חשבו HMAC-SHA256 על {t}.{raw_body} (הערך של t, נקודה והגוף הגולמי), עם ה־secret של ה־endpoint כמפתח. קודדו את התוצאה ל־hex.
  4. השוו את התוצאה ל־v1 בהשוואה בזמן קבוע (constant-time), לא ב־==.
  5. דחו את הבקשה אם t רחוק מהשעון שלכם ביותר מ־5 דקות. כך בקשה שהוקלטה לא תתקבל שוב.
  6. פענחו את ה־JSON וטפלו באירוע רק אחרי שכל הבדיקות עברו.

כל אחת מהדוגמאות הבאות היא שרת מלא שאפשר להריץ, שקורא את ה־secret ממשתנה הסביבה ZESTT_WEBHOOK_SECRET.

// npm install express
const crypto = require("node:crypto");
const express = require("express");

const SECRET = process.env.ZESTT_WEBHOOK_SECRET;
if (!SECRET) throw new Error("ZESTT_WEBHOOK_SECRET is not set");
const TOLERANCE_SECONDS = 300;

function verifyZesttSignature(rawBody, header, secret) {
  if (typeof header !== "string") return false;

  let timestamp = null;
  const signatures = [];
  for (const part of header.split(",")) {
    const i = part.indexOf("=");
    if (i === -1) continue;
    const key = part.slice(0, i).trim();
    const value = part.slice(i + 1).trim();
    if (key === "t") timestamp = value;
    else if (key === "v1") signatures.push(value);
  }
  if (!timestamp || !/^\d+$/.test(timestamp) || signatures.length === 0) return false;

  const age = Math.abs(Math.floor(Date.now() / 1000) - Number(timestamp));
  if (age > TOLERANCE_SECONDS) return false;

  const expected = crypto
    .createHmac("sha256", secret)
    .update(timestamp + ".")
    .update(rawBody)
    .digest();

  return signatures.some(
    (sig) => /^[0-9a-f]{64}$/i.test(sig) && crypto.timingSafeEqual(expected, Buffer.from(sig, "hex"))
  );
}

const app = express();

// express.raw() keeps the body as a Buffer, byte for byte.
// Do not put express.json() in front of this route.
app.post("/zestt/webhooks", express.raw({ type: "*/*" }), (req, res) => {
  const header = req.get("Zestt-Signature");
  if (!Buffer.isBuffer(req.body) || !verifyZesttSignature(req.body, header, SECRET)) {
    return res.status(400).send("invalid signature");
  }

  const event = JSON.parse(req.body.toString("utf8"));
  res.status(200).send("ok");

  // Respond first, then process. In production, write the event to a queue or a table.
  setImmediate(() => handleEvent(event));
});

function handleEvent(event) {
  console.log(event.id, event.type, event.data.id);
}

app.listen(3000);

הסיבה הנפוצה לחתימה שלא תואמתה־framework פענח את ה־JSON לפני שהקוד שלכם רץ, והחתימה חושבה על JSON שנבנה מחדש. רווח, סדר השדות או קידוד העברית (\u05e9 במקום ש) משנים את ה־bytes, והבדיקה נכשלת. חשבו את החתימה תמיד על הגוף הגולמי.

אם האימות נכשל, החזירו 400 ואל תעבדו את הבקשה. כל תשובה שאינה 2xx נחשבת משלוח שנכשל, ולכן Zestt שולח שוב אירוע אמיתי שנדחה בגלל באג בבדיקה שלכם. ראו טיפול בניסיונות חוזרים ובכפילויות.

בדיקת חתימה

הדביקו משלוח אמיתי כדי לבדוק את החתימה שלו, או חתמו על payload לבדיקה וקבלו פקודת cURL ששולחת אותו ל־endpoint שלכם.

הכל מחושב בדפדפן. ה־secret והגוף לא נשלחים לשום מקום.

טיפול בניסיונות חוזרים ובכפילויות

תשובה מהירה

החזירו 2xx מיד אחרי שאימתתם את החתימה ושמרתם את האירוע, ועבדו אותו ברקע. תשובה שמחכה לעיבוד ארוך ב־ERP עלולה להיחשב משלוח שנכשל, והאירוע יישלח שוב. בנו את ה־handler כך:

  1. אמתו את החתימה. אם האימות נכשל, החזירו 400.
  2. הכניסו את id של האירוע לטבלה עם אילוץ ייחודיות (unique). אם ה־id כבר קיים, האירוע כבר התקבל: החזירו 200 וסיימו.
  3. הכניסו את האירוע לתור והחזירו 200.
  4. טפלו באירוע מהתור בתהליך ברקע. אם התהליך צריך את המצב העדכני, הוא קורא את המשאב בבקשת GET.

ניסיונות חוזרים והשבתה

  • כל תשובה שאינה 2xx נחשבת משלוח שנכשל. Zestt שולח אותו שוב במרווחים שהולכים וגדלים (exponential backoff), עד 8 ניסיונות לאורך כ־24 שעות.
  • אחרי 24 שעות של כישלון רצוף, Zestt משבית את ה־endpoint: ה־status שלו הופך ל־disabled, ו־Zestt שולח אירוע webhook.disabled ומייל.

אחרי שתיקנתם את הבעיה, החזירו את ה־status של ה־endpoint ל־active:

cURL
curl -X PATCH "https://sandbox-api.zester.co.il/v2https://api.zester.co.il/v2/webhooks/wh_01J9KR2B7N" \
  -H "Authorization: Bearer zk_test_XXXXXXXX_…zk_live_XXXXXXXX_…" \
  -H "Content-Type: application/json" \
  -d '{ "status": "active" }'

אחר כך שלחו שוב את המשלוחים שנכשלו. מצאו אותם עם GET /webhooks/{webhook_id}/deliveries ו־status=failed, ושלחו כל אחד מהם שוב עם POST /webhooks/{webhook_id}/deliveries/{delivery_id}/redeliver.

סדר וכפילויות

  • הסדר לא מובטח. order.updated יכול להגיע לפני order.created של אותה הזמנה. אל תסיקו מצב מסדר ההגעה. השוו את occurred_at, או קראו את המשאב.
  • אותו אירוע יכול להגיע יותר מפעם אחת, למשל כשהתשובה שלכם לא הגיעה ל־Zestt. שמרו את id של כל אירוע, ודלגו על אירוע שה־id שלו כבר שמור אצלכם.

כתובות IP

Zestt שולח webhooks מטווח IP קבוע ומפורסם, ואפשר לאשר אותו בחומת האש. רשימת IP מורשים לא מחליפה את החתימה. אמתו את החתימה בכל בקשה, גם כשהיא מגיעה מכתובת מוכרת.

קטלוג האירועים

בחרו בשדה events של ה־endpoint אילו אירועים הוא יקבל. פיד האירועים משתמש באותו קטלוג. אירועים עם קישור מתוארים במלואם ב־API Reference.

ספקים

אירועמתי הוא נשלח
order.createdקניין שלח לכם הזמנה חדשה. ההזמנה בסטטוס pending_approval.
order.updatedהשורות, התאריכים או הסטטוס של הזמנה השתנו, כולל ביטול אחרי אישור (cancelled_after_approval).
order.cancelledהזמנה בוטלה.
document.receivedהקניין קלט מסמך ששלחתם. data.differences[] מפרט את ההבדלים בין מה ששלחתם למה שנקלט.
document.disputedהקניין חלק על מסמך ששלחתם.
document.approved_for_exportהקניין אישר מסמך ששלחתם לייצוא להנהלת החשבונות.
buyer.linkedקניין חדש קושר לחשבון שלכם.
price_list.assignedמחירון שויך לקניין.

רשתות וקניינים

אירועמתי הוא נשלח
order.status_changedהסטטוס של הזמנה השתנה, למשל כשהספק אישר או דחה אותה.
document.createdנוסף מסמך רכש: תעודת משלוח, חשבונית, זיכוי או חשבונית מרכזת.
document.updatedמסמך רכש השתנה.
expense.createdנוסף מסמך הוצאה.
expense.approvedמסמך הוצאה אושר.
journal_lines.approved_for_exportשורות פקודת יומן אושרו לייצוא ומוכנות למשיכה ל־ERP שלכם.
inventory_count.completedספירת מלאי הושלמה.
supplier.updatedכרטיס ספק השתנה.
card_transaction.importedיובאו תנועות כרטיס אשראי.

כל החשבונות

אירועמתי הוא נשלח
api_client.suspendedאינטגרציה בחשבון הושעתה, למשל אחרי מעבר לחבילה שלא כוללת גישה ל־API.
webhook.disabledendpoint הושבת אחרי 24 שעות של כישלון רצוף.
pingקראתם ל־POST /webhooks/{webhook_id}/test כדי לשלוח אירוע בדיקה.