Webhooks
כשמשהו קורה בחשבון, ריל מוכן, פוסט עלה או היתרה עומדת להיגמר, Reely שולח לכתובת שלכם בקשה חתומה. אין צורך לשאול כל כמה דקות מה השתנה.
- בקשה
POSTעם גוף JSON לכתובת https שלכם, בפורט 443.- חתימה
- HMAC-SHA256 לפי Standard Webhooks, עם סוד נפרד לכל כתובת.
- תשובה
- כל קוד 2xx תוך 15 שניות. הפניות (3xx) לא נעקבות ונחשבות כישלון.
- ניסיונות
- עד 8 ניסיונות במשך יותר מיממה, ואחריהם אפשר לשלוח שוב מהמסוף.
מתחילים
מוסיפים כתובת
במסוף המפתחים מוסיפים כתובת ובוחרים אירועים. עד 10 כתובות לחשבון. אוטומציה יכולה גם לפתוח כתובת בעצמה, דרך ה־API.
שומרים את הסוד
לכל כתובת סוד חתימה משלה, שמתחיל ב־
whsec_. שומרים אותו במשתני הסביבה של השרת שלכם, ואפשר לחזור אליו במסוף.שולחים בדיקה
שליחת בדיקה מגיעה עם האירוע
webhook.test, חתומה כמו כל אירוע. ביומן השליחות של הכתובת רואים מה השרת שלכם ענה.
מה מגיע
השדות type ו־data הם החוזה. title ו־url נועדו לאדם שקורא יומן, והניסוח שלהם יכול להשתנות.title הוא שם האירוע, אותו שם בכל פעם: הוא לא מצטט פוסט ולא נוקב בשם של לקוח. את הפרטים מביאים מה־API לפי המזהים שב־data. אירועים שנוצרו עם מפתח בדיקה מגיעים עם livemode: false.
video.ready
{
"id": "evt_7AT9bo_yZyzovG9ps_BOfSFI3Q",
"type": "video.ready",
"timestamp": "2026-10-22T09:30:00.000Z",
"livemode": true,
"data": {
"kind": "reel",
"render_id": "rnd_2b7Q9xWm4KpLsXeA",
"video_id": "vid_8fN3tR6yHc1DqWzE",
"brand_id": null
},
"title": "סרטון מוכן",
"url": "https://app.reely.co.il/studio/vid_8fN3tR6yHc1DqWzE"
}האירועים
מי שרוצה כל סרטון גמור נרשם ל־video.ready: השדה data.kind אומר איזה סרטון זה, ולידו המזהה שלו. אירועי אבטחה של החשבון לא נשלחים ב־Webhook.
video.readyסרטון מוכן: ריל, ריל מסרטון ארוך, סרטון AI, דיבוב או אווטר מדבר
A finished video of any kind.
data.kindisreel,clip(a reel cut from a long recording),ai_video(an AI video, ready in the studio asvideo_id),dub(one language of a dub:dub_idandlanguage) oravatar_video(a talking avatar's video:avatar_video_id), with that kind's id.render.readyהריל מוכן להורדה
A reel finished rendering.
render.failedרינדור לא הצליח (בלי חיוב)
A render failed. Nothing was charged.
clips.waitingניתוח של סרטון ארוך מחכה לאישור שפת הדיבור
A long recording's analysis, approved in Reely before its length was known, waits for its spoken language: Hebrew wasn't heard for sure, so it didn't start and nothing was charged. Ask the person which language is spoken (
detected_languagein GET /v1/clips/{clip_id} is only a suggestion), price it with POST /v1/clips/{clip_id}/analysis/estimate and thatlanguage, and start it with POST /v1/clips/{clip_id}/analysis.clips.readyהרגעים מסרטון ארוך מוכנים לבחירה
A long recording's moments are ready to choose from.
clips.failedייבוא או ניתוח של סרטון ארוך לא הצליח (בלי חיוב)
A long recording's import or analysis failed. Nothing was charged.
creative.readyקרוסלה, באנר, תמונות ממוזערות או סרטון מוכנים להורדה
An export is ready: a carousel, a banner, an AI video, or a YouTube thumbnail with one to three versions (1280×720 JPEGs).
data.kindsays which:carousel,banner,ai_videoorthumbnail.creative.failedייצוא לא הצליח (בלי חיוב)
An export failed. Nothing was charged.
dub.reviewהתרגום מוכן לבדיקה
One language of a dub is translated and waits for the person: its lines (GET /v1/dubs/{dub_id}/lines) and its price, before it is voiced.
data.languagesays which.dub.readyדיבוב מוכן לצפייה ולהורדה
One language of a dub is voiced and ready to watch and download.
data.languagesays which.dub.failedדיבוב לא נוצר (בלי חיוב)
A dub, or one of its languages, wasn't made. Nothing was charged.
data.languageis null when the video itself couldn't be heard.avatar.video_readyסרטון של אווטר מדבר מוכן
A talking avatar's video is ready.
avatar.video_failedסרטון של אווטר מדבר לא נוצר (בלי חיוב)
A talking avatar's video wasn't made. Nothing was charged.
post.publishedפוסט עלה לרשת
A post went live on a network.
post.failedפוסט לא עלה
A post didn't go out.
post.heldהמועד הגיע והלקוח עוד לא אישר
A post's time came before its client approved it.
post.dueהגיע הזמן לפרסם פוסט ידני
A post to publish by hand is due.
approval.receivedלקוח אישר או ביקש שינוי
A client approved a post or asked for a change.
comment.receivedתגובה או הודעה חדשה
A new comment or message on a connected account.
account.disconnectedחשבון ברשת התנתק
A connected social account lost its connection.
balance.lowהיתרה עומדת להיגמר
The balance is about to run out.
topup.receivedהעברה בנקאית נכנסה ליתרה
A bank transfer reached the balance.
api.budgetפרויקט API הגיע ל-80% או ל-100% מהתקציב
An API project reached 80% or 100% of its monthly budget.
אימות החתימה
כל בקשה מגיעה עם שלוש כותרות: webhook-id, webhook-timestamp בשניות מאז 1970, ו־webhook-signature. החתימה היא HMAC-SHA256 של {id}.{timestamp}.{body} במפתח שבסוד, אחרי whsec_ ובפענוח base64, ומגיעה בצורה v1,<base64>. אחרי החלפת סוד מגיעות שתי חתימות מופרדות ברווח, ומספיק שאחת מהן תתאים.
בודקים את הגוף כמו שהגיע, לפני כל פענוח, ודוחים בקשה שהשעה שלה רחוקה ביותר מחמש דקות. הספריות של Standard Webhooks עושות את כל זה, בכל שפה:
Node, עם standardwebhooks
import { Webhook } from "standardwebhooks";
const reely = new Webhook(process.env.REELY_WEBHOOK_SECRET); // whsec_…
const event = reely.verify(rawBody, headers); // throws when it isn't from ReelyNode, בלי ספרייה
import crypto from "node:crypto";
// rawBody: the body exactly as it arrived, before JSON.parse.
export function isFromReely(secret, headers, rawBody) {
const id = headers["webhook-id"];
const timestamp = headers["webhook-timestamp"];
const sentAt = Number(timestamp);
if (!id || !Number.isInteger(sentAt) || Math.abs(Date.now() / 1000 - sentAt) > 300) return false;
const key = Buffer.from(secret.replace(/^whsec_/, ""), "base64");
const expected = crypto.createHmac("sha256", key).update(`${id}.${timestamp}.${rawBody}`).digest();
return (headers["webhook-signature"] ?? "").split(" ").some((part) => {
const [version, signature] = part.split(",");
const given = Buffer.from(signature ?? "", "base64");
return version === "v1" && given.length === expected.length && crypto.timingSafeEqual(given, expected);
});
}Python
import base64, hashlib, hmac, time
# raw_body: the body's bytes exactly as they arrived.
def is_from_reely(secret: str, headers, raw_body: bytes) -> bool:
msg_id = headers.get("webhook-id", "")
timestamp = headers.get("webhook-timestamp", "0")
if not msg_id or not timestamp.isdigit() or abs(time.time() - int(timestamp)) > 300:
return False
key = base64.b64decode(secret.removeprefix("whsec_"))
digest = hmac.new(key, f"{msg_id}.{timestamp}.".encode() + raw_body, hashlib.sha256).digest()
expected = base64.b64encode(digest).decode()
return any(
hmac.compare_digest(part.split(",", 1)[1], expected)
for part in headers.get("webhook-signature", "").split(" ")
if part.startswith("v1,")
)כדי לבדוק את האימות שלכם בלי לחכות לשליחה: עם הסוד, הכותרות והגוף שכאן, החתימה שתחשבו צריכה להיות זהה לזו שב־webhook-signature. ההודעה מ־1 בינואר 2026, אז בבדיקה הזאת מדלגים על חלון חמש הדקות. בגוף יש עברית, כדי שקוד שחותם על תווים, או על JSON שנכתב מחדש, ייכשל כאן ולא מול שליחה אמיתית.
וקטור בדיקה
{
"secret": "whsec_GStfF/j2xoZDw4imE562igWX3tClPnX2lNHdL1V8oDE=",
"headers": {
"webhook-id": "evt_2bU8q0Lx3N7cV4kT9mZ1rP6wYe",
"webhook-timestamp": "1767225600",
"webhook-signature": "v1,t6EWTgeYZF+eTxESDKZ4pcLhxzt/AIJvJh4jRH513Iw="
},
"body": "{\"id\":\"evt_2bU8q0Lx3N7cV4kT9mZ1rP6wYe\",\"type\":\"video.ready\",\"timestamp\":\"2026-01-01T00:00:00.000Z\",\"livemode\":true,\"data\":{\"kind\":\"reel\",\"render_id\":\"ren_00000000-0000-4000-8000-000000000001\",\"video_id\":\"vid_00000000-0000-4000-8000-000000000002\",\"brand_id\":null},\"title\":\"סרטון מוכן\"}"
}ניסיונות חוזרים
שליחה שלא קיבלה 2xx בזמן נשלחת שוב, עם אותו מזהה ואותו גוף:
| ניסיון 2 | אחרי 5 שניות |
|---|---|
| ניסיון 3 | אחרי 5 דקות |
| ניסיון 4 | אחרי 30 דקות |
| ניסיון 5 | אחרי שעתיים |
| ניסיון 6 | אחרי 5 שעות |
| ניסיון 7 | אחרי 10 שעות |
| ניסיון 8 | אחרי 10 שעות |
כתובת שלא ענתה לאף שליחה 5 ימים ברציפות נעצרת, ומקבלים על זה הודעה. מפעילים אותה מחדש במסוף. יומן השליחות שומר 30 יום.
תשובה 410 אומרת שהכתובת כבר לא קיימת. כתובת שהוספתם במסוף נעצרת מיד, ומקבלים על זה הודעה. כתובת שאוטומציה פתחה נמחקת, יחד עם יומן השליחות שלה.
כפילויות וסדר
אותו אירוע יכול להגיע יותר מפעם אחת, למשל כשהשרת שלכם קיבל אותו אבל התשובה לא חזרה בזמן. webhook-id זהה בכל הניסיונות: שומרים אותו ומתעלמים ממה שכבר טופל. הסדר לא מובטח, אז timestamp שבגוף אומר מתי האירוע קרה.
עונים מהר ומטפלים אחר כך: מכניסים את האירוע לתור ומחזירים 200, גם כשהעבודה עצמה לוקחת זמן.
כתובות שאוטומציה פותחת
טריגר באוטומציה שאתם בונים פותח לעצמו כתובת כשמפעילים אותו, וסוגר אותה כשמכבים. הוא עושה את זה עם מפתח API שיש לו הרשאת webhooks. רק מפתחות מקבלים את ההרשאה הזאת: חיבור של עוזר AI לא מקבל אותה.
בוחרים אירועים
GET /v1/webhooks/eventsמחזיר את האירועים שפתוחים בחשבון עכשיו, ולכל אחדsample: גוף כמו של שליחה אמיתית, עם מזהים וסכומים מומצאים. כך הטריגר מכיר את השדות עוד לפני האירוע הראשון, ולא מציע אירוע שלא יגיע.פותחים כתובת
POST /v1/webhooksעםurl,eventsו־description, השם שיופיע במסוף. התשובה,201, מחזירה את ה־idואתsecret. רק בתשובה הזאת מופיע הסוד, אז שומרים אותו ומאמתים בו כל שליחה.פתיחת כתובת
curl -X POST 'https://app.reely.co.il/api/v1/webhooks' \ -H "Authorization: Bearer $REELY_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "url": "https://hooks.example.com/reely/7d3f9a", "events": [ "video.ready", "render.failed" ], "description": "אוטומציה: ריל מוכן לתיקייה המשותפת" }'סוגרים כשמכבים
DELETE /v1/webhooks/{webhook_id}מוחק את הכתובת ואת יומן השליחות שלה. תשובה404אומרת שהיא כבר לא קיימת, וזה בסדר: הטריגר כבוי.
פתיחת כתובת לא מקבלת Idempotency-Key, כי הסוד לא נשמר בשביל תשובה חוזרת. אם התשובה לא הגיעה, בקשה חוזרת עם אותה כתובת ואותו מפתח מקבלת 409 webhook-exists עם webhook_id. מוחקים את הכתובת הזאת ופותחים אותה מחדש.
- כל מפתח רואה ומנהל רק את הכתובות שהוא פתח. מפתח שהחלפתם ממשיך עם הכתובות של המפתח הקודם.
- כתובת שמפתח פתח מקבלת רק אירועים של המצב שלו. מפתח חי מקבל אירועים חיים, ומפתח בדיקה רק את מה שמפתחות בדיקה עשו: רינדורים חינמיים עם סימן מים. כך אוטומציה אמיתית לא מפרסמת רינדור של בדיקה.
- הכללים של המסוף חלים גם כאן: https בפורט 443 לכתובת ציבורית, בבדיקה כשהכתובת נפתחת ובכל שליחה, והעצירה אחרי 5 ימים בלי שליחה שהגיעה.
- עד 50 כתובות שמפתחות פתחו לחשבון, בנוסף ל־10 של המסוף. מעבר לזה מקבלים
422webhook-limit-reached. - כשמפתח מבוטל או שתוקפו פג, הכתובות שלו נכבות מיד ולא נספרות יותר, ומקבלים על זה הודעה אחת. הן לא חוזרות לפעול: אוטומציה עם מפתח בתוקף פותחת חדשות.
- הכתובות מופיעות במסוף, ושם אפשר לכבות או למחוק כל אחת. כתובת שכובתה חוזרת לפעול רק מהמסוף.
שליחה היא סימן, לא פקודה. לפני שמשהו רץ בעקבותיה, ובטח משהו שעולה כסף, מאמתים את החתימה וקוראים מה־API את הפריט לפי המזהה שב־data. כך גוף מזויף או ישן לא מפעיל כלום.