diff --git a/js/docs/src/content/docs/lex-reference/live/place-stream-live-getclip.md b/js/docs/src/content/docs/lex-reference/live/place-stream-live-getclip.md
new file mode 100644
index 000000000..db64f1b7b
--- /dev/null
+++ b/js/docs/src/content/docs/lex-reference/live/place-stream-live-getclip.md
@@ -0,0 +1,85 @@
+---
+title: place.stream.live.getClip
+description: Reference for the place.stream.live.getClip lexicon
+---
+
+**Lexicon Version:** 1
+
+## Definitions
+
+
+
+### `main`
+
+**Type:** `query`
+
+Clip a livestream between two instants as a flat (faststart) MP4. The clip is cut on MUXL segment boundaries: it holds every segment whose start falls within [start, end], so it may begin up to one segment late and run up to one segment past end. Segments come from the node's livestream recordings and from the recent stream it holds in memory; each segment's C2PA signature passes through verbatim. Only published (live) segments are clipped. Requires a signed-in caller: an OAuth session, or a service-auth token bound to this method (lxm), so third-party apps can call it through the PDS with the rpc:place.stream.live.getClip scope.
+
+**Parameters:**
+
+| Name | Type | Req'd | Description | Constraints |
+| ---------- | -------- | ----- | ------------------------------------------------------------------------------- | ------------------ |
+| `streamer` | `string` | ✅ | The streamer: a DID or a Bluesky handle (resolved to its DID). | |
+| `start` | `string` | ✅ | Start of the clip (wall-clock time). | Format: `datetime` |
+| `end` | `string` | ✅ | End of the clip (wall-clock time). Must be after start, at most one hour later. | Format: `datetime` |
+
+**Output:**
+
+- **Encoding:** `video/mp4`
+- **Schema:**
+
+_Schema not defined._
+**Possible Errors:**
+
+- `ClipNotFound`: This node has no published segments of the stream in the requested range.
+- `ClipUnavailable`: Part of the requested range is still being recorded and is held only by the node the streamer is streaming to; retry there, or later once the recording has been written.
+
+---
+
+## Lexicon Source
+
+```json
+{
+ "lexicon": 1,
+ "id": "place.stream.live.getClip",
+ "defs": {
+ "main": {
+ "type": "query",
+ "description": "Clip a livestream between two instants as a flat (faststart) MP4. The clip is cut on MUXL segment boundaries: it holds every segment whose start falls within [start, end], so it may begin up to one segment late and run up to one segment past end. Segments come from the node's livestream recordings and from the recent stream it holds in memory; each segment's C2PA signature passes through verbatim. Only published (live) segments are clipped. Requires a signed-in caller: an OAuth session, or a service-auth token bound to this method (lxm), so third-party apps can call it through the PDS with the rpc:place.stream.live.getClip scope.",
+ "parameters": {
+ "type": "params",
+ "required": ["streamer", "start", "end"],
+ "properties": {
+ "streamer": {
+ "type": "string",
+ "description": "The streamer: a DID or a Bluesky handle (resolved to its DID)."
+ },
+ "start": {
+ "type": "string",
+ "format": "datetime",
+ "description": "Start of the clip (wall-clock time)."
+ },
+ "end": {
+ "type": "string",
+ "format": "datetime",
+ "description": "End of the clip (wall-clock time). Must be after start, at most one hour later."
+ }
+ }
+ },
+ "output": {
+ "encoding": "video/mp4"
+ },
+ "errors": [
+ {
+ "name": "ClipNotFound",
+ "description": "This node has no published segments of the stream in the requested range."
+ },
+ {
+ "name": "ClipUnavailable",
+ "description": "Part of the requested range is still being recorded and is held only by the node the streamer is streaming to; retry there, or later once the recording has been written."
+ }
+ ]
+ }
+ }
+}
+```
diff --git a/js/docs/src/content/docs/lex-reference/openapi.json b/js/docs/src/content/docs/lex-reference/openapi.json
index 3e4268ca5..73f18aef1 100644
--- a/js/docs/src/content/docs/lex-reference/openapi.json
+++ b/js/docs/src/content/docs/lex-reference/openapi.json
@@ -3511,6 +3511,84 @@
}
}
},
+ "/xrpc/place.stream.live.getClip": {
+ "get": {
+ "summary": "Clip a livestream between two instants as a flat (faststart) MP4. The clip is cut on MUXL segment boundaries: it holds every segment whose start falls within [start, end], so it may begin up to one segment late and run up to one segment past end. Segments come from the node's livestream recordings and from the recent stream it holds in memory; each segment's C2PA signature passes through verbatim. Only published (live) segments are clipped. Requires a signed-in caller: an OAuth session, or a service-auth token bound to this method (lxm), so third-party apps can call it through the PDS with the rpc:place.stream.live.getClip scope.",
+ "operationId": "place.stream.live.getClip",
+ "tags": ["place.stream.live"],
+ "responses": {
+ "200": {
+ "description": "Success",
+ "content": {
+ "video/mp4": {
+ "schema": {}
+ }
+ }
+ },
+ "400": {
+ "description": "Bad Request",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "required": ["error", "message"],
+ "properties": {
+ "error": {
+ "type": "string",
+ "oneOf": [
+ {
+ "const": "ClipNotFound"
+ },
+ {
+ "const": "ClipUnavailable"
+ }
+ ]
+ },
+ "message": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "parameters": [
+ {
+ "name": "streamer",
+ "in": "query",
+ "required": true,
+ "description": "The streamer: a DID or a Bluesky handle (resolved to its DID).",
+ "schema": {
+ "type": "string",
+ "description": "The streamer: a DID or a Bluesky handle (resolved to its DID)."
+ }
+ },
+ {
+ "name": "start",
+ "in": "query",
+ "required": true,
+ "description": "Start of the clip (wall-clock time).",
+ "schema": {
+ "type": "string",
+ "description": "Start of the clip (wall-clock time).",
+ "format": "date-time"
+ }
+ },
+ {
+ "name": "end",
+ "in": "query",
+ "required": true,
+ "description": "End of the clip (wall-clock time). Must be after start, at most one hour later.",
+ "schema": {
+ "type": "string",
+ "description": "End of the clip (wall-clock time). Must be after start, at most one hour later.",
+ "format": "date-time"
+ }
+ }
+ ]
+ }
+ },
"/xrpc/place.stream.live.getLiveUsers": {
"get": {
"summary": "Get a list of livestream segments for a user",
diff --git a/js/e2e-web/flows/live-clip.spec.ts b/js/e2e-web/flows/live-clip.spec.ts
new file mode 100644
index 000000000..3de08fa52
--- /dev/null
+++ b/js/e2e-web/flows/live-clip.spec.ts
@@ -0,0 +1,88 @@
+import { expect, test } from "@playwright/test";
+
+// place.stream.live.getClip, called the way a third-party app does: with a
+// service-auth token the user's PDS mints for exactly this method (the rpc
+// scope), not the node's own OAuth session. The clip of the harness's live
+// stream must be a playable MP4. Backend-only for now: there is no app surface
+// yet, so there is no mobile counterpart.
+test("live-clip: a service-auth caller clips the live stream to a playable MP4", async ({
+ page,
+}) => {
+ const pdsUrl = process.env.PDS_HTTPS_URL;
+ test.skip(!pdsUrl, "needs the harness's HTTPS mode for the PDS");
+ const did = process.env.ACCOUNT_DID!;
+
+ await page.goto("/");
+ await expect(page.getByTestId("home-stream-card").first()).toBeVisible({
+ timeout: 30_000,
+ });
+
+ // Everything runs in the page: the browser is what trusts the harness's
+ // certificates, and a browser is where a third-party web app would call from.
+ const result = await page.evaluate(
+ async ({ pdsUrl, handle, password, did }) => {
+ const nodeDid = (await (await fetch("/.well-known/did.json")).json()).id;
+ const session = await (
+ await fetch(`${pdsUrl}/xrpc/com.atproto.server.createSession`, {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({ identifier: handle, password }),
+ })
+ ).json();
+ const lxm = "place.stream.live.getClip";
+ const { token } = await (
+ await fetch(
+ `${pdsUrl}/xrpc/com.atproto.server.getServiceAuth?aud=${encodeURIComponent(nodeDid)}&lxm=${lxm}`,
+ { headers: { Authorization: `Bearer ${session.accessJwt}` } },
+ )
+ ).json();
+
+ const end = new Date();
+ const start = new Date(end.getTime() - 20_000);
+ const url = `/xrpc/${lxm}?streamer=${encodeURIComponent(did)}&start=${start.toISOString()}&end=${end.toISOString()}`;
+ const anonymous = (await fetch(url)).status;
+ const res = await fetch(url, {
+ headers: { Authorization: `Bearer ${token}` },
+ });
+ const bytes = new Uint8Array(await res.arrayBuffer());
+ const contentLength = res.headers.get("content-length");
+
+ // A plain