Crimbo.AI

תיעוד API

שליחת תבניות וואטסאפ ללקוחות שלכם, הורדת קבצים מהטבלאות שלכם, וקבלת Webhook חתום בכל פעם שמשהו משתנה בעסק. אימות באמצעות מפתח API שיוצרים בהגדרות העסק.

כתובת בסיס

כל בקשות ה-API נשלחות לכתובת ה-Gateway:

https://crimbo-gateway-833321703837.me-west1.run.app

אימות

כל בקשה חייבת לכלול את מפתח ה-API שלכם בכותרת X-API-Key. המפתחות מונפקים מעמוד הגדרות העסק ומוגבלים לעסק שלכם.

X-API-Key: crm_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

אין לכם מפתח? פתחו הגדרות עסק ← גישת API. ערך המפתח בפורמט טקסט מופיע פעם אחת בלבד ביצירה - העתיקו אותו אז, או בטלו וצרו חדש.

הרשאות (Scopes)

לכל מפתח יש הרשאה אחת או יותר שקובעות מה מותר לו לעשות. ההרשאות נבחרות ביצירת המפתח ולא ניתן לשנות אותן אחר כך - כדי לשנות מה אינטגרציה רשאית לעשות, צרו מפתח חדש ובטלו את הישן.

הרשאהתיאור
messages:sendשליחת הודעות וואטסאפ: /send-template ו-/send-action. מפתח שנוצר בלי לבחור הרשאה מקבל את זו.
collections:readהורדת קבצים שנשמרו בטבלאות שלכם. לא יכול לשלוח שום דבר, ולכן זה המפתח הבטוח לתת למערכת שמקבלת Webhooks.
client_tags:readקבלת רשימת התגיות שלכם וקריאת התגיות של כל לקוח. קריאה בלבד.
client_tags:writeהוספה, החלפה והסרה של תגיות אצל הלקוחות שלכם. מפתח עם הרשאה זו יכול לשנות את חלוקת הלקוחות שלכם לקבוצות, ולכן תנו אותה רק למערכות שאתם סומכים עליהן.

קריאה ל-endpoint שהמפתח שלכם לא מורשה אליו מחזירה 403:

{
  "error": "This API key does not have the \"collections:read\" scope. Issue a key with that scope in Settings.",
  "code": "API_KEY_SCOPE_MISSING",
  "required_scope": "collections:read"
}

שמירה על המפתח

  • קראו ל-API רק מהשרת שלכם. לעולם אל תשימו מפתח באתר, באפליקציה לנייד או בכל דבר אחר שרץ על המכשירים של הלקוחות שלכם - שם כל אחד יכול לקרוא אותו.
  • שמרו מפתחות במשתני סביבה או במנהל סודות, לעולם לא בקוד המקור או בניהול גרסאות, והרחיקו אותם מהלוגים.
  • תנו לכל אינטגרציה מפתח משלה עם ההרשאות שהיא צריכה בלבד, כך שתוכלו לבטל אחד בלי לשבור את האחרים.
  • אם ייתכן שמפתח דלף, בטלו אותו מיד בהגדרות העסק וצרו חדש. הביטול חל כבר מהבקשה הבאה.

מגבלות קצב

כל מפתח API יכול לבצע עד 600 בקשות בדקה, בחלון זמן נע. בנוסף, בקשות מכתובת IP אחת מוגבלות ל-1,200 בדקה בסך הכול, בכל המפתחות יחד.

חריגה מחזירה 429. המתינו כמה שניות ונסו שוב, והאריכו את ההמתנה אם ה-429 חוזר.

HTTP/1.1 429 Too Many Requests

{
  "error": "Too many requests",
  "message": "Please slow down and try again shortly."
}

שליחת תבנית וואטסאפ

POST/api/public/v1/send-template

שולח תבנית וואטסאפ מאושרת של Meta למספר טלפון. אם אין לקוח עם המספר הזה בעסק שלכם, ייווצר אוטומטית - שלחו client.name / client.email אופציונליים כדי לאכלס את הפרטים.

גוף הבקשה

