> ## Documentation Index > Fetch the complete documentation index at: https://bunny.net/docs/llms.txt > Use this file to discover all available pages before exploring further. # Migrate from Cloudflare Stream to Bunny Stream > A step-by-step walkthrough for migrating a Cloudflare Stream video library into Bunny Stream using the Bunny CLI. > [!TIP] > > Run `bunny skills install --global` so AI coding agents, such as Claude Code, Cursor, and Codex, can run this migration for you. Then ask your agent: "Migrate my Cloudflare Stream videos to Bunny Stream." > [!WARNING] > > `bunny stream import` is in [Public Preview](https://bunny.net/docs/product-release-stages.md#stage-2-public-preview) and still in development. Commands, flags, and output may change, so what you see can differ from the examples in this guide. To report a bug, [open an issue on the Bunny CLI repository](https://github.com/BunnyWay/cli/issues/new). [Bunny Stream](https://bunny.net/stream/) bills only for the storage and bandwidth you use. See the [Bunny Stream pricing page](https://bunny.net/pricing/stream/) for current rates. ## Overview ![Diagram of the Bunny CLI listing Cloudflare Stream videos and Bunny Stream fetching each MP4 download directly from Cloudflare](https://mintcdn.com/bunnynet-cb9733c2/ZkttdKBCsq-yq0qO/images/stream/migrate-from-cloudflare-stream/how-bunny-stream-import-works.png?fit=max\&auto=format\&n=ZkttdKBCsq-yq0qO\&q=85\&s=feba6e4c4ff9949754b3de343968ff75) The Bunny CLI lists every ready video on your Cloudflare Stream account, asks Cloudflare to generate an MP4 download for each one, and hands that download to Bunny Stream, which fetches the file directly from Cloudflare. Nothing is downloaded to your machine. Each imported video is tagged with its Cloudflare Stream ID under the `cfStreamId` metatag, so running the import again skips videos that are already imported. ## Scope The import copies the video files, names, and descriptions. Settings that control how and where videos play stay on Cloudflare, so plan to set up their Bunny equivalents. | Cloudflare Stream item | Migrated | Notes | | - | - | - | | Video file | Yes | Copied from an MP4 download that Cloudflare generates for the import. | | Name | Yes | Taken from the video's `name` metadata. A video without a name keeps its video ID as its title. | | Description | Yes | Stored in the `description` metatag on the Bunny video, when the video has one. | | Video ID | Yes | Stored in the `cfStreamId` metatag on the Bunny video. | | Videos that are not ready to stream | No | Only videos that finished processing on Cloudflare are listed. | | Videos with `requireSignedURLs` | Not until you change them | They fail the import. See the privacy note below. | | Signed URLs and allowed origins | No | Use Bunny's library security settings instead. See the privacy note below. | | Captions | No | Upload them with `bunny stream videos captions add`, or generate new ones with [transcription](https://bunny.net/docs/stream/transcribing.md). | | Thumbnails | No | Bunny generates new thumbnails when it encodes each video. | | Watermark profiles | No | They are not carried over as settings. | | Live inputs | No | Only recorded videos are imported. | | Analytics | No | Bunny Stream collects its own [statistics](https://bunny.net/docs/stream/statistics.md) from the first view. | > [!WARNING] > > **Signed URLs and allowed origins are not migrated.** Every imported video follows the security settings of its Bunny library, and a new library allows playback from any site. > > * **`requireSignedURLs`**: The CLI cannot import a video that requires signed URLs, so it fails these videos with an error that names the setting. To import them, turn off **Require Signed URLs** on each one in Cloudflare Stream, run the import, then turn it back on. While it is off, the video plays on Cloudflare without a token, so keep that window short. > * **Allowed origins**: Cloudflare's per-video allowed origins are not copied. Add the same sites to the library's [Allowed domains](https://bunny.net/docs/stream/security-options.md#domains) on Bunny. > > Before you publish the new URLs, turn on [embed view token authentication](https://bunny.net/docs/stream/token-authentication.md) or [CDN token authentication](https://bunny.net/docs/stream/security.md#cdn-token-authentication) if you signed your Cloudflare playback URLs. ## Prerequisites * A [bunny.net account](https://dash.bunny.net/auth/register) * A [Cloudflare](https://dash.cloudflare.com) account with Cloudflare Stream ## Migrate your videos **1. Install and authenticate the Bunny CLI** Install the Bunny CLI with the shell installer or npm: > [tab] curl > > ```sh theme={null} > curl -fsSL https://cli.bunny.net/install.sh | sh > ``` > > The installer downloads a prebuilt binary for your platform and places it on your `PATH`. > [tab] npm > > ```sh theme={null} > npm install -g @bunny.net/cli > ``` See [Installation](https://bunny.net/docs/cli/installation.md) for more options. Then authenticate the CLI with your bunny.net account: ```sh theme={null} bunny login ``` `bunny login` opens your browser to complete authentication and stores the credentials locally. The first time you log in from an interactive terminal, it also offers to install the agent skill for AI coding tools. On a remote machine or in CI without a browser, run `bunny login --api-key "$BUNNYNET_API_KEY"` instead, with a key from [Account Settings > API key](https://dash.bunny.net/account/api-key). **2. Create a Bunny Stream library** Create a library for the imported videos, or skip this step to use an existing one: ```sh theme={null} bunny stream libraries create migrate-from-cloudflare-stream --replication-regions NY,SG ``` ```sh theme={null} ✓ Created video library migrate-from-cloudflare-stream (ID: 12345), replicated to DE, NY, SG. ``` DE is always the main storage region. Replicate to the regions closest to your audience, from `UK`, `SE`, `NY`, `LA`, `SG`, `SYD`, `BR`, and `JH`. You can't remove a [replication region](https://bunny.net/docs/stream/replication.md) later. > [!NOTE] > > Set encoding options, such as `--resolutions` or `--encoding-tier`, when you create the library. Changes you make later apply only to new uploads, not to videos that are already imported. Run `bunny stream libraries create --help` to see every option, or read [Encoding](https://bunny.net/docs/stream/encoding.md). When the CLI asks how to add videos, choose **Later**. You start the import in the next steps. Copy the library ID from the output. You pass it to `--library` in the import commands. The flag also accepts the library's name, and `--lib` works as a short alias. **3. Create a Cloudflare Stream API token** The Bunny CLI needs your Cloudflare **Account ID** and an API token with the **Stream:Edit** permission. Edit lets the CLI list your videos and generate an MP4 download for any video that does not already have one. To find your Account ID, open the [Cloudflare dashboard](https://dash.cloudflare.com) and go to **Build > Images & Stream > Hosted videos**. The Account ID appears in the sidebar under **Account details**. ![The Hosted videos page in the Cloudflare dashboard with the Account ID under Account details](https://mintcdn.com/bunnynet-cb9733c2/ZkttdKBCsq-yq0qO/images/stream/migrate-from-cloudflare-stream/cloudflare-account-id.png?fit=max\&auto=format\&n=ZkttdKBCsq-yq0qO\&q=85\&s=682869ab687e67edd4a4b24c690200b9) To create the token, open [User API Tokens](https://dash.cloudflare.com/profile/api-tokens) and click **+ Create Token**. ![The User API Tokens page in Cloudflare with the Create Token button](https://mintcdn.com/bunnynet-cb9733c2/ZkttdKBCsq-yq0qO/images/stream/migrate-from-cloudflare-stream/cloudflare-create-token.png?fit=max\&auto=format\&n=ZkttdKBCsq-yq0qO\&q=85\&s=b8527c76ab31b155db79b1eb1d4a9b4e) Click **Create Custom Token**, enter a token name, such as migrate-to-bunny, and add the permission **Account > Stream > Edit**. Edit includes read access, so this one permission covers both listing videos and generating their MP4 downloads. Click **Continue to summary**, then **Create Token**. ![The custom token form in Cloudflare with the Account Stream Edit permission](https://mintcdn.com/bunnynet-cb9733c2/ZkttdKBCsq-yq0qO/images/stream/migrate-from-cloudflare-stream/cloudflare-custom-token-permissions.png?fit=max\&auto=format\&n=ZkttdKBCsq-yq0qO\&q=85\&s=9448302569165e74b8dea0a4b0030642) Copy the token. Cloudflare shows it only once. ![The Cloudflare confirmation page showing the new API token](https://mintcdn.com/bunnynet-cb9733c2/ZkttdKBCsq-yq0qO/images/stream/migrate-from-cloudflare-stream/cloudflare-token-created.png?fit=max\&auto=format\&n=ZkttdKBCsq-yq0qO\&q=85\&s=44ea5c0261999b3427c9a7055c96e069) **4. Preview the migration with a dry run** Run the import with `--dry-run`, using the library ID from step 2: ```sh theme={null} bunny stream import --library 12345 --source cloudflare --dry-run ``` `--dry-run` prints the import plan without making any changes to either platform. The CLI then prompts for your Cloudflare credentials. Paste the API token at the **API Token** prompt and the Account ID at the **Account ID** prompt: ```sh theme={null} Cloudflare Stream credentials https://dash.cloudflare.com/profile/api-tokens (permission: Stream:Edit) ✔ API Token: … **************************************** ✔ Account ID: … your-account-id ``` Once Cloudflare accepts them, the CLI offers to save them so later runs skip these prompts. Press Enter to accept: ```sh theme={null} ✔ Save Cloudflare Stream credentials for next time? … yes ✓ Saved Cloudflare Stream credentials to the "default" profile. ``` > [accordion] Where the CLI stores credentials > > The CLI stores saved credentials in `~/.config/bunnynet/stream-import-credentials.json` (or under `$XDG_CONFIG_HOME/bunnynet/` when it is set). Only your user account can read the file, and it is separate from the main Bunny CLI config. Saved credentials belong to the active Bunny CLI profile. If Cloudflare later rejects them, for example after you revoke the token, the CLI deletes them and prompts again on the next run. If you answer no, the CLI prompts on every run. > > The `CLOUDFLARE_API_TOKEN` and `CLOUDFLARE_ACCOUNT_ID` environment variables always take priority over saved credentials. The dry run then prints a summary and the import plan: ```sh theme={null} Cloudflare Stream to Bunny Stream Key Value Library migrate-from-cloudflare-stream (12345) Source Cloudflare Stream: 6 videos in 0 folders Already imported 0 To import 6 Duration 2:01 Size 126 MB Import plan (no collection) (6 videos) - 13534292_1920_1080_25fps.mp4 - 3195394-uhd_3840_2160_25fps.mp4 - 6567878-hd_1080_1920_25fps.mp4 - 4972699-hd_1920_1080_30fps.mp4 - 12647590_1920_1080_50fps.mp4 - 1585622-hd_720_1280_30fps.mp4 ℹ Dry run. Remove --dry-run to import for real. ``` **Already imported** counts videos that are already in the library, and **To import** counts the ones a real run would import. Every video is listed under (no collection), since Cloudflare Stream has no folders. Bunny Stream accepts sources up to 2160p (3840x2160, or 2160x3840 in portrait) and up to 72 hours long. The dry run warns about any video beyond those limits, as it will fail on Bunny: ```sh theme={null} ⚠ 1 videos are beyond Bunny's limits and will fail: ⚠ - 7136213-uhd_4096_2160_25fps.mp4: 4096x2160 is above Bunny's 2160p limit ``` The dry run does not check `requireSignedURLs`. A video with **Require Signed URLs** turned on still appears in the plan and fails during the import, so turn the setting off on those videos first, as described in [Scope](#scope). **5. Run the import** Run the same command without `--dry-run`: ```sh theme={null} bunny stream import --library 12345 --source cloudflare ``` The CLI prints the same summary, then asks for confirmation. Type `y` and press Enter to start: ```sh theme={null} ? Import 6 videos into migrate-from-cloudflare-stream? › (y/N) ``` The CLI skips videos that are already imported, has Cloudflare generate an MP4 download for each new one, hands those to Bunny three at a time, and tags each video with `cfStreamId`. It returns once every video is queued. Bunny then fetches each file directly from Cloudflare and encodes it. ```sh theme={null} ✔ Import 6 videos into migrate-from-cloudflare-stream? … yes ✓ Queued 6 videos into migrate-from-cloudflare-stream. Bunny is fetching and encoding them now. ℹ Check progress with bunny stream import status --library 12345 --source cloudflare ``` The import command also accepts these flags: * `--wait` keeps the command running, with a live encoding progress indicator, until Bunny has encoded every video. * `--force` skips the confirmation prompt and the offer to retry failed videos, for unattended or CI runs. * `--concurrency N` sets how many videos the CLI passes to Bunny in parallel, from 1 to 20. * `--clean-failed` deletes the failed Bunny copies of videos before importing them again, including videos that Bunny rejected. * `--retry-stalled` deletes and imports again any video stuck at zero bytes on Bunny. * `--request-timeout` sets how many seconds one request to Cloudflare or Bunny can take before it gives up (default 30). * `--video-timeout` sets how many seconds the hand-off of one video to Bunny, or with `--wait` its encoding, can take before it gives up (default 5400). Run `bunny stream import --help` for the full list. **6. Watch encoding progress** The import returns before Bunny finishes encoding. Check progress with the status command: ```sh theme={null} bunny stream import status --library 12345 --source cloudflare ``` It prints one row per queued video, with its status on Bunny, encode percentage, size, and when it was queued: ```sh theme={null} Cloudflare Stream to migrate-from-cloudflare-stream Video Bunny status Encoded Size Queued 13534292_1920_1080_25fps.mp4 Finished 100% 37 MB just now 3195394-uhd_3840_2160_25fps.mp4 Finished 100% 15 MB just now 6567878-hd_1080_1920_25fps.mp4 Transcoding 45% just now 4972699-hd_1920_1080_30fps.mp4 Transcoding 5% just now 12647590_1920_1080_50fps.mp4 Processing 5% just now 1585622-hd_720_1280_30fps.mp4 Processing 5% just now ℹ 2 finished, 4 processing, 0 failed. ``` Run it again until the last line reports 0 processing. The command exits with a non-zero code when any video has failed, so you can also use it as a check in scripts. **7. Verify in the bunny.net dashboard** Open the [bunny.net dashboard](https://dash.bunny.net) and go to **Stream > Your library > Manage library** to confirm every video has encoded. ![The Manage library page in the bunny.net dashboard listing the imported videos](https://mintcdn.com/bunnynet-cb9733c2/ZkttdKBCsq-yq0qO/images/stream/migrate-from-cloudflare-stream/manage-library-videos.png?fit=max\&auto=format\&n=ZkttdKBCsq-yq0qO\&q=85\&s=18114cd205627df9e22414f3da035d5d) Set up the library's [security settings](https://bunny.net/docs/stream/security-options.md) if you have not already, then replace the Cloudflare Stream embed URLs and player references in your site with their Bunny Stream equivalents. See [Embedding videos](https://bunny.net/docs/stream/embedding.md) for the embed and player options. **8. Clean up the MP4 downloads on Cloudflare** The import generates an MP4 download for every video that did not already have one. According to [Cloudflare's documentation](https://developers.cloudflare.com/stream/viewing-videos/download-videos/), MP4 downloads add no storage cost, but Cloudflare bills for the video's duration each time one is downloaded. Bunny's fetch counts as one download per video. If you plan to delete your videos from Cloudflare Stream, deleting a video also deletes its downloads, so there is nothing else to do. If you are keeping the videos on Cloudflare for now, set `CLOUDFLARE_ACCOUNT_ID` and `CLOUDFLARE_API_TOKEN` in your shell, then delete the downloads you do not need through the Cloudflare API: ```sh theme={null} curl -X DELETE \ "https://api.cloudflare.com/client/v4/accounts/$CLOUDFLARE_ACCOUNT_ID/stream/$VIDEO_UID/downloads/default" \ -H "Authorization: Bearer $CLOUDFLARE_API_TOKEN" ``` `$VIDEO_UID` is the Cloudflare video ID, stored in the `cfStreamId` metatag on each Bunny video. To list the video IDs on the account, run: ```sh theme={null} bunny stream import list --source cloudflare --videos ``` The command lists at most 10,000 videos. On a larger account, read the remaining IDs from the `cfStreamId` metatags or the Cloudflare API. The CLI reuses MP4 downloads that existed before the migration. If your site already serves some of those downloads, leave them in place. ## Troubleshooting > [accordion] Some videos failed during the import > > When videos fail during an interactive import, the CLI lists each failure and offers to retry them: > > ```sh theme={null} > ✖ 4972699-hd_1920_1080_30fps.mp4: No download available: the video was not found at the source or has no usable file > ✖ 12647590_1920_1080_50fps.mp4: Cloudflare video requires signed URLs; turn off Require Signed URLs on it in Cloudflare Stream, then re-run the import > ℹ Resume with bunny stream import --library 12345 --source cloudflare --resume > ? 2 videos failed. Retry them now? › (y/N) > ``` > > Answer `y` to retry them in the same run. If the import was interrupted, or you declined the retry, run it again with `--resume`: > > ```sh theme={null} > bunny stream import --library 12345 --source cloudflare --resume > ``` > > `--resume` retries failed videos, processes any that were still outstanding, and re-applies the `cfStreamId` tag to videos Bunny fetched before the run stopped. It stops retrying a video after three failed attempts and skips videos that Bunny rejected. To retry those too, add `--clean-failed`, which deletes their Bunny copies first. > > Running the import again is always safe. Videos that already carry the `cfStreamId` tag are skipped, and the CLI reports **Everything is already imported** once nothing is left. > [accordion] No download available > > Cloudflare did not finish generating the MP4 download within five minutes, which can happen with long or high-resolution videos. It can also mean the video was deleted from Cloudflare after the import started. > > Cloudflare keeps generating the download after the CLI stops waiting, so wait a few minutes, then retry. The retry picks up the finished download instead of requesting a new one. > [accordion] Cloudflare video requires signed URLs > > The video has `requireSignedURLs` turned on. Turn off **Require Signed URLs** on it in Cloudflare Stream, retry, then turn it back on. See [Scope](#scope). > [accordion] Cloudflare could not generate an MP4 download > > Cloudflare reported an error while generating the download, and the CLI prints its reason. Check the video in the Cloudflare dashboard. A retry only helps once Cloudflare can generate the download. > [accordion] Videos are stuck with no data > > If `bunny stream import status` reports videos stuck at zero bytes, run the import again with `--retry-stalled` to delete and fetch them again. This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.