---
title: Merchant-facing embeds and links
description: How to embed shop-locked dashboards and create stable links without exposing Metabase filters.
---

# Merchant-facing embeds and links

Merchants never need a Metabase account. The data plane locks each dashboard to a shop UUID, mints a short-lived Metabase token, and sends the browser to that shop-specific view. Shop names never belong in embed URLs or query parameters.

## Choose the right link

| Use case | URL | Authentication | Lifetime |
| --- | --- | --- | --- |
| Product iframe | `GET /v1/embed/reports/shop_sales?shopId=<uuid>` | Data-plane bearer token with shop access | Returned Metabase URL lasts 10 minutes; refetch it |
| Internal share or bookmark | `GET /e/shop_sales/<uuid>` | Valid session/bearer with shop access at click time | The Joe URL never expires |
| Merchant email, Slack, or iframe without a session | `GET /e/shop_sales/<uuid>?exp=<unix>&sig=<hmac>` | Signature in the URL | 7, 30, or 90 days; 90 days by default |

Both `/e/` forms mint a fresh 10-minute Metabase token when clicked and respond with a `302` redirect. A bookmarked `/e/` URL therefore does not inherit the Metabase token's expiry.

## Copy-paste examples

Use canonical shop UUIDs only:

```text
https://data-plane.joe.coffee/e/shop_sales/4a2729c3-5be1-4d1e-9b0d-985ac242f0e3
```

The equivalent query form is supported for clients that cannot put the shop in the path:

```text
https://data-plane.joe.coffee/e/shop_sales?shop=4a2729c3-5be1-4d1e-9b0d-985ac242f0e3
```

Do not hand-write `sig` values. Joe staff should open the [reports admin](https://data-plane.joe.coffee/v1/reports/admin), search for the shop by name, choose the report and 7/30/90-day expiry, then copy the generated no-login link. The page also shows the plain authenticated link for internal sharing.

For an application iframe, request a fresh URL from the authenticated API:

```http
GET /v1/embed/reports/shop_sales?shopId=4a2729c3-5be1-4d1e-9b0d-985ac242f0e3
Authorization: Bearer <short-lived-shop-scoped-token>
```

```json
{
  "ok": true,
  "data": {
    "iframeUrl": "https://joe-metabase.fly.dev/embed/dashboard/<short-lived-token>#bordered=false&titled=false",
    "expiresAt": "2026-07-20T21:10:00.000Z"
  },
  "meta": {}
}
```

Set `iframe.src` to `data.iframeUrl` and refetch before `expiresAt`. Never append a shop name, `shop_id`, or a replacement filter to the Metabase URL.

## Security model

- The authenticated path calls the same shop-access assertion as the embed API. A control-plane token can only open a UUID listed in its `shopIds` claim.
- No-login links use HMAC-SHA256 with the server-only Metabase embed secret and a domain-separated `link:v1:` payload. The signature covers report key, shop UUID, and optional Unix expiry.
- The Worker checks the report registry and the `shops` table before minting. Unknown reports, unknown shops, invalid signatures, and expired signatures receive a generic branded `404` or `403` page without configuration details.
- The URL signature authorizes the stable Joe link, not Metabase directly. The Metabase JWT remains short-lived and keeps `shop_id` locked.
- Treat a no-login URL like a password until it expires. Forwarding it forwards its access. Generate a shorter replacement when that is the safer choice.

Scheduled merchant-report emails use 90-day signed `/e/` links. Internal report emails continue to link directly to authenticated Metabase dashboards.

## Adding an embeddable report

1. Build the dashboard in Metabase with a mandatory `shop_id` filter and merchant-safe labels.
2. Enable static embedding and mark `shop_id` as a locked parameter.
3. Add the report key, human label, dashboard binding, and locked-param function to `src/embed/report-registry.ts`.
4. Add the optional dashboard ID binding to `src/env.ts` and document deployment configuration.
5. Verify the authenticated API, both `/e/` forms, the admin dropdown, and generic failure pages.

See [Merchant Manager integration](./merchant-manager-integration.md) for an application migration recipe.
