# Mystery Box widget

> Once-a-day surprise reward — a weighted-random coin drop with a deterministic daily seed.

Source: https://docs.hatched.live/docs/reference/widgets/mystery-box

## Mount

```html
<script src="https://cdn.hatched.live/widget.js" data-session-token="SESSION_TOKEN" defer></script>

<div data-hatched-mount="mystery-box"></div>
```

Opening the box is a write, so the Mystery Box widget must be mounted with a
**session token**. The `state` endpoint is readable with an embed token so the
widget can render its eligible / capped / locked face.

## Script attributes

| Attribute            | Values                 | Default                           |
| -------------------- | ---------------------- | --------------------------------- |
| `data-session-token` | widget session token   | claim enabled                     |
| `data-embed-token`   | embed token            | read-only state                   |
| `data-theme`         | `light` `dark`         | `light`                           |
| `data-api-base-url`  | API origin + `/api/v1` | `https://api.hatched.live/api/v1` |

## Required scopes

- `read` renders the box state.
- Claiming requires a **session token** minted with the `mysterybox:claim`
  scope; a session token without `mysterybox:claim` is rejected `403`, and
  embed tokens are read-only.

## Plan & capability

- Capability: `mystery_box` (tenant toggle in Settings → Capabilities).
- Minimum plan: **Growth**.
- Two distinct 403s can come back on both `/state` and `/claim` — the
  capability is gated at the controller level, so the guard rejects before
  the handler runs:
  - `403 plan_feature_locked` — the plan does not entitle the capability.
    Details carry `required_plan` and an `upgrade_url`.
  - `403 capability_disabled` — the plan entitles it but an operator turned the
    Settings toggle off for this workspace.

The `{ locked: true }` state is the entitled-but-unavailable face, not the
not-entitled response: when the plan does not entitle `mystery_box`, both
`/state` and `/claim` return `403 plan_feature_locked` before that branch runs.

## Endpoints

| Method | Path                       | Purpose                                          |
| ------ | -------------------------- | ------------------------------------------------ |
| `GET`  | `/widget/mystery-box/state`| `locked` / `eligible` / `next_eligible_at` / last reward |
| `POST` | `/widget/mystery-box/claim`| Open the box — `409 mystery_box_daily_cap` once spent |

The daily cap is one open per buddy and resets at midnight UTC. The reward draw
is deterministically seeded per `(buddy, UTC day)`, so it can never be re-rolled.
