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

# Payload CMS

> Wire a Payload upload collection to PixGlory by hand, today, ahead of the official storage adapter.

<Note>
  This is the manual, interim version. A `payload-storage-pixglory` adapter package, implementing
  Payload's standard cloud-storage plugin interface, is planned next. Until it ships, this guide
  is how you wire a Payload upload collection to PixGlory yourself: a `beforeChange` hook that
  presigns and confirms through the machine API. When the adapter ships, it replaces this
  collection config with a few lines of plugin configuration; nothing about your stored data
  changes, since it will still be a `url` field pointing at a `cdn.pixglory.com` link.
</Note>

## Before you start

You need a PixGlory API key and a CDN project. See the [Quickstart](/quickstart) to create a key,
and create a project from your [dashboard](https://pixglory.com/pix/cdn) if you don't have one
yet. Set both as environment variables Payload's server can read:

```bash .env theme={null}
PIXGLORY_API_KEY=pxg_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
PIXGLORY_PROJECT_ID=proj_xxxxxxxxxxxxxxxxxxxxxxxxxxxx
```

Find your project's id with a request against `GET /v1/projects`:

```bash theme={null}
curl https://pixglory.com/api/v1/projects \
  -H "Authorization: Bearer $PIXGLORY_API_KEY"
```

## The collection

Set `disableLocalStorage: true` so Payload never writes the file to its own disk, and add a
`beforeChange` hook that uploads to PixGlory instead. The trick that keeps the read side simple:
override Payload's own `url` field with the file's real PixGlory link, so every place your app
already reads `doc.url` keeps working unchanged, with no custom static route to serve it.

```ts collections/Media.ts theme={null}
import type { CollectionConfig } from "payload";

const PIXGLORY_API = "https://pixglory.com/api/v1";

export const Media: CollectionConfig = {
  slug: "media",
  upload: {
    disableLocalStorage: true,
    mimeTypes: ["image/*"],
  },
  fields: [
    {
      name: "alt",
      type: "text",
    },
    {
      // The path PixGlory confirmed this file under, kept for reference.
      // The upload/lookup/delete verbs a full adapter needs are on the
      // way (plan-24 slice 2a); until they ship, there is no API call
      // this field can drive, so it's just a record.
      name: "pixgloryPath",
      type: "text",
      admin: { hidden: true },
    },
  ],
  hooks: {
    beforeChange: [
      async ({ req, data, operation }) => {
        if (operation !== "create" || !req.file) return data;

        const presignRes = await fetch(`${PIXGLORY_API}/uploads`, {
          method: "POST",
          headers: {
            Authorization: `Bearer ${process.env.PIXGLORY_API_KEY}`,
            "Content-Type": "application/json",
          },
          body: JSON.stringify({
            projectId: process.env.PIXGLORY_PROJECT_ID,
            files: [
              {
                name: req.file.name,
                size: req.file.size,
                type: req.file.mimetype,
              },
            ],
          }),
        });
        if (!presignRes.ok) {
          throw new Error(`PixGlory presign failed: ${presignRes.status}`);
        }
        const { uploads } = await presignRes.json();
        const [upload] = uploads;

        await fetch(upload.uploadUrl, {
          method: "PUT",
          headers: { "Content-Type": upload.contentType },
          body: req.file.data,
        });

        const confirmRes = await fetch(`${PIXGLORY_API}/uploads/confirm`, {
          method: "POST",
          headers: {
            Authorization: `Bearer ${process.env.PIXGLORY_API_KEY}`,
            "Content-Type": "application/json",
          },
          body: JSON.stringify({
            projectId: process.env.PIXGLORY_PROJECT_ID,
            paths: [upload.path],
          }),
        });
        if (!confirmRes.ok) {
          throw new Error(`PixGlory confirm failed: ${confirmRes.status}`);
        }
        const { files: confirmed } = await confirmRes.json();

        return {
          ...data,
          url: confirmed[0].publicUrl,
          pixgloryPath: upload.path,
        };
      },
    ],
  },
};
```

Use it in your app the same way you'd use any other upload collection's document:

```tsx theme={null}
<img src={mediaDoc.url} alt={mediaDoc.alt} />
```

## What this interim version does not do

**Deleting the Payload document does not delete the file from PixGlory.** The machine API has no
delete or lookup-by-id route yet (`GET`/`DELETE /v1/files/{id}` is plan-24 slice 2a, not shipped),
so an `afterDelete` hook here has nothing to call. Until that ships, deleting media in Payload
only removes the Payload record; the file stays in your CDN project and you'd remove it from the
[dashboard](https://pixglory.com/pix/cdn) by hand if you want it gone. This gap, plus a
filename-keyed lookup, is exactly what the official adapter needs to implement `handleDelete`
correctly, which is why it's built before the adapter package rather than guessed at.

**Re-uploading a replacement image creates a new file rather than replacing one in place.** The
machine API's `replace` operation addresses a file by the id PixGlory assigned it, which this
collection never stores. That's a reasonable thing to add to `pixgloryPath` above if you need it
before the adapter ships; the adapter itself will handle it as part of `handleUpload`.

## Handling errors

A `402` response means the organisation's storage quota would be exceeded; a `429` means you've
hit the rate limit and should back off using the `Retry-After` header. Every error body has the
same shape:

```json theme={null}
{ "error": { "message": "...", "code": "CDN_STORAGE_QUOTA_EXCEEDED", "details": { "..." : "..." } } }
```

Branch on `error.code`, not `error.message`, which can change wording without notice. The full
list of status codes and error codes is in the **API Reference** tab. Throwing inside the
`beforeChange` hook above (as the presign/confirm failure checks do) fails the whole create
operation, so a customer never ends up with a Payload document that has no file behind it.


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