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

# Hero image

> Generate cover imagery for a unit.

Generates a banner image for a unit of learning, used as the unit's cover in the
planner and library.

> [!WARNING]
>
> This endpoint takes a **`unit_id`, not a subject and topic**. It reads the
> unit's `subject`, `topic` and `year_group` from the database itself, so the
> caller cannot vary the prompt.

## Request

**unit\_id** `string` *required*

The unit to generate a cover for. Must belong to the calling user — this is
one of the few endpoints that checks per-row ownership and returns `403` when
it fails.

```bash cURL theme={null}
curl -X POST "$BASE_URL/unit-hero-image" \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "unit_id": "8f2c1e40-..." }'
```

## Response

**success** `boolean`

`true` when a URL is available, whether generated or served from cache.

**url** `string`

Public URL of the banner in the `unit-hero-images` bucket.

**cached** `"row" | "content" | false`

Which of the three paths served the request. See below.

```json Response theme={null}
{
  "success": true,
  "url": "https://<project>.supabase.co/storage/v1/object/public/unit-hero-images/a1b2c3d4e5f60718.png",
  "cached": "row"
}
```

## Caching

`cached` tells you which of three paths served the response, and the distinction
matters because only one of them costs a generation:

| Value | Meaning |
| - | - |
| `"row"` | The unit already pointed at a current-composition image. No work done. |
| `"content"` | Another unit with the same subject, topic and year group had already generated this exact image. Reused, no generation. |
| `false` | Nothing matched. An image was generated and uploaded. |

The cache key is a SHA-256 over `subject`, `topic` and `year_group` — normalised
for case and whitespace — salted with a composition version. Two units with the
same three values share one stored object rather than generating twice.

> [!NOTE]
>
> Bumping the composition version invalidates every key at once. Existing units
> keep serving their old banner until next viewed, then lazily regenerate in the
> new composition. Old objects are left in place rather than deleted.

## Errors

| Status | Cause |
| - | - |
| 400 | `unit_id` missing. |
| 403 | The unit exists but belongs to another user. |
| 404 | No unit with that id. |
| 500 | Generation or storage upload failed. |

> [!NOTE]
>
> If the image uploads but the `units` row fails to update, the call still returns
> `200` with the URL — the image is in storage and usable, only the row is stale.
> That unit will regenerate its key on next view and land on the `"content"` cache
> path, so the failure costs a lookup rather than a second generation.


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