# Pinning a widget version

> Pin a frozen loader release with `cdn.hatched.live/v/<version>/widget.js` so your embed surface stays byte-for-byte stable across deploys.

Source: https://docs.hatched.live/docs/guides/widget-versioning

By default, `cdn.hatched.live/widget.js` always serves the latest loader.
That's the right choice for most tenants: security patches and bug fixes
reach every partner page within a five-minute cache window without anyone
re-pasting a snippet.

If your integration needs a frozen contract — regulated industry, a long
QA cycle, a slow change-management process — the loader also ships under
**pinned, immutable** paths:

```
https://cdn.hatched.live/v/<version>/widget.js
```

Every released loader version gets a directory under `/v/<version>/`
that contains the loader **and** every widget bundle it imports. Once
written, an object at one of these keys never changes content.

## Quick comparison

| Path | Cache | Update behavior | When to use |
| --- | --- | --- | --- |
| `cdn.hatched.live/widget.js` | `max-age=300, must-revalidate` | Auto-updates with every deploy | Default. Always-fresh. |
| `cdn.hatched.live/v/<version>/widget.js` | `max-age=31536000, immutable` | Never changes | Audit-grade reproducibility, slow QA cycles. |

The unversioned path is the right answer 95% of the time. Use the pinned
path only when you have a real reason — version pinning is opt-in
because it freezes you out of security fixes that would otherwise reach
your site automatically.

## Pinning

Swap the `src` attribute:

```html
<script
  src="https://cdn.hatched.live/v/0.4.2/widget.js"
  data-embed-token="EMBED_OR_SESSION_TOKEN"
  defer
></script>

<div data-hatched-mount="buddy"></div>
```

The loader's bundle base is **URL-derived**: a pinned loader pulls every
widget bundle from the same versioned tree, so a `v/0.4.2/widget.js`
install only ever fetches `v/0.4.2/widget/v1/buddy-widget.min.js`. There
is no way for a pinned page to accidentally load a newer widget bundle.

## Verifying the pin took effect

The loader exposes its own version at runtime:

```js
window.HatchedWidgets.version; // → "0.4.2"
window.HatchedWidgets.buildId; // → "abc123def456"
```

Use these in:

- **CI smoke tests** — assert the deployed pin resolved to the expected
  version.
- **Error reports** — include both fields in any client-side error
  pipeline so you can correlate field bugs to a specific deploy.

## Choosing a version

The loader version comes from `apps/widgets/package.json`, and each
release is mirrored to `/v/<version>/` at deploy time. The
[changelog](/docs/reference/changelog) lists each release. The latest
unversioned `widget.js` is always the head of `main`.

A safe pinning policy:

- **Pin to the latest version when you cut a new tenant build.** Don't
  pin once and forget — pinned loaders never receive security patches.
- **Bump within ~30 days of a new minor.** Older pins still serve
  forever (the objects are immutable), but you'll miss bug fixes.
- **Subscribe to the [docs changelog](/docs/reference/changelog).** Every
  loader release lists what changed, so you can review it before bumping
  your pin.

## Rollback playbook

A pinned loader makes rollback trivial — flip the `src` attribute back
to a known-good version and redeploy your host page. Because the older
path is still served (immutable), the previous behavior comes back as
soon as the new HTML lands.

If you're on the unversioned path and need to roll back without waiting
for a Hatched deploy, the pinned route is your escape hatch:

```diff
- <script src="https://cdn.hatched.live/widget.js" ...></script>
+ <script src="https://cdn.hatched.live/v/0.4.1/widget.js" ...></script>
```

When the issue is resolved upstream, point the `src` back at the
unversioned path or the new version.

## Staging and previews

The staging mirror at `cdn.hatched.live/staging/widget.js` exists for
testing against the staging API. It does **not** receive pinned version
mirrors — staging is short-lived and version freezing it would defeat
the purpose. Pin only in production.

## What pinning does NOT pin

The loader pin freezes the **client bundle**: the loader, the widget
bundles, the locale catalogs. It does **not** pin:

- **API responses.** The Hatched API ships forward-compatible changes
  (additive fields, new event types). Existing fields never change shape
  within a major version.
- **CSS variables consumed by `themeVars`.** New `--hw-*` variables can
  appear in newer loader versions; pinning to an old loader version
  means you don't get them automatically.
- **Webhook event payloads.** Webhook delivery is server-side. See
  [webhook payloads](/docs/reference/webhook-payloads) for the contract.

If you need a fully end-to-end frozen surface, pin the SDK
(`@hatched/sdk-js`) and the loader together — both follow semver.
