> ## 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.

# Get Started with UniFi Protect Cameras

> Install a Seam SDK, connect a UniFi Protect console through a Seam Bridge, retrieve its cameras, receive motion events, and retrieve event clips. Beta.

## Overview

Seam provides a universal API to connect and control many brands of devices. This guide provides a rapid introduction to connecting your [UniFi Protect](https://www.seam.co/docs/device-and-system-integration-guides/unifi-protect-cameras.md) cameras to Seam, retrieving them as devices, receiving their motion events, and retrieving event clips. Seam reaches UniFi Protect on the local network through a [Seam Bridge](https://www.seam.co/docs/capability-guides/seam-bridge.md).

To learn more about other device brands supported by Seam, head over to our [integration page](https://www.seam.co/supported-devices-and-systems).

> [!WARNING]
>
> UniFi Protect cameras are in **beta**. Behavior and API shapes may change before general availability.

***

## 1 — Install Seam SDK

Seam provides client libraries for many languages, such as JavaScript, Python, Ruby, PHP, and others, as well as a Postman collection and [OpenAPI](https://connect.getseam.com/openapi.json) spec.

* JavaScript / TypeScript ([npm](https://www.npmjs.com/package/seam), [GitHub](https://github.com/seamapi/javascript))
* Python ([pip](https://pypi.org/project/seam/), [GitHub](https://github.com/seamapi/python))
* Ruby Gem ([rubygem](https://rubygems.org/gems/seam), [GitHub](https://github.com/seamapi/ruby))
* PHP ([packagist](https://packagist.org/packages/seamapi/seam), [GitHub](https://github.com/seamapi/php))
* C# ([nuget](https://www.nuget.org/packages/Seam), [GitHub](https://github.com/seamapi/csharp))
* Java ([Maven](https://central.sonatype.com/artifact/co.seam/java), [GitHub](https://github.com/seamapi/java))
* Go ([GitHub](https://github.com/seamapi/go))

```bash JavaScript theme={"dark"}
npm i seam
```

```bash Python theme={"dark"}
pip install seam
# For some development environments, use pip3 in this command instead of pip.
```

```bash Ruby theme={"dark"}
bundle add seam
```

```bash PHP theme={"dark"}
composer require seamapi/seam
```

```bash C# theme={"dark"}
Install using nuget: https://www.nuget.org/packages/Seam
```

```bash Java theme={"dark"}
// Add to your pom.xml or build.gradle — see Maven Central for details.
```

```bash Go theme={"dark"}
go get github.com/seamapi/go
```

```bash cURL (bash) theme={"dark"}
# cURL is already installed on most systems. No additional installation needed.
# Export your API key as an environment variable:
export SEAM_API_KEY=seam_test2ZTo_0mEYQW2TvNDCxG5Atpj85Ffw
```

```bash Seam CLI theme={"dark"}
npm install -g @seamapi/cli
```

Once installed, [sign up for Seam](https://console.seam.co/) to get your API key, and export it as an environment variable:

```bash theme={"dark"}
export SEAM_API_KEY=seam_test2ZTo_0mEYQW2TvNDCxG5Atpj85Ffw
```

> [!NOTE]
>
> This guide uses a Sandbox Workspace. Only virtual devices can be connected. If
> you need to connect a real UniFi console, use a non-sandbox workspace and API
> key.

> [!NOTE]
>
> UniFi Protect is a new provider. Use a Seam SDK version that includes it, or
> call the endpoints with the cURL or Go examples, which use plain HTTP
> requests.

***

## 2 — Link a UniFi Console with Seam

To use UniFi Protect through the Seam API, you must first authorize your Seam workspace to reach the console. To do so, Seam provides [Connect Webviews](https://www.seam.co/docs/core-concepts/connect-webviews.md): pre-built UX flows that walk you through pairing a Seam Bridge and entering the console's connection details.

Include `unifi_protect` in the Connect Webview's `accepted_providers` list.

#### Request a Connect Webview

```javascript JavaScript theme={"dark"}
import { Seam } from 'seam'

const seam = new Seam()

const connectWebview = await seam.connectWebviews.create({
  accepted_providers: ['unifi_protect'],
})

console.log(connectWebview.login_successful) // false

// Send the webview URL to your user
console.log(connectWebview.url)
```

```bash cURL theme={"dark"}
curl -X 'POST' \
  'https://connect.getseam.com/connect_webviews/create' \
  -H "Authorization: Bearer ${SEAM_API_KEY}" \
  -H 'Content-Type: application/json' \
  -d '{
    "accepted_providers": ["unifi_protect"]
  }'
```

```python Python theme={"dark"}
from seam import Seam

seam = Seam()

webview = seam.connect_webviews.create(
    accepted_providers=["unifi_protect"]
)

assert webview.login_successful is False

# Send the webview URL to your user
print(webview.url)
```

```ruby Ruby theme={"dark"}
require "seam"

seam = Seam.new()

webview = seam.connect_webviews.create(
  accepted_providers: ["unifi_protect"]
)

puts webview.login_successful # false

# Send the webview URL to your user
puts webview.url
```

```php PHP theme={"dark"}
<?php
use Seam\SeamClient;

$seam = new SeamClient("YOUR_API_KEY");

$webview = $seam->connect_webviews->create(
  accepted_providers: ["unifi_protect"]
);

echo $webview->login_successful; // false

// Send the webview URL to your user
echo $webview->url;
```

```csharp C# theme={"dark"}
using Seam.Client;

var seam = new SeamClient(apiToken: "YOUR_API_KEY");

var webview = seam.ConnectWebviews.Create(
  acceptedProviders: new List<string> { "unifi_protect" }
);

Console.WriteLine(webview.LoginSuccessful); // false

// Send the webview URL to your user
Console.WriteLine(webview.Url);
```

```java Java theme={"dark"}
import co.seam.Seam;
import co.seam.api.types.ConnectWebview;

Seam seam = Seam.builder().apiKey("YOUR_API_KEY").build();

ConnectWebview webview = seam.connectWebviews().create(
  ConnectWebviewsCreateRequest.builder()
    .acceptedProviders(List.of("unifi_protect"))
    .build()
);

System.out.println(webview.getLoginSuccessful()); // false

// Send the webview URL to your user
System.out.println(webview.getUrl());
```

```go Go theme={"dark"}
import (
    "context"
    "fmt"
    "net/http"
    "os"
    "time"

    "github.com/seamapi/go/core"
)

caller := core.NewCaller(&core.CallerParams{
    Client: &http.Client{Timeout: 30 * time.Second},
    MaxAttempts: 1,
})
headers := http.Header{
    "Authorization": []string{"Bearer " + os.Getenv("SEAM_API_KEY")},
}
var response map[string]interface{}
err := caller.Call(context.Background(), &core.CallParams{
    URL: "https://connect.getseam.com/connect_webviews/create",
    Method: http.MethodPost,
    Headers: headers,
    Request: map[string]interface{}{"accepted_providers": []string{"unifi_protect"}},
    Response: &response,
})
if err != nil {
    panic(err)
}
connectWebview := response["connect_webview"].(map[string]interface{})

fmt.Println(connectWebview["login_successful"]) // false

// Send the webview URL to your user
fmt.Println(connectWebview["url"])
```

```bash Seam CLI theme={"dark"}
seam connect-webviews create --accepted-providers '["unifi_protect"]'
```

#### Authorize Your Workspace

Navigate to the URL returned by the Webview object. Since you are using a sandbox workspace, you don't need to install Seam Bridge. Continue past the Seam Bridge instructions, submit the prefilled sandbox pairing code, and then enter the UniFi Protect [sandbox console](https://www.seam.co/docs/device-and-system-integration-guides/unifi-protect-cameras/sandbox-unifi-protect-cameras.md) details below:

* **Console address:** unifi.test
* **HTTPS port:** 443
* **Protect API key:** unifi-protect-test-api-key
* **Local user for event recordings (optional):** protect-viewer
* **Local user password (optional):** protect-viewer-password

Confirm the Connect Webview was successful by querying its status:

```javascript JavaScript theme={"dark"}
const updatedWebview = await seam.connectWebviews.get(
  connectWebview.connect_webview_id,
)

console.log(updatedWebview.login_successful) // true
console.log(updatedWebview.connected_account_id)
```

```bash cURL theme={"dark"}
curl -X 'POST' \
  'https://connect.getseam.com/connect_webviews/get' \
  -H "Authorization: Bearer ${SEAM_API_KEY}" \
  -H 'Content-Type: application/json' \
  -d "{
    \"connect_webview_id\": \"${CONNECT_WEBVIEW_ID}\"
  }"
```

```python Python theme={"dark"}
updated_webview = seam.connect_webviews.get(
    connect_webview_id=webview.connect_webview_id
)

assert updated_webview.login_successful  # true
print(updated_webview.connected_account_id)
```

```ruby Ruby theme={"dark"}
updated_webview = seam.connect_webviews.get(
  connect_webview_id: webview.connect_webview_id
)

puts updated_webview.login_successful # true
puts updated_webview.connected_account_id
```

```php PHP theme={"dark"}
<?php
$updated_webview = $seam->connect_webviews->get(
  connect_webview_id: $webview->connect_webview_id
);

echo $updated_webview->login_successful; // true
echo $updated_webview->connected_account_id;
```

```csharp C# theme={"dark"}
var updatedWebview = seam.ConnectWebviews.Get(
  connectWebviewId: webview.ConnectWebviewId
);

Console.WriteLine(updatedWebview.LoginSuccessful); // true
Console.WriteLine(updatedWebview.ConnectedAccountId);
```

```java Java theme={"dark"}
ConnectWebview updatedWebview = seam.connectWebviews().get(
  ConnectWebviewsGetRequest.builder()
    .connectWebviewId(webview.getConnectWebviewId())
    .build()
);

System.out.println(updatedWebview.getLoginSuccessful()); // true
System.out.println(updatedWebview.getConnectedAccountId());
```

```go Go theme={"dark"}
import (
    "context"
    "fmt"
    "net/http"
    "os"
    "time"

    "github.com/seamapi/go/core"
)

caller := core.NewCaller(&core.CallerParams{
    Client: &http.Client{Timeout: 30 * time.Second},
    MaxAttempts: 1,
})
headers := http.Header{
    "Authorization": []string{"Bearer " + os.Getenv("SEAM_API_KEY")},
}
var response map[string]interface{}
err := caller.Call(context.Background(), &core.CallParams{
    URL: "https://connect.getseam.com/connect_webviews/get",
    Method: http.MethodPost,
    Headers: headers,
    Request: map[string]string{"connect_webview_id": "11111111-1111-1111-1111-111111111111"},
    Response: &response,
})
if err != nil {
    panic(err)
}
connectWebview := response["connect_webview"].(map[string]interface{})

fmt.Println(connectWebview["login_successful"]) // true
fmt.Println(connectWebview["connected_account_id"])
```

```bash Seam CLI theme={"dark"}
seam connect-webviews get --connect-webview-id "$CONNECT_WEBVIEW_ID"
```

***

## 3 — Retrieve the UniFi Protect Cameras

After a UniFi console is linked with Seam, each camera adopted by that console is a device with the `unifi_protect_camera` device type. List the devices for the connected account.

The top-level [`can_stream_live_video` capability flag](https://www.seam.co/docs/capability-guides/device-and-system-capabilities#cameras) is `true` when the camera supports live video and is online. The console and its paired Seam Bridge must both be online. Check the flag again immediately before [opening a live view](https://www.seam.co/docs/capability-guides/cameras/stream-live-video.md).

```javascript JavaScript theme={"dark"}
const cameras = await seam.devices.list({
  connected_account_id: updatedWebview.connected_account_id,
})

const camera = cameras[0]

console.log(camera.device_type) // unifi_protect_camera
console.log(camera.properties.online) // true
console.log(camera.can_stream_live_video)

console.log(camera)
/*
{
  device_id: '...',
  device_type: 'unifi_protect_camera',
  properties: {
    online: true,
    manufacturer: 'ubiquiti',
    name: 'Front Door',
    unifi_protect_metadata: {
      // camera_id, camera_name, model, mac, has_mic, nvr_name (all optional)
    }
  },
  ...
}
*/
```

```bash cURL theme={"dark"}
curl -X 'POST' \
  'https://connect.getseam.com/devices/list' \
  -H "Authorization: Bearer ${SEAM_API_KEY}" \
  -H 'Content-Type: application/json' \
  -d "{
    \"connected_account_id\": \"${CONNECTED_ACCOUNT_ID}\"
  }"
```

```python Python theme={"dark"}
cameras = seam.devices.list(
    connected_account_id=updated_webview.connected_account_id
)

camera = cameras[0]

assert camera.device_type == "unifi_protect_camera"
assert camera.properties["online"] is True
print(camera.can_stream_live_video)

print(camera)
```

```ruby Ruby theme={"dark"}
camera = seam.devices.list(
  connected_account_id: updated_webview.connected_account_id
).first

puts camera.device_type # unifi_protect_camera
puts camera.properties.online # true
puts camera.can_stream_live_video

puts camera
```

```php PHP theme={"dark"}
<?php
$cameras = $seam->devices->list(
  connected_account_id: $updated_webview->connected_account_id
);

$camera = $cameras[0];

echo $camera->device_type; // unifi_protect_camera
echo $camera->properties->online; // true
echo $camera->can_stream_live_video;
```

```csharp C# theme={"dark"}
var cameras = seam.Devices.List(
  connectedAccountId: updatedWebview.ConnectedAccountId
);

var camera = cameras[0];

Console.WriteLine(camera.Properties.Online); // true
Console.WriteLine(camera.CanStreamLiveVideo);
```

```java Java theme={"dark"}
var cameras = seam.devices().list(
  DevicesListRequest.builder()
    .connectedAccountId(updatedWebview.getConnectedAccountId())
    .build()
);

var camera = cameras.get(0);

System.out.println(camera.getProperties().getOnline()); // true
System.out.println(camera.getCanStreamLiveVideo());
```

```go Go theme={"dark"}
import (
    "context"
    "fmt"
    "net/http"
    "os"
    "time"

    "github.com/seamapi/go/core"
)

caller := core.NewCaller(&core.CallerParams{
    Client: &http.Client{Timeout: 30 * time.Second},
    MaxAttempts: 1,
})
headers := http.Header{
    "Authorization": []string{"Bearer " + os.Getenv("SEAM_API_KEY")},
}
var response map[string]interface{}
err := caller.Call(context.Background(), &core.CallParams{
    URL: "https://connect.getseam.com/devices/list",
    Method: http.MethodPost,
    Headers: headers,
    Request: map[string]string{"connected_account_id": "22222222-2222-2222-2222-222222222222"},
    Response: &response,
})
if err != nil {
    panic(err)
}
devices := response["devices"].([]interface{})
camera := devices[0].(map[string]interface{})

fmt.Println(camera["device_type"]) // unifi_protect_camera
fmt.Println(camera["can_stream_live_video"])
```

```bash Seam CLI theme={"dark"}
seam devices list --connected-account-id "$CONNECTED_ACCOUNT_ID"
```

***

## 4 — Receive Motion Events

When UniFi Protect detects motion on a connected camera, Seam creates a `camera.activated` event with `activation_reason` set to `motion_detected`. If UniFi Protect reports a smart detection, the event also has a `motion_sub_type`: `human`, `vehicle`, `package`, or `other`. To receive these events as they happen, [set up a webhook](https://www.seam.co/docs/developer-tools/webhooks.md). You can also [list events](https://www.seam.co/docs/api/events/list.md) for the camera.

> [!NOTE]
>
> Verify motion events against a real console in a non-sandbox workspace.

```javascript JavaScript theme={"dark"}
const events = await seam.events.list({
  device_id: camera.device_id,
  event_type: 'camera.activated',
  since: '2026-01-01T00:00:00Z',
})

console.log(events)
```

```bash cURL theme={"dark"}
curl -X 'POST' \
  'https://connect.getseam.com/events/list' \
  -H "Authorization: Bearer ${SEAM_API_KEY}" \
  -H 'Content-Type: application/json' \
  -d "{
    \"device_id\": \"${DEVICE_ID}\",
    \"event_type\": \"camera.activated\",
    \"since\": \"2026-01-01T00:00:00Z\"
  }"
```

```python Python theme={"dark"}
events = seam.events.list(
    device_id=camera.device_id,
    event_type="camera.activated",
    since="2026-01-01T00:00:00Z"
)

print(events)
```

```ruby Ruby theme={"dark"}
events = seam.events.list(
  device_id: camera.device_id,
  event_type: "camera.activated",
  since: "2026-01-01T00:00:00Z"
)

puts events.inspect
```

```php PHP theme={"dark"}
<?php
$events = $seam->events->list(
  device_id: $camera->device_id,
  event_type: "camera.activated",
  since: "2026-01-01T00:00:00Z"
);

echo json_encode($events);
```

```csharp C# theme={"dark"}
var events = seam.Events.List(
  deviceId: camera.DeviceId,
  eventType: Seam.Api.Events.ListRequest.EventTypeEnum.CameraActivated,
  since: "2026-01-01T00:00:00Z"
);

foreach (var cameraEvent in events)
{
  Console.WriteLine(cameraEvent);
}
```

```java Java theme={"dark"}
var events = seam.events().list(
  EventsListRequest.builder()
    .deviceId(camera.getDeviceId())
    .eventType("camera.activated")
    .since("2026-01-01T00:00:00Z")
    .build()
);

System.out.println(events);
```

```go Go theme={"dark"}
import (
    "context"
    "fmt"
    "net/http"
    "os"
    "time"

    "github.com/seamapi/go/core"
)

caller := core.NewCaller(&core.CallerParams{
    Client: &http.Client{Timeout: 30 * time.Second},
    MaxAttempts: 1,
})
headers := http.Header{
    "Authorization": []string{"Bearer " + os.Getenv("SEAM_API_KEY")},
}
var response map[string]interface{}
err := caller.Call(context.Background(), &core.CallParams{
    URL: "https://connect.getseam.com/events/list",
    Method: http.MethodPost,
    Headers: headers,
    Request: map[string]string{
        "device_id":  "33333333-3333-3333-3333-333333333333",
        "event_type": "camera.activated",
        "since":      "2026-01-01T00:00:00Z",
    },
    Response: &response,
})
if err != nil {
    panic(err)
}
fmt.Println(response["events"])
```

```bash Seam CLI theme={"dark"}
seam events list --device-id "$DEVICE_ID" --event-type camera.activated --since "2026-01-01T00:00:00Z"
```

**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",
  "activation_reason": "motion_detected",
  "motion_sub_type": "human",
  "media_ids": [
    "01a0d985-621c-7b3e-9f2d-4a5b6c7d8e9f",
    "01a0d985-621c-7c4f-8a3e-5b6c7d8e9f0a"
  ],
  "occurred_at": "2026-11-02T17:03:12.000Z",
  "created_at": "2026-11-02T17:03:12.412Z"
}
```

***

## 5 — Retrieve Event Clips

If you enter a local-only user with View Only Protect access when you connect a real console, Seam retrieves a 20-second clip and a thumbnail for each motion event from the console's recordings. The event's `media_ids` list both. Fetch each one with [`/media/get`](https://www.seam.co/docs/api/media/get). The returned `url` expires within minutes. In Seam's testing, a clip was available about 30 seconds after the motion started. If the media is not yet `available`, call `/media/get` again later.

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

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

```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"
}'
```

```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"}
<?php
$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());
```

```go Go theme={"dark"}
import (
    "context"
    "fmt"
    "net/http"
    "os"
    "time"

    "github.com/seamapi/go/core"
)

caller := core.NewCaller(&core.CallerParams{
    Client: &http.Client{Timeout: 30 * time.Second},
    MaxAttempts: 1,
})
headers := http.Header{
    "Authorization": []string{"Bearer " + os.Getenv("SEAM_API_KEY")},
}
var response map[string]interface{}
err := caller.Call(context.Background(), &core.CallParams{
    URL: "https://connect.getseam.com/media/get",
    Method: http.MethodPost,
    Headers: headers,
    Request: map[string]string{"media_id": "01a0d985-621c-7b3e-9f2d-4a5b6c7d8e9f"},
    Response: &response,
})
if err != nil {
    panic(err)
}
media := response["media"].(map[string]interface{})

fmt.Println(media["status"], media["url"])
```

```bash Seam CLI theme={"dark"}
seam media get --media-id "01a0d985-621c-7b3e-9f2d-4a5b6c7d8e9f"
```

The sandbox console accepts the sandbox local user so that you can test the Connect Webview. Test clips against a real console. For the full flow, see [Retrieve Event Media](https://www.seam.co/docs/capability-guides/cameras/retrieve-event-media.md) and [UniFi Protect event clips](https://www.seam.co/docs/device-and-system-integration-guides/unifi-protect-cameras.md#event-clips-and-thumbnails).

***

## 6 — Stream Live Video

Before streaming a UniFi Protect camera's live video to a browser over WebRTC, check that its [`can_stream_live_video` capability flag](https://www.seam.co/docs/capability-guides/device-and-system-capabilities#cameras) is `true`. The flag is `false` while the console or its Seam Bridge is offline. Live view uses a [client session token](https://www.seam.co/docs/core-concepts/authentication/client-session-tokens.md) scoped to the camera, so your users' browsers never see your API key. The first live view of a camera turns on that camera's low-quality RTSPS stream in UniFi Protect. The sandbox live view plays a test pattern. For the full flow and session errors to handle, see [Stream Live Video](https://www.seam.co/docs/capability-guides/cameras/stream-live-video.md).

***

## Next Steps

Now that you've completed this guide, you can try to connect a real UniFi console. To do so, make sure to switch to a non-sandbox workspace and API key, as real devices cannot be connected to sandbox workspaces. See the [UniFi Protect Setup Guide](https://www.seam.co/docs/device-and-system-integration-guides/unifi-protect-cameras/unifi-protect-setup-guide.md) for the Seam Bridge, API key, and local user steps.

In addition, if you'd like to explore other aspects of Seam, here is a list of helpful resources:

* [Receiving webhooks](https://www.seam.co/docs/developer-tools/webhooks.md) for [device events](https://www.seam.co/docs/api/events/list.md)
* [Stream Live Video](https://www.seam.co/docs/capability-guides/cameras/stream-live-video.md)
* [Retrieve Event Media](https://www.seam.co/docs/capability-guides/cameras/retrieve-event-media.md)
* [Seam Bridge](https://www.seam.co/docs/capability-guides/seam-bridge.md)
* [Core Concepts](https://www.seam.co/docs/core-concepts/overview.md)

If you have any questions or want to report an issue, email us at [support@seam.co](mailto:support@seam.co).


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.