# Migrate objects from another provider

> Copy a bucket from Amazon S3, Cloudflare R2, Google Cloud Storage, Backblaze B2, Wasabi, DigitalOcean Spaces or another S3 service.

Source: https://www.coritan.com/docs/object-storage/migrate-to-object-storage/

In the dashboard:

- /dashboard/storage/…/migrations: https://www.coritan.com/dashboard/storage

A *migration* copies the objects of a bucket at another provider into one of your buckets. We read from the other provider with an access key you give us, copy each object with its content headers and metadata, and keep going in the background until every object is copied. Nothing changes at the other provider. Copies are free; the objects they create are billed as storage.

## Before you begin

- Sign in to the [dashboard](https://www.coritan.com/dashboard/storage) and open the Object Storage service. The service must be `active`.
- Create the bucket to copy into ([Create and delete buckets](/docs/object-storage/buckets/)). It can already hold objects.
- At the other provider, create an access key that can list and read the bucket. [Source details by provider](#providers) says where each provider keeps it. A key that can only read is enough, and is the safest choice.

## Source details by provider {#providers}

Each provider needs a bucket name and an access key. Some also need a region or an endpoint, which the dialog asks for when you choose the provider.

| Provider | Region | Endpoint | Access key |
| --- | --- | --- | --- |
| Amazon S3 | The bucket's region, such as `us-east-1`. The S3 console shows it beside the bucket. | Built from the region | An IAM user's access key with `s3:ListBucket` and `s3:GetObject` on the bucket. |
| Cloudflare R2 | Not asked for | Your account's S3 API address, `https://<account-id>.r2.cloudflarestorage.com`, under R2 → Overview in Cloudflare | An R2 API token with Object Read on the bucket. Use its access key ID and secret access key. |
| Google Cloud Storage | Not asked for | `https://storage.googleapis.com` | An HMAC key from Cloud Storage → Settings → Interoperability, for an account that can read the bucket. |
| Backblaze B2 | The part between `s3.` and `.backblazeb2.com` in the bucket's endpoint, such as `us-west-004` | Built from the region | An application key that can list and read the bucket. Its key ID is the access key ID. |
| Wasabi | The bucket's region, such as `us-east-1` | Built from the region | An access key whose policy can list and read the bucket. |
| DigitalOcean Spaces | The data centre in the Space's address, such as `nyc3` | Built from the region | A Spaces access key with read access to the Space. |
| Another S3-compatible service | Optional; `us-east-1` when left empty | The service's S3 address, such as `https://s3.example.com` | A key that can list and read the bucket. |

An endpoint you type must use HTTPS and be on the public internet. We refuse one that points at a private or reserved address, and one that is Coritan Object Storage itself: to copy between your own buckets, use [replication](/docs/object-storage/replication/) or an S3 client.

## Start a migration

1. In the dashboard, go to [**Object Storage**](https://www.coritan.com/dashboard/storage), open the service, then the **Migrations** tab.
2. Select **New migration…**.
3. Under **Copy from**, choose the **Provider**. Fill in the **Endpoint** or **Region** when the dialog asks for one ([Source details by provider](#providers)).
4. Enter the source **Bucket** name. To copy part of the bucket, enter a **Prefix**: only keys that start with it are copied, such as `photos/` for `photos/2026/beach.jpg`.
5. Enter the **Access key ID** and **Secret access key**.
6. Under **Copy to**, choose the **Bucket** to copy into. To put the copies under a folder, enter a **Prefix**, such as `imported/`: `photos/beach.jpg` becomes `imported/photos/beach.jpg`. Leave it empty to keep the keys as they are.
7. Leave **Copy every object again** off to skip an object your bucket already holds with the same key, size and ETag. Turn it on to copy every object whatever your bucket holds.
8. Optionally, select **Check source**. We list the source with the key and show the first keys we found, so you know the details are right. Nothing is copied yet.
9. Select **Start migration**. We check the source again before we start.

## Result

The migration appears at the top of the list as **Queued** and starts within a minute. It runs in the background, so you can close the dashboard. The list refreshes itself while a migration is queued or copying.

| Status | API word | Meaning |
| --- | --- | --- |
| **Queued** | `queued` | Waiting to start. |
| **Copying** | `running` | Copying now. **Progress** counts the objects done so far, and once we have listed the whole source, how many there are in all. |
| **Done** | `done` | Every object under the prefix was copied or was already there. |
| **Done with failures** | `done` | Finished, but some objects could not be copied. [Failed objects and retries](#failed-objects). |
| **Failed** | `failed` | The migration stopped. The reason shows under the status. |
| **Cancelled** | `cancelled` | You cancelled it. The objects already copied stay. |

**Progress** also shows how much data was copied, how many objects were already in your bucket and skipped, and how many failed.

When a migration ends, in any of the last four states, we delete the access key you gave us.

## What is copied {#what-is-copied}

- Each object's current version, under the destination prefix followed by its source key.
- Its `Content-Type`, `Cache-Control`, `Content-Disposition`, `Content-Encoding` and `Content-Language` headers, and its user metadata (`x-amz-meta-*`).
- Its ETag, in most cases. An object the source uploaded in parts is copied in parts of the same size, so S3 clients that compare ETags see the same value at both ends.

These are not copied: earlier versions of an object, object tags, access control lists, storage classes, retention and legal holds, and the bucket's own settings (CORS, lifecycle, public access, notifications). Set those on your bucket yourself.

An object that changes at the source while we copy it fails with "The object changed while we copied it". Retry the migration to copy its new version.

## How long a migration takes

It depends mostly on how fast the source sends data, and on how many objects there are: a bucket of many small objects takes longer than one of a few large objects with the same total size. We copy several small objects at once, and an object larger than 16 MB in parts. The **Migrations** tab shows how far each migration has got.

A service runs at most 3 migrations at a time. To copy several buckets, start one migration for each; the fourth waits until one ends.

## Failed objects and retries {#failed-objects}

An object that cannot be copied does not stop the migration: we record it as failed and move on. When the source is busy or does not answer, we stop for now and carry on from the same place a minute later. The reasons an object fails:

- "The source refused to send it (AccessDenied)": the key cannot read that object. Change its permissions at the source.
- "The source keeps it in an archive class that must be restored first": the object is in an archive class, such as S3 Glacier. Restore it at the source.
- "The object changed while we copied it", or "The source no longer has it".
- "Objects over 625 GB are not copied. Copy it with an S3 client".
- "The key is longer than 1024 bytes with the destination prefix": choose a shorter destination prefix.

To see them, open the migration's menu (**…**) and select **Show failed objects**. **Failed objects** lists the most recent first, with the reason and how many times we tried. We keep the last 1000 failures of each migration.

To try again after fixing the cause:

1. Select **Retry failed objects…**, in the migration's menu or on the **Failed objects** card.
2. Enter the **Access key ID** and **Secret access key** again. We deleted them when the migration ended. The dialog shows the last four characters of the key the migration used.
3. Select **Retry failed objects**.

The migration is queued again. It copies the failed objects first, then anything it had not reached. When more objects failed than the 1000 we keep, it walks the whole source again and skips what your bucket already holds.

A migration that **Failed** has **Retry migration…** instead. It carries on from where it stopped.

A migration stops with **Failed** when the source refuses the key or the bucket, when the destination bucket is deleted, when the first 100 objects all fail, or when the source has not answered 10 times in a row.

## Cancel a migration

1. Open the migration's menu (**…**) and select **Cancel migration…**.
2. Select **Cancel migration** to confirm, or **Keep copying** to leave it running.

We stop copying and delete the access key. The objects already copied stay in your bucket, and nothing changes at the source. A cancelled migration cannot be started again: start a new one, and leave **Copy every object again** off so it skips what is already copied.

## Billing

Migrations are free: the requests we make to your bucket are not counted as operations. The objects they create count as storage in your service, like any object you upload ([How Object Storage is billed](/docs/object-storage/usage-and-billing/)). Your old provider may charge for the data and the requests we read from it; check its pricing.

## Limits

| Limit | Value |
| --- | --- |
| Migrations queued or running in one service | 3 |
| Largest object copied | 625 GB |
| Longest destination key (prefix and source key) | 1024 bytes |
| Source and destination prefix | 1024 bytes each |
| Failed objects kept per migration | 1000 |
| Migrations started, cancelled or retried by one account | 30 an hour |
| **Check source** by one account | 30 in ten minutes |

## With the API

Every request takes your access token as `Authorization: Bearer $CORITAN_TOKEN`. Find the service's id and the bucket's id with `GET /api/v1/client/object-storage` and `GET /api/v1/client/object-storage/{service_id}/buckets`.

Check the source. It answers 422 with the reason when the source refuses, and stores nothing:

```bash
curl -X POST https://api.coritan.com/api/v1/client/object-storage/106/migrations/preflight \
  -H "Authorization: Bearer $CORITAN_TOKEN" -H "Content-Type: application/json" \
  -d '{"provider": "aws", "region": "us-east-1", "source_bucket": "example-media",
       "source_prefix": "", "access_key_id": "AKIAEXAMPLE", "secret_access_key": "..."}'
```

```json
{
  "ok": true,
  "endpoint": "https://s3.us-east-1.amazonaws.com",
  "region": "us-east-1",
  "bucket": "example-media",
  "prefix": "",
  "sample": [{ "key": "index.html", "size": 18204, "last_modified": "2026-09-08T19:41:15+00:00" }],
  "more": true
}
```

`provider` is one of `aws`, `r2`, `gcs`, `b2`, `wasabi`, `digitalocean` and `other`. `endpoint` is required for `r2` and `other`; `region` for `aws`, `b2`, `wasabi` and `digitalocean`.

Start the migration with the same fields, plus `bucket_id`, `dest_prefix` and `overwrite` (**Copy every object again**). It answers 201 with the migration:

```bash
curl -X POST https://api.coritan.com/api/v1/client/object-storage/106/migrations \
  -H "Authorization: Bearer $CORITAN_TOKEN" -H "Content-Type: application/json" \
  -d '{"provider": "aws", "region": "us-east-1", "source_bucket": "example-media",
       "access_key_id": "AKIAEXAMPLE", "secret_access_key": "...",
       "bucket_id": 501, "dest_prefix": "", "overwrite": false}'
```

```json
{
  "id": 42,
  "bucket_id": 501,
  "bucket_name": "u7-assets",
  "provider": "aws",
  "provider_label": "Amazon S3",
  "source_bucket": "example-media",
  "status": "queued",
  "listed_all": false,
  "objects_listed": 0,
  "objects_copied": 0,
  "objects_skipped": 0,
  "objects_failed": 0,
  "bytes_copied": 0,
  "can_retry": false,
  "error": null
}
```

`GET .../migrations` lists the service's migrations, newest first, with the provider presets and `limits`. `GET .../migrations/{migration_id}` answers one. `objects_listed` is the total only once `listed_all` is `true`.

`POST .../migrations/{migration_id}/cancel` cancels a queued or running migration. `POST .../migrations/{migration_id}/retry`, with `access_key_id` and `secret_access_key`, queues one whose `can_retry` is `true` again. `GET .../migrations/{migration_id}/failures?limit=100&offset=0` lists its failed objects:

```json
{
  "items": [{ "key": "db/2026-01-10/dump-000.sql.gz", "size": 116391936, "error": "The object changed while we copied it",
              "attempts": 2, "retry_pending": false, "updated_at": "2026-10-06T18:41:15+00:00" }],
  "total": 19,
  "kept_max": 1000
}
```

The API answers 404 for a service, bucket or migration that is not yours, 409 when the service is not active or already runs 3 migrations, and 429 past the rate limits above.

## Troubleshooting

"The source did not accept the access key ID" or "The source did not accept the secret access key"
: Copy the key again from the provider. A key with a session token does not work; create a long-lived access key.

"The key cannot list *bucket*. Give it permission to list and read the bucket"
: The key exists but has no permission on that bucket. Add list and read permission at the provider.

"The source has no bucket named *bucket*"
: Check the bucket name, and for providers with regions, the region.

"*bucket* is in another region. Choose the bucket's region and try again"
: Choose the region the provider shows for the bucket.

"We could not find *host*. Check the endpoint and the region"
: The endpoint's host name does not exist. Check for a typo, and that the region is a real one for the provider.

"*host* points at a private or reserved address. Migrations copy only from public endpoints"
: Use the service's public HTTPS address.

"This service already has 3 migrations in progress. Wait for one to finish or cancel one"
: Wait for a migration to end, or cancel one, then start the next.

## Next steps

- [Replicate a bucket to another region](/docs/object-storage/replication/)
- [Connect an S3 client](/docs/object-storage/connect-an-s3-client/)
- [How Object Storage is billed](/docs/object-storage/usage-and-billing/)

## API

- `GET /api/v1/client/object-storage/{service_id}/migrations`: List migrations (https://www.coritan.com/docs/api/reference/client/object-storage/object-storage-migrations/#op-get-api-v1-client-object-storage-service-id-migrations)
- `POST /api/v1/client/object-storage/{service_id}/migrations`: Start copying objects from another provider into one of the service's buckets (https://www.coritan.com/docs/api/reference/client/object-storage/object-storage-migrations/#op-post-api-v1-client-object-storage-service-id-migrations)
- `POST /api/v1/client/object-storage/{service_id}/migrations/preflight`: Check migration source (https://www.coritan.com/docs/api/reference/client/object-storage/object-storage-migrations/#op-post-api-v1-client-object-storage-service-id-migrations-preflight)
- `GET /api/v1/client/object-storage/{service_id}/migrations/{migration_id}`: One migration, as the list writes it (https://www.coritan.com/docs/api/reference/client/object-storage/object-storage-migrations/#op-get-api-v1-client-object-storage-service-id-migrations-migration-id)
- `POST /api/v1/client/object-storage/{service_id}/migrations/{migration_id}/cancel`: Stop a queued or running migration and delete its source keys (https://www.coritan.com/docs/api/reference/client/object-storage/object-storage-migrations/#op-post-api-v1-client-object-storage-service-id-migrations-migration-id-cancel)
- `POST /api/v1/client/object-storage/{service_id}/migrations/{migration_id}/retry`: Retry migration (https://www.coritan.com/docs/api/reference/client/object-storage/object-storage-migrations/#op-post-api-v1-client-object-storage-service-id-migrations-migration-id-retry)
- `GET /api/v1/client/object-storage/{service_id}/migrations/{migration_id}/failures`: List migration failures (https://www.coritan.com/docs/api/reference/client/object-storage/object-storage-migrations/#op-get-api-v1-client-object-storage-service-id-migrations-migration-id-failures)