שדהסוגחובהתיאור
template_namestringכןשם תבנית מאושרת של Meta (תלוי-רישיות).
languagestringכןשפת התבנית כפי שרשומה ב-Meta (לדוגמה "he", "en_US").
phone_numberstringכןמספר טלפון של הנמען. פורמט E.164 ("+972…") עדיף; מספרים ישראליים מקומיים מנורמלים אוטומטית.
parametersstring[]לאערכים מסודרים עבור {{1}}, {{2}}, … בגוף התבנית. הסדר חשוב.
header_media{ type, url, filename? }לאחובה כשבתבנית יש כותרת של תמונה, וידאו או מסמך; אחרת יש להשמיט. type הוא "image", "video" או "document" וחייב להתאים לכותרת התבנית. url חייב להיות קישור https ציבורי: וואטסאפ מורידה ממנו את הקובץ. filename אופציונלי ורלוונטי למסמכים בלבד.
button_parameters{ index, value, type? }[]לאחובה כשהקישור של כפתור מסתיים ב-{{1}}; אחרת נדרש רק כדי לקבוע payload של כפתור תשובה מהירה. index הוא מיקום הכפתור בתבנית, החל מ-0. עבור קישור, value הוא רק החלק שמחליף את {{1}}: וואטסאפ מוסיפה אותו לסוף הקישור שהתבנית מגדירה. type הוא "url" (ברירת המחדל), או "quick_reply" כדי לקבוע את ה-payload שתקבלו כשהלקוח לוחץ על הכפתור.
client.namestringלאשם תצוגה המשמש ביצירת לקוח אוטומטית. מתעלם אם כבר קיים לקוח עם הטלפון הזה.
client.emailstringלאאימייל המשמש ביצירת לקוח אוטומטית. מתעלם אם כבר קיים לקוח עם הטלפון הזה.

דוגמת בקשה

curl -X POST https://crimbo-gateway-833321703837.me-west1.run.app/api/public/v1/send-template \
  -H "X-API-Key: $CRIMBO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "template_name": "appointment_reminder",
    "language": "he",
    "phone_number": "+972501234567",
    "parameters": ["John", "10:00"],
    "client": { "name": "John Doe" }
  }'

תבניות עם כותרת של וידאו, תמונה או מסמך

וואטסאפ דוחה תבנית כזו בשגיאה 132012 אלא אם השליחה מציינת את הקובץ. שלחו header_media עם קישור https ציבורי; ה-type חייב להתאים לכותרת התבנית. תשובת 200 מעידה רק שוואטסאפ קיבלה את הבקשה: קישור שאי אפשר להשתמש בו (לא נגיש, גדול מדי, או וידאו שאינו mp4 עם וידאו H.264 ואודיו AAC) ייכשל מאוחר יותר, כשגיאה 131053 ב-webhook של סטטוס המשלוח.

curl -X POST https://crimbo-gateway-833321703837.me-west1.run.app/api/public/v1/send-template \
  -H "X-API-Key: $CRIMBO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "template_name": "product_launch",
    "language": "en_US",
    "phone_number": "+972501234567",
    "header_media": { "type": "video", "url": "https://example.com/launch.mp4" }
  }'

כפתורים עם קישור דינמי

אם הקישור של כפתור בתבנית מסתיים ב-{{1}} (למשל https://example.com/invoices/{{1}}), שלחו button_parameters עם ה-index של הכפתור ורק החלק שמחליף את {{1}}. שלחו "abc123" ולא את הכתובת המלאה: וואטסאפ מוסיפה את הערך שלכם לסוף הקישור שהתבנית מגדירה, והקישור שמתקבל לא נבדק. מעברי שורה, טאבים ורווחים חוזרים בערך הופכים לרווח יחיד.

curl -X POST https://crimbo-gateway-833321703837.me-west1.run.app/api/public/v1/send-template \
  -H "X-API-Key: $CRIMBO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "template_name": "invoice_ready",
    "language": "en_US",
    "phone_number": "+972501234567",
    "parameters": ["John"],
    "button_parameters": [{ "index": 0, "value": "abc123" }]
  }'

דוגמת תשובה (200 OK)

{
  "success": true,
  "message_id": "wamid.HBgLOTcyNTAxMjM0NTY3FQIAERgSMjA4Q0...",
  "client_id": 1234,
  "client_created": false
}

message_id הוא ה-WAMID של Meta עבור התבנית שנשלחה. client_created מציין אם הקריאה יצרה רשומת לקוח חדשה.

שליחה במבנה ה-JSON שלכם

POST/api/public/v1/send-action

השתמשו בזה במקום ב-/send-template כשהמערכת שלכם כבר שולחת מבנים משלה. אתם ממשיכים לשלוח את שמות הפעולות ושמות השדות שלכם - מקוננים איך שתרצו - ואנחנו מתרגמים אותם לתבנית שמטא מצפה לה. אותו מפתח API, אותה שליחה, אותה תשובה.

