# Embedded Reports and Dashboards

> Show your customers their own usage, errors and latency inside your app: a saved report or dashboard, filtered to each customer by a token your server signs.

Source: https://www.apitraffic.io/docs/integrations/embed-reports

Put a [saved report](https://www.apitraffic.io/docs/reports) or a [dashboard](https://www.apitraffic.io/docs/dashboards) inside your own product, so each of your customers sees their slice of it: "your API calls this month", "your error rate", "your slowest endpoints". Your server signs a short-lived embed token that locks a filter, typically your customer's id. The viewer can pick a time range and nothing else.

> **Note:** Embedding needs the **Embeds** and **Custom reports** plan features. Your plan may also limit how many reports and dashboards allow embedding at once (**Embedded reports and dashboards**).

## How it works

```mermaid
sequenceDiagram
    participant Browser as Your customer's browser
    participant Server as Your server
    participant AT as ApiTraffic

    Browser->>Server: GET /api/usage-embed-token (signed in)
    Server->>AT: POST /v1/accounts/{id}/embeds/token
    AT-->>Server: { token, expiresAt }
    Server-->>Browser: { token }
    Browser->>AT: The embed loads the dashboard with the token
    AT-->>Browser: Only this customer's numbers
```

1. In ApiTraffic, turn on **Allow embedding** for the report or dashboard.
2. Your **backend** mints an embed token naming it, with a filter for the signed-in customer (`tag:customerId == "org_8f2a"`).
3. Your **frontend** places `<apitraffic-dashboard>` (or `<apitraffic-report>`) and hands it the token.
4. The embed draws the report with the token's filter ANDed onto the report's own, every time. A viewer cannot remove it, change it or read the report's own filter.

## 1. Allow embedding

Open the report or dashboard and click **Embed**. Turn on **Allow embedding**.

![The Embed this dashboard dialog for Your API usage: an explanation of embedding, the Allow embedding switch turned on, and a Preview section with the test filter tag:customerId == &quot;org_1c77&quot; and a live preview of the dashboard below it](https://www.apitraffic.io/docs/images/reports/embed-dialog.png)

- Only an account owner or admin can turn it on, and only for a report or dashboard shared with **Everyone in the account**.
- Anyone who can edit it can turn it off, also after a plan downgrade. Turning it off stops every embed of it at once, on its next request; so does making it private or deleting it. Tokens already handed out stop working with it; turning embedding back on makes the ones that have not expired yet (at most 24 hours old) work again. Deleting an embedded report or dashboard is recorded in the account's activity as **Embedding was turned off**.
- While a report or dashboard is embedded, only owners and admins can change what it shows: a report's definition, or a dashboard's filter, scope and tiles. That includes a report shown on an embedded dashboard. Anyone else who can edit it can still rename it or turn embedding off; the report builder and the dashboard tell them so up front.
- A report that groups by a catalogue attribute of a tag classified **Personal** (or whose filter uses one) can't be embedded, because embeds mask personal values: **Allow embedding** is refused with the reason, shown next to the switch. The same goes for a dashboard whose tiles or filter do so.
- A report that groups by **Firewall policy** or **Deciding rule** (`waf_policy`, `waf_rule`, or the tags `tag:waf.policy`, `tag:waf.rule`), or by any other firewall tag except the action, matched marker and score (such as `tag:waf.reason` or `tag:waf.match.1`), or whose filter uses any of them, can't be embedded either: policy and rule IDs, descriptions and messages are your firewall configuration, and a reason or match quotes the input that matched, not something your customers should see. The same goes for a dashboard whose tiles or filter do so. **Firewall action**, **Firewall matched** and the firewall counts and scores can be embedded.
- A report that groups by **IP address** (`ip_address`) can't be embedded: a client's IP address is personal data. The same goes for a dashboard with such a tile. **Country**, and a distinct count of IP addresses, can be embedded. An embed showing countries shows the geo data's credit under the report, as the data's licence asks.
- An embeddable dashboard embeds its tiles: their reports do not need **Allow embedding** themselves.
- Reports and dashboards that allow embedding are marked **Embeddable** in their lists, and listed on the **Embed Setup** page (in the account settings menu, under **Integration** → **Embed**).

The **Embed** dialog also shows the report's or dashboard's ID (**Report ID** or **Dashboard ID**, with a copy button) and has a **Preview**: type a **Test filter** (for example `tag:customerId == "org_8f2a"`) and click **Preview**, or press Enter, to see exactly what that customer would see. The preview uses a five-minute token; when it runs out, click **Refresh preview**.

> **Warning:** An embeddable report shows its data to anyone holding a token for it, so think of **Allow embedding** like sharing. Check the report's own filter and grouping first: the token's filter narrows it, but a report grouped by a tag shows that tag's values (masked when the tag is classified **Personal**).

## 2. Mint a token on your server

Serve the token from a route behind your own sign-in, and take the filter value from the signed-in user, never from the request.

> **Note:** Minting an embed token takes an **Administrator** [API token](https://www.apitraffic.io/docs/account/api-tokens), set on your server as `API_TRAFFIC_EMBED_TOKEN` for the SDK helper. The ingest token your SDK sends traffic with can't mint them. See [Embed tokens](https://www.apitraffic.io/docs/integrations/node#embed-tokens).

**ApiTraffic SDK**

```javascript
const apitraffic = require('@apitraffic/node');

app.get('/api/usage-embed-token', requireUser, async (req, res) => {
  const { token, expiresAt } = await apitraffic.embedToken({
    view: 'dashboard',                       // or 'report' with reportSid
    dashboardSid: 'dash_8VtW2kq0',
    filters: {
      // Written as tag:customerId == "…", with the value escaped.
      tags: { customerId: req.user.customerId }
    },
    timeRanges: ['last_24h', 'last_7d', 'last_30d'],
    timeRange: { relative: 'last_30d' },
    ttl: 900
  });
  res.json({ token, expiresAt });
});
```

**HTTP**

```bash
curl -X POST https://api.apitraffic.io/v1/accounts/$ACCOUNT_SID/embeds/token \
  -H "Authorization: Bearer $APITRAFFIC_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "view": "dashboard",
    "dashboardSid": "dash_8VtW2kq0",
    "filters": { "filter": "tag:customerId == \"org_8f2a\"" },
    "timeRanges": ["last_24h", "last_7d", "last_30d"],
    "timeRange": { "relative": "last_30d" },
    "ttl": 900
  }'
```

Inside a double-quoted value, escape `"` as `\"` and `\` as `\\`. A value with an unescaped quote is refused, so an id such as `x" OR method == "GET` cannot add a condition of its own.

### Token parameters

| Parameter | Description |
| --- | --- |
| `view` | `report` or `dashboard`. |
| `reportSid` / `dashboardSid` | What the embed shows. It must allow embedding. The **Embed** dialog shows it as **Report ID** or **Dashboard ID**, with a copy button (it is also in the address bar). |
| `filters.filter` | A filter in the [report filter language](https://www.apitraffic.io/docs/reports#filters), ANDed with the report's (and dashboard's) own. It may use a tag classified **Personal**: your server chose the value. A tag classified **Secret** is refused. A filter on the firewall's decisions (such as `waf_action == "block"`) needs the firewall in your plan (`403 FEATURE_NOT_IN_PLAN` otherwise). |
| `filters.environmentSid` | Only count this environment. |
| `bucketSid` | Only count this bucket. Without it the report keeps its own scope. |
| `timeRanges` | The ranges the viewer may pick: `last_1h`, `last_24h`, `last_7d`, `last_30d`, `last_90d`, `today`, `yesterday`, `this_month`, `previous_month`. Default: all of them. |
| `timeRange` | The range the embed opens with, one of `timeRanges`. Default: the report's or dashboard's own range when allowed, else **Last 24 hours**, else the first allowed. |
| `ttl` | Lifetime in seconds, 60 to 86,400 (default 3,600). |
| `theme` | `light`, `dark` or `auto`. |

The request log's `criteria`, `streamViewSid`, `tabs` and `features` do not apply to reports and are refused.

## 3. Place the embed

```html
<script
  src="https://unpkg.com/@apitraffic/embed@1.1.0/dist/apitraffic-embed.iife.js"
  integrity="sha384-<base64 SHA-384 of that exact file>"
  crossorigin="anonymous"></script>

<apitraffic-dashboard id="usage" frame-title="Your API usage"></apitraffic-dashboard>

<script>
  const el = document.getElementById('usage');
  const refresh = () => fetch('/api/usage-embed-token')
    .then((r) => { if (!r.ok) throw new Error(`token route answered ${r.status}`); return r.json(); })
    .then(({ token }) => { el.token = token; })
    .catch((error) => console.warn('Could not refresh the embed token', error));

  refresh();
  // Fires about 30 seconds before the token expires, and when it has expired or ApiTraffic refuses it.
  el.addEventListener('apitraffic:token-expired', refresh);
  el.addEventListener('apitraffic:error', (e) => console.warn(e.detail.code, e.detail.message));
</script>
```

Version 1.1.0 is the first with `<apitraffic-report>` and `<apitraffic-dashboard>`. Pin the exact version with its `integrity` hash, so a changed file is refused, and compute the hash of the file you pin. Replace the placeholder before shipping: depending on the browser, a placeholder hash makes it refuse the script (Firefox) or ignore the check (Chrome):

```bash
curl -fsSL https://unpkg.com/@apitraffic/embed@1.1.0/dist/apitraffic-embed.iife.js \
  | openssl dgst -sha384 -binary | openssl base64 -A
```

Or, with a bundler: `import '@apitraffic/embed';`. Use `<apitraffic-report>` for a single report.

The element draws the report in an iframe served by ApiTraffic and sizes it to its content, so your page's styles never reach it and it never reads your page. The token is handed over with `postMessage`, never in a URL.

| Attribute / property | Description |
| --- | --- |
| `token` | **Required.** The embed token. Set it as a property (`el.token = …`) so it does not appear in the DOM. Setting a new one reloads the embed. |
| `frame-title` | The iframe's accessible name (default "ApiTraffic dashboard" or "ApiTraffic report"): say what it shows. A `title` on the element is moved here, so the name is not announced twice. |
| `app-url` | The ApiTraffic app the embed is served from (default `https://app.apitraffic.io`). Only its origin and path are used: the frame loads `/embed`. |

| Event | When |
| --- | --- |
| `apitraffic:ready` | The embed has loaded. |
| `apitraffic:token-expired` | Set a new token. `detail.reason` is `expiring` about 30 seconds before the token's expiry (read against ApiTraffic's clock where the token shows the viewer's clock is off, and in any case a wrong clock cannot make it loop), and `expired` once it has expired or ApiTraffic refused it as expired (an invalid token arrives as `apitraffic:error` instead). Repeated notices back off (1 s, 2 s, 4 s …), so a token problem never turns into a loop against your server. |
| `apitraffic:error` | Something else went wrong. `detail.code` is the error, for example `EMBED_NOT_AVAILABLE`, `INVALID_REPORT` (the report could not run), or `TOKEN_EXPIRED` (your page handed back the same token after `apitraffic:token-expired`, and it has expired). |

### Without the element

Any page can frame the embed page directly. Its path is exactly `/embed` on the app: `https://app.apitraffic.io/embed`. Give it the token in one of two ways:

- **In the URL fragment:** `https://app.apitraffic.io/embed#token=`. The fragment (after `#`) is never sent to a server, and the page removes it from its address before anything else runs. To refresh, set a new `#token=` on the frame's address; the page reads it and removes it again.
- **By `postMessage`**, once the frame has loaded and again whenever you have a new token:

```javascript
frame.contentWindow.postMessage(
  { type: 'apitraffic:token', token },
  'https://app.apitraffic.io'   // the app's origin only, never '*'
);
```

Only messages from the frame's parent window are read.

The page posts these messages to its parent (with `'*'` as the target origin, since it cannot know who frames it; they carry nothing but these fields). Check `event.source === frame.contentWindow` and `event.origin === 'https://app.apitraffic.io'` before acting on one:

| Message `type` | Fields | When |
| --- | --- | --- |
| `apitraffic:embed-ready` | - | The report or dashboard has loaded. |
| `apitraffic:embed-resize` | `height` (pixels) | Its content changed height: size the frame to it. |
| `apitraffic:embed-token-expired` | `reason` (`expired`) | ApiTraffic refused the token as expired: send a new one. The page keeps showing what it had while it waits. |
| `apitraffic:embed-error` | `code`, `message` | Something else failed. `message` is the plain text the viewer sees. |

Without the element nothing warns you before the token expires: refresh it yourself ahead of its `expiresAt`.

### Content Security Policy

Allow the frame: `frame-src https://app.apitraffic.io`. The element's script needs `script-src` for wherever it comes from (`https://unpkg.com` for the CDN build).

Your page must be served from an `http(s)` origin. A page opened from `file:` or from an app-specific scheme (`app://`, `capacitor://` and the like) cannot frame the embed.

## What your customer sees

![An invented customer's app, Acme Shop, on its API usage page, with the embedded Your API usage dashboard: a Time range menu set to Last 7 days, and tiles for Active customers, Top customers showing only Globex, Usage by plan and Customer latency (p95) trend](https://www.apitraffic.io/docs/images/reports/embed-dashboard.png)

On a phone the tiles stack in one column:

![The same embedded dashboard at 320 pixels wide: the tiles stacked in one column under the Time range menu](https://www.apitraffic.io/docs/images/reports/embed-dashboard-320.png)

- The report or dashboard, drawn as in ApiTraffic, with a time range menu showing only the ranges you allowed. **Today**, **Yesterday** and the month ranges are UTC days, and the embed says so.
- No links into ApiTraffic, no filters, and no way to change what is counted.
- Under a chart, the numbers follow the report's [Table setting](https://www.apitraffic.io/docs/reports#the-table-under-a-chart): behind **Show the numbers** (the default), open under the chart, or hidden so only the chart shows (screen readers still get the numbers). Change it on the report and the embed follows on its next load.
- Values of tags classified **Personal** are masked (`d***@acme.com`), as for a member who may not view personal tags.
- If you turn off **Allow embedding**, they see **This report is no longer available.** (or **This dashboard is no longer available.**).
- When a renewed token arrives, what is shown stays on screen, with a quiet **Refreshing…**, until the new numbers are in. A token for another customer or report starts afresh, so one customer's numbers never show under another's. While waiting for a new token after one expired, they see a quiet **Access has expired. Waiting for new access…** line, not an error.
- If a dashboard tile fails, that tile alone says so in plain words (**This tile couldn't load.**, or **This tile can't be shown right now.** when the report itself can't run), with **Try again**; the reason is logged on ApiTraffic's side, never shown to the viewer.

## Security

**Can a viewer see another customer's data?**

No. The filter is signed into the token, and every run ANDs it onto the report's own filter on ApiTraffic's side. A run accepts only a time range, and only one of the ranges the token allows.

**Does the embed reveal how the report is built?**

It shows the report's name, description, chart type, groupings and metrics: what it needs to draw the chart. The report's own filter and scope, and which data sources answered, are never sent.

**How do I stop an embed?**

Turn off **Allow embedding**, make the report or dashboard private, or delete it: every token for it stops at once. Tokens also expire on their own (at most 24 hours).

**Who can mint tokens?**

Any API token that can send traffic, as for the request log embed: keep it on your server. A token can only name a report or dashboard that allows embedding, which only an owner or admin can turn on. Turning embedding on or off is recorded in the account's activity.

## API reference

The element calls these for you. They take the embed token as `Authorization: Bearer ` and answer CORS from any origin. A request log token is refused (`403 EMBED_VIEW_MISMATCH`), and a report or dashboard that no longer allows embedding answers `404 EMBED_NOT_AVAILABLE`.

### Get the embed

```
GET /v1/embed/view
```

```json
{
  "view": "dashboard",
  "theme": "light",
  "timeRange": { "relative": "last_30d" },
  "timeRanges": ["last_24h", "last_7d", "last_30d"],
  "dashboard": {
    "name": "Your API usage",
    "description": null,
    "tiles": [
      { "id": "t1", "x": 0, "y": 0, "w": 6, "h": 3, "title": "Your requests",
        "report": { "name": "Requests", "description": null, "visualization": "line", "display": { "table": "hidden" },
                    "definition": { "metrics": ["count"], "interval": "auto" } } }
    ]
  },
  "tagNames": { "customerId": "Customer" }
}
```

A `report` token gets `report` (`name`, `description`, `visualization`, `display`, `definition`) in place of `dashboard`. `display.table` says whether the table under a chart is `hidden`, `collapsed` or `shown`. A definition here holds only `groupBy`, `metrics`, `interval`, `compareTo`, `orderBy`, `limit`, `secondaryLimit` and `includeOther`. A tile's `title` is present when it was renamed on the dashboard. `tagNames` maps the tag keys the reports read to their display names, where one is set.

### Run a report

```
POST /v1/embed/report/run
{ "timeRange": { "relative": "last_7d" } }
```

### Run a dashboard tile

```
POST /v1/embed/dashboard/tiles/{tileId}/run
{ "timeRange": { "relative": "last_7d" } }
```

Both return a [report result](https://www.apitraffic.io/docs/api-reference/reports#the-report-result) (`rows`, `totals`, and `points` for a time series), with a shorter `meta`: `window`, `compareWindow`, `intervalSeconds`, `masked`, `otherShare`, `missingValueShare`, `approximations`, `exact`, `clamped` and `report` (`name`, `visualization`). Bucket and environment values in `rows` are their names, never their sids. `timeRange` may be left out (the range the embed opens with); any other range, or any other field, is `400`. Each account's embeds share a rate limit, and each of your customers (everyone viewing the same report or dashboard with the same filter and scope) has a share of its own, a quarter of it (or the per-address limit, if that is larger) (`429` with `Retry-After`).

## Troubleshooting

**The token request says the report "could not be located, or it does not allow embedding".** Turn on **Allow embedding** in its **Embed** dialog, and check it is shared with **Everyone in the account**.

**The embed says This report (or dashboard) is no longer available.** Embedding was turned off, or the report or dashboard was made private or deleted.

**Allow embedding is refused: "This report can't be embedded. It groups by a catalogue attribute of …".** The report groups by (or filters on) a catalogue attribute of a tag classified **Personal**, which an embed would have to mask. Group by something else, or change the tag's classification.

**Allow embedding is refused: "This report can't be embedded. It groups by waf_policy …" (or waf_rule, waf.reason, waf.match.1, or "Its filter uses waf_policy …").** Embeds never show firewall policies, rules, reasons or match evidence. Group by **Firewall action** or **Firewall matched** instead, or remove the filter.

**Allow embedding is refused: "This report can't be embedded. It groups by ip_address …".** Embeds never show client IP addresses, which are personal data. Group by **Country** instead.

**Saving a report or dashboard is refused: "… is embedded in your app, so only account owners and admins can change what it shows".** Ask an owner or admin to make the change, or turn embedding off first.

**A filter is refused.** It uses a tag classified **Secret**, a request or response body, or a value with an unescaped quote. Use `filters.tags` in the SDK, which escapes values for you.

**The embed stays on "Waiting for access…".** No token reached it. Check your token route, and that the token is set on the element (`el.token = …`).

**A tile says "This tile couldn't load. Try a shorter time range."** Very long ranges over stored requests can time out. Offer shorter ranges in `timeRanges`, or, for fast long ranges, [promote](https://www.apitraffic.io/docs/tags) the tag you filter on.

**A tile or report says it "can't be shown right now".** ApiTraffic could not run the report for this embed; the reason is logged on ApiTraffic's side. Open the report in ApiTraffic and run it: the builder says what is wrong.

For the request log embed, see [Embeddable Request Log](https://www.apitraffic.io/docs/integrations/embed).
