> ## 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.

# Cloud Quickstart

> Create your Openinary Cloud account, mint an API key, upload your first file, and deliver a transformed URL, in about five minutes.

Four steps to an account, a server-side API key, a file in storage, and a public URL that resizes it on the fly.

## 1. Create your account

**1. Open the app**

Go to [app.openinary.dev](https://app.openinary.dev) and enter your email address. Signing in and signing up are the same form: a new address creates the account.

**2. Enter the code**

You receive a **6-digit code** by email (valid 10 minutes). Enter it and you're in, on the Free plan. You can also use **Google**. There is no password.

Signed in, you land on the media browser for your default bucket, **Main**.

## 2. Create an API key

Your key is what lets your backend talk to `https://cdn.openinary.dev`.

**1. Open the API keys tab**

Click your account in the bottom-left of the dashboard → **Settings** → **API keys**.

**2. Fill in the three fields**

* **Name**, so you can tell keys apart later, e.g. `Production`.
* **Bucket**, the one this key can read and write. It **cannot be changed later**, create a second key rather than re-pointing this one.
* **Expires**, in days, from 1 to 365 (default 365).

**3. Copy the key immediately**

The key (`oik_…`) is displayed **once**, right after creation. Store it in your secret manager or `.env` now, there is no way to read it back.

> [!WARNING]
>
> The key writes to and deletes from your bucket. Keep it server-side only:
> never in a browser bundle, a mobile app, or a public repo. For browser uploads
> use [presigned signatures](https://docs.openinary.dev/cloud/file-uploader.md) instead.

Disable a key with the **Power** icon or delete it outright, both take effect immediately.

## 3. Upload a file

Send the key as `x-api-key`, or as `Authorization: Bearer`, either works.

> [tab] curl
>
> ```bash theme={null}
> curl -X POST https://cdn.openinary.dev/upload \
>   -A "Mozilla/5.0" \
>   -H "x-api-key: $OPENINARY_API_KEY" \
>   -F "files=@photo.jpg" \
>   -F "folder=products"
> ```

> [tab] Node.js
>
> ```ts theme={null}
> const form = new FormData();
> form.append("files", new Blob([await readFile("photo.jpg")]), "photo.jpg");
> form.append("folder", "products");
>
> const res = await fetch("https://cdn.openinary.dev/upload", {
>   method: "POST",
>   headers: { "x-api-key": process.env.OPENINARY_API_KEY! },
>   body: form,
> });
> const { files } = await res.json();
> ```

The response tells you where the file landed, and gives you your delivery path:

```json theme={null}
{
  "success": true,
  "files": [
    {
      "path": "products/photo.jpg",
      "name": "photo.jpg",
      "filename": "photo.jpg",
      "size": 248713,
      "url": "/b/1f0c7a2e-5b64-4d0a-9b1a-6f2d9c33e871/t/products/photo.jpg"
    }
  ]
}
```

That `url` carries your **bucket ID**, the `/b/{bucketId}/` segment. It is not a secret, it appears in every public URL you serve, and you can also read it off any file in the dashboard's asset details panel.

## 4. Deliver it, transformed

Prefix the returned `url` with `https://cdn.openinary.dev` and insert transformation parameters right after `/t/`:

```text theme={null}
https://cdn.openinary.dev/b/{bucketId}/t/w_400,h_400,c_fill/products/photo.jpg
```

> [tab] Thumbnail
>
> ```text theme={null}
> /b/{bucketId}/t/w_400,h_400,c_fill/products/photo.jpg
> ```

> [tab] WebP at quality 80
>
> ```text theme={null}
> /b/{bucketId}/t/w_1200,f_webp,q_80/products/photo.jpg
> ```

> [tab] Original, untransformed
>
> ```text theme={null}
> /b/{bucketId}/t/products/photo.jpg
> ```

The first request queues the transformation and answers `202 {"status":"processing"}` rather than image bytes. Request it again once its `Retry-After` has passed (a few seconds more if the processing server was idle and has to wake up first, see [waiting on a 202](https://docs.openinary.dev/api-reference/media/transform.md#waiting-on-a-202)) and you get the result, served from the CDN cache from then on. The parameter list is exactly the same as the self-hosted version, see [Image transformations](https://docs.openinary.dev/media-transformations/image-transformations.md) and [Video transformations](https://docs.openinary.dev/media-transformations/video-transformations.md).

Open that URL in a browser: the first load may show the `202` JSON rather than the picture. Reload once and the image appears. That is the pipeline working, not a failure.

## 5. Use it in a real app

The pattern that holds in production: **uploads and storage calls run on your server** with the API key, **delivery URLs are public** and go straight to the CDN.

```ts app/api/products/route.ts theme={null}
// Server-side: the key never reaches the browser.
const CDN = "https://cdn.openinary.dev";

export async function POST(request: Request) {
  const incoming = await request.formData();
  const file = incoming.get("file");
  if (!(file instanceof File)) {
    return Response.json({ error: "No file" }, { status: 400 });
  }

  const form = new FormData();
  form.append("files", file, file.name);
  form.append("folder", "products");

  const res = await fetch(`${CDN}/upload`, {
    method: "POST",
    headers: { "x-api-key": process.env.OPENINARY_API_KEY! },
    body: form,
  });
  if (!res.ok) {
    // 402 means a plan limit was reached, see /cloud/api#errors
    return Response.json(await res.json(), { status: res.status });
  }

  const { files } = await res.json();
  // Store the path; build URLs from it wherever you render.
  return Response.json({ path: files[0].path, url: `${CDN}${files[0].url}` });
}
```

```tsx components/product-image.tsx theme={null}
// Client-side: public URLs only, no key involved.
export function ProductImage({ path }: { path: string }) {
  const base = `https://cdn.openinary.dev/b/${process.env.NEXT_PUBLIC_OPENINARY_BUCKET}/t`;
  return (
    <img
      src={`${base}/w_800,c_fill,f_webp/${path}`}
      srcSet={`${base}/w_400,c_fill,f_webp/${path} 400w, ${base}/w_800,c_fill,f_webp/${path} 800w`}
      sizes="(max-width: 640px) 400px, 800px"
      alt=""
    />
  );
}
```

```bash .env theme={null}
OPENINARY_API_KEY=oik_...                  # server only
NEXT_PUBLIC_OPENINARY_BUCKET=1f0c7a2e-...  # public, it's in every URL
```

Proxying `/upload` through an unauthenticated public route with your key attached leaks the key just as surely as shipping it. Either authenticate the server route yourself, as above, or use [presigned signatures](https://docs.openinary.dev/cloud/file-uploader.md) so the browser only holds a short-lived token.

## Videos

Videos work the same way, with one difference: a transformation is processed asynchronously. The first request for a transformed video returns `202` while it encodes; poll the status endpoint (or listen to [`/queue/events`](https://docs.openinary.dev/cloud/api#video-processing)) until it's `completed`.

```bash theme={null}
curl "https://cdn.openinary.dev/video-status/w_640/clips/demo.mp4" \
  -H "x-api-key: $OPENINARY_API_KEY"
```

```json theme={null}
{ "status": "processing", "progress": 62 }
```

A video URL with no parameters, `/t/clips/demo.mp4`, is never transcoded. It streams the stored file with range requests, so seeking works. Transcoding happens only when the URL asks for it.

## Next steps

> [card] [Cloud API Reference](https://docs.openinary.dev/cloud/api.md)
>
> Every endpoint, authentication, errors and rate limits.

> [card] [File Uploader](https://docs.openinary.dev/cloud/file-uploader.md)
>
> A ready-made React uploader wired to your Cloud bucket.

> [card] [Transformations](https://docs.openinary.dev/media-transformations/overview.md)
>
> The complete parameter reference.

> [card] [Plans & limits](https://docs.openinary.dev/cloud/overview.md#plans-and-limits)
>
> What's included, and what happens past it.


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