זה עובד על בסיס הגדרה חד-פעמית: השותף שלכם מגדיר בעמוד השותף אילו תבנית כל פעולה שולחת ואיזה שדה שלכם ממלא כל משתנה בתבנית. ההגדרה הזו תקפה לכל העסקים שהוא מנהל, כך שחיבור עסק נוסף לא דורש שינוי מצדכם.

גוף הבקשה

שדהסוגחובהתיאור
actionTypestringכןשם הפעולה שהשותף שלכם הגדיר. ההתאמה אינה תלוית-רישיות.
recipientstringכןמספר טלפון של הנמען. פורמט E.164 ("+972…") עדיף; מספרים ישראליים מקומיים מנורמלים אוטומטית. phone_number נתמך כשם נרדף.
languagestringלאשפת התבנית. נופל בחזרה לשפה שהוגדרה לפעולה, וחובה כשלפעולה אין שפה.
…any other fieldstring | number | booleanלאכל השאר שלכם. קננו איך שהמערכת שלכם כבר עושה - ההגדרה מצביעה על כל ערך לפי נתיב, לדוגמה customer.customerName או extraFields.invoiceUrl.

שדות כמו business.businessName או customer.customerName הם ערכים בלבד. הם ממלאים משתנים בהודעה; הם לא קובעים איזה עסק שולח או איזה לקוח מקבל. מפתח ה-API מזהה את השולח ו-recipient מזהה את הלקוח.

דוגמת בקשה

curl -X POST https://crimbo-gateway-833321703837.me-west1.run.app/api/public/v1/send-action \
  -H "X-API-Key: $CRIMBO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "actionType": "OPTIMUS_UPDATE_EVENT_TIME",
    "recipient": "0501234567",
    "language": "he",
    "customer": {
      "customerName": "יוסי כהן"
    },
    "appointment": {
      "eventDate": "15/06/26",
      "eventStartTime": "10:00"
    },
    "business": {
      "businessName": "מספרת דנה",
      "businessPhone": "031234567"
    }
  }'

פעולה אחרת, עם קישור

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

דבר אחד שחשוב לדעת אם ערך מזין קישור של כפתור: וואטסאפ מוסיפה אותו לסוף הקישור שהתבנית כבר מגדירה, כך שתבנית שמקשרת ל-https://example.com/{{1}} צריכה רק את המזהה. אתם יכולים להמשיך לשלוח כתובות מלאות אם זה מה שיש לכם - השותף שלכם מגדיר איזו התחלה להסיר, וקישור שמצביע למקום לא צפוי נדחה במקום להישלח שבור.

{
  "actionType": "OPTIMUS_INCOMING_INVOICE_NO_BUTTON",
  "recipient": "0501234567",
  "language": "he",
  "business": {
    "businessName": "מספרת דנה",
    "businessPhone": "031234567"
  },
  "extraFields": {
    "invoiceUrl": "https://example.com/invoices/abc123"
  }
}

דוגמת תשובה (200 OK)

{
  "success": true,
  "message_id": "wamid.HBgLOTcyNTAxMjM0NTY3FQIAERgSMjA4Q0...",
  "client_id": 1234,
  "client_created": false
}

כששדה חסר

שדה ממופה שחסר או ריק מכשיל את הבקשה לפני שנשלח משהו, כי וואטסאפ דוחה בכל מקרה הודעה עם משתנה ריק. כל השדות החסרים מוחזרים יחד, כל אחד עם הנתיב שבו הוא היה מצופה, כך שסבב אחד מספר לכם הכול. השותף שלכם יכול להגדיר ערך ברירת מחדל לכל שדה כדי להפוך אותו לאופציונלי.

{
  "error": "Payload is missing values for: body variable {{1}} (expects \"customer.customerName\")",
  "code": "ACTION_FIELD_MISSING",
  "missing_fields": [
    { "target": "body", "position": 1, "path": "customer.customerName" }
  ]
}

הורדת קובץ מטבלה

GET/api/public/v1/collections/:collectionId/rows/:rowId/files/:columnKey

מחזיר את הקובץ שנשמר בתא אחד בטבלה. זה ה-downloadUrl שכל תא קובץ מכיל ב-Webhook של collection_row, כך שבדרך כלל לא צריך לבנות אותו בעצמכם. דורש את ההרשאה collections:read.

