# Migration API

> Export a whole Broadcast channel, including subscribers, templates, sequences, email servers, users and file assets, with the read-only Migration API. Admin API tokens only.

Source: https://sendbroadcast.net/docs/api-migration

The Migration API is a read-only export of an entire channel. It exists to move a channel, or a whole installation, to another Broadcast server: it returns everything needed to rebuild the channel elsewhere, including the secrets the regular API never returns.

It lives under `/api/migration/v1`, separate from the regular `/api/v1` API, and nothing in it writes data.

Warning

**Treat a token used here like a database backup.** The Migration API returns email server credentials (SMTP passwords, AWS secret keys, provider API keys) and channel API token values in plain text, and lists every user on the installation with their sudo flag. It accepts any channel ID. Use a dedicated admin API token for a migration, keep it out of logs and shared tools, and delete it when the migration is done.

## Authentication

Every request needs an **admin API token** in the `Authorization` header. Channel tokens are rejected, because the export reaches across channels. Create admin API tokens under **Application → Admin API Tokens**, see [API Authentication](https://sendbroadcast.net/docs/api-authentication).

```bash
curl https://broadcast.example.com/api/migration/v1/manifest?broadcast_channel_id=1 \
  -H "Authorization: Bearer YOUR_ADMIN_API_TOKEN"
```

Any valid, unexpired admin API token can read the Migration API; its resource permissions do not narrow it. The whole API is switched off in demo mode and answers `403`.

Every endpoint except `channels` and `users` needs a `broadcast_channel_id` query parameter naming the channel to export. Start with `channels` to find the IDs.

| Status | Meaning |
|--------|---------|
| `400 Bad Request` | `broadcast_channel_id` is missing |
| `401 Unauthorized` | No token, an unknown token, a channel token, or an expired admin token |
| `403 Forbidden` | The installation is in demo mode |
| `404 Not Found` | The channel or file asset does not exist |

## Manifest

```
GET /api/migration/v1/manifest?broadcast_channel_id=1
```

Sizes the export before you start: how many of each record the channel has, so you can check the import afterwards.

```json
{
  "export_format_version": 1,
  "exported_at": "2026-10-03T14:30:00Z",
  "broadcast_channel": { "id": 1, "name": "Newsletter", "slug": "newsletter" },
  "counts": {
    "subscribers": 12840,
    "templates": 14,
    "segments": 6,
    "sequences": 3,
    "email_servers": 2,
    "opt_in_forms": 4,
    "broadcasts": 11,
    "webhook_endpoints": 1,
    "tokens": 2,
    "global_suppressions": 37,
    "unsubscribed_emails": 412,
    "tags": 18,
    "link_redirects": 230,
    "file_assets": 41
  },
  "recent_history": {
    "days": 90,
    "broadcasts": 11,
    "outbound_receipts": 98412
  }
}
```

`broadcasts` and `recent_history` cover only the last `days_history` days (see below). `global_suppressions` counts the installation-wide list plus anything recorded against this channel.

Note

`unsubscribed_emails` is the channel’s own suppression list, exported by `/unsubscribed_emails` (Broadcast 2.40.0 or later). `global_suppressions` is the installation-wide list, exported by `/suppressions`. A complete export needs both.

## Resources

Each resource is a `GET` that returns `{ "data": [...] }`.

| Endpoint | Returns | Paginated |
|----------|---------|-----------|
| `/channels` | Every channel on the installation | No |
| `/users` | Every user, with their sudo flag, system permissions and channel permissions | No |
| `/subscribers` | The channel's subscribers, including custom data and tags | Yes |
| `/tags` | Subscriber tags used in the channel | No |
| `/subscriber_histories` | Subscriber history entries | Yes |
| `/suppressions` | Global suppressions, plus any recorded against this channel | Yes |
| `/unsubscribed_emails` | The channel's own suppression list (2.40.0 or later) | Yes |
| `/templates` | Templates | Yes |
| `/segments` | Segments with their rule groups and rules | No |
| `/sequences` | Sequences with their steps | No |
| `/email_servers` | Email servers, **including credentials** | No |
| `/opt_in_forms` | Opt-in forms | No |
| `/webhook_endpoints` | Webhook endpoints | No |
| `/tokens` | Channel API tokens, **including their values** | No |
| `/broadcasts` | Broadcasts from the last `days_history` days | Yes |
| `/outbound_receipts` | Delivery receipts for those broadcasts | Yes |
| `/link_redirects` | Tracked links | Yes |
| `/link_clicks` | Link clicks from the last `days_history` days | Yes |
| `/file_assets` | File asset metadata | Yes |

All paths are relative to `/api/migration/v1`.

### Pagination

Paginated endpoints take `limit` (1 to 250, default 250) and `offset` (default 0), and return a `pagination` object next to `data`:

```json
{
  "data": [ ... ],
  "pagination": { "total": 12840, "limit": 250, "offset": 0, "has_more": true }
}
```

Keep requesting with `offset` increased by the `limit` in the response until `has_more` is `false`. Advance by the returned `limit`, not the one you asked for: a `limit` above 250 is reduced to 250.

### Recent history

`broadcasts`, `outbound_receipts`, `link_clicks` and the manifest accept `days_history` (1 to 365, default 90). Older sends are left out so an export of a busy channel stays a manageable size.

### The channel suppression list

```
GET /api/migration/v1/unsubscribed_emails?broadcast_channel_id=1
```

Returns every address on the channel's own suppression list, paginated like the other resources. Its `pagination.total` matches `counts.unsubscribed_emails` in the manifest, so you can check that nothing was missed.

```json
{
  "data": [
    {
      "id": 88,
      "email": "former.subscriber@example.com",
      "broadcast_channel_id": 1,
      "created_at": "2026-03-14T09:12:00Z",
      "updated_at": "2026-03-14T09:12:00Z"
    }
  ],
  "pagination": { "total": 412, "limit": 250, "offset": 0, "has_more": true }
}
```

This is a different list from `/suppressions`, which holds the installation-wide global list. Export both. On the new server, add these addresses to the channel with the bulk endpoint of the [Suppressions API](https://sendbroadcast.net/docs/api-suppressions).

Before 2.40.0 this endpoint did not exist. On an older server, read the channel list with `GET /api/v1/suppressions` instead.

## Downloading file assets

```
GET /api/migration/v1/file_assets/:id/download?broadcast_channel_id=1
```

Returns the original file's bytes, for re-uploading on the new server. A file asset whose file is missing answers `404`.

## Using an SDK

The official SDKs wrap the Migration API, handling pagination for you: `client.migration.manifest` and `client.migration.each_record` in the [Ruby SDK](https://sendbroadcast.net/docs/ruby-sdk), with the same methods in the Node, Python and PHP SDKs. See [SDKs & Client Libraries](https://sendbroadcast.net/docs/sdks).

| SDK | Channel suppression list |
|-----|--------------------------|
| Ruby | `client.migration.each_record(:unsubscribed_emails)` |
| Node | `client.migration.eachRecord('unsubscribedEmails')` |
| Python | `client.migration.each_record("unsubscribed_emails")` |
| PHP | `$client->migration->eachRecord('unsubscribedEmails')` |

These collections need Ruby 0.6.0, Node 0.3.0, Python 0.3.0 or PHP 0.4.0 or later, and Broadcast 2.40.0 or later.
