> ## Documentation Index
> Fetch the complete documentation index at: https://www.seam.co/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Retrieve Event Media

> Fetch the video clip and thumbnail for a camera motion or doorbell event using media IDs and the /media/get endpoint.

When a camera detects motion or a doorbell is pressed, Seam sends an event that lists the IDs of the media captured for it: usually a short video clip and a thumbnail image. You pass each ID to [`/media/get`](/docs/api/media/get) to get the media's status and a short-lived signed URL for downloading or playing it.

<Warning>
  Camera event media is in **beta**. For Arlo and Tapo, Seam turns on camera events and event media for each workspace separately, so [contact Seam](mailto:support@seam.co) and ask for them to be turned on for yours. Ring events include `media_ids` without extra setup. The request and response shapes on this page may change before general availability.
</Warning>

***

## How It Works

```mermaid theme={"dark"}
flowchart TD
  A[Camera detects motion or a doorbell press] --> B["Seam event with media_ids"]
  B --> C[Your webhook receiver stores the media IDs]
  C --> D["/media/get with a media_id"]
  D -->|status: pending| E[Wait, then call /media/get again]
  E --> D
  D -->|status: available| F[Use the signed url right away]
  D -->|status: unavailable or failed| G[Stop retrying]
```

1. Seam creates a `camera.activated` or `device.doorbell_rang` event. The event's `media_ids` array lists one ID for each piece of media Seam expects for the event.
2. You call `/media/get` with a `media_id`. Seam returns a `media` object with a `status` and, once the media is ready, a signed `url`.
3. You download or play the media from `url` right away. The URL expires within minutes, so call `/media/get` again whenever you need a new one.

Store the `media_id`, not the `url`. Signed URLs expire within minutes, while a media ID stays valid for about 30 days. After that, `/media/get` returns `media_not_found` for it.

***

## Supported Providers