התשובה היא הפניה 302 לקישור אחסון חתום שתוקפו פג אחרי 15 דקות. ה-downloadUrl עצמו לא פג לעולם, כך שבטוח לשמור אותו ולהוריד מאוחר יותר. הוסיפו ?disposition=inline כדי שהקובץ יוצג בדפדפן במקום שיורד.

# 1. Ask for the file. The key goes only to us; the answer is a redirect.
read -r STATUS SIGNED_URL < <(curl -s -o response.json -w '%{http_code} %{redirect_url}' \
  -H "X-API-Key: $CRIMBO_API_KEY" \
  https://crimbo-gateway-833321703837.me-west1.run.app/api/public/v1/collections/12/rows/4471/files/quote)
if [ "$STATUS" != "302" ]; then
  echo "No download link (HTTP $STATUS):" >&2; cat response.json >&2; exit 1
fi

# 2. Download from the signed link, without the key.
curl -f -o quote.pdf "$SIGNED_URL"

אל תתנו ללקוח ה-HTTP שלכם לעקוב אחרי ההפניה אוטומטית עם המפתח: curl -L וספריות רבות ישלחו את ה-X-API-Key שלכם גם לשרת האחסון. קראו את כותרת Location, ואז הורידו ממנה בלי המפתח. הקישור החתום עובד עבור כל מי שמחזיק בו עד שתוקפו פג, לכן אל תרשמו אותו בלוגים ואל תעבירו אותו הלאה.

תגיות ללקוחות

קריאה ושינוי של התגיות של הלקוחות שלכם - למשל כדי לסווג לקוח ממערכת ה-CRM שלכם ברגע שהוא משלם. התגיות מזוהות לפי שם, והן אותן תגיות שאתם מנהלים בדשבורד.

תגיות נוצרות בדשבורד (בעמוד הסיווגים) ולא דרך ה-API. שימוש בשם תגית שלא קיימת מחזיר שגיאה, והקריאות האלה לעולם לא יוצרות תגיות או לקוחות.

שיטהנתיבהרשאהתיאור
GET/api/public/v1/tagsclient_tags:readקבלת רשימת התגיות שהוגדרו בעסק שלכם.
GET/api/public/v1/clients/tags?phone_number=…client_tags:readקבלת התגיות שיש כרגע ללקוח מסוים.
POST/api/public/v1/clients/tagsclient_tags:writeהוספת תגיות ללקוח. תגיות שכבר יש ללקוח נשארות כמו שהן, ולכן אפשר לחזור על הקריאה בבטחה.
PUT/api/public/v1/clients/tagsclient_tags:writeהחלפת כל התגיות של הלקוח בדיוק בתגיות ששלחתם. מערך ריק מסיר את כל התגיות.
DELETE/api/public/v1/clients/tagsclient_tags:writeהסרת התגיות שברשימה מהלקוח. תגיות שאין ללקוח מתעלמים מהן. את גוף ה-JSON שולחים בבקשת ה-DELETE.

גוף הבקשה

שדהסוגחובהתיאור
phone_numberstringכןמספר הטלפון של הלקוח, מנורמל באותו אופן כמו ב-/send-template. הוא נשלח בגוף הבקשה (ב-GET - בפרמטר phone_number בשאילתה) ולעולם לא בנתיב ה-URL. בשאילתה יש לכתוב + בתחילת המספר כ-%2B.
tagsstring[]כןשמות תגיות, כפי שהם מופיעים בדשבורד. ההתאמה אינה תלויה באותיות גדולות או קטנות ומתעלמת מרווחים בקצוות; כפילויות מתעלמים מהן. כל איבר חייב להיות מחרוזת לא ריקה; עד 100 שמות בבקשה, ועד 100 תווים לכל שם. לא בשימוש ב-GET.

דוגמאות לבקשות

# Add tags (POST). PUT replaces the whole set; DELETE removes the listed tags.
curl -X POST https://crimbo-gateway-833321703837.me-west1.run.app/api/public/v1/clients/tags \
  -H "X-API-Key: $CRIMBO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "phone_number": "0501234567",
    "tags": ["VIP", "newsletter"]
  }'

# Read one client's tags (GET) and list all your tags
curl -H "X-API-Key: $CRIMBO_API_KEY" \
  "https://crimbo-gateway-833321703837.me-west1.run.app/api/public/v1/clients/tags?phone_number=%2B972501234567"
