---
name: applicaster-video-preload
description: "Video Preload / Fetch Entry API integration: use when building or hardening the customer-hosted endpoint the Applicaster Video Preload plugin calls immediately before playback to resolve one entry into playable form — entitlement-checked stream sources, DRM key systems, personalized ad macros, or extra metadata. Works in any stack. Enforces the single hardest rule of this integration: the entry returned by Fetch Entry must keep the SAME type.value as the origin feed entry that linked to it, or the player rejects it with 'no compatible source'. Validates the request boundary (ctx, entitlements) and the Pipes2 response boundary, and ships a video-preload-e2e tool that walks origin feed -> link.href -> preload response against a running service."
---

# applicaster-video-preload

Implement or harden the **Fetch Entry endpoint** — the customer-hosted Pipes2 route the
Applicaster **Video Preload** plugin calls as a player hook, immediately before playback,
to turn one catalogue entry into a playable one. The deliverable is a single-entry Pipes2
feed that satisfies the documented contract, with validation on **both** the request
boundary (path/query, `ctx`, entitlements) and the response boundary, plus an executable
proof via the skill-local [video-preload-e2e tool](tools/video-preload-e2e/README.md).

This is the **Fetch Entry** side of the Video Preload plugin, and it runs **first** — the
plugin's other action, calling a JSON:API **signing service**, runs afterwards on the
`content.src` this route produced.

