דילוג לתוכן
תיעוד ה־API

Webhooks

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

בקשה
POST עם גוף JSON לכתובת https שלכם, בפורט 443.
חתימה
HMAC-SHA256 לפי Standard Webhooks, עם סוד נפרד לכל כתובת.
תשובה
כל קוד 2xx תוך 15 שניות. הפניות (3xx) לא נעקבות ונחשבות כישלון.
ניסיונות
עד 8 ניסיונות במשך יותר מיממה, ואחריהם אפשר לשלוח שוב מהמסוף.

מתחילים

  1. מוסיפים כתובת

    במסוף המפתחים מוסיפים כתובת ובוחרים אירועים. עד 10 כתובות לחשבון. אוטומציה יכולה גם לפתוח כתובת בעצמה, דרך ה־API.

  2. שומרים את הסוד

    לכל כתובת סוד חתימה משלה, שמתחיל ב־whsec_. שומרים אותו במשתני הסביבה של השרת שלכם, ואפשר לחזור אליו במסוף.

  3. שולחים בדיקה

    שליחת בדיקה מגיעה עם האירוע 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.kind is reel, clip (a reel cut from a long recording), ai_video (an AI video, ready in the studio as video_id), dub (one language of a dub: dub_id and language) or avatar_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_language in GET /v1/clips/{clip_id} is only a suggestion), price it with POST /v1/clips/{clip_id}/analysis/estimate and that language, 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.kind says which: carousel, banner, ai_video or thumbnail.

  • 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.language says which.

  • dub.ready

    דיבוב מוכן לצפייה ולהורדה

    One language of a dub is voiced and ready to watch and download. data.language says which.

  • dub.failed

    דיבוב לא נוצר (בלי חיוב)

    A dub, or one of its languages, wasn't made. Nothing was charged. data.language is 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 Reely

Node, בלי ספרייה

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 לא מקבל אותה.

  1. בוחרים אירועים

    GET /v1/webhooks/events מחזיר את האירועים שפתוחים בחשבון עכשיו, ולכל אחד sample: גוף כמו של שליחה אמיתית, עם מזהים וסכומים מומצאים. כך הטריגר מכיר את השדות עוד לפני האירוע הראשון, ולא מציע אירוע שלא יגיע.

  2. פותחים כתובת

    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": "אוטומציה: ריל מוכן לתיקייה המשותפת"
    }'
  3. סוגרים כשמכבים

    DELETE /v1/webhooks/{webhook_id} מוחק את הכתובת ואת יומן השליחות שלה. תשובה 404 אומרת שהיא כבר לא קיימת, וזה בסדר: הטריגר כבוי.

פתיחת כתובת לא מקבלת Idempotency-Key, כי הסוד לא נשמר בשביל תשובה חוזרת. אם התשובה לא הגיעה, בקשה חוזרת עם אותה כתובת ואותו מפתח מקבלת 409 webhook-exists עם webhook_id. מוחקים את הכתובת הזאת ופותחים אותה מחדש.

  • כל מפתח רואה ומנהל רק את הכתובות שהוא פתח. מפתח שהחלפתם ממשיך עם הכתובות של המפתח הקודם.
  • כתובת שמפתח פתח מקבלת רק אירועים של המצב שלו. מפתח חי מקבל אירועים חיים, ומפתח בדיקה רק את מה שמפתחות בדיקה עשו: רינדורים חינמיים עם סימן מים. כך אוטומציה אמיתית לא מפרסמת רינדור של בדיקה.
  • הכללים של המסוף חלים גם כאן: https בפורט 443 לכתובת ציבורית, בבדיקה כשהכתובת נפתחת ובכל שליחה, והעצירה אחרי 5 ימים בלי שליחה שהגיעה.
  • עד 50 כתובות שמפתחות פתחו לחשבון, בנוסף ל־10 של המסוף. מעבר לזה מקבלים 422 webhook-limit-reached.
  • כשמפתח מבוטל או שתוקפו פג, הכתובות שלו נכבות מיד ולא נספרות יותר, ומקבלים על זה הודעה אחת. הן לא חוזרות לפעול: אוטומציה עם מפתח בתוקף פותחת חדשות.
  • הכתובות מופיעות במסוף, ושם אפשר לכבות או למחוק כל אחת. כתובת שכובתה חוזרת לפעול רק מהמסוף.

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

העמוד לא נטען עד הסוף.