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

# מוסכמות ה-API

כללים שחלים על כל נתיב של `https://api.viewstream.co.il/v1`. כל כלל מקושר לקוד שממנו הוא נובע.

## בקשות ותשובות

- **JSON נכנס, JSON יוצא.** שלחו `Content-Type: application/json`. התשובות הן `application/json`, או
  `application/problem+json` עבור שגיאות.
- **שדות לא מוכרים נדחים.** גוף הבקשה מפוענח באופן קפדני: שדה שהנתיב לא מכיר מחזיר
  `400 validation_error` עם `detail: "invalid JSON body: json: unknown field …"`. גודל הגוף מוגבל לפי נתיב
  (בדרך כלל 4 KB – 1 MB) (`decodeJSON` ב-problem.go (באנגלית)).
- **המזהים הם UUID** (UUIDv7, ולכן הם ממוינים לפי זמן היצירה). לערוצים ולארגונים יש גם slug (`main`,
  `acme`) שמופיע בכתובות ההפצה.
- **אין שמירה במטמון.** כל תשובה של ה-API נושאת `Cache-Control: no-store`. המסמכים הציבוריים שמחוץ ל-`/v1` (ייצוא EPG,
  תצורות נגן, ה-API של Sites Delivery, מניפסטים) קובעים כותרות מטמון משלהם.
- **מזהה בקשה.** כל תשובה נושאת `X-Request-Id`. שלחו מזהה משלכם (עד 128 תווים) כדי לקשר בין לוגים; הוא
  מוחזר אליכם. גוף השגיאה חוזר עליו בשדה `request_id`. ציינו אותו בפניות לתמיכה
  (telemetry (באנגלית)).

## פורמטי זמן ואזורי זמן

| איפה | פורמט |
|---|---|
| חותמות זמן בתשובות | RFC 3339 ב-UTC, למשל `2026-10-06T08:15:00Z` |
| פרמטרי שאילתה של זמן (`from`, `to`, `at` בסטטיסטיקה, בצפייה חוזרת ובתמונות ממוזערות של הקלטה) | RFC 3339 **או** אלפיות שנייה מאז epoch (`parseInstant` ב-thumbs.go (באנגלית), `statsFilter` ב-stats.go (באנגלית)) |
| משכי זמן | השם כולל את היחידה: `duration_ms`, `duration_s`, `ttl_s`, `for_s` |
| חלונות של צפייה חוזרת / קליפ בכתובות צפייה | אלפיות שנייה מאז epoch (`/m/catchup/<channel>/<start ms>/<end ms or live>/…`) |
| ייצוא XMLTV ציבורי | הזמן המקומי של לוח השידורים, `Asia/Jerusalem` (הגדרה `GuideLocation`) |
| `epg?day=` ב-API של Sites Delivery | יום קלנדרי ב-`Asia/Jerusalem` |
| `X-VS-Timestamp` של Webhook | שניות Unix |

אם משמיטים את `from`/`to` בנתיבי הסטטיסטיקה, מקבלים את 24 השעות האחרונות. טווחי סטטיסטיקה יכולים להגיע עד שנתיים;
ייצוא גולמי עד 31 ימים (stats.go (באנגלית)).

## שגיאות (problem+json)

השגיאות בנויות לפי RFC 9457 (problem.go (באנגלית)):

```json
{
  "type": "https://viewstream.co.il/problems/validation_error",
  "title": "Validation failed",
  "status": 422,
  "detail": "invalid api key",
  "request_id": "0192a4c1-…",
  "errors": [
    {"field": "scopes", "detail": "you cannot grant a scope you do not hold: billing:read"}
  ]
}
```

התנו את הטיפול ב-`status` ובמקטע האחרון בנתיב של `type`; `title` קבוע לכל סוג, ו-`detail` נועד לקריאה אנושית ועשוי
להשתנות.