| Provider | Events with media | Where the media comes from | Requirements |
| - | - | - | - |
| [Arlo](/docs/device-and-system-integration-guides/arlo-cameras) | `camera.activated` | The recording in the Arlo cloud library. | An active Arlo subscription that includes cloud recording. |
| [Ring](/docs/device-and-system-integration-guides/ring-cameras) | `camera.activated`, `device.doorbell_rang` | Ring's cloud. | An active Ring Protect subscription on the Ring account. |
| [TP-Link Tapo](/docs/device-and-system-integration-guides/tapo-cameras#event-recordings) | `camera.activated` | The camera's microSD card, through the paired Seam Bridge. | The Tapo account password entered in the Connect Webview, a microSD card with recording turned on, and Third-Party Compatibility turned on in the Tapo app. |

For Arlo and Tapo, Seam copies the clip into Seam's private storage and extracts a JPEG thumbnail from it. Seam converts clips to MP4 with H.264 video so that browsers can play them. Ring media stays with Ring: Seam requests a signed URL from Ring each time you call `/media/get`.

***

## Before You Begin

To retrieve event media, you need:

* For Arlo and Tapo, a workspace with camera events and event media turned on. Ring event media doesn't need to be turned on.
* A [connected account](/docs/core-concepts/connected-accounts) with a supported camera or doorbell.
* A [webhook](/docs/developer-tools/webhooks) subscribed to `camera.activated` and, for Ring doorbells, `device.doorbell_rang`.
* An [API key](/docs/core-concepts/authentication/api-keys), or a [client session token](/docs/core-concepts/authentication/client-session-tokens) that has access to the device.

***

## Step 1: Receive a Camera Event

Subscribe your webhook to `camera.activated` and `device.doorbell_rang`. Verify the webhook signature, then store the event's `media_ids` along with its `event_id` and `device_id`.

**Example `camera.activated` event:**

```json theme={"dark"}
{
  "event_id": "0b7f0d6a-5b1e-4d0c-9d5c-2d8f6a3e1c44",
  "event_type": "camera.activated",
  "workspace_id": "398d80b7-3f96-47c2-b85a-6f8ba21d07be",
  "device_id": "a83690b2-2b70-409a-9a94-426699b84c97",
  "connected_account_id": "c9d3f2a1-7b4e-4f0a-8e2d-5a6b7c8d9e0f",
  "occurred_at": "2026-09-25T17:03:12.000Z",
  "created_at": "2026-09-25T17:03:12.412Z",
  "activation_reason": "motion_detected",
  "media_ids": [
    "01a0d985-621c-7b3e-9f2d-4a5b6c7d8e9f",
    "01a0d985-621c-7c4f-8a3e-5b6c7d8e9f0a"
  ],
  "video_url": "https://connect.getseam.com/media/get?media_id=01a0d985-621c-7b3e-9f2d-4a5b6c7d8e9f&format=redirect",
  "image_url": "https://connect.getseam.com/media/get?media_id=01a0d985-621c-7c4f-8a3e-5b6c7d8e9f0a&format=redirect"
}
```

* `media_ids` doesn't guarantee any order. Call `/media/get` for each ID and read `media_type` to find the clip (`video`) and the thumbnail (`image`).
* `media_ids` is missing when Seam isn't capturing media for the event. For example, event media might not be turned on for an Arlo or Tapo workspace, or the provider might not supply media for that event type.
* `motion_sub_type` (`human`, `vehicle`, `package`, or `other`) is included when the provider reports it.
* `video_url` and `image_url` are still included for compatibility. They point to `/media/get` with `format=redirect`, and requests to them need the same authentication as any other Seam API request. New integrations should use `media_ids`.

<Info>
  For Arlo and Tapo, media usually isn't ready when the event arrives. Seam sends the event first, and then retrieves the recording in the background. Ring media is `available` as soon as the event arrives.
</Info>

***

## Step 2: Get the Media

Call [`/media/get`](/docs/api/media/get) with a `media_id`. The default `format` is `json`, which returns the [`media`](/docs/api/media/object) object.

**Request:**

<CodeGroup>
  ```javascript JavaScript theme={"dark"}
  const { media } = await seam.media.get({
    media_id: '01a0d985-621c-7b3e-9f2d-4a5b6c7d8e9f',
  })

  console.log(media.status, media.url)
  ```

  ```python Python theme={"dark"}
  media = seam.media.get(
      media_id="01a0d985-621c-7b3e-9f2d-4a5b6c7d8e9f"
  )

  print(media.status, media.url)
  ```

  ```ruby Ruby theme={"dark"}
  media = seam.media.get(
    media_id: "01a0d985-621c-7b3e-9f2d-4a5b6c7d8e9f"
  )

  puts media.status, media.url
  ```

  ```php PHP theme={"dark"}
  $media = $seam->media->get(
    media_id: "01a0d985-621c-7b3e-9f2d-4a5b6c7d8e9f"
  );

  echo $media->status, $media->url;
  ```

  ```csharp C# theme={"dark"}
  var media = seam.Media.Get(
    mediaId: "01a0d985-621c-7b3e-9f2d-4a5b6c7d8e9f"
  );

  Console.WriteLine($"{media.Status} {media.Url}");
  ```

  ```java Java theme={"dark"}
  var media = seam.media().get(
    MediaGetRequest.builder()
      .mediaId("01a0d985-621c-7b3e-9f2d-4a5b6c7d8e9f")
      .build()
  );

  System.out.println(media.getStatus() + " " + media.getUrl());
  ```

  ```bash cURL theme={"dark"}
  # Use GET or POST.
  curl -X 'POST' \
    'https://connect.getseam.com/media/get' \
    -H 'accept: application/json' \
    -H "Authorization: Bearer ${SEAM_API_KEY}" \
    -H 'Content-Type: application/json' \
    -d '{
    "media_id": "01a0d985-621c-7b3e-9f2d-4a5b6c7d8e9f"
  }'
  ```
</CodeGroup>

**Response:**

```json theme={"dark"}
{
  "media": {
    "media_id": "01a0d985-621c-7b3e-9f2d-4a5b6c7d8e9f",
    "workspace_id": "398d80b7-3f96-47c2-b85a-6f8ba21d07be",
    "device_id": "a83690b2-2b70-409a-9a94-426699b84c97",
    "event_id": "0b7f0d6a-5b1e-4d0c-9d5c-2d8f6a3e1c44",
    "media_type": "video",
    "content_type": "video/mp4",
    "status": "available",
    "url": "https://media.getseam.com/camera-event-media/...&X-Amz-Signature=...",
    "expires_at": "2026-10-02T17:04:40.000Z",
    "created_at": "2026-09-25T17:03:12.412Z"
  }
}
```

### Media Object

See the [media object reference](/docs/api/media/object) for the complete property list.

| Field | Type | Description |
| - | - | - |
| `media_id` | string (UUID) | ID of the media. |
| `workspace_id` | string (UUID) | ID of the workspace that contains the media. |
| `device_id` | string (UUID) or `null` | ID of the device that captured the media. |
| `event_id` | string (UUID) or `null` | ID of the event that the media belongs to. |
| `media_type` | `video` or `image` | Whether the media is a video clip or a still image. |
| `content_type` | string or `null` | MIME type of the media, such as `video/mp4` or `image/jpeg`. |
| `status` | `pending`, `available`, `unavailable`, or `failed` | See [Media Statuses](#media-statuses). |
| `url` | string or `null` | Short-lived signed URL from which you can download the media. `null` unless `status` is `available`. The URL expires within minutes. |
| `expires_at` | string (date-time) or `null` | When the media itself stops being available. This is not when `url` expires. `null` when Seam doesn't know when the media expires, as for Ring media. |
| `created_at` | string (date-time) | When the media was created. |

The response has no field for when `url` expires, and the lifetime varies by provider. Treat every URL as valid for only a few minutes, and call `/media/get` again for a new one rather than caching it.

### Media Statuses

| `status` | Meaning | `url` | What to do |
| - | - | - | - |
| `pending` | Seam is still retrieving the media from the provider. | `null` | Call `/media/get` again later. See [Step 3](#step-3-wait-for-pending-media). |
| `available` | The media is ready. | A signed URL | Download or play it right away. |
| `unavailable` | No recording exists or can be retrieved for the event. For example, the provider has no matching recording (for Arlo, Seam waits about three minutes for one), the Tapo camera was connected without the Tapo account password, or the camera is no longer connected. | `null` | Stop retrying. |
| `failed` | Seam couldn't retrieve, convert, or store the media, including when the media was still `pending` after Seam's last retry. | `null` | Stop retrying. |

`pending` is the only status that changes. After the media's `expires_at` passes, `/media/get` returns a `media_not_available` error for it. About 30 days after the event, Seam deletes the media record entirely, and `/media/get` returns `media_not_found`.

***

## Step 3: Wait for Pending Media

Seam can't retrieve an Arlo or Tapo recording until the provider finishes it, so media often stays `pending` for a minute or more after the event. Seam doesn't send an event when media becomes available, so poll `/media/get` with backoff:

* Wait about 15 seconds after the event before the first request.
* Double the delay after each `pending` response, up to 30 seconds between requests.
* Stop after about 10 minutes and treat the media as unavailable.
* Poll from a background job, not from the webhook handler. Return a `2xx` from the webhook handler as soon as you store the event.

```javascript JavaScript theme={"dark"}
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms))

async function waitForMedia(seam, media_id) {
  const deadline = Date.now() + 10 * 60 * 1000
  let delay_ms = 15_000

  while (Date.now() < deadline) {
    await sleep(delay_ms)
    const { media } = await seam.media.get({ media_id })
    if (media.status !== 'pending') return media
    delay_ms = Math.min(delay_ms * 2, 30_000)
  }

  return null
}
```

***

## Step 4: Use the Signed URL

The `url` is a signed, short-lived link to the media file. It expires within minutes, and the exact lifetime varies by provider. The response doesn't say when: `expires_at` is when the media itself stops being available, not when the URL expires.

* **Don't store the URL.** Store the `media_id` and call `/media/get` when you need a URL.
* **Request a new URL when playback fails.** A video player can request byte ranges from the URL long after the page loads, for example when the user seeks. If the player reports an error, call `/media/get` again and set the new URL as the source.
* **Keep the URL private.** Anyone who has the URL can download the media until it expires. Don't log it or put it in analytics events.
* **Keep your own copy if you need it longer.** Seam keeps stored Arlo and Tapo media for 7 days, and `expires_at` tells you when. After that, `/media/get` returns `media_not_available`. Ring media has no `expires_at`: it's available for up to 30 days, subject to the retention of the Ring account's Ring Protect plan.

### JSON vs. Redirect

`/media/get` accepts a `format` parameter:

| `format` | Response | Use it when |
| - | - | - |
| `json` (default) | `200` with the `media` object. | You want to check `status` or `expires_at`, or set the `src` of an `<img>` or `<video>` element in a browser. |
| `redirect` | `302` to the signed URL. | A server-side client downloads the file and follows redirects, such as `curl -L`. |

Browsers can't add an `Authorization` header to an `<img>` or `<video>` element's request, so you can't point one straight at `/media/get`. In a browser, request the JSON format with your client session token, then set the returned `url` as the element's `src`. This works for media that Seam stores, such as Arlo clips:

```javascript JavaScript theme={"dark"}
const response = await fetch('https://connect.getseam.com/media/get', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'seam-client-session-token': clientSessionToken,
  },
  body: JSON.stringify({ media_id: mediaId }),
})
const { media } = await response.json()

if (media.status === 'available') {
  document.querySelector('video').src = media.url
}
```

To download a clip on a server:

```bash cURL theme={"dark"}
curl -L -o clip.mp4 \
  -H "Authorization: Bearer ${SEAM_API_KEY}" \
  'https://connect.getseam.com/media/get?media_id=01a0d985-621c-7b3e-9f2d-4a5b6c7d8e9f&format=redirect'
```

<Warning>
  Ring video URLs may not play directly in a browser `<video>` element. When a Ring clip is shorter than the window Seam requests, Ring's servers answer with a partial-content response that browsers reject. For Ring clips, download the file on your server and serve it to the browser yourself.
</Warning>

If the media isn't available yet, the redirect format returns an error instead of a `302`. See [Errors](#errors).

***

## Authorization

`/media/get` accepts the same credentials as other Seam read endpoints: an API key, a personal access token with a workspace, or a client session token.

A client session can only get media for the devices it has access to. That is, devices in the connected accounts, Connect Webviews, or user identity that you [associated with the client session](/docs/core-concepts/authentication/client-session-tokens) when you created it, or, for a client session created with a `customer_key`, that customer's devices. For any other device's media, Seam returns `media_not_found`, the same error it returns for an ID that doesn't exist, so a client can't probe for media in other accounts.

Use client sessions to show media in your users' browsers or mobile apps without exposing your API key.

***

## Errors

| HTTP status | `error.type` | Cause |
| - | - | - |
| `404` | `media_not_found` | The `media_id` doesn't exist in this workspace, Seam has deleted it, or your credentials don't give access to the device that captured it. |
| `404` | `media_not_available` | Seam has no URL for the media. With `format=redirect`, this happens whenever the media isn't `available`, so use `format=json` to read its status. With either format, it also happens when the media's `expires_at` has passed, or when the provider can't return a URL, for example for a Ring account without an active Ring Protect subscription. |

***

## Provider Notes

### TP-Link Tapo

* Seam retrieves the clip from the camera's microSD card through the paired Seam Bridge, so the Bridge and the camera must stay online after the event.
* Seam needs the Tapo account password, which is the password for the Tapo app, not the Camera Account password. Enter it in the optional **Tapo account password** field of the Connect Webview. For a camera connected without it, media ends up `unavailable`. To add it, reconnect the camera. Recordings start with the next motion event. See [Tapo event recordings](/docs/device-and-system-integration-guides/tapo-cameras#event-recordings).
* Without a microSD card, or when the card has no recording that contains the event, the media ends up `unavailable`.
* The sandbox Tapo camera has no microSD recordings.

### Arlo

* Seam retrieves the clip from the Arlo cloud library. Without a subscription that includes cloud recording, Arlo stores no recording, and the media ends up `unavailable`.
* Arlo clips are often recorded in HEVC. Seam converts them to H.264 so that browsers can play them.

### Ring

* Ring media is available for both `camera.activated` and `device.doorbell_rang` events.
* Ring only keeps clips for accounts with an active Ring Protect subscription.
* Seam requests a new signed URL from Ring on each `/media/get` call. Ring media is `available` as soon as the event arrives, and its `expires_at` is `null`. If Ring no longer has the recording, `/media/get` returns `media_not_available`.
* Ring video URLs may not play directly in a browser. See [JSON vs. Redirect](#json-vs-redirect).

***

## Next Steps

* [Get Media](/docs/api/media/get): Full request and response reference for `/media/get`.
* [Stream live video](./stream-live-video): Watch a camera's live feed in the browser over WebRTC.
* [Webhooks](/docs/developer-tools/webhooks): Receive and verify camera events.
* [Client session tokens](/docs/core-concepts/authentication/client-session-tokens): Give your users' browsers scoped access to their devices.