curl -H "X-API-Key: $CRIMBO_API_KEY" https://crimbo-gateway-833321703837.me-west1.run.app/api/public/v1/tags

דוגמת תשובה (200 OK)

{
  "client_id": 1234,
  "tags": [
    { "id": 7, "name": "VIP", "color": "#FF5733" },
    { "id": 12, "name": "newsletter", "color": "#3366FF" }
  ]
}

POST, PUT, DELETE ו-GET על /clients/tags מחזירים כולם את קבוצת התגיות המעודכנת של הלקוח. GET /tags מחזיר את אותם אובייקטי תגית תחת המפתח tags, עבור כל תגית שהוגדרה בעסק שלכם.

{
  "tags": [
    { "id": 7, "name": "VIP", "color": "#FF5733" },
    { "id": 12, "name": "newsletter", "color": "#3366FF" }
  ]
}

הכול או כלום

כתיבה מתבצעת במלואה או בכלל לא. אם אחד השמות ב-tags לא קיים, שום דבר לא משתנה והקריאה מחזירה 404 TAG_NOT_FOUND עם כל השמות הלא מוכרים ב-missing_tags. מספר טלפון שאין לו לקוח מחזיר 404 CLIENT_NOT_FOUND.

{
  "error": "One or more tags do not exist. Create them in the dashboard first.",
  "code": "TAG_NOT_FOUND",
  "missing_tags": ["prospect"]
}

Webhooks

Webhooks שולחים בקשת HTTP POST לכתובת שלכם כשמשהו קורה בעסק. כל Webhook עוקב אחרי סוג משאב אחד ואחרי האירועים שבחרתם עבורו.

הגדרת Webhooks נעשית בלוח הבקרה: אינטגרציות ← מפתחים ← Webhooks מותאמים. שם גם נמצאים הסוד לחתימה של כל Webhook, דוגמת payload לכל אירוע, ו-100 המשלוחים האחרונים שלו.

אירועים

אירועתיאור
task.created · task.updated · task.deletedמשימה נוצרה, השתנתה או נמחקה - על ידי סוכן ה-AI או על ידי מישהו בצוות.
calendar_event.created · calendar_event.updated · calendar_event.deletedפגישה נקבעה, הוזזה או בוטלה.
collection_row.created · collection_row.updated · collection_row.deletedשורה נוספה, השתנתה או הוסרה בטבלה היחידה שה-Webhook עוקב אחריה. התאים מגיעים תחת data.values, לפי מפתחות העמודות שלכם.
template_reply.receivedמישהו לחץ על כפתור תגובה מהירה בתבנית וואטסאפ ששלחתם. כולל את טקסט התגובה, מספר הטלפון שלו ואת ה-payload של הכפתור.
client.createdנוסף לקוח חדש: הודעה ראשונה בכל ערוץ, טופס לידים, ה-API הזה או הדשבורד. כולל clientId, fullName, phoneNumber, email, source, tags, platformUserId, isActive ו-createdAt. ייבוא מקובץ CSV לא נשלח. לעיתים רחוקות אותו לקוח עשוי להגיע פעמיים, לכן בצעו סינון כפילויות לפי data.clientId.

Payload

אם לא הגדרתם תבנית payload, כל משלוח הוא אובייקט JSON עם השדות הבאים:

שדהסוגתמיד קייםתיאור
eventstringכןסוג האירוע, לדוגמה "task.updated".
businessIdnumberכןמזהה העסק שלכם.
deliveryIdstring (UUID)כןייחודי לכל משלוח. שמרו אותו כדי להתעלם ממשלוח שכבר טיפלתם בו.
timestampstring (ISO 8601)כןמתי המשלוח נשלח, ב-UTC.
dataobjectכןהמשאב עצמו: המשימה, הפגישה, שורת הטבלה, התגובה המהירה או הלקוח החדש, כולל הלקוח שאליו הוא שייך כשיש כזה.
changesobjectלארק באירועי *.updated: כל שדה שהשתנה עם הערך לפני ואחרי.
{
  "event": "task.updated",
  "businessId": 42,
  "deliveryId": "3f1c2a9e-8b7d-4c51-9a0e-2d6f4b8e1c07",
  "timestamp": "2026-10-02T17:30:00.000Z",
  "data": {
    "taskId": 1234,
    "businessId": 42,
    "clientId": 88,
    "client": {
      "id": 88,
      "fullName": "Dana Levi",
      "phoneNumber": "+972501234567",
      "email": "dana@example.com"
    },
    "title": "Call back about the quote",
    "content": "Asked for a price on the premium package.",
    "priority": 2,
    "dueDate": "2026-10-04T09:00:00.000Z"
  },
  "changes": {
    "priority": { "before": 1, "after": 2 }
  }
}

