> העתק ציבורי של מדריך המפתחים של ViewStream (לקריאה גם על ידי סוכני AI). רשימת המדריכים: https://www.viewstream.co.il/he/developers/guide/index.md · תיעוד ה-API: https://api.viewstream.co.il/docs/

# מתכונים ל-API

דוגמאות מעשיות עם `curl`. כל הדוגמאות מניחות:

```bash
export API=https://api.viewstream.co.il/v1
export VS_KEY='vs_ab12cd34_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'   # placeholder — your key
H=(-H "Authorization: Bearer $VS_KEY" -H "Content-Type: application/json")
```

המזהים ושמות המארחים בתשובות מקוצרים או בדויים. רשימות השדות לקוחות מה-handlers שמצוינים בכל מתכון;
הסכמות המלאות נמצאות במדריך העזר של ה-API בכתובת `https://api.viewstream.co.il/docs/`.

- [העלאת סרטון](#העלאת-סרטון)
- [רישום סרטון מכתובת URL או מדלי הקליטה שלכם](#רישום-סרטון-מכתובת-url-או-מדלי-הקליטה-שלכם)
- [רשימת תוכניות לצפייה חוזרת וחיפוש בהן](#רשימת-תוכניות-לצפייה-חוזרת-וחיפוש-בהן)
- [קבלת כתובת צפייה או טוקן חתום](#קבלת-כתובת-צפייה-או-טוקן-חתום)
- [יצירת קליפ](#יצירת-קליפ)
- [קריאת סטטיסטיקה וייצוא נתונים גולמיים](#קריאת-סטטיסטיקה-וייצוא-נתונים-גולמיים)
- [ניהול ניטורים](#ניהול-ניטורים)
- [Webhooks](#webhooks)
- [ניקוי מטמון (purge) וחימום מראש (pre-warm) של ה-CDN](#ניקוי-מטמון-purge-וחימום-מראש-pre-warm-של-ה-cdn)

---

## העלאת סרטון

הרשאה (scope): `assets:write` (לארגוני פלטפורמה בלבד). מקור: `startUpload`, `putUploadPart`, `completeUpload` בקובץ
assets.go (באנגלית).

ההעלאה היא העלאת S3 מרובת חלקים (multipart) ב**חלקים של 64 MiB** (67 108 864 בתים), עד 200 GB לקובץ, שצריכה
להסתיים תוך שעה אחת.

**1. התחילו את ההעלאה.**

```bash
SIZE=$(stat -c %s news.mp4)
curl -s -X POST "$API/uploads" "${H[@]}" \
  -d "{\"filename\":\"news.mp4\",\"size_bytes\":$SIZE,\"content_type\":\"video/mp4\"}"
```
```json
{"upload_id": "…", "key": "ingest/…/in/0192…/news.mp4", "part_size": 67108864,
 "parts": [{"part_number": 1, "url": "…presigned…"}, {"part_number": 2, "url": "…"}],
 "expires_at": "2026-10-06T09:15:00Z"}
```

**2. שלחו כל חלק דרך ה-API.** כתובות ה-`url` החתומות מראש (presigned) מפנות לאחסון פנימי ש**אינו נגיש מחוץ**
לרשת שלנו; השתמשו במקומן ב-`PUT /v1/uploads/{upload_id}/parts/{n}?key=<key>`. גוף הבקשה חייב להיות בדיוק
`Content-Length` בתים, ולכל היותר `part_size`. קודדו את `upload_id` ואת `key` בקידוד URL.

```bash
# UPLOAD_ID and KEY are upload_id and key from step 1
split -b 67108864 -d -a 4 news.mp4 part.
n=1; for f in part.*; do
  curl -s -X PUT "$API/uploads/$UPLOAD_ID/parts/$n?key=$(python3 -c 'import urllib.parse,sys;print(urllib.parse.quote(sys.argv[1],safe=""))' "$KEY")" \
    -H "Authorization: Bearer $VS_KEY" --data-binary @"$f"
  n=$((n+1))
done
# each answer: {"part_number": 1, "etag": "\"9b2c…\""}
```

אפשר לשלוח חלקים במקביל. חלק שנכשל מחזיר `409 conflict`; שלחו את החלק הזה שוב.

**3. השלימו את ההעלאה ורשמו את הסרטון.**

```bash
curl -s -X POST "$API/uploads/$UPLOAD_ID/complete" "${H[@]}" -d '{
  "key": "'"$KEY"'",
  "parts": [{"part_number": 1, "etag": "\"9b2c…\""}, {"part_number": 2, "etag": "\"77aa…\""}],
  "external_id": "cms-48213",
  "title": "Evening news 6 Oct",
  "publish": "manual",
  "metadata": {"description": "…"}
}'
```

התשובה היא `201` עם הסרטון (`status: "probing"`). שדות אופציונליים:

| שדה | ערכים |
|---|---|
| `external_id` | המזהה שלכם, 1–200 תווים של אותיות, ספרות ו-`. _ : / -`; ייחודי בכל ארגון (חזרה על מזהה מחזירה `409`) |
| `title` | עד 500 תווים |
| `ladder` | שם סולם הקידוד; ברירת המחדל = סולם הקידוד שהוגדר כברירת מחדל לארגון |
| `publish` | `auto` (ברירת מחדל: הגדרת הפרסום האוטומטי של הארגון קובעת מה קורה כשהסרטון מוכן) או `manual` (נשאר טיוטה) |
| `metadata` | אובייקט JSON עד 16 KB; `metadata.ads` = `{disabled?, cues?: [seconds…]}` (לכל היותר 50 נקודות) |

**4. המתינו עד שהסרטון יהיה מוכן** — ה-Webhook `asset.ready`, או תשאול (polling) של `GET /v1/assets/{id}` עד ש-`status`
הוא `ready` (או `failed`, עם `error`). לאחר מכן פרסמו טיוטה:

```bash
curl -s -X PATCH "$API/assets/$ASSET_ID" "${H[@]}" -d '{"publish": true}'   # needs assets:publish
```

פרסום של סרטון שעדיין לא מוכן מחזיר `409`.

## רישום סרטון מכתובת URL או מדלי הקליטה שלכם

הרשאה: `assets:write`. ‏`POST /v1/assets` עם `source`:

```bash
curl -s -X POST "$API/assets" "${H[@]}" -d '{
  "source": {"kind": "url", "url": "https://media.example.com/promo.mp4"},
  "external_id": "cms-48214", "title": "Promo"
}'
```

| `source.kind` | שדה | כלל |
|---|---|---|
| `url` | `url` | http(s); הקובץ נמשך בצד השרת, ולכן כתובות פרטיות ופנימיות נדחות (הגנת SSRF) |
| `s3` | `key` | אובייקט תחת `ingest/<prefix>/in/` של הארגון שלכם |
| `upload` | — | נדחה כאן; השתמשו ב-`/v1/uploads` |

כדי לייבא ספרייה קיימת שלמה ממפת האתר של האתר שלכם, השתמשו ב-`/v1/library/imports` (יצירה → סקירת הפריטים →
`/start`; ראו את מדריך העזר של ה-API, תגית *library-imports*).

## רשימת תוכניות לצפייה חוזרת וחיפוש בהן

הרשאה: `channels:read`. מקור: livevod.go (באנגלית),
catchup_search.go (באנגלית).

קודם מצאו את מזהה הערוץ: `GET /v1/channels`.

**רשימת התוכניות** — ברירת המחדל היא 24 השעות האחרונות, מהחדש לישן; בין `from` ל-`to` לכל היותר 8 ימים, והטווח
נחתך לפי תקופת השמירה של הערוץ:

```bash
curl -s "$API/channels/$CH/catchup?from=2026-10-05T00:00:00Z&to=2026-10-06T00:00:00Z" "${H[@]}"
```
```json
{"from": "…", "to": "…", "retention_from": "…", "excluded_hidden": 3,
 "items": [{
   "id": "…", "title": "חדשות הערב", "start_at": "2026-10-05T17:00:00Z", "end_at": "2026-10-05T18:00:00Z",
   "duration_s": 3600, "airing": false, "coverage": 1, "status": "recorded",
   "play_start": "2026-10-05T17:00:42Z", "play_end": "2026-10-05T17:58:10Z",
   "bounds_status": "detected", "bounds_method": "opener", "adjusted_ms": 42000,
   "thumbnail": "https://cdn…/…jpg", "vod": null }]}
```

| שדה | משמעות |
|---|---|
| `status` | `not_recorded`, `partial` (חלק ממנה הוקלט), `recorded` (≥ 95 % מתוכנית שהסתיימה, או תוכנית בשידור שהוקלט ממנה משהו), או מצב ה-VOD: `publishing`, `published`, `failed` |
| `coverage` | החלק שהוקלט, 0–1 |
| `play_start`, `play_end` | חלון הצפייה אחרי זיהוי גבולות התוכנית ב-AI; נגנו את אלה, ולא את `start_at`/`end_at` |
| `bounds_status` | `confirmed` (עורך תיקן), `detected` (AI), `unverified`, `epg` (רק זמני לוח השידורים) |
| `excluded_hidden` | תוכניות שהושמטו בגלל כללי החרגה מצפייה חוזרת |
| `vod` | `{asset_id, status, trigger, …}` אחרי פרסום לספרייה |

**חיפוש בכל חלון השמירה** — בכותרת, בתיאור מלוח השידורים, בסיכום ה-AI (מגישים, נושאים) ובמה שנאמר (הכתוביות בעברית:
תוצאות כאלה כוללות `moments` — השנייה מתחילת התוכנית והשורה; `transcript=false` מדלג עליהן). `q` הוא 2–100 תווים,
`limit` הוא 1–50:

```bash
curl -s -G "$API/channels/$CH/catchup/search" --data-urlencode 'q=תקציב' --data-urlencode 'limit=20' "${H[@]}"
```

הפריטים זהים לאלה שלמעלה, ובנוסף `match: {field, snippet}`; `truncated: true` פירושו שהיו יותר מ-`limit` תוצאות.
חיפוש בספרייה הוא `GET /v1/assets?q=…` (2–100 תווים, אותה התאמה).

**פרסום תוכנית לספרייה** (נדרשת `assets:write`, ובנוסף `assets:publish` כשהתוצאה מתפרסמת):

```bash
curl -s -X POST "$API/channels/$CH/programmes/$PROGRAMME_ID/publish-vod" "${H[@]}" -d '{"publish": false}'
# optional: "start_at"/"end_at" (trimmed bounds, saved on the programme), "collection_id", "replace": true
```

`202`; עקבו אחרי ההתקדמות ב-`vod.status` שברשימה. תוכניות מוחרגות מחזירות `422`. לכמה תוכניות בבת אחת:
`POST /v1/channels/{id}/programmes/publish-vod`.

**ניגון תוכנית**: בנו את המניפסט של הצפייה החוזרת מ-`play_start`/`play_end` במילישניות epoch:
`https://<cdn_hostname>/m/catchup/<channel slug>/<start ms>/<end ms>/master.m3u8?c=<tenant>`. אם לערוץ יש מדיניות
צפייה שדורשת טוקנים, בקשו במקום זאת כתובת חתומה (המתכון הבא, יעד `catchup`).

## קבלת כתובת צפייה או טוקן חתום

### תוכן לא מוגן

- **VOD**: התשובה של `GET /v1/assets/{id}` לסרטון מוכן כוללת `playback {hls, dash?, poster, sprite, thumbs_vtt, download}`.
- **קליפ**: `GET /v1/clips/{id}` → `playback.hls` (`https://<cdn>/m/clips/<id>/master.m3u8?c=<tenant>`).
- **שידור חי**: `https://<cdn_hostname>/live/<tenant>/<channel slug>/master.m3u8` (את `cdn_hostname` תמצאו ב-`GET /v1/me`).

### תוכן מוגן — טוקנים מה-backend שלכם

הרשאה: `playback:sign`. מקור: `postPlaybackToken` ו-`targetPaths` בקובץ
protection.go (באנגלית); מבנה הטוקן ב-
tokens.md (באנגלית).

```bash
curl -s -X POST "$API/playback/tokens" "${H[@]}" -d '{
  "channel": "main",
  "ttl_s": 21600,
  "viewer_ip": "203.0.113.7"
}'
```
```json
{"src": "https://cdn.acme.vustream.net/t/eyJ2Ijox…/live/acme/main/master.m3u8",
 "url": "…same as src…", "token": "eyJ2Ijox…", "sid": "01J9…", "expires_at": "2026-10-06T14:00:00Z",
 "protected": true, "thumbs": "https://cdn…/t/…/rec/…/main/thumbs/"}
```

ציינו יעד אחד בדיוק:

| יעד | גוף הבקשה |
|---|---|
| ערוץ חי | `"channel": "<slug>"` |
| סרטון VOD | `"asset": "<asset id>"` |
| קליפ | `"clip": "<clip id>"` |
| חלון צפייה חוזרת | `"catchup": {"channel": "<slug>", "start": "<ms>", "end": "<ms or live>"}` |
| צפייה מההתחלה | `"startover": {"channel": "<slug>", "programme": "<programme id>"}` |

אפשרויות: `ttl_s` בין 0 ל-604 800 (0 = ברירת המחדל של המדיניות: ה-TTL לשידור חי, או אורך הסרטון ועוד התוספת
שהמדיניות מגדירה לתוכן לפי דרישה), `viewer_ip` ו-`viewer_asn` (כדי לקשור את הטוקן לרשת של הצופה, כפי שהמדיניות
דורשת), `viewer_id`. הטוקן נמצא ב**נתיב** (`/t/<token>/…`), כך שכתובות מקטעים יחסיות יורשות אותו.

- העבירו את `src` לנגן. בקשו כתובת חדשה כשהנגן מדווח `token_expired`.
- `sid` מזהה את סשן הצפייה; זיהוי דליפות ו**ביטול** (`POST /v1/security/revocations`, הרשאה
  `security:manage`) פועלים עליו.
- כדי לחתום בלי לקרוא ל-API בכל ניגון, בעלים יכולים להוריד את מפתח החתימה ולחתום ב-backend שלכם
  (קוד לדוגמה ב-Go, ב-Node וב-PHP: tokens.md (באנגלית)).

הנגן המתארח של ViewStream לא צריך את ה-backend שלכם: הוא קורא בעצמו למנפיק הציבורי
`POST /v1/playback/session {tenant, channel|asset|clip|catchup|startover, page_origin}`, שבודק את כללי המדיניות:
מפנה (referrer), מדינה, ASN, מרכז נתונים וקצב.

**עדיין לא זמין:** DRM. הצפנת AES-128 של VOD עובדת; ריבוי DRM של FairPlay/Widevine דורש ספק רישיונות ועדיין אינו זמין. AES-128 לשידור חי אינו זמין עם ה-packager הנוכחי.

## יצירת קליפ

הרשאה: `clips:write`. מקור: `createClip` בקובץ live.go (באנגלית).

קליפים נחתכים מה**הקלטה** של ערוץ לפי זמן שעון:

```bash
curl -s -X POST "$API/clips" "${H[@]}" -d '{
  "channel_id": "'"$CH"'",
  "start_at": "2026-10-06T07:12:05Z",
  "end_at":   "2026-10-06T07:14:40Z",
  "title": "Interview — budget",
  "precision": "frame"
}'
```
```json
{"id": "…", "status": "finalizing", "precision": "frame", "renditions": ["1080p", "720p", "…"],
 "playback": {"hls": "https://cdn…/m/clips/…/master.m3u8?c=acme"}, "…": "…"}
```

| שדה | כלל |
|---|---|
| `channel_id`, `start_at`, `end_at` | חובה; `end_at` אחרי `start_at`, לכל היותר 6 שעות |
| `precision` | `segment` (ברירת מחדל — ניתן לצפייה מיד, נחתך בגבולות מקטעים, `status: ready`) או `frame` (מדויק לפריים; `status: finalizing` → `final` אחרי משימת קידוד מחדש) |
| `renditions` | תת-קבוצה אופציונלית מסולם הקידוד של הערוץ; ברירת המחדל = כל איכות שהוקלטה בטווח |
| `title` | אופציונלי |

- `422` עם `field: start_at` כשלא הוקלט דבר בטווח.
- קליפים מסרטון בספרייה (`asset_id`) **עדיין לא מומשו** (`422 "asset clips arrive with M4"`).
- תוקפם של קליפים בדיוק מקטע פג יחד עם תקופת שמירת ההקלטה של הערוץ (`expires_at`).
- `publish: true` רק בודק שיש `clips:publish`; כרגע הוא לא שומר דבר (ראו
  פערים וסתירות ידועים). אירועים: `clip.ready`, ואחריו `clip.final` לקליפים מדויקים לפריים.

## קריאת סטטיסטיקה וייצוא נתונים גולמיים

הרשאה: `stats:read`. מקורות: stats.go (באנגלית),
stats_export.go (באנגלית),
stats/stats.go (באנגלית).

פרמטרי שאילתה משותפים: `from`, `to` (RFC 3339 או מילישניות epoch; ברירת המחדל היא 24 השעות האחרונות; עד שנתיים),
ומסננים `country`, `device`, `pathway`, `channel` (slug או מזהה), `asset`, `clip`, `page_host`, `live=true|false`.

```bash
# headline numbers, compared with the previous period
curl -s "$API/stats/overview?from=2026-10-05T00:00:00Z&to=2026-10-06T00:00:00Z" "${H[@]}"
# plays per hour by country
curl -s "$API/stats/timeseries?metric=plays&interval=1h&group_by=country" "${H[@]}"
# top 10 assets by watch time
curl -s "$API/stats/top?entity=assets&metric=watch_time&limit=10" "${H[@]}"
# concurrent viewers now
curl -s "$API/stats/realtime" "${H[@]}"
```

המדדים כוללים `plays`, `attempts`, `viewers`, `watch_time`, `rebuffer_ratio`, `error_rate`, `startup_avg`,
`ad_impressions`, `concurrent`, ומדדי תעבורה `bytes`, `gbps`, `requests`, `cache_hit_ratio`, `origin_bytes`.
`interval` הוא `1m`, `1h` או `1d` (ברירת מחדל: `1m` עד יום אחד, `1h` עד 90 יום, אחרת `1d`; לכל מרווח יש טווח מרבי). `/stats/top` מקבל `entity=assets|clips|channels`, ו-`limit` עד 100. נתיבים נוספים: `/stats/breakdown`, `/stats/traffic`,
`/stats/qoe`, `/stats/cdn`, `/stats/completion`, `/stats/programmes`, `/stats/sessions`, `/stats/ads`, `/stats/sites`.
נתוני הסטטיסטיקה מחזירים `403 feature_disabled` כשמאגר האנליטיקה לא מוגדר בפריסה.

### ייצוא נתונים גולמיים

דוחות: `sessions`, `player_events`, `edge_requests`, `site_events`, `programmes`. פורמטים: `csv` (UTF-8 עם BOM, נפתח
ב-Excel עם עברית) או `ndjson`.

**בהזרמה** (טווחים קטנים):

```bash
curl -s -G "$API/stats/export" "${H[@]}" -d report=sessions -d format=csv \
  -d from=2026-10-05T00:00:00Z -d to=2026-10-06T00:00:00Z -o sessions.csv -D headers.txt
```

`X-Export-Total-Rows` ו-`X-Export-Truncated` מגיעות עם הכותרות (headers); כותרת הסיום (trailer) `X-Export-Rows`
מציינת כמה שורות נכתבו. אם ההעברה נקטעת באמצע, החיבור מבוטל — התייחסו להעברה חלקית כאל כישלון.

**כקובץ** (טווחים גדולים):

```bash
curl -s -X POST "$API/stats/exports" "${H[@]}" -d '{"report": "edge_requests", "format": "ndjson",
  "from": "2026-09-06T00:00:00Z", "to": "2026-10-06T00:00:00Z"}'
# 202 {"id": "…", "status": "queued", …}
curl -s "$API/stats/exports/$EXPORT_ID" "${H[@]}"
# when "status": "ready": "download_url" (signed, valid 15 minutes), "file_name": "viewstream-…ndjson.gz"
```

מגבלות (לכל ארגון): ייצוא אחד רץ בכל רגע (`409`), 10 ייצואים בשעה ו-3 קבצים מוכנים שממתינים (`429`,
`Retry-After: 600`), לכל היותר 31 ימים לייצוא, לכל היותר 1 000 000 שורות, 20 דקות לייצוא. הקבצים נשמרים 7 ימים.
עמודות ברמת הצופה (מזהה צופה, IP מגובב, כתובת העמוד, user agent, מפנה, מחרוזות שאילתה) נכללות רק כשלמפתח יש
`stats:pii`, ורק מי שיש לו `stats:pii` יכול להוריד קובץ כזה. `"email": true` מיועד רק למשתמשי Studio מחוברים
(למפתח API אין כתובת). כל ייצוא נרשם ביומן הביקורת.

## ניהול ניטורים

צפייה: `stats:read` או `channels:read`. שינוי: `notifications:manage`. מקור:
monitors.go (באנגלית),
monitors/checks.go (באנגלית).

```bash
# the check catalogue (types, units, default thresholds and severities, presets)
curl -s "$API/monitors/checks" "${H[@]}"

# one-call basic monitor for a channel: feed down, recorder gap, startup p95, error rate
curl -s -X POST "$API/monitors/presets/basic" "${H[@]}" -d '{"channel_id": "'"$CH"'"}'

# a custom monitor
curl -s -X POST "$API/monitors" "${H[@]}" -d '{
  "name": "Main — silence",
  "target_kind": "channel", "target_id": "'"$CH"'",
  "checks": [{"type": "audio_silence", "threshold": 30, "for_s": 60, "severity": "critical"}],
  "destinations": [], "inapp": true, "notify_resolve": true, "repeat_min": 30,
  "quiet_hours": {"from": "01:00", "to": "06:00", "tz": "Asia/Jerusalem", "allow_critical": true}
}'

# maintenance: mute notifications for 2 hours (alerts are still recorded)
curl -s -X POST "$API/monitors/$MON/snooze" "${H[@]}" -d '{"minutes": 120, "reason": "encoder swap"}'
curl -s -X DELETE "$API/monitors/$MON/snooze" "${H[@]}"
```

- `target_kind`: `channel`, `asset`, `site` (עם `target_host`) או `tenant`. עד 100 ניטורים לארגון.
- בדיקה: `type`, `threshold`, `for_s` (כמה זמן התנאי צריך להתקיים), `window_s` אופציונלי, `severity`
  (`info`, `warning`, `critical`), `params` אופציונלי. בדקו בקטלוג אילו סוגים מתאימים לאיזה יעד.
- השתקה (snooze): מדקה אחת עד 7 ימים (`minutes` או `until`).
- יעדים (`/v1/alert-destinations`): `POST {kind: "email", target, name?}` — כתובת של חבר בארגון פעילה מיד, וכל
  כתובת אחרת צריכה קודם לאשר בדוא"ל. צ'אטים של Telegram מקושרים עם
  `POST /v1/alert-destinations/telegram-link`. עד 50 יעדים.
- היסטוריה: `GET /v1/alerts`, אישור קבלה עם `POST /v1/alerts/{id}/ack`. אירועי Webhook: `alert.firing`,
  `alert.resolved`. ‏`GET /v1/service-status` מציג את שכבות הפלטפורמה המשותפות.
- אי אפשר ליצור בדיקות סנכרון שפתיים (lip-sync) כל עוד הן כבויות בפלטפורמה (כרגע הן כבויות).

## Webhooks

הרשאה: `webhooks:manage`. מקור: m4.go (באנגלית),
delivery/webhook.go (באנגלית); רקע ב-
webhooks.md (באנגלית). ב-Studio: **אינטגרציות ← Webhooks**.

**יצירת נקודת קצה**:

```bash
curl -s -X POST "$API/webhooks" "${H[@]}" -d '{"url": "https://cms.example.com/vs-hook", "events": ["asset.ready", "clip.ready"]}'
```

התשובה `201` כוללת את `secret` (`whsec_…`) **פעם אחת בלבד**; ברשימות מאוחרות יותר מופיע רק `secret_hint`.
`events: ["*"]` = הכול. אירועים שאפשר להירשם אליהם (הרשימה הקובעת היא `event_types` ב-`GET /v1/webhooks`):

`asset.ready`, `asset.published`, `asset.failed`, `clip.ready`, `clip.final`, `channel.feed_changed`,
`channel.ingest_failover`, `channel.recording_status`, `channel.programme_started`, `prewarm.finished`,
`security.leak_suspected`, `security.revoked`, `policy.changed`, `epg.published`, `epg.delivery_failed`,
`alert.firing`, `alert.resolved`, `ping`.

הכתובת חייבת להיות HTTPS ציבורית; כתובות פרטיות ופנימיות נדחות ביצירה ובכל משלוח.

**מה תקבלו**: בקשת `POST` עם

```json
{"id": "<event id>", "type": "asset.ready", "at": "2026-10-06T08:00:00Z", "customer_id": "…", "data": {"…": "…"}}
```

ועם הכותרות `X-VS-Event`, `X-VS-Delivery` (קבוע בין ניסיונות חוזרים), `X-VS-Timestamp` (שניות Unix),
`X-VS-Signature: sha256=<hex HMAC-SHA256(secret, "<timestamp>.<raw body>")>`, `User-Agent: ViewStream-Webhooks/1`.

**אמתו את החתימה** על הגוף הגולמי, לפני שאתם מפענחים אותו:

```python
import hmac, hashlib, time
def verify(secret: str, body: bytes, ts: str, sig: str) -> bool:
    if abs(time.time() - int(ts)) > 300:
        return False
    want = "sha256=" + hmac.new(secret.encode(), ts.encode() + b"." + body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(want, sig)
```

```js
// Node (express.raw({type: 'application/json'}) so req.body is a Buffer)
const crypto = require('node:crypto');
function verify(secret, body, ts, sig) {
  if (Math.abs(Date.now() / 1000 - Number(ts)) > 300) return false;
  const want = 'sha256=' + crypto.createHmac('sha256', secret).update(ts + '.').update(body).digest('hex');
  return want.length === sig.length && crypto.timingSafeEqual(Buffer.from(want), Buffer.from(sig));
}
```

**ענו מהר**: כל תשובת `2xx` בתוך 10 שניות מאשרת קבלה. הפניות (redirects) לא נעקבות. אחרת המשלוח מנוסה שוב
אחרי 10 שניות, דקה, 5 דקות, 30 דקות, שעתיים ו-12 שעות; אחרי הניסיון השביעי שנכשל הוא מסומן `dead`.
המשלוחים רצים במקביל, ולכן אירועים עלולים להגיע **שלא לפי הסדר** — מיינו לפי `at`, והסירו כפילויות לפי `X-VS-Delivery`.

**תפעול**: `POST /v1/webhooks/{id}/test` (אירוע `ping` חתום), `GET /v1/webhooks/{id}/deliveries` (סטטוס `pending`,
`failed`, `delivered`, `dead`), `POST /v1/webhooks/{id}/deliveries/{delivery_id}/redeliver`, `PATCH` להפעלה/השבתה,
`DELETE`.

**Hooks נכנסים (CMS → ViewStream)**: `POST /v1/inbound-hooks {kind: "cms_publish", mapping}` מחזיר כתובת וסוד
`hksec_…`; ה-CMS שלכם חותם באותו אופן על הקריאות ל-`POST /v1/hooks/{hook_id}`, ו-ViewStream מפרסם את הסרטונים
המוכנים שהותאמו ומחמם אותם מראש (webhooks.md (באנגלית)).

**ייצוא אירועים** — סטטיסטיקות צפייה ואירועי פלטפורמה באצוות לנקודת קצה HTTPS שלכם, לדלי S3, לנושא Kafka
או ל-GA4 — הוא תכונה נפרדת תחת `/v1/event-destinations`: ראו
event-export.md (באנגלית).

## כתבות AI: כתיבה, סקירה, פרסום וקבלה

הרשאות: `assets:write` (בקשת כתבה), `ai:read` (קריאת התור), `ai:write` (עריכה, דחייה), `ai:publish` (אישור = פרסום).
מקור: aiart.go,
worker/article.go. כל תוצר AI הוא טיוטה עד שעורך מפרסם אותו; רק תוצרים
שפורסמו מגיעים לאתר ול-webhooks.

```bash
# 1. בקשת טיוטת כתבה לתוכנית שהסתיימה (בערך 5–10 דקות)
curl -s -X POST "$API/programmes/$PROGRAMME_ID/article" "${H[@]}"
curl -s "$API/programmes/$PROGRAMME_ID/article" "${H[@]}"      # {job: {status}, artifact: {…} | null}

# 2. תור הסקירה; תיקון הכותרת לפני האישור (כל שינוי נשמר כתיקון)
curl -s "$API/ai/artifacts?status=draft&kind=article" "${H[@]}"
curl -s -X PATCH "$API/ai/artifacts/$ARTIFACT_ID" "${H[@]}" -d '{"body": {"headline": "…", "standfirst": "…"}}'

# 3. אישור = פרסום (האתר מציג; artifact.published נשלח פעם אחת) — או דחייה עם סיבה
curl -s -X POST "$API/ai/artifacts/$ARTIFACT_ID/approve" "${H[@]}"
curl -s -X POST "$API/ai/artifacts/$ARTIFACT_ID/reject" "${H[@]}" -d '{"reason": "מספרים שגויים"}'
```

ה-`body` של כתבה: `headline`, `standfirst`, `summary`, `chapters[{title, description, paragraphs, start_ms, end_ms}]`,
`quotes[{text, role, start_ms, end_ms}]` (מילה במילה מהכתוביות; `role` מגיש / אורח / כתבה, אף פעם לא שם שנוחש),
`entities`, `tags`. הזמנים במילישניות מתחילת ההשמעה של התוכנית — קישור לרגע באתר הוא `<עמוד>?t=<start_ms / 1000>`.

**קבלה ב-CMS שלכם**: רשמו endpoint ל-`artifact.published` (ראו Webhooks למטה). המטען הוא
`{artifact, subject, url, moment_url, jsonld}` — `jsonld` הוא NewsArticle של schema.org עם ה-VideoObject של התוכנית
ו-`Clip` לכל פרק, מוכן להטמעה. מקבל Next.js של 20 שורות שבודק את החתימה:
examples/nextjs-webhook-receiver.

## ניקוי מטמון (purge) וחימום מראש (pre-warm) של ה-CDN

הרשאות: `delivery:write` (ניקוי מטמון), `prewarm` (חימום מראש). שתיהן עובדות גם לארגוני CDN בלבד. מקור:
m4.go (באנגלית).

```bash
# purge: urls, a prefix, or everything of an asset or clip
curl -s -X POST "$API/purge" "${H[@]}" -d '{"asset_id": "'"$ASSET_ID"'"}'
# 202 {"job_id": "…", "keys": 42, "prefix": "…"} — follow with GET /v1/jobs/{job_id}

# pre-warm the edges before a premiere (6 per minute per tenant)
curl -s -X POST "$API/prewarm" "${H[@]}" -d '{"asset_id": "'"$ASSET_ID"'"}'
# also "clip_id", "channel_id" or "urls": [...]; 202 → GET /v1/prewarm/{id}, webhook prewarm.finished
```