Because this route already runs immediately before playback and already holds the user's
credentials, **it can return an already-signed `content.src` and make the second action
unnecessary**. Prefer that. Use `applicaster-signed-urls` when a separate signing system is
already involved and needs adapting, when the signing key must not be reachable from this
service, when different entries need different signers via `extensions.signer_api`, or when the
token lifetime is too short to mint at entry-resolution time. See
[plugin-and-endpoint-config.md](references/plugin-and-endpoint-config.md#the-two-plugin-actions-in-order).

## Scope

- Use for a new or existing backend that must expose a Fetch Entry route, whether it fronts a
  third-party video platform or OVP, an entitlements service, a DRM license issuer, or the
  customer's own catalogue.
- **Adapt to the target project's stack** — language, framework, router, validation library,
  HTTP client, test/build commands, config/secrets, logging, and deployment conventions. Do
  not assume JavaScript, and do not add a validation or HTTP dependency the project lacks a
  use for.
- The contract is fixed and documented. Confirm live docs; do not invent entry fields,
  extension names, or status codes.

## Non-Negotiable Gates

1. **Type echo.** The one entry returned by Fetch Entry must have the same `type.value` as
   the origin feed entry whose `link.href` pointed at it. The plugin **replaces the entry in
   place**, so a mismatch makes the client unable to match the resolved entry and playback
   fails with "no compatible source". This is the most common way this integration breaks in
   production, and it is invisible in a curl of the endpoint alone — it only shows when the
   origin entry and the preload response are compared. Carry the origin type explicitly — a
   query parameter on the self-link, conventionally named `overrideType` — rather than
   hard-coding `video`. See [video-preload-contract.md](references/video-preload-contract.md#the-type-echo-rule).
2. **Exactly one entry.** The response is a valid Pipes2 feed with
   `Content-Type: application/vnd+applicaster.pipes2+json` and an `entry` array of length 1.
   Not zero, not the whole collection.
3. Validate the **request** before doing any work — path/query values and, when the route is
   protected, the decoded `ctx` or Bearer credential — and validate the **rendered response**
   against the Pipes2 profile before returning it, through the target project's own
   validator. A response-validation failure logs sanitized diagnostics and returns a safe
   `5xx`; it never emits malformed Pipes2 JSON. See
   [validation-patterns.md](references/validation-patterns.md).
4. **`401` and `403` are different failures and both must be handled.** `401` is about *who you
   are* — no credential, or one that is malformed, expired, or signature-invalid; the app
   refreshes the token or sends the user to sign in. `403` is about *what you may watch* — the
   credential is valid and identifies a real user who simply has no entitlement for this item,
   or is blocked by territory; the app shows the storefront or a region message. Ask "would a
   fresh token fix this?" — if yes it is `401`, if no it is `403`.

   Conflating them breaks the app in both directions: `403` for an expired token shows a paywall
   to a paying subscriber, and `401` for an unentitled user sends someone to sign in who already
   is, looping with no purchase path. Malformed `ctx` or a bad entry id is neither — that is
   `400`.

   Both use the documented body shape `{ "statusText": <message>, "status": <code> }`, are fixed
   in the implementation contract, and are asserted separately by the validator. Never return a
   `200` carrying a playable `content.src` to a request that failed either check, and never
   return the stream URL, token, `ctx`, DRM key material, or an upstream body in an error. A
   geo-restricted route whose `signedDeviceInfoToken` is absent, malformed, expired, or fails
   signature verification **refuses** — it never falls back to serving the content or to a
   default country. See
   [video-preload-contract.md](references/video-preload-contract.md#401-versus-403--they-are-different-failures).
5. An entitlement-gated or signed response must not be cached by shared caches. Set an
   explicit `Cache-Control` (`no-store` for per-user streams; a short public max-age only
   when the response is genuinely identical for every user). Decide it deliberately and
   record it in the implementation contract — do not inherit a framework default.
6. **A selected capability is never silently dropped.** Entitlements, geo restriction, DRM,
   video ads, Play Next, preview/autoplay, resume position, and stream variants are each
   opted into in §2. Every one that is selected gets implemented, tested, and declared in the
   validator contract, or is reported as a named blocker. Equally, do not implement one that
   was not selected. See [playback-capabilities.md](references/playback-capabilities.md).
7. Do not claim end-to-end validation without a successful `video-preload-e2e` run against a
   running service, or a concrete named blocker (no runnable URL, no safe test credential,
   no expected entry id).
8. Do not report the work complete without a `## Zapp Studio Configuration` section covering
   the Endpoint and its context keys, the Feed, the **Video Preload** plugin parameters, the
   **Preload Video Screen**, and attaching that screen as the player's storyboard hook.

## 1. Establish facts

1. Fetch the current live documentation and reconcile the local references toward it — live
   docs are semantic authority:
   - <https://docs.applicaster.com/integrations/video-preload>
   - <https://docs.applicaster.com/integrations/video-preload-fetch-entry>
   - <https://docs.applicaster.com/integrations/pipes2-video-preload-endpoint-guide>
   - <https://docs.applicaster.com/using-zapp/player/video-preload-hook>
   - <https://docs.applicaster.com/integrations/feed-json-protocol>
   Then read [video-preload-contract.md](references/video-preload-contract.md),
   [playback-capabilities.md](references/playback-capabilities.md),
   [plugin-and-endpoint-config.md](references/plugin-and-endpoint-config.md), and
   [validation-patterns.md](references/validation-patterns.md).
2. Inspect the target project before asking anything. Record, from the code: the stack and
   test/build commands; the existing Pipes2 routes and how entries are rendered; **whether
   an origin feed already emits `link.rel: "self"` / `link.href` per entry, and which
   `type.value` those entries carry**; the validation convention; config/secrets; the
   deployed URL shape; and any existing Zapp configuration or docs.
   Then inventory, per capability, **which upstream can actually answer it** — entitlements,
   territorial rights and the DI public key, DRM key systems and license URLs, ad tags and
   macro inputs, next-item ordering, preview assets, playback position, stream variants. A capability with no available data
   source is a blocker to raise in §2, not something to discover mid-implementation.
3. If a Fetch Entry route already exists, inventory its consumers and tests, and treat any
   change to its output, routing, auth, or caching as a compatibility impact.

Completion: the stack, the origin feed's entry types and link emission, the playback data
source, the validation convention, and the live-contract reconciliation are evidenced in a
concise sourced fact summary.

## 2. Resolve developer decisions

Present the fact summary, then ask only what the project and docs cannot settle. **One
question at a time**, each carrying the discovered constraint, a recommended answer, and the
consequence of choosing otherwise. If a question exposes a missing fact, go investigate and
come back with a recommendation rather than asking the developer to decide blind.

Ask first, because it determines the URL shape and everything downstream:

1. **How does the plugin reach the endpoint?** Per-entry `link.href` on the origin feed
   entry (recommended — the entry decides its own preload URL, and it is the only route that
   supports heterogeneous entry types), or the plugin's **Default Entry Source** feed with a
   dynamic `{{id}}` locator, or both with the entry overriding the default. See
   [plugin-and-endpoint-config.md](references/plugin-and-endpoint-config.md#reaching-the-endpoint).

Then resolve, only where facts leave real ambiguity:

2. **Which origin entry types link to preload, and how does the endpoint learn the type it
   must echo?** Enumerate the `type.value` of every entry type that will carry a preload
   link (`video`, and frequently custom types such as `story`, `page`, `channel`, `program`).
   Then pick the mechanism: echo an `overrideType` query parameter carried on the self-link
   (recommended, and the only option that works when one route serves several origin types),
   or derive it deterministically from the route when a route serves exactly one type.
   Gate 1 fails if this is left implicit.

3. **Which capabilities are in scope?** A multi-select over the list below — this is the
   question that decides how many of the remaining ones get asked, so ask it before them and
   record the answer verbatim. Resolving the playable `content.src` is always in scope and is
   not offered as a choice.

   | Capability | What Fetch Entry resolves |
   | --- | --- |
   | Entitlements | whether this user may watch this item, and what they get if not |
   | Geo restriction | whether this item may be watched *from where the viewer is* |
   | DRM | `extensions.drm` license/certificate data per key system |
   | Video ads | `extensions.video_ads`, with macros resolved from `ctx` |
   | Play Next | `extensions.play_next_feed_url` for the next item |
   | Preview / autoplay | `extensions.preview_playback` and `autoplay_settings` on the origin entry |
   | Resume position | `resumeTime`, `progress`, `auto_resume_completion_threshold` |
   | Stream variants | platform-specific container and the matching DRM system |
   | Extra metadata | chapters, analytics properties, trailer, share, live/download flags |

   Ask the following **only for the capabilities selected**, in this order — each one's answer
   constrains the next. Read
   [playback-capabilities.md](references/playback-capabilities.md) before asking, and bring its
   documented field shapes into the question rather than asking open-endedly.

4. **Entitlements.** What is the source of truth — an in-house subscription service, the payment
   provider's entitlement API, a claim inside the access token, or the video platform itself?
   Then settle each of these, because they are separate fields with separate jobs: which entries
   set `extensions.free`; whether `extensions.ds_product_ids` is used and with which provider
   product ids (comma-separated, `provider:product` form, **any-of** semantics — and blank means
   *any* entitlement unlocks it, which is more permissive than most customers expect); and which
   entries set `extensions.requires_authentication`. Confirm the origin feed's advertised state
   and Fetch Entry's enforcement agree, or the user sees an unlocked cell and then an error.
   If the entitlement requirement has to travel from the origin entry on the `link.href`, say so
   explicitly — it is then caller-supplied and must be re-verified server-side, never trusted.

4a. **Is any of this content geo-blocked or geo-varied?** Ask it explicitly — rights are
   usually territorial, and it is rarely volunteered. If yes, the `signedDeviceInfoToken`
   context key becomes **required** on the Endpoint, and the route must:
   - verify the JWT against the DI public key from `https://di.applicaster.com/public`, held in
     the project's config convention — verify, never decode-and-trust, or the gate is spoofable;
   - read `country` (ISO 3166-1) from the **verified** claims and apply the customer's
     allow-list or block-list;
   - **fail closed** when the token is absent, malformed, expired, or invalid (Gate 4).

   Then settle: refuse outright or serve a different regional stream for the same entry; what a
   blocked viewer sees, distinct from the unentitled message; whether the origin feed also
   filters by country or only playback is gated; and the caching consequence — a response that
   varies by country must not be shared-cached.

   **Confirm the DI Token plugin is installed on every native platform in the build.** On
   Android, Apple, and Roku the key is supplied by that plugin and is simply absent without it;
   on Samsung/LG WebTV it is built in. A fail-closed gate plus a missing plugin blocks every
   native viewer — raise it as a blocker rather than loosening the gate. See
   [playback-capabilities.md](references/playback-capabilities.md#geo-restriction--signeddeviceinfotoken).

5. **Authorization transport — how does the token actually reach the endpoint?** A Zapp Endpoint
   can deliver a context key in **four** documented ways, and this is a real choice, not a
   formality:

   | Delivery | Shape | When to choose it |
   | --- | --- | --- |
   | Authorization Bearer header | `Authorization: Bearer <token>` | Recommended when the token is the only credential needed — standard, and it stays out of URLs and logs |
   | Abbreviated custom query param | `?token=<token>` | An upstream or CDN that cannot read headers; the value lands in access logs, so treat it accordingly |
   | Custom header | `X-Whatever: <token>` | An existing upstream contract already expects that header |
   | `ctx` query param | base64url JSON of every configured key | Only when the route genuinely needs several context values at once |

   **Do not reach for `ctx` by default just because an entitlement check is needed.** If the
   route only needs the access token, expose it as its own key — a Bearer header or a named
   query param — and leave `ctx` for the cases that need device, locale, or ad inputs too. A
   route that decodes a whole `ctx` blob to read one value is harder to validate, easier to log
   by accident, and forces every caller to send more than it should.

   Then name the key itself — commonly `quick-brick-login-flow.access_token`, or
   `quick-brick-login-flow.id_token` where the upstream validates an ID token instead. Never
   invent a key; consult the live
   [available context keys](https://docs.applicaster.com/integrations/available-context-keys).
   Whichever transport is chosen, the value is a credential: validate it as strictly as a Bearer
   token, and never log it. Confirm the exact unauthorized status and body shape (question 5a).

5a. **Error responses — walk the customer through `401` versus `403` explicitly.** The body
   shape is `{ "statusText": <message>, "status": <code> }` with the matching HTTP status. Do not
   ask "what status do you return on failure"; ask about each case, because customers frequently
   return one status for all of them:
   - **No token, or a malformed/expired/invalid one → `401`.** Confirm the app refreshes the
     token and retries, rather than showing a paywall.
   - **Valid token, no entitlement for this item → `403`.** Confirm the app opens the storefront
     for this item, rather than bouncing to sign-in.
   - **Valid token, blocked by territory → `403`.** Decide whether the app must distinguish this
     from the unentitled case to show a region message; if so, agree the mechanism — a distinct
     `statusText`, or a machine-readable field alongside it. Do not have the app parse prose.
   - **Missing or malformed `ctx`, or a bad entry id → `400`.** A client bug, not a user-facing
     state.

5b. **Does the message shown to the viewer vary — by language, or by the reason behind the
   failure?** Two separate questions, both easy to miss until the app ships:

   - **Localized messages.** If `statusText` is displayed to the viewer, does it need to be in
     the viewer's language? If so the **`languageCode` context key becomes required** on the
     Endpoint (with `languageLocale` where regional variants matter), and the route selects the
     copy from it. Settle the fallback for an unrecognised or absent language, and confirm who
     owns the translations — the service or the app. The alternative is for the service to return
     a **stable machine-readable code** and let the app hold the localized copy; recommend that
     when the app already has a translation system, since it keeps copy changes out of a backend
     deploy. Decide one, do not do both by accident.
   - **Distinct messages per underlying reason.** A single `403` covers several very different
     situations — no subscription, subscription lapsed, wrong tier for this item, geo-blocked,
     concurrent-stream limit — and customers usually want different copy and different actions
     for them ("Subscribe at example.com" versus "Not available in your region"). Enumerate the
     upstream failures the entitlements or playback provider can return, and map each explicitly
     to a status plus a distinguishing field. Agree that field's shape — a stable code, not prose
     the app has to pattern-match — and, where the message points the viewer somewhere to buy,
     agree whether that destination comes from the service or from app configuration. Also
     confirm what an **unmapped** upstream error does: it must fall back to a safe generic
     failure, never leak the upstream body, and never resolve to `200`.

5c. Confirm whether the app surfaces `statusText` to the viewer at all — which decides whether it
   must be human-readable, or stays a diagnostic string — and whether an existing app-side error
   convention overrides the documented shape. Whatever is agreed goes into the implementation
   contract and both validator `bodySchema`s, so each error path is asserted rather than assumed.
   Note that the published example uses `403` with the wording "Unauthorized Request", which
   blurs the two statuses; follow the semantics, not that wording.

6. **DRM.** Which key systems does the app need, derived from the platforms in the build:
   `widevine`/`playready` for Android, `fairplay` for iOS/tvOS? For each, where do the license
   URL — and, for FairPlay, the `certificate_url`, `license_server_url`,
   `license_server_request_content_type` and `license_server_request_object_key` — come from?
   Is `extensions.custom_data`/`integration` needed, and does the target player support it? Is
   the license URL itself user-scoped or tokenized (which makes the whole block a credential)?
   Confirm the container and key system are chosen from the same input, so DASH pairs with
   Widevine/PlayReady and HLS with FairPlay.

7. **Video ads.** VAST (an array of `{ offset, ad_url }` breaks, `offset` being `preroll`,
   `postroll` case-insensitively, or an integer of seconds) or VMAP (a single URL string)? Then
   the part that justifies resolving ads here at all: **which macros are substituted server-side
   and from which `ctx` key**, and the fallback for each key that can legitimately be absent —
   `advertisingIdentifier` is missing whenever the user denies tracking. Also: do entitled users
   see ads at all, and are there consent signals to honour? Note that Roku uses its own
   advertising framework and does not consume `video_ads`.

8. **Play Next.** What does "next" mean — next episode in a season, next item in a playlist or
   channel, or a recommendation service? Confirm `play_next_feed_url` is an absolute **feed**
   URL (pointing it at the next item's Fetch Entry URL is what gives that item its own
   entitlement check), where the chain terminates so it does not loop, and whether the overlay
   timing is per-content via `overlay_timestamp` or a `show_play_next` chapter action.

9. **Preview / autoplay.** Which entries get a preview, and where does the clip come from — a
   dedicated free asset, or a time range of the full stream via `autoplay_settings`
   (`"MM:SS"` start/end)? Does preview bypass the entitlement check? It normally should, which
   means the preview source must be genuinely free rather than a range of the protected stream —
   an entitlement decision, not a UI one. Remember these fields live on the **origin** entry,
   because the cell needs them while scrolling; and if a preview resolves through Fetch Entry,
   the type-echo rule applies to it too.

10. **Resume position.** Is Continue Watching the source of truth for `resumeTime` / `progress` /
    `auto_resume_completion_threshold`, or a customer playback-position service? Only one should
    write. Confirm what a just-finished item returns, so it does not resume in the credits.

11. **Does the stream itself differ between platforms — HLS versus DASH?** Ask it directly; it
    is the most common reason a preload route exists at all, and it is often assumed rather than
    stated. If yes, the **`platform` context key becomes required** on the Endpoint (with
    `deviceType` alongside it where tablet/TV also change the variant), and the route must:
    - map platform → container → DRM key system from that **same** input, so the pairing stays
      consistent: DASH with Widevine/PlayReady, HLS with FairPlay. Choosing the source from one
      input and the DRM block from another is how a stream ends up encrypted for a key system
      the device cannot use;
    - handle the documented platform values — `ios`, `android`, `amazon_fire_tv`, `android_tv`,
      `tvos`, `roku`, `samsung_tv`, `lg_tv` — rather than an `ios`/`else` split, and decide
      explicitly what an unrecognised value gets;
    - **not silently default when `platform` is absent.** Serving one platform's container to
      everyone fails on every other platform, and it fails at playback with no error the
      endpoint ever sees. Return the agreed sanitized error, or serve a provably universal
      source — decide which, and record it.

    Also settle whether `content.chromecast_src` is needed (the receiver often needs a different
    URL from the sender's), and whether `browser` matters on web, where all iOS browsers report
    `safari` and typically need the HLS variant.

12. **Extra metadata.** Which of `chapter_markers` (with `show_skip` / `show_play_next` actions),
    `analyticsCustomProperties`, `trailer_feed_url` (a feed URL containing the trailer, not a
    bare video URL), `share_url`/`share_message`, `live`, and `hqme` apply, and which of them
    belong on the origin entry rather than the preload response.

Then close out, regardless of capability selection:

13. **Caching** (Gate 5): `no-store` for per-user entitled, signed, or ad-personalized responses,
    or a short public max-age when the response is genuinely identical for every user.
14. **Does the origin entry stay playable?** If the origin feed already carries a direct
    `content.src`, decide whether preload replaces it or the origin should instead omit
    `content` and set `extensions.requires_authentication: true` so unentitled users never
    receive a URL at all (recommended when the stream itself is protected).
15. The target project's response-validation convention, and the runnable `video-preload-e2e`
    inputs: origin feed URL, expected entry id, safe test credential, and expected error shape.

Completion: every non-discoverable choice affecting routing, type echo, auth, caching, and each
selected capability has a recorded developer answer.

## 3. Confirm before edits

Present a short implementation contract and wait for explicit approval:

- Reach mechanism (entry link and/or Default Entry Source) and the exact route/locator.
- The origin entry types in play and the concrete type-echo mechanism, with a worked example:
  origin entry `type.value` → preload URL → preload entry `type.value`.
- **A table of the selected capabilities**: capability → the fields it writes → the field's
  home (origin entry or preload response) → the upstream call behind it → how it is tested.
  Anything not selected is listed as explicitly out of scope, so nothing is ambiguous later.
- For ads, the macro ↔ `ctx` key mapping and the fallback for each optional key. For DRM, the
  key systems and the platform → container → key-system mapping. For Play Next, where the chain
  terminates.
- Request validation, credential transport and required `ctx` keys, the unauthorized status
  and body shape, and the `Cache-Control` value.
- The target-project response validator, the files changing, and the compatibility impact.
- The exact `video-preload-e2e` command and its prerequisites, or the specific blocker.
- The Zapp Studio changes.

Refresh the contract if a material decision changes.

## 4. Implement the contract

1. Emit the link on the origin feed. Each entry that should preload carries
   `link: { rel: "self", href: <absolute preload URL> }`, with the type-echo parameter
   bound to **that entry's own** `type.value` — not a constant. Where the origin is
   protected, also set `extensions.requires_authentication: true` and omit `content`.
   Set the origin-side capability fields here too: `free` / `ds_product_ids` /
   `requires_authentication` for entitlements, and `preview_playback` /
   `autoplay_settings` for preview, since the cell needs them while scrolling.
2. Implement the Fetch Entry route: validate path/query and the credential first; resolve
   entitlements; where geo restriction is in scope, **verify** the `signedDeviceInfoToken` JWT
   against the DI public key and apply the country rule, refusing on an absent or invalid token
   rather than falling back; then resolve each selected capability; render **one** entry.
3. Set the resolved entry's `type.value` from the echoed origin type, defaulting only where
   a route provably serves a single origin type. Give the entry an absolute `content.src`
   with a correct media `content.type` (`video/hls`, `application/dash+xml`, `video/mp4`, …),
   choosing container and DRM key system from the same platform input.
4. Attach each selected capability with the documented shape from
   [playback-capabilities.md](references/playback-capabilities.md):
   - **DRM** — `license_url` for `widevine`/`playready`; `certificate_url` **and**
     `license_server_url` for `fairplay`, plus its request content type and object key.
   - **Video ads** — a VMAP string or a VAST array of `{ offset, ad_url }`. Substitute every
     macro from its mapped `ctx` value and apply the agreed fallback; **never emit a URL with
     an unresolved placeholder still in it**, which fails silently at the ad server. Omit
     `video_ads` entirely for ad-free entitled users rather than sending an empty array.
   - **Play Next** — an absolute feed URL for the next item, omitted on the last item, and
     never pointing at the entry's own preload URL.
   - **Resume position** — `resumeTime` / `progress` /
     `auto_resume_completion_threshold` from the single agreed source.
   - **Extra metadata** — chapters, analytics properties, and the remaining flags, each on the
     entry the §3 table assigned it to.
5. Validate the rendered feed against the Pipes2 profile plus the single-entry and type-echo
   assertions **and each selected capability's shape**, then set
   `Content-Type: application/vnd+applicaster.pipes2+json` and the agreed `Cache-Control`, and
   return it. On validation failure log a sanitized rule identifier and return a safe `5xx`.
6. Return `401` when the credential is absent or untrustworthy, `403` when it is valid but the
   user is not entitled or is geo-blocked, and `400` for missing/malformed `ctx` or an invalid
   path/query — the last of these before any upstream call. Keep the three paths distinct in the
   code, not collapsed into one error branch. Map each upstream failure through the agreed table
   to a status plus its distinguishing code, localizing the message from `languageCode` if that
   was chosen; an unmapped upstream failure returns the safe generic error and never the upstream
   body.
7. Never log or echo back the token, the decoded `ctx`, the signed stream URL, DRM license URLs
   or `custom_data`, resolved ad URLs, or upstream response bodies. Read credentials from the
   project's existing config/secret convention.
8. Make only the changes a compliant integration needs; report unrelated findings separately.

Completion: origin entries link out, the route validates both boundaries, the resolved entry
echoes its origin type, and every selected capability is present in the shape §3 agreed.

## 5. Verify and hand off

1. Run the target project's formatter, typecheck, and its own tests for the changed routes —
   including a test that a non-`video` origin type round-trips unchanged, tests for every
   invalid-input and missing-context-key case, **separate tests that an absent or invalid
   credential returns `401` and that a valid credential without entitlement returns `403`**, and **one test per selected
   capability**: a geo-restricted route refuses when `signedDeviceInfoToken` is absent, forged,
   or signed by the wrong key, and serves the right regional variant when valid; each DRM key
   system carries its required URLs; ad URLs contain no unresolved macro; Play Next terminates
   and never self-references; preview does not leak the protected stream; resume position is
   bounded by duration.
2. Run the skill-local `video-preload-e2e` tool against a running service with a versioned
   contract ([tool README](tools/video-preload-e2e/README.md)). It is black-box and
   stack-agnostic: it fetches the origin feed, follows the entry's own `link.href`, and
   asserts the media type, the Pipes2 profile, exactly one entry, playable content, and the
   **type echo** — plus the unauthenticated rejection, every declared `ctx` negative case, and
   every capability declared in the contract: DRM key systems and their URLs, the VAST/VMAP
   shape with no unresolved macros, an absolute non-self-referencing Play Next URL, preview
   fields on the origin entry, and the origin entry's entitlement metadata. Declare every
   capability selected in §2 — an undeclared capability is an untested one. Attach the
   sanitized `--json-report`.
   For local validation, define the server lifecycle in the implementation contract: start
   the service with explicit safe test configuration on a non-conflicting port, capture its
   process id, wait with a bounded timeout for the origin URL to return `2xx`, run the CLI,
   and stop that specific process in a cleanup step. A readiness timeout, a missing runnable
   URL, or a missing safe credential is a **blocker and a failing result, never a skip**.
3. Report files changed, commands and results, the validator result or precise blocker,
   required config/secrets, compatibility notes, and remaining deployment prerequisites.
4. Include this exact final section.

## Zapp Studio Configuration

Give the customer concrete values and ordered steps.

**1. Endpoint.** In Zapp Studio → Data Sources → Endpoints, add the narrowest HTTPS prefix
that covers the Fetch Entry route, and attach the context keys the route requires. Warn that
where prefixes overlap, **only the longest matching Endpoint supplies context keys**.

| Endpoint | HTTPS base URL | Context keys | Delivery | Reason |
| --- | --- | --- | --- | --- |
| `<name>` | `<url>` | `<key or none>` | `<ctx / Bearer>` | `<reason>` |

**2. Feed.** Register the preload feed under that Endpoint with its Feed Locator, naming any
dynamic placeholder and the referring-entry field it binds to, plus a resolved URL example.
This feed is only needed for the **Default Entry Source** route; a per-entry `link.href`
needs the Endpoint but no Feed.

**2a. DI Token plugin (only when content is geo-restricted).** Add `signedDeviceInfoToken` to
the Endpoint's context keys, and install the **Applicaster DI Token plugin** on **every native
platform in the build** — all Android-based, Apple, and Roku. Without it the key is absent on
those platforms and the fail-closed gate blocks every native viewer. Samsung/LG WebTV have it
built in. List any platform still missing the plugin as an explicit blocker.

**3. Video Preload plugin.** Add **Video Preload** to every relevant app version and
platform, then set its parameters:

| Parameter | Value | Note |
| --- | --- | --- |
| Fetch Entry | on | Required for this integration |
| Default Entry Source | `<feed or none>` | Only if using a default URL; entry `link.href` overrides it |
| Use Signing URLs Service | `<on/off>` | On only if a separate signer is in scope — see `applicaster-signed-urls` |
| Signer API | `<feed or none>` | Overridable per entry via `extensions.signer_api` |

**4. Screen and hook.** Add the **Preload Video Screen**, then open the player screen, scroll
to SCREEN STORYBOARD, select **Preload Video** from the plugins dropdown, click `+`, and save
the layout.

**5. Rebuild** the app so the plugin takes effect, then play a linked entry end to end.

Finish with explicit blockers: missing plugin on a platform, missing token, unregistered
Endpoint, or absent safe test data.