הדוגמה מקוצרת. לוח הבקרה מציג את הדוגמה המלאה לכל אירוע, ובאירועי collection_row היא נבנית מהטבלאות שלכם.

כותרות

כל משלוח נשלח עם Content-Type: application/json, עם הכותרות המותאמות שהוספתם, ועם הכותרות הבאות:

כותרתתיאור
X-Webhook-Eventסוג האירוע. אותו ערך כמו event בגוף.
X-Webhook-Deliveryמזהה המשלוח. אותו ערך כמו deliveryId בגוף.
X-Webhook-Signaturesha256= ואחריו HMAC-SHA256 בקידוד hex של גוף הבקשה הגולמי, עם הסוד של ה-Webhook כמפתח.
User-Agentתמיד CrimboWebhooks/1.0.

אימות החתימה

כל מי שיודע את הכתובת שלכם יכול לשלוח אליה POST, לכן בדקו את החתימה לפני שאתם סומכים על משלוח. חשבו HMAC-SHA256 של גוף הבקשה הגולמי עם הסוד של ה-Webhook, קודדו ב-hex, הוסיפו בהתחלה sha256=, והשוו ל-X-Webhook-Signature בהשוואה בזמן קבוע. השתמשו בגוף בדיוק כפי שהתקבל: פענוח ה-JSON וסידורו מחדש משנים את הבתים ושוברים את ההתאמה.

חתימה תקפה מוכיחה שאנחנו שלחנו את הגוף, לא שזו הפעם הראשונה שהוא נשלח. כדי למנוע שליחה חוזרת של משלוח שיורט, דחו כל משלוח שה-timestamp שלו ישן מ-5 דקות, וזכרו כל deliveryId שכבר טיפלתם בו כדי להתעלם מחזרה. שני הערכים נמצאים בתוך הגוף החתום, כך שאי אפשר לשנות אותם.

Node.js (Express)

const crypto = require('crypto');
const express = require('express');

const app = express();
const SECRET = process.env.CRIMBO_WEBHOOK_SECRET;
if (!SECRET) throw new Error('CRIMBO_WEBHOOK_SECRET is not set');

const MAX_AGE_MS = 5 * 60 * 1000;

// express.raw keeps the body as bytes: the signature covers them exactly as sent.
app.post('/webhooks/crimbo', express.raw({ type: 'application/json', limit: '1mb' }), async (req, res) => {
  if (!Buffer.isBuffer(req.body)) return res.status(400).end(); // not JSON: not from us

  const expected = Buffer.from(
    'sha256=' + crypto.createHmac('sha256', SECRET).update(req.body).digest('hex')
  );
  const received = Buffer.from(req.get('X-Webhook-Signature') || '');
  if (received.length !== expected.length || !crypto.timingSafeEqual(received, expected)) {
    return res.status(401).end();
  }

  const event = JSON.parse(req.body);
  const ageMs = Date.now() - Date.parse(event.timestamp);
  if (!(Math.abs(ageMs) <= MAX_AGE_MS)) {
    return res.status(400).end(); // missing, too old or in the future: possibly replayed
  }
  // One atomic step, e.g. INSERT into a table with a unique deliveryId column.
  // A separate "check, then save" lets two copies through at the same moment.
  if (!(await recordDelivery(event.deliveryId))) {
    return res.status(200).end(); // duplicate: acknowledge, do nothing
  }

  await enqueue(event); // your job queue; do the real work there
  res.status(200).end(); // answer within 5 seconds
});

Python (Flask)

import hashlib, hmac, json, os
from datetime import datetime, timedelta, timezone
from flask import Flask, request, abort

app = Flask(__name__)
app.config["MAX_CONTENT_LENGTH"] = 1024 * 1024
SECRET = os.environ["CRIMBO_WEBHOOK_SECRET"].encode()
MAX_AGE = timedelta(minutes=5)

