# Fleet links CLI commands

> Canonical HTML: https://screenrig.ai/docs/cli/fleet-links

Create fleet links for device managers and move another signage platform's media, playlists, schedules and screens into screenRIG. Every command here runs as described in the [CLI reference](https://screenrig.ai/docs/cli.md): global flags, JSON output, exit codes, revisions and retries.

## Fleet links

A fleet link is one Player URL that you put in a device manager, such as a kiosk app in the Google Admin console. Every device that opens it becomes its own screen in this project, with no pairing code. An organization has at most 50 fleet links across its projects.

Each new screen gets the link's tags, fields and playlist, and a label from the device: its administrator asset ID, then its location, else the next default label. A device keeps its one screen across reloads and relaunches. After a powerwash or reinstall, a device that reports the serial or DUID of a screen a fleet link of this project created recovers that screen instead of creating another, once its old key has been silent for ten minutes. `screen show` returns `fleet_link_id` for a screen a fleet link created, and `--human` prints `Created by fleet link: flk_…`. The id stays after the link is deleted.

A link can end at a time you choose: `--expires-at` stops it creating and recovering screens from then on, as a disabled link does, and a device that opens it shows its pairing code. The screens it created keep playing. `fleet-links show --android-provisioning` prints the payload that provisions a factory-reset Android device as the device owner; see [Android device owner by QR code](https://screenrig.ai/docs/players.md#android-device-owner).

The URL, `https://play.screenrig.ai/player/#fleet=srfl_…`, is a credential that creates screens. Give it only to the device manager. The secret travels in the URL fragment, which browsers never send to a server. A Read only credential receives `data.url` empty with `url_redacted: true`, and human output then says `URL: withheld from a Read only credential`. Otherwise human output prints the URL under a `Sensitive fleet link URL` label, and `fleet-links list --human` never prints it. The CLI checks a returned URL before printing it: HTTPS, the `/player/` path, and the secret only in `#fleet=`. It withholds a URL that fails the check, with the warning `fleet_link_url_withheld`, and it keeps fleet link answers out of its operation log.

| Problem code | Exit | Meaning |
| --- | --- | --- |
| `fleet_link_limit_reached` | `5` | The organization already has 50 fleet links. Nothing was created. Delete one first. |
| `invalid_request` | `8` | A field was refused, such as a `--playlist-id` that names no playlist in this project. `error.next` is `playlist list` for an unknown playlist. |
| `not_found` | `4` | No fleet link has this id in the project. |
| `revision_conflict` | `6` | The `--expect-rev` revision was stale. Run `fleet-links show`, then retry. |
| `fleet_link_secret_unavailable` | `3` | `fleet-links show --android-provisioning` received no usable URL, as for a Read only credential. A Manage sign-in receives it. |

### `fleet-links create`

Create a fleet link and receive its Player URL.

```console
fleet-links create --name NAME [--tag TAG]... [--field NAME=VALUE]... [--playlist-id ID] [--max-screens N]
                   [--expires-at TIME] [--disabled]
```

| Parameter | Type | Required | Default | Notes |
| --- | --- | --- | --- | --- |
| `--name NAME` | string | yes | none | The link's name, 1 to 80 characters without control characters. |
| `--tag TAG` | string | no | no tags | A tag for each new screen, 1 to 32 letters or digits. Repeat it, or list tags comma-separated; at most 16. |
| `--field NAME=VALUE` | string | no | no fields | A field for each new screen, which cards read as `{screen.field.NAME}`. `NAME` is 1 to 32 lower case letters, digits or underscores and `VALUE` 1 to 200 characters. Repeat it; at most 32. |
| `--playlist-id ID` | string | no | none | The `pl_` playlist assigned to each new screen. |
| `--max-screens N` | integer | no | no cap | The most screens the link creates, 1 to 10000. |
| `--expires-at TIME` | string | no | never | When the link stops creating and recovering screens: an RFC 3339 time with seconds and a zone, such as `2026-12-31T23:59:59Z`. A time already past expires the link at once. |
| `--disabled` | boolean | no | off | Create it disabled. It creates no screens until `fleet-links update --enable`. |

```console
screenrig fleet-links create --name "Lobby displays" --tag Lobby --tag Floor2 --playlist-id pl_01EXAMPLEPLAYLIST000000
```

```console
screenrig fleet-links create --name "Trade show" --field booth=A12 --expires-at 2026-12-31T23:59:59Z
```

The envelope is the fleet link with its Player URL in `data.url`. Put that URL in the device manager. After an interrupted or ambiguous create, rerun the identical command within 24 hours: it reuses the saved idempotency key, and the server returns the same link instead of creating another.

### `fleet-links list`

List this project's fleet links, oldest first.

```console
fleet-links list
```

```console
screenrig fleet-links list
```

A Read only credential receives each `data.items[].url` empty with `url_redacted: true`. `--human` prints `ID`, `LABEL`, `ENABLED`, `SCREENS`, `TAGS`, and `LAST_USED`, adds `EXPIRES` while a listed link has an expiry and marks one past it `(expired)`, and never prints the URL. `SCREENS` is the number of screens the link created, or created/max with `--max-screens`; recoveries are not counted.

### `fleet-links show`

Inspect a fleet link and receive its Player URL.

```console
fleet-links show <id> [--android-provisioning]
```

| Parameter | Type | Required | Default | Notes |
| --- | --- | --- | --- | --- |
| `<id>` | string | yes | none | The `flk_` fleet link id. |
| `--android-provisioning` | boolean | no | off | Print the Android device owner provisioning JSON for this link in place of its details, to encode as a QR code for a factory-reset device. It carries the link's secret, so it needs a Manage sign-in. |

```console
screenrig fleet-links show flk_01EXAMPLELINK0000
```

```console
screenrig fleet-links show flk_01EXAMPLELINK0000 --android-provisioning
```

`screens_created` counts the screens the link created, and `last_used_at` is the last time it created or recovered one. `expires_at` is present when the link has an expiry, and human output marks it `(expired)` once it has passed.

With `--android-provisioning`, `data.id` and `data.provisioning` replace the link's details. The JSON carries the link's secret: show its QR code only to the person at the device. A Read only credential receives no URL to build it from and gets `fleet_link_secret_unavailable` (exit 3). [Android device owner by QR code](https://screenrig.ai/docs/players.md#android-device-owner) lists the payload.

### `fleet-links update`

Change what a fleet link gives the screens it creates from now on.

```console
fleet-links update <id> [--name NAME] [--enable | --disable] [--tag TAG]... [--clear-tags]
                        [--field NAME=VALUE]... [--clear-fields]
                        [--playlist-id ID | --clear-playlist] [--max-screens N | --clear-max-screens]
                        [--expires-at TIME | --clear-expires-at] [--expect-rev REVISION]
```

| Parameter | Type | Required | Default | Notes |
| --- | --- | --- | --- | --- |
| `<id>` | string | yes | none | The `flk_` fleet link id. |
| `--name NAME` | string | at least one change | unchanged | 1 to 80 characters. |
| `--enable` | boolean | at least one change | off | The link creates and recovers screens again. |
| `--disable` | boolean | at least one change | off | The link creates and recovers no screens; a device opening it shows its pairing code instead. |
| `--tag TAG` | string | at least one change | unchanged | Replaces the whole tag set. Repeat it, or list tags comma-separated; at most 16. |
| `--clear-tags` | boolean | at least one change | off | New screens get no tags. |
| `--field NAME=VALUE` | string | at least one change | unchanged | Replaces the whole field set, as in `fleet-links create`. Repeat it; at most 32. |
| `--clear-fields` | boolean | at least one change | off | New screens get no fields. |
| `--playlist-id ID` | string | at least one change | unchanged | The `pl_` playlist for new screens. |
| `--clear-playlist` | boolean | at least one change | off | New screens get no playlist. |
| `--max-screens N` | integer | at least one change | unchanged | The most screens the link creates in all, 1 to 10000. |
| `--clear-max-screens` | boolean | at least one change | off | Remove the cap. |
| `--expires-at TIME` | string | at least one change | unchanged | Set or move the expiry: an RFC 3339 time with seconds and a zone. A time already past expires the link at once. |
| `--clear-expires-at` | boolean | at least one change | off | Remove the expiry; the link never expires. |
| `--expect-rev REVISION` | string | no | none | Optional revision precondition. |

```console
screenrig fleet-links update flk_01EXAMPLELINK0000 --playlist-id pl_01EXAMPLEPLAYLIST000000
```

`update` changes only the fields you pass, and only for screens the link creates from now on. Screens it already created keep their tags, playlist, and label. An expired link behaves as a disabled one until its expiry moves or is cleared. The URL is unchanged; use `rotate` to replace it.

### `fleet-links rotate`

Replace a fleet link's URL and receive the new one.

```console
fleet-links rotate <id> [--expect-rev REVISION]
```

| Parameter | Type | Required | Default | Notes |
| --- | --- | --- | --- | --- |
| `<id>` | string | yes | none | The `flk_` fleet link id. |
| `--expect-rev REVISION` | string | no | none | Optional revision precondition. |

```console
screenrig fleet-links rotate flk_01EXAMPLELINK0000
```

The envelope carries the new URL in `data.url`. The old URL stops working at once: a device that opens it shows its pairing code. Screens the link already created keep playing, because a paired Player never presents the link again. Put the new URL in the device manager. After an interrupted or ambiguous rotation, rerun the identical command: it reuses the saved idempotency key, so the server returns the rotation that already happened instead of rotating again.

### `fleet-links delete`

Delete a fleet link. Its URL stops working at once, and the screens it created stay.

```console
fleet-links delete <id> [--expect-rev REVISION]
```

| Parameter | Type | Required | Default | Notes |
| --- | --- | --- | --- | --- |
| `<id>` | string | yes | none | The `flk_` fleet link id. |
| `--expect-rev REVISION` | string | no | none | Optional revision precondition. |

```console
screenrig fleet-links delete flk_01EXAMPLELINK0000
```

Screens the link created keep their playlists and tags.

## Migrate

`migrate` moves your previous signage platform's media, playlists, layouts, schedules and screens into the project. It runs in the CLI on your machine: the platform's key is read from a `--credentials FILE` of `NAME=VALUE` lines, goes only to that platform, and is never stored, printed, logged or sent to screenRIG, which sees ordinary media, playlist, fleet link, campaign and screen writes. `migrate vendors` lists the platforms it reads, and the `files` fallback reads a folder or `.zip` of media from any other platform.

Run the steps in order: `migrate vendors` (what a platform takes), `migrate scan` (read the account and report what can and cannot come over; it needs no screenRIG sign-in), `migrate apply --dry-run` (the plan), `migrate apply` (needs Manage access), then `migrate pair` for each display.

`--dir DIR` is the migration directory, `./screenrig-migration-VENDOR` by default, created with mode `0700`. It holds `scan.json` (the scan and its report), `map.json` (which object each old item became, bound to this API URL, project, vendor and source account) and `staging/` (downloads in progress). `apply` and `pair` use the one `screenrig-migration-*` directory in the working directory, or take `--dir`; one of them runs in a directory at a time.

| Problem code | Exit | Meaning |
| --- | --- | --- |
| `migration_dir_mismatch` | `2` | The directory was made for another project, API URL, vendor or source account. Use another `--dir`. |
| `vendor_refused` | `3` | The platform refused the key. Make a read-only key (`migrate vendors VENDOR` shows how), fix the `--credentials` file and run the same command again. |
| `vendor_not_found` | `4` | The platform answered 404. `--url` is usually wrong. |
| `vendor_rate_limited` | `7` | The platform is rate limiting the key. Run the command again later. |
| `vendor_file_gone` | `8` | A media file address expired or is protected. `migrate scan` again refreshes it, and `--files` supplies a file that still fails. |
| `vendor_file_stalled` | `8` | A media download went silent. Run `apply` again. |
| `vendor_bad_response` | `9` | The platform sent an answer the CLI cannot use. |
| `vendor_unavailable` | `9` or `10` | The platform could not be reached or kept failing. |

### `migrate vendors`

List the platforms `migrate` reads, what each takes and how to make its read-only key.

```console
migrate vendors [VENDOR]
```

| Parameter | Type | Required | Default | Notes |
| --- | --- | --- | --- | --- |
| `VENDOR` | string | no | every platform | One platform's id. An id it does not list is `usage_error` (exit 2) naming the ids that exist. |

```console
screenrig migrate vendors
screenrig migrate vendors files
```

It works offline and needs no sign-in. `data.vendors` holds, for each platform, its `id` and `name`; `inputs`, with the `flags` it takes (each with `flag` and `required`, and `values` for a region) and the `credentials` lines its key file holds (`name`, `secret`, `required` and `help`); `connect`, the steps that make a read-only key; `maps`, what comes over; `skips`, what does not; `docs`; and `example`, a working `migrate scan` command. Platforms are sorted by id with `files` last. `files` takes a folder or `.zip` and an optional time zone, and needs no key.

### `migrate scan`

Read a signage account and report what can and cannot come over. It writes `scan.json` and sends nothing to screenRIG.

```console
migrate scan --from VENDOR [--url ADDRESS] [--region REGION] [--timezone ZONE] [--files PATH]
    [--credentials FILE] [--dir DIR]
```

| Parameter | Type | Required | Default | Notes |
| --- | --- | --- | --- | --- |
| `--from VENDOR` | string | yes | none | The platform to read: an id from `migrate vendors`, or `files`. |
| `--url ADDRESS` | string | per platform | none | The address of a self-hosted platform. |
| `--region REGION` | string | per platform | none | The region of a hosted account, for a platform that has regions. |
| `--timezone ZONE` | string | no | none | An IANA time zone name such as `America/Toronto`, set on each screen whose old platform names none. Without it a screen follows the zone its Player reports. |
| `--files PATH` | path | per platform | none | A folder or `.zip`. With `--from files` it holds the media; with another platform it holds the originals that platform's API cannot hand out, matched to its media by file name. |
| `--credentials FILE` | path | per platform | none | The key file: `NAME=VALUE` lines with the names `migrate vendors VENDOR` lists. Blank lines and `#` comments are skipped, and a value may sit in one pair of quotes. A required name missing from the file is `usage_error` (exit 2) naming it. |
| `--dir DIR` | path | no | `./screenrig-migration-VENDOR` | The migration directory. |

```console
screenrig migrate scan --from files --files ./signage-media
screenrig migrate scan --from files --files ./signage-media.zip --dir ./old-signage --timezone America/Toronto
```

A flag the platform does not take, or one it needs and lacks, is `usage_error` (exit 2) naming what the platform takes. No flag takes the key itself. A new scan replaces `scan.json` and keeps `map.json`, and it exits `0` when it completes, even when some items cannot come over.

`data` is the report: `vendor`, `label`, `scanned_at`, `found` and `importable` (counts of `media`, `playlists`, `layouts`, `schedules`, `groups` and `screens`), `not_imported` (each with `kind`, `source_id`, `name`, a `reason` and usually a `next` step), `lossy` (each with `kind`, `source_id`, `name` and the `change` that happens on the way), `notes`, `dir` and `scan_file`. A file the platform hands out downloads at `apply`. A file it does not, or hands out only as a re-encoded copy, is matched by name in `--files`; with no match, or several, it is in `not_imported` or `lossy` with the step that fixes it.

`--from files` needs no key. It reads each image, video and audio file by its contents, not its extension. An optional `playlists.csv` at the root has the columns `playlist`, `file` (a path or a unique file name), `duration` (seconds) and `order`; without it each folder that holds media becomes a playlist in natural file-name order, and the files at the root form one named after the folder or zip. An optional `screens.csv` has `name`, `group` (several separated by `;`), `playlist` and `timezone`, and each row becomes a waiting screen with its groups as tags. Other files and encrypted zip entries are in `not_imported`. A `files` scan has no schedules: create them with `campaign create` once the playlists exist.

### `migrate apply`

Import what the scan found: media, playlists and layouts, waiting screens and campaigns. It is safe to rerun.

```console
migrate apply [--dir DIR] [--dry-run] [--only KINDS] [--folder ID] [--canvas WIDTHxHEIGHT]
    [--credentials FILE] [--concurrency N] [--no-transcode]
```

| Parameter | Type | Required | Default | Notes |
| --- | --- | --- | --- | --- |
| `--dir DIR` | path | no | the one `./screenrig-migration-*` directory | The migration directory that holds `scan.json`. |
| `--dry-run` | boolean | no | off | Read the project and print the plan. It writes nothing and needs no key. |
| `--only KINDS` | string | no | all four | Comma-separated, each at most once: `media`, `playlists`, `screens`, `schedules`. |
| `--folder ID` | string | no | a new folder named `Imported from` and the platform's name | An existing media folder (`mfd_…`) for the imported media. |
| `--canvas WIDTHxHEIGHT` | string | no | `1920x1080` | The canvas of plain playlists, each side at least 16 pixels. A layout keeps its own size. |
| `--credentials FILE` | path | no | none | The key file, read only when a media file is downloaded from the platform. |
| `--concurrency N` | integer | no | 4 | Media imported at once, 1 to 8. |
| `--no-transcode` | boolean | no | off | Upload the source bytes unchanged, as `media upload` does. |

```console
screenrig migrate apply --dry-run
screenrig migrate apply --dir ./screenrig-migration-files
screenrig migrate apply --only media,playlists --concurrency 2
```

`apply` imports in order: media into one folder, playlists and layouts, one waiting screen per old screen, and a campaign per schedule that [campaigns](https://screenrig.ai/docs/cli/campaigns.md#campaigns) can express. It reads the platform again only to download media files. A playlist has one page per item: images and videos fill the canvas, a web address becomes an `iframe` page, audio becomes the soundtrack and a nested playlist is inlined. A layout becomes a playlist on its own canvas with one page per item of its main zone. A schedule that repeats by the month, year, hour or minute, or on a numbered weekday of the month, cannot become a campaign: `scan` lists it in `not_imported`.

`--dry-run` reads only the project and answers `data.plan` (for `media`, `playlists`, `layouts`, `screens` and `schedules`: `create`, `reuse`, `recreate`, `waiting` and `fail` counts), `data.items`, `data.bytes` to download, `data.placeholders` counts and `data.project`, the fleet links and campaigns in use against their maximums of 50 and 100. Progress goes to stderr as JSON lines (`migrate_progress` with `phase`, `done` and `total`), or as text with `--human`. A rerun reuses what still exists, makes again anything you deleted and never changes what it made. A real run answers `data.applied` (the same counts), `data.waiting` (items that wait on media or a playlist that is not imported, each with a `reason`), `data.failed`, `data.placeholders` (one entry per old screen with its `source_id`, `name` and `state`: `linked`, `unlinked`, `paired`, `finished` or `waiting`), `data.tags` and `data.folder_id`.

A group becomes a tag made of letters and digits, numbered when it collides with a tag another screen carries. A campaign targets the tags of the groups and screens its schedule names, with its dates, days, hours and priority. A campaign reads its dates and hours in each screen's own timezone and needs a default playlist (`no_default_playlist`): `scan` notes the screens that lack one, and `screen assign` sets it after pairing.

A waiting screen is a [fleet link](https://screenrig.ai/docs/cli/fleet-links.md#fleet-links) of one screen that carries the old name, the groups as tags and the playlist. An organization holds at most 50 fleet links across its projects; the rest are `unlinked` in `data.placeholders` and wait for a Player code (`migrate pair`). A Player that opens a link becomes a screen, and running `apply` again gives it the old name and time zone and deletes the link. `apply` never prints a fleet link URL: [`fleet-links show`](https://screenrig.ai/docs/cli/fleet-links.md#cmd-fleet-links-show) does.

A failing item keeps `ok` `true` with `data.failed` (each with `key`, `name`, `code`, `status` and `detail`) and the warning `migration_items_failed`; the items that depend on it wait, and the exit code follows the status of the last failure. A media file that fails alone (an expired address, an answer that is not a file, a download that goes silent) fails that media only; for an expired address run `migrate scan` again. A refusal that would repeat for every item (`quota_exceeded`, `campaign_limit_reached`, `insufficient_access`, `pending_review`, the platform refusing the key, a lost connection) ends the run with `data.stopped` (`code`, `detail` and `next`) and that problem's exit code. Fix the cause and run the same command again: it continues where it stopped.

### `migrate pair`

Pair a Player code to one waiting screen, under its old name, playlist, time zone and tags.

```console
migrate pair <CODE...> --screen ID_OR_NAME [--dir DIR]
```

| Parameter | Type | Required | Default | Notes |
| --- | --- | --- | --- | --- |
| `<CODE...>` | string | yes | none | The code the Player shows, such as `ABC 234`: six characters from `23456789ABCDEFGHJKMNPQRSTUVWXYZ`, in either case. Spaces and dashes between them are ignored, so it needs no quotes. |
| `--screen ID_OR_NAME` | string | yes | none | The waiting screen: its source id or its exact old name. A name that two screens share lists their ids. |
| `--dir DIR` | path | no | the one `./screenrig-migration-*` directory | The migration directory. |

```console
screenrig migrate pair ABC 234 --screen "Front desk"
screenrig migrate pair ABC234 --screen 3 --dir ./old-signage
```

Run `migrate apply` first: pairing needs the map and the playlist it made. `pair` claims the code for the screen under its old name, sets its playlist, time zone and `migrated_from` field, sets its tags, deletes the waiting screen's link and records the result. `data` holds `source_id`, `name`, `screen_id`, `steps` (`paired`, `updated`, `tagged`, `link deleted`, `recorded`), `next` and, when this run claimed the code, `public_url`. Each step is recorded as it lands, so a rerun finishes what an interrupted pair left. A screen that is already paired is `resource_conflict` (exit 5) naming it. The Player finishes pairing the next time it contacts the service; `screen show` then reports `state` `active`.

## Other command groups

- [Projects](https://screenrig.ai/docs/cli/projects.md)
- [Service clients](https://screenrig.ai/docs/cli/service-clients.md)
- [Help and support](https://screenrig.ai/docs/cli/help.md)
- [Media](https://screenrig.ai/docs/cli/media.md)
- [Media sync](https://screenrig.ai/docs/cli/media-sync.md)
- [Playlists](https://screenrig.ai/docs/cli/playlists.md)
- [Cards](https://screenrig.ai/docs/cli/cards.md)
- [Applications](https://screenrig.ai/docs/cli/applications.md)
- [Screens](https://screenrig.ai/docs/cli/screens.md)
- [Schedules](https://screenrig.ai/docs/cli/schedules.md)
- [Campaigns](https://screenrig.ai/docs/cli/campaigns.md)
- [Device health](https://screenrig.ai/docs/cli/device.md)
- [Reviews and notes](https://screenrig.ai/docs/cli/reviews.md)
- [Notifications](https://screenrig.ai/docs/cli/notifications.md)
- [Reports](https://screenrig.ai/docs/cli/reports.md)
- [Data sources](https://screenrig.ai/docs/cli/data-sources.md)
- [BI dashboards](https://screenrig.ai/docs/cli/bi.md)
- [Triggers](https://screenrig.ai/docs/cli/triggers.md)
- [Advertising](https://screenrig.ai/docs/cli/ads.md)
