> ## Documentation Index
> Fetch the complete documentation index at: https://docs.openinary.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Migrate from Cloudinary

> Copy your files, switch your uploads and update your URLs. Five steps, the same for ten images or a million.

Openinary speaks the same URL language as Cloudinary. Moving over mostly means copying your files and changing the start of your URLs:

```diff theme={null}
- res.cloudinary.com/acme/image/upload/w_400,c_fill/v17/jane.jpg
+ cdn.openinary.dev/b/BUCKET_ID/t/w_400,c_fill/jane.jpg
```

## Before you start

Openinary covers resizing, cropping, formats, quality and video trimming. It has the common effects (`e_blur`, `e_fill_light`, `e_grayscale` and more) but not the AI ones, and no text overlays, AI features, adaptive streaming or webhooks yet. If you rely on one of these, read the [full comparison](https://docs.openinary.dev/cloudinary-comparison.md) first.

## Migrate in five steps

**1. Create your Openinary account**

Sign up for [Openinary Cloud](https://docs.openinary.dev/cloud/quickstart.md) and create an API key under **Settings → API keys**. You can also [self-host](https://docs.openinary.dev/quickstart.md).

The Free plan includes 1 GB of storage. If your Cloudinary library is bigger, switch to the Alpha plan first. See [plans](https://docs.openinary.dev/cloud/overview.md#plans-and-limits).

**2. Copy your files**

Our migration script copies every file from Cloudinary to Openinary and keeps the same paths. Find your Cloudinary API key and secret in the Cloudinary console settings, then run:

```bash theme={null}
curl -O https://raw.githubusercontent.com/openinary/openinary/main/apps/docs/scripts/migrate-from-cloudinary.mjs

export CLOUDINARY_CLOUD_NAME=your-cloud-name
export CLOUDINARY_API_KEY=your-cloudinary-key
export CLOUDINARY_API_SECRET=your-cloudinary-secret
export OPENINARY_API_KEY=oik_your_api_key

node migrate-from-cloudinary.mjs
```

Add `--dry-run` to see what would be copied without uploading anything. If the script stops, run it again: it picks up where it left off.

When it finishes, keep `cloudinary-migration.jsonl`. It lists every file with its old and new URL.

**3. Send new uploads to Openinary**

Replace your Cloudinary upload call with a request to [`POST /upload`](https://docs.openinary.dev/api-reference/files/upload):

```js theme={null}
const form = new FormData();
form.set("folder", "products");
form.append("files", file);

const res = await fetch("https://cdn.openinary.dev/upload", {
  method: "POST",
  headers: { Authorization: `Bearer ${process.env.OPENINARY_API_KEY}` },
  body: form,
});
const { files } = await res.json(); // files[0].path replaces public_id
```

Uploading from the browser, like with the Upload Widget? Use the [File Uploader](https://docs.openinary.dev/guides/file-uploader.md) for [Next.js](https://docs.openinary.dev/guides/integrate/nextjs.md) or [React](https://docs.openinary.dev/guides/integrate/react.md).

Then run the migration script once more, to copy anything uploaded to Cloudinary in the meantime.

**4. Update your URLs**

Replace the start of your Cloudinary URLs with your Openinary one, and remove the version (`v1712345678`). Your bucket ID is in the `to` column of the migration log.

Most transformations stay exactly the same. A few need a change, see [transformations that change](#transformations-that-change) below.

> [!WARNING]
>
> Remove any transformation Openinary doesn't support. A single unknown parameter, like `e_vignette` in `w_400,e_vignette:30`, makes the whole URL return `404`.

URLs saved in a database? Use the [conversion function](#convert-stored-urls) below.

**5. Test, then switch**

Open your main pages on a preview deployment and check that images and videos load. A `404` in the network tab almost always means an unsupported parameter is left in a URL.

Then deploy. Each image size is generated once, on its first visit, so the first loads are a little slower.

Keep your Cloudinary account for 30 days, so old links in emails and caches keep working.

## Transformations that change

| Cloudinary | On Openinary |
| - | - |
| `q_auto:good`, `g_auto:subject` | `q_auto`, `g_auto` |
| `dpr_auto` | Serves 1x: Client Hints are not read. Numeric `dpr_2` works as is |
| `t_thumbnail` (named transformation) | Write its parameters in the URL |
| `l_<public_id>` image overlay | `l_` with Openinary's own parameters, see [overlays](https://docs.openinary.dev/media-transformations/overlays.md) |
| `d_` | Works when self-hosting; on Cloud it is accepted and ignored |
| `e_` effects outside [this list](https://docs.openinary.dev/api-reference/media/transform.md#effects), `u_`, `fl_`, `x_`, `y_`, `z_`, `vc_` | Not supported, remove them |

Everything else you likely use, `w`, `h`, `c`, `g`, `f`, `q`, `ar`, `a`, `r`, `b`, `bo`, `o`, `co`, `dpr`, `so`, `eo` and the supported `e_` effects, works as is. See the [transformation reference](https://docs.openinary.dev/api-reference/media/transform.md).

## More help

> [accordion] Replacing the SDK in your code
>
> You don't need an SDK. A small helper builds your URLs:
>
> ```js theme={null}
> const BASE = "https://cdn.openinary.dev/b/YOUR_BUCKET_ID";
>
> export const media = (path, transformations) =>
>   transformations ? `${BASE}/t/${transformations}/${path}` : `${BASE}/t/${path}`;
>
> media("avatars/jane.jpg", "w_400,c_fill"); // was cloudinary.url("avatars/jane", ...)
> ```
>
> Note that Openinary paths include the file extension.

> [accordion] Replacing CldImage in Next.js
>
> Use Next.js's own `<Image>` with this loader:
>
> ```js openinary-loader.js theme={null}
> export default function openinaryLoader({ src, width, quality }) {
>   return `https://cdn.openinary.dev/b/YOUR_BUCKET_ID/t/w_${width},q_${quality ?? "auto"}/${src}`;
> }
> ```
>
> ```js next.config.js theme={null}
> module.exports = {
>   images: { loader: "custom", loaderFile: "./openinary-loader.js" },
> };
> ```
>
> Each width Next.js asks for is a new transformation. Trim `images.deviceSizes` to the widths you really use.

> [accordion] Convert stored URLs
>
> Run each saved Cloudinary URL through this function. It changes the prefix, drops the version and keeps only the parameters Openinary supports.
>
> ```js to-openinary.js theme={null}
> const BASE = "https://cdn.openinary.dev/b/YOUR_BUCKET_ID";
>
> const SUPPORTED = {
>   w: /^(\d+|auto)$/,
>   h: /^(\d+|auto)$/,
>   c: /^(fill|lfill|fill_pad|fit|limit|mfit|scale|crop|thumb|pad|lpad)$/,
>   g: /^(center|north|south|east|west|[cnsew]|faces?|faces?_center|north_center|south_center|auto)$/,
>   q: /^(\d+|auto)$/,
>   f: /^(webp|jpe?g|png|avif|gif|mp4|webm|mov|auto)$/,
>   a: /^(-?\d+|auto)$/,
>   ar: /^(\d+:\d+|\d+(\.\d+)?)$/,
>   b: /^(transparent|white|black|rgb:[0-9a-fA-F]{3,8})$/,
>   r: /^(max|\d+(:\d+){0,3})$/,
>   so: /^\d+(\.\d+)?$/,
>   eo: /^\d+(\.\d+)?$/,
> };
>
> export function toOpeninary(url) {
>   const match = url.match(/^https?:\/\/res\.cloudinary\.com\/[^/]+\/(?:image|video|raw)\/upload\/(.+)$/);
>   if (!match) return url;
>
>   // Transformations sit before the version (v1712345678). Without a version,
>   // they are the leading segments shaped like "w_400,c_fill".
>   const segments = match[1].split("/");
>   const version = segments.findIndex((s) => /^v\d+$/.test(s));
>   const start = version !== -1
>     ? version
>     : segments.findIndex((s) => !s.split(",").every((p) => /^[a-z]{1,4}_/.test(p)));
>   const transforms = segments.slice(0, start);
>   const path = segments.slice(version !== -1 ? start + 1 : start).join("/");
>
>   const params = new Map();
>   for (const part of transforms.join(",").split(",")) {
>     const [key, ...rest] = part.split("_");
>     let value = rest.join("_");
>     if (!["ar", "b", "r"].includes(key)) value = value.split(":")[0]; // q_auto:good -> q_auto
>     if (SUPPORTED[key]?.test(value)) params.set(key, value);
>   }
>
>   const t = [...params].map(([key, value]) => `${key}_${value}`).join(",");
>   return `${BASE}/t/${t ? `${t}/` : ""}${path}`;
> }
> ```
>
> If a URL has no file extension, find the right path in the migration log.

> [accordion] Large libraries
>
> The script copies four files at a time. For hundreds of thousands of files, run several copies in parallel, one per folder, each with its own log:
>
> ```bash theme={null}
> MIGRATION_LOG=products.jsonl node migrate-from-cloudinary.mjs --prefix=products/
> ```
>
> Self-hosting? Raise `MAX_FILE_SIZE_MB` above your largest file first. The default is 50 MB.

> [accordion] What the script doesn't copy
>
> * Private and authenticated files
> * Tags and metadata
> * File types Openinary doesn't accept, like SVG, PDF or TIFF. They are listed as `unsupported` in the log.
> * Upload presets, named transformations and webhooks. Recreate what you need by hand.

Stuck? [Ask in the discussions](https://github.com/openinary/openinary/discussions), we read every migration question.


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