@app.post("/webhooks/crimbo")
def crimbo_webhook():
    body = request.get_data()
    expected = b"sha256=" + hmac.new(SECRET, body, hashlib.sha256).hexdigest().encode()
    received = request.headers.get("X-Webhook-Signature", "").encode()
    if not hmac.compare_digest(received, expected):
        abort(401)

    try:
        event = json.loads(body)
        sent_at = datetime.fromisoformat(event["timestamp"].replace("Z", "+00:00"))
        age = abs(datetime.now(timezone.utc) - sent_at)
    except (ValueError, KeyError, TypeError, AttributeError):
        abort(400)  # malformed, or a timestamp without a timezone
    if age > MAX_AGE:
        abort(400)  # too old or in the future: possibly replayed
    # One atomic step, e.g. INSERT into a table with a unique deliveryId column.
    if not record_delivery(event["deliveryId"]):
        return "", 200  # duplicate: acknowledge, do nothing

    enqueue(event)  # your job queue; do the real work there
    return "", 200  # answer within 5 seconds

אבטחת ה-endpoint שלכם

  • השתמשו בכתובת https://. אנחנו דוחים http רגיל, וכתובות ברשתות פרטיות, עבור Webhooks.
  • התייחסו לסוד החתימה כמו לסיסמה: שמרו אותו כמו שאתם שומרים מפתחות API. אם ייתכן שדלף, מחקו את ה-Webhook וצרו חדש כדי לקבל סוד חדש.
  • התייחסו לתוכן של data כקלט לא מהימן גם אחרי שהחתימה אומתה. שמות, טקסט של משימות וטקסט של תגובות מהירות מוקלדים על ידי הלקוחות והצוות שלכם, לכן בצעו escape לפני הצגתם ב-HTML והשתמשו בשאילתות עם פרמטרים כשאתם שומרים אותם.
  • אל תחזירו פרטי שגיאה או stack trace בתשובה. אנחנו שומרים רק את קוד הסטטוס, כך ש-401 או 400 בלבד מספיקים.

משלוח וניסיונות חוזרים

כל אירוע נשלח פעם אחת. כל תשובת 2xx בתוך 5 שניות נחשבת כמשלוח מוצלח. חריגה מהזמן, תשובה שאינה 2xx או כתובת לא זמינה נרשמות ככישלון בהיסטוריית המשלוחים, והאירוע לא נשלח שוב - לכן ענו מהר ובצעו את העבודה עצמה ברקע. גם הפניות (redirects) לא נעקבות: תשובת 3xx נחשבת כישלון, לכן רשמו את הכתובת הסופית.

אין הבטחה שהמשלוחים יגיעו לפי הסדר. השתמשו ב-timestamp כדי לדעת איזה שינוי חדש יותר, וב-deliveryId כדי לדלג על מה שכבר טיפלתם בו.

שמירת Webhook ו"שליחת אירוע בדיקה" שולחים שניהם נתונים סינתטיים המסומנים ב-data._test: true, עם אותן כותרות, תבנית וחתימה כמו אירוע אמיתי. התעלמו מהם בצד שלכם אם אינכם רוצים שורות בדיקה.

תבניות payload

כדי לשלוח ישירות לשירות אחר כמו Monday או Notion, הגדירו ל-Webhook תבנית payload: ה-JSON שאתם רוצים שיישלח, עם מצייני מקום לערכים שלנו. {{path}} מכניס ערך כמחרוזת מוגנת, לשימוש בתוך מירכאות. {{{path}}} מכניס אותו כמו שהוא, למספרים, ערכים בוליאניים ואובייקטים שלמים. הנתיבים מתחילים מ-event, timestamp, businessId, deliveryId, data ו-changes.

{
  "name": "{{data.title}}",
  "priority": {{{data.priority}}},
  "client_phone": "{{data.client.phoneNumber}}",
  "source_event": "{{event}}",
  "deliveryId": "{{deliveryId}}",
  "timestamp": "{{timestamp}}"
}

החתימה מחושבת על התבנית אחרי המילוי, כך שהאימות עובד אותו דבר עם תבנית או בלעדיה. השאירו את {{deliveryId}} ואת {{timestamp}} בתבנית, כמו בדוגמה: הכותרת X-Webhook-Delivery אינה חתומה, כך שבלעדיהם בגוף אי אפשר לבדוק שליחות חוזרות.

קודי שגיאה

שגיאות מחזירות גוף JSON עם שדות error ו-code יציבים והודעת error קריאה. סטטוס ה-HTTP משקף את הקטגוריה.