| סיומת `type` | סטטוס | סיבה אופיינית (דוגמה ל-`detail` מהקוד) |
|---|---|---|
| `invalid_credentials` | 401 | אין מפתח או שהמפתח שגוי — `"API key is unknown or invalid"`, `"API key is revoked"` |
| `insufficient_scope` | 403 | `"this route requires scope assets:write"` |
| `forbidden` | 403 | אין ארגון נוכחי (`"no current tenant; call POST /v1/me/tenant first"`) |
| `tenant_suspended` | 403 | הארגון מושעה: הצפייה ממשיכה לעבוד, שינויים חסומים (admin_p6.go (באנגלית)) |
| `feature_disabled` | 403 / 503 | ארגון CDN בלבד (`"assets, uploads, channels, clips and player config are not available to CDN-only tenants"`), או יכולת שלא הופעלה בפריסה הזו |
| `not_found` | 404 | המזהה לא קיים **או ששייך לארגון אחר** (ה-API אף פעם לא מבחין בין השניים) |
| `conflict` | 409 | `"an asset with that external_id exists for this tenant"`, `"only a ready asset can be published (status encoding)"`, ייצוא סטטיסטיקה אחר כבר רץ |
| `lock_window` | 409 | שינוי ב-EPG נוגע בשעות הנעולות של לוח השידורים |
| `misdirected_request` | 421 | כותרת `Host` שלא מוגשת כאן |
| `validation_error` | 400 / 422 | 400 = הגוף אינו JSON תקין או שיש בו שדות לא מוכרים; 422 = הערכים שגויים, `errors[]` מפרט `{field, detail}` |
| `no_recording` | 404 | מניפסטים (`/m/…`) בלבד: חלון של צפייה מההתחלה / צפייה חוזרת שלא הוקלט בו דבר, או תוכנית מוחרגת |
| `rate_limited` | 429 | ראו בהמשך; תמיד עם `Retry-After` |
| `internal_error` | 500 / 502 | תקלה אצלנו; נסו שוב מאוחר יותר וציינו את `request_id` |
| `not_ready` | 503 | רכיב שהשירות תלוי בו מושבת או לא מוגדר (`"search is not available"`, `"object storage is not configured"`) |

דוגמה — מפתח בלי ההרשאה:

```bash
curl -s -X POST https://api.viewstream.co.il/v1/clips -H "Authorization: Bearer $VS_KEY" -d '{}'
```
```json
{"type":"https://viewstream.co.il/problems/insufficient_scope","title":"The API key lacks the scope this route needs","status":403,"detail":"this route requires scope clips:write","request_id":"…"}
```

## עימוד

רשימות שיכולות לגדול בלי הגבלה משתמשות ב**עימוד עם סמן (cursor)**, מהחדש לישן:

```
GET /v1/assets?limit=50                 → {"items": [...], "next_cursor": "MDE5Mj…"}
GET /v1/assets?limit=50&cursor=MDE5Mj…  → {"items": [...], "next_cursor": null}
```

- ברירת המחדל של `limit` היא 50; המקסימום הוא 200 בסרטונים, בקליפים ובמשימות (`422` מחוץ לטווח 1–200). המקסימום של כל נתיב
  מופיע בתיעוד ה-API.
- `next_cursor: null` = העמוד האחרון. הסמנים אטומים; אל תבנו ואל תפענחו אותם.
- חלק מהרשימות הן חלונות ולא עמודים: רשימת הצפייה החוזרת מקבלת `from`/`to` (לכל היותר 8 ימים ביניהם), והחיפוש בצפייה חוזרת
  מחזיר לכל היותר 50 תוצאות, עם `truncated: true` כשיש יותר.

## הגבלות קצב

| מגבלה | ערך | מקור |
|---|---|---|
| כל בקשה מאומתת, לכל מפתח API (או לכל סשן של Studio) | 20 בקשות בשנייה, פרץ של 100 | `ratelimit.New(20, 100, …)` ב-public.go (באנגלית), ratelimit (באנגלית) |
| בדיקות מפתח שנכשלו | 30 לכל כתובת IP של לקוח / 10 לכל קידומת מפתח ב-10 דקות | middleware.go (באנגלית) |
| `POST /v1/prewarm` | 6 בדקה לכל ארגון | `PrewarmPerMinute` |
| ייצוא נתונים גולמיים של סטטיסטיקה | אחד רץ בכל פעם, 10 בשעה, 3 קבצים מוכנים בהמתנה | stats_export.go (באנגלית), statsexport/store.go (באנגלית) |
| `POST /v1/playback/session` (מנפיק ציבורי) | לכל כתובת IP של לקוח, וגם קצב ההנפקה של המדיניות עצמה | protection.go (באנגלית) |

מעבר למגבלה, ה-API עונה `429 rate_limited` עם `Retry-After` בשניות. ב-`429` וב-`503` המתינו בהשהיה
אקספוננציאלית (exponential backoff).

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

## אידמפוטנטיות וניסיונות חוזרים

**אין כותרת `Idempotency-Key`**. מה בטוח לנסות שוב:

- `GET`, `PUT` ו-`DELETE` הן אידמפוטנטיות. `DELETE /v1/api-keys/{id}` על מפתח שכבר בוטל עונה `404`.
- `POST /v1/assets` (ו-`POST /v1/uploads/{id}/complete`) עם `external_id`: ניסיון חוזר של יצירה שכבר
  הצליחה עונה `409 conflict` במקום ליצור סרטון שני. שלחו תמיד `external_id` מה-CMS שלכם.
- משלוחי Webhook נושאים `X-VS-Delivery`, שנשאר קבוע בין ניסיונות חוזרים — הסירו כפילויות לפיו.
- בקשות `POST` אחרות (קליפים, prewarm, purge, ייצוא) יוצרות אובייקט חדש בכל פעם.

