---
title: Delivery API
description: Request the placeholder through the Delivery API and render it in Next.js or Nuxt, including native BlurHash and ThumbHash values.
seo:
  image: /og/delivery-api.png
---

The package leaves Umbraco's shallow media converter alone, so `blurPlaceholder` never appears in a media response you did not ask for. You request it explicitly instead.

Start by enabling the Delivery API and its media endpoints in the host application:

```json
{
  "Umbraco": {
    "CMS": {
      "DeliveryApi": {
        "Enabled": true,
        "PublicAccess": true,
        "Media": {
          "Enabled": true,
          "PublicAccess": true
        }
      }
    }
  }
}
```

Then ask for the property by name:

```http
GET /umbraco/delivery/api/v2/media/item/{mediaId}?expand=properties[$all]&fields=properties[blurPlaceholder]
```

The same expansion and field selection works for a media picker response. Every other media response keeps its usual shape, so the extra payload only reaches the clients that ask for it.

## Example API response

The requested property appears in the media item's `properties` object as one plain string:

```json
{
  "path": "/station-platform.png/",
  "createDate": "2026-08-10T08:46:31.004237Z",
  "updateDate": "2026-08-10T08:46:31.004237Z",
  "id": "d55605e1-c63a-42cd-86a6-a99adca2a565",
  "name": "station-platform.png",
  "mediaType": "Image",
  "url": "/media/n1hbay0t/station-platform.png",
  "extension": "png",
  "width": 1536,
  "height": 1024,
  "bytes": 2468341,
  "properties": {
    "blurPlaceholder": "data:image/webp;base64,UklGRoIAAABXRUJQVlA4IHYAAABwAwCdASoQAAsALoVCoVClJSUlBQCESzgE6AxZblsod8ldbAAA/vs0YnnRmszUoA9/XeE6xi8oqvuYhNTIbmf34VbF388vudNZmZ7B4pF4N5Kwiixxuf2/w1XnA/yuEyJteMig85jSjuP1fcG9JRe+aOBYEAAA"
  },
  "focalPoint": {
    "left": 0.5,
    "top": 0.5
  },
  "crops": []
}
```

## Render the default output

With the default `DecodeToDataUrl: true`, every algorithm produces a WebP data URL that can be passed directly to the framework's image component.

<CodeGroup>

```tsx Next.js
import Image from "next/image";

export function MediaImage({ media }: { media: MediaItem }) {
  return (
    <Image
      src={media.url}
      alt={media.alt}
      width={media.width}
      height={media.height}
      placeholder={media.properties.blurPlaceholder ? "blur" : "empty"}
      blurDataURL={media.properties.blurPlaceholder ?? undefined}
    />
  );
}
```

```vue Nuxt
<script setup lang="ts">
defineProps<{ media: MediaItem }>();
</script>

<template>
  <NuxtImg
    :src="media.url"
    :alt="media.alt"
    :width="media.width"
    :height="media.height"
    :placeholder="media.properties.blurPlaceholder || undefined"
  />
</template>
```

</CodeGroup>

No browser decoder is involved, which is why this is the default. Remember the data URL is a placeholder; the real image still loads from `media.url`.

## Consume a native hash

Set `DecodeToDataUrl` to `false` when the smaller stored value is worth decoding it on your application server.

Native values keep their prefix. `blurhash:` adds 9 characters and `thumbhash:` adds 10, and both compress to almost nothing once the Delivery API response uses gzip or Brotli. The prefix is what stops a configuration change from quietly feeding a ThumbHash to a BlurHash decoder.

Install `blurhash`, `thumbhash`, and `sharp`. Both examples below decode in a server component and pass only the resulting WebP data URL to the image component:

<CodeGroup>

```tsx Next.js
import Image from "next/image";
import { decodePlaceholder } from "@/lib/decode-placeholder.server";

// App Router components are Server Components unless marked "use client".
export async function MediaImage({ media }: { media: MediaItem }) {
  const placeholder = await decodePlaceholder(
    media.properties.blurPlaceholder,
    media.width,
    media.height,
  );

  return (
    <Image
      src={media.url}
      alt={media.alt}
      width={media.width}
      height={media.height}
      placeholder={placeholder ? "blur" : "empty"}
      blurDataURL={placeholder}
    />
  );
}
```

```vue Nuxt · MediaImage.server.vue
<script setup lang="ts">
import { decodePlaceholder } from "~/utils/decode-placeholder.server";

const props = defineProps<{ media: MediaItem }>();
const placeholder = await decodePlaceholder(
  props.media.properties.blurPlaceholder,
  props.media.width,
  props.media.height,
);
</script>

<template>
  <NuxtImg
    :src="media.url"
    :alt="media.alt"
    :width="media.width"
    :height="media.height"
    :placeholder="placeholder"
  />
</template>
```

</CodeGroup>

Both components share one server-only decoder module. It branches on the prefix, so it handles either algorithm without extra configuration:

```ts title="decode-placeholder.server.ts"
import sharp from "sharp";
import { decode } from "blurhash";
import { thumbHashToRGBA } from "thumbhash";

export async function decodePlaceholder(
  value: string | null | undefined,
  sourceWidth: number,
  sourceHeight: number,
): Promise<string | undefined> {
  if (!value) return undefined;
  if (value.startsWith("data:image/")) return value;

  if (value.startsWith("thumbhash:")) {
    return decodeThumbHash(value.slice("thumbhash:".length));
  }

  if (value.startsWith("blurhash:")) {
    return decodeBlurHash(value.slice("blurhash:".length));
  }

  throw new Error("Unknown blur placeholder representation.");

  async function decodeThumbHash(encodedHash: string) {
    const bytes = Uint8Array.from(Buffer.from(encodedHash, "base64"));
    const { w, h, rgba } = thumbHashToRGBA(bytes);
    return rgbaToWebpDataUrl(rgba, w, h);
  }

  function decodeBlurHash(hash: string) {
    const scale = 32 / Math.max(sourceWidth, sourceHeight);
    const width = Math.max(1, Math.round(sourceWidth * scale));
    const height = Math.max(1, Math.round(sourceHeight * scale));
    const rgba = decode(hash, width, height);
    return rgbaToWebpDataUrl(rgba, width, height);
  }
}

async function rgbaToWebpDataUrl(
  rgba: Uint8Array | Uint8ClampedArray,
  width: number,
  height: number,
) {
  const webp = await sharp(Buffer.from(rgba), {
    raw: { width, height, channels: 4 },
  })
    .webp({ quality: 60 })
    .toBuffer();

  return `data:image/webp;base64,${webp.toString("base64")}`;
}
```

Next.js App Router components are Server Components by default. Nuxt's `.server.vue` component convention requires [`experimental.componentIslands: true`](https://nuxt.com/docs/3.x/directory-structure/components#server-components) in `nuxt.config.ts`. In both cases, the hash decoder and `sharp` stay on the server; the rendered component receives only the WebP data URL.

The utility calls the [Wolt BlurHash JavaScript decoder](https://github.com/woltapp/blurhash/tree/master/TypeScript) and the [official ThumbHash JavaScript implementation](https://github.com/evanw/thumbhash). BlurHash does not contain an aspect ratio, so the example derives its 32px decode size from the media dimensions; ThumbHash restores its encoded dimensions itself.