{
  "error": "Invalid or revoked API key.",
  "code": "API_KEY_INVALID"
}
סטטוסקודתיאור
401API_KEY_MISSINGלא נשלחה כותרת X-API-Key.
401API_KEY_INVALIDהמפתח לא ידוע, בוטל או פגום.
403API_KEY_SCOPE_MISSINGהמפתח תקף אך חסרה לו ההרשאה שה-endpoint דורש. התשובה מציינת אותה ב-`required_scope`.
400TEMPLATE_NAME_REQUIREDtemplate_name חסר או ריק.
400LANGUAGE_REQUIREDlanguage חסר או ריק.
400PHONE_NUMBER_REQUIREDphone_number חסר או ריק.
400PHONE_NUMBER_INVALIDלא ניתן לנרמל את מספר הטלפון לצורה תקפה.
400PARAMETERS_INVALIDparameters סופק אך אינו מערך JSON.
400HEADER_MEDIA_INVALIDheader_media אינו אובייקט, ה-type שלו אינו image, video או document, או ש-url שלו אינו קישור https תקין.
400BUTTON_PARAMETERS_INVALIDbutton_parameters אינו מערך של אובייקטים { index, value }, index מחוץ לטווח 0-9 או חוזר, value ריק, אינו מחרוזת, מספר או בוליאני, או ארוך מ-2000 תווים, או ש-type אינו "url" או "quick_reply".
403CLIENT_BLOCKEDהנמען חסום על ידי העסק שלכם.
424WA_NOT_CONNECTEDהעסק שלכם טרם השלים את חיבור הוואטסאפ.
400ACTION_TYPE_REQUIREDactionType חסר או ריק בבקשת /send-action.
400ACTION_TYPE_UNKNOWNלא קיימת הגדרה פעילה ל-actionType הזה. השותף שלכם מוסיף אותה בעמוד השותף.
400ACTION_FIELD_MISSINGשדה שהפעולה ממפה חסר או ריק בגוף הבקשה. התשובה מפרטת כל אחד מהם ב-`missing_fields`.
400ACTION_FIELD_INVALIDשדה ממופה הגיע אך לא ניתן לשלוח אותו כמו שהוא - לדוגמה ערך ארוך ממה שוואטסאפ מקבלת. התשובה מפרטת כל אחד מהם ב-`invalid_fields` יחד עם הסיבה.
400PARTNER_NOT_CONFIGUREDהעסק שמאחורי מפתח ה-API הזה אינו מנוהל על ידי שותף, ולכן אין לו הגדרת פעולות. השתמשו ב-/send-template.
400RECIPIENT_REQUIREDrecipient חסר או ריק בבקשת /send-action.
400RECIPIENT_INVALIDלא ניתן לנרמל את מספר הנמען לצורה תקפה.
400TEMPLATE_SEND_FAILEDMeta דחתה את השליחה. התשובה כוללת את קוד השגיאה של Meta (`meta_code`) ו-trace id (`meta_trace_id`) כשזמינים.
400INVALID_PATH_PARAMמזהה טבלה, מזהה שורה או מפתח עמודה בכתובת הורדת הקובץ אינם תקינים.
400NOT_A_FILE_COLUMNהעמודה בכתובת הורדת הקובץ אינה עמודת קבצים.
404ROW_NOT_FOUNDאין שורה כזו בטבלה הזו.
404CELL_EMPTYהתא הזה לא מכיל קובץ.
404FILE_UNAVAILABLEהקובץ שהתא מצביע עליו כבר לא שמור.
500FILE_LINK_FAILEDהקובץ קיים אך לא ניתן היה ליצור קישור הורדה. נסו שוב בעוד רגע.
400TAGS_INVALIDtags חסר, אינו מערך, שאחד האיברים בו אינו מחרוזת לא ריקה, או שחרג מהמגבלות: עד 100 שמות בבקשה ועד 100 תווים לכל שם.
404CLIENT_NOT_FOUNDאין בעסק שלכם לקוח עם מספר הטלפון הזה. קריאות התגיות לעולם לא יוצרות לקוחות.
404TAG_NOT_FOUNDשם תגית אחד או יותר לא קיימים בעסק שלכם. שום דבר לא שונה, והתשובה מפרטת את השמות הלא מוכרים ב-`missing_tags`.
500CLIENT_TAGS_FAILEDבקשת תגיות ללקוח נכשלה בצד שלנו. ייתכן שהשינוי הוחל וייתכן שלא. בטוח לנסות שוב, כי הוספה, החלפה והסרה הן פעולות אידמפוטנטיות.