## בקרת מקביליות אופטימית

רק בונה האתרים של Sites משתמש בה: `PATCH /v1/pages/{id}` דורש `If-Match: <draft_rev>` (`428` בלעדיו, `409` כשהגרסה ישנה)
(sites.go (באנגלית)).

## עבודה אסינכרונית: 202 ותשאול

עבודה ארוכה רצה כ**משימות**. הדפוס:

1. הבקשה עונה `201` (האובייקט קיים והעבודה התחילה) או `202 Accepted` (העבודה נכנסה לתור). התשובה
   מציינת במה לעקוב — מזהה משימה, אובייקט עם `status`, או `{"queued": true}`.
2. עקבו באמצעות **Webhook** (מומלץ), **זרם האירועים**, או **תשאול (polling)**.

| מה מתחיל את העבודה | תשובה | מעקב |
|---|---|---|
| `POST /v1/assets`, `POST /v1/uploads/{id}/complete` | `201` סרטון, `status: probing`, `jobs[]` | Webhook של `asset.ready` / `asset.failed`, או `GET /v1/assets/{id}` עד ש-`status` הוא `ready` או `failed` |
| `POST /v1/assets/{id}/reencode` | `202 {job, version}` | `GET /v1/jobs/{id}` |
| `POST /v1/clips` עם `precision: "frame"` | `201` קליפ, `status: finalizing` | Webhook של `clip.final` או `GET /v1/clips/{id}` (`final` / `failed`) |
| `POST /v1/prewarm` | `202 {id, status, run}` | Webhook של `prewarm.finished` או `GET /v1/prewarm/{id}` |
| `POST /v1/purge` | `202 {job_id, keys, prefix}` | `GET /v1/jobs/{id}` |
| `POST /v1/stats/exports` | `202` ייצוא, `status: queued` | `GET /v1/stats/exports/{id}` עד `ready` |
| `POST /v1/channels/{id}/programmes/{pid}/publish-vod` | `202` | `vod.status` ברשימת הצפייה החוזרת |
| `POST /v1/library/imports/{id}/start` | `202` ייבוא | `GET /v1/library/imports/{id}` |

סטטוסים של סרטון: `registered → probing → queued → encoding → packaging → ready`, או `failed`; `deleted` = באשפה.
סטטוסים של משימה: `queued`, `dispatched`, `running`, `succeeded`, `failed`, `cancelled`
(jobs.go (באנגלית)). `GET /v1/jobs/{id}` מחזיר את המשימה עם `events[]` שלה;
מטקסטי השגיאה מוסרות כתובות פנימיות.

תשאלו בעדינות: פעם ב-5–10 שניות לאובייקט בודד זה די והותר, וזכרו את התקציב של 20 בקשות בשנייה.

## זרם האירועים (Server-Sent Events)

`GET /v1/events/stream` דוחף את האירועים של הארגון שלכם ברגע שהם קורים (sse.go (באנגלית),
sse/hub.go (באנגלית)). מפתח API צריך את `events:read`.

```bash
curl -N https://api.viewstream.co.il/v1/events/stream -H "Authorization: Bearer $VS_KEY"
```
```
: connected

id: 01J9…
event: asset.status
data: {"asset_id":"…","status":"encoding"}

: ping
```

- המסגרות הן `id:` / `event:` / `data:` (JSON). הערה `: ping` מגיעה כל 15 שניות.
- **התחברו מחדש עם `Last-Event-ID`**: האירועים של 5 הדקות האחרונות נשלחים שוב. אם המזהה שלכם ישן יותר, המסגרת הראשונה
  היא `event: resync` — טענו מחדש את המצב שחשוב לכם.
- חיבור נמשך לכל היותר שעה; הוא מסתיים ב-`event: reconnect` (`{"reason":"max_lifetime"}`). התחברו מחדש.
- הזרם נושא גם אירועי ממשק (`asset.status`, `job.status`, `job.progress`, `stats.realtime`, …) שלעולם לא
  נשלחים כ-Webhooks. לאינטגרציה בין שרתים העדיפו [Webhooks](recipes.md#webhooks): הם נשלחים שוב במשך
  12 שעות; הזרם הוא best-effort בלבד.

## ארגונים

מפתח שייך לארגון אחד ותמיד פועל בו. משתמשי Studio עם כמה ארגונים עוברים ביניהם עם
`POST /v1/me/tenant` (בסשן בלבד). אובייקטים של ארגון אחר עונים `404`, אף פעם לא `403`.

ארגון **מושעה** הוא לקריאה בלבד: פעולות כתיבה עונות `403 tenant_suspended`; הצפייה ממשיכה לעבוד.
