@apitraffic/embed

@apitraffic/embed puts ApiTraffic inside your own product. It ships three Web Components:
<apitraffic-request-log>: a live, styled request log with expandable request detail, rendered in Shadow DOM so your page's CSS and the component's never collide. No iframe.
<apitraffic-report> and <apitraffic-dashboard> (from 1.1.0): one of your saved reports or dashboards, locked to one customer's slice of the traffic, drawn in an iframe of the ApiTraffic app.
Every embed is driven by a short-lived embed token that your server signs. The token, not the page, decides what the embed may show: the bucket, the filters, the request detail tabs and the features. A viewer editing the DOM cannot widen any of it.
The package also has a side-effect-free headless entry (the API client and token helpers, without any element) for rendering your own UI. React users: see @apitraffic/embed-react.
Contents
Requirements
- A plan that includes Embeds. Report and dashboard embeds also need Custom reports. If your plan stops including them, new embed tokens are refused and embeds already on your pages stop loading.
- An Administrator API token with no access scopes on your server, to mint embed tokens. The ingest token your SDK sends traffic with, and User tokens, are refused (
403, code 2010). Only account owners and admins can create one.
- A server endpoint of your own that mints a token for the signed-in user. The browser never sees your API token.
- A browser with Custom Elements v1, Shadow DOM and ES2020 (see Browser support).
- Network access from the viewer's browser to
https://api.apitraffic.io (request log) or https://app.apitraffic.io (reports and dashboards).
- Node.js 20 or newer for tooling that imports the package (bundlers, SSR, tests). The package has no runtime dependencies.
Installation
npm install @apitraffic/embed
pnpm add @apitraffic/embed
yarn add @apitraffic/embed
CDN (no build step)
The package's unpkg and jsdelivr fields point at a minified IIFE build for <script> tags. It registers the elements and exposes the exports on a global ApiTrafficEmbed.
<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>
Pin an exact version (@1.1.0, never @latest or no version) and add an integrity hash, so the script that runs on your page is the one you reviewed, and a changed or compromised CDN file is refused by the browser instead of executed. Compute the hash of the file you pin:
curl -fsSL https://unpkg.com/@apitraffic/embed@1.1.0/dist/apitraffic-embed.iife.js \
| openssl dgst -sha384 -binary | openssl base64 -A
Update the version and the hash together, and replace the placeholder before shipping: depending on the browser, a placeholder hash makes it refuse the script (Firefox) or ignore the check (Chrome).
Package entry points
The package ships ESM, CommonJS and TypeScript declarations, resolved through its exports map. Bundlers pick the ESM build; the IIFE is only for <script> tags.
import '@apitraffic/embed' | ESM (dist/apitraffic-embed.esm.js) | Bundlers (Vite, webpack, esbuild), native ESM. Registers the elements. |
require('@apitraffic/embed') | CJS (dist/apitraffic-embed.cjs) | CommonJS, Jest, SSR. Registers the elements in a browser. |
@apitraffic/embed/headless | ESM / CJS (dist/headless.*) | The API client and token helpers only. Registers nothing. |
dist/apitraffic-embed.iife.js | IIFE, minified | CDN / <script> tag (unpkg, jsdelivr). |
dist/index.d.ts, dist/index.d.cts, dist/headless.d.ts, dist/headless.d.cts | TypeScript | Types for import and require. |
Files under dist/ stay importable by path (@apitraffic/embed/dist/...). Tools that ignore the exports map (TypeScript moduleResolution: "node", Jest before 28, webpack 4) still resolve @apitraffic/embed/headless, through typesVersions and a headless/package.json that points into dist/.
Quick start
1. Mint an embed token on your server
If you already run an ApiTraffic Node SDK, embedToken() does it in one call. It is exported by @apitraffic/node, @apitraffic/express, @apitraffic/fastify, @apitraffic/koa and @apitraffic/hapi (and the framework packages built on @apitraffic/node). Set an Administrator API token with no access scopes as API_TRAFFIC_EMBED_TOKEN; the bucket, account and host come from the SDK you already configured.
const apitraffic = require('@apitraffic/node');
app.get('/api/embed-token', requireUser, async (req, res) => {
try {
const { token, expiresAt } = await apitraffic.embedToken({
filters: {
tags: { customerId: req.user.customerId }
},
ttl: 900,
tabs: ['request', 'response', 'connection', 'tags'],
features: { copyRequest: false, includeBodies: false }
});
res.json({ token, expiresAt });
} catch (error) {
console.error(error);
res.status(502).json({ error: 'Could not create an embed token' });
}
});
Not on Node, or in a service that does not run the SDK? Call the API directly. It is the same endpoint embedToken() uses:
const response = await fetch(`https://api.apitraffic.io/v1/accounts/${ACCOUNT_SID}/embeds/token`, {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.API_TRAFFIC_EMBED_TOKEN}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
bucketSid: 'your-bucket-sid',
filters: { criteria: 'method == "GET"' },
ttl: 900,
tabs: ['request', 'response', 'connection', 'tags'],
features: { copyRequest: false, includeBodies: false }
})
});
if (!response.ok) throw new Error(`ApiTraffic refused the embed token (${response.status})`);
const { token, expiresAt, tabs, features } = await response.json();
See Minting embed tokens for every parameter, and the Node SDK's embed token docs for what embedToken() fills in.
2. Render the element
Replace the integrity placeholder with the real hash of the file, computed as shown in CDN (no build step), or the browser refuses to run the script.
<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-request-log id="log" theme="light" page-size="25"></apitraffic-request-log>
<script>
const el = document.getElementById('log');
async function loadToken() {
const res = await fetch('/api/embed-token');
if (!res.ok) throw new Error(`token route answered ${res.status}`);
const { token } = await res.json();
el.token = token;
}
const reload = () => loadToken().catch((error) => console.error('embed token:', error));
reload();
el.addEventListener('apitraffic:token-expired', reload);
</script>
Or with a bundler:
import '@apitraffic/embed';
const el = document.querySelector('apitraffic-request-log');
el.token = await getTokenFromYourBackend();
That is the whole integration. The component loads the first page as soon as it is on the page and has a token.
Minting embed tokens (server-side)
[!WARNING]
The token that mints embed tokens is a read credential to your captured traffic. An Administrator API token can mint embed tokens for any bucket in its account, with any filters or none, so whoever holds it can read all of the account's captured traffic through an embed. Keep it in a secret store (never in browser code, container images or a repository), and rotate it immediately if it may have been exposed. Deleting it stops it minting embed tokens; embed tokens it already minted stay valid until they expire (ttl, at most 24 hours).
Protect the route that mints tokens. An embed token is a read credential for the traffic it is scoped to, so the endpoint on your server that creates one is as sensitive as that data:
- Require your user's sign-in on it. Never mint a token for an anonymous caller.
- Decide
filters, tabs, features and ttl on the server, from the signed-in user. Never copy them from the request body or query string.
- Keep
ttl short (minutes, not hours). The component asks for a fresh token before expiry.
The sample app's server.js (in examples/sample-app in the ApiTraffic source, not the npm package) shows all three.
Request log token parameters
POST https://api.apitraffic.io/v1/accounts/{accountSid}/embeds/token, authenticated with an Administrator API token with no access scopes.
bucketSid | string | - | Required on the HTTP endpoint. embedToken() defaults it to the bucket the SDK reports to. |
view | string | 'request-log' | 'request-log', 'report' or 'dashboard'. See Reports and dashboards for the other two. |
filters.environmentSid | string | - | Only this environment. |
filters.criteria | string | - | Search criteria, same syntax as the request log (method == "GET"). Conditions are joined with AND; OR between conditions is refused, and so is a value with an unescaped quote (inside a double-quoted value, escape " as \" and \ as \\). With the Node SDK, use filters.tags or escapeCriteriaValue(). |
filters.streamViewSid | string | - | Use a saved stream view's filter. |
ttl | number | 3600 | Token lifetime in seconds, 60 to 86400. |
theme | string | 'light' | 'light', 'dark' or 'auto'. Returned to the embed as config.theme. The request log element itself is themed by its theme attribute. |
tabs | string[] | default set | The request detail tabs this embed may show. See Tabs. [] hides the request detail entirely. |
features.copyRequest | boolean | false | See Features. |
features.includeBodies | boolean | false | See Features. |
The response is { token, expiresAt, tabs, features }. tabs and features are the resolved values: order normalized, defaults filled in. An unknown tab name is refused with 400, not dropped. Store or log them if you need a record of what a given embed was allowed to show.
<apitraffic-request-log> reference
Attributes
token | string | - | Required. The signed embed JWT. Set the token property instead (below); the attribute is read for compatibility, but anything in an attribute is readable by every script on the page and recorded by session-replay tools. |
theme | string | 'light' | 'light', 'dark' or 'auto' (follows the OS via prefers-color-scheme). |
api-url | string | 'https://api.apitraffic.io/v1' | The API base URL (self-hosted or staging). |
page-size | number | 25 | Requests per page. The API accepts 1 to 100. |
show-detail | string | 'true' | 'false' disables expanding a row into its detail. |
nonce | string | - | Copied onto the fallback <style> element in browsers without constructable stylesheets. See Content Security Policy. |
All attributes except nonce are reactive (nonce is read once, when the element is connected, so set it before inserting the element). Changing api-url or page-size reloads the list (once, however many change together); changing theme re-renders nothing, since the stylesheet keys off the attribute; turning show-detail off collapses an open row.
There is deliberately no attribute for tabs or features: the token decides, so a viewer editing the DOM cannot reveal one.
Properties
token | string | null | The embed JWT. Held privately in the component and never written to an attribute. Setting a new token clears the previous token's rows and loads afresh; el.token = null clears the log. |
theme | string | Reflects theme. |
apiUrl | string | Reflects api-url. |
pageSize | number | Reflects page-size. |
showDetail | boolean | Reflects show-detail. |
tabs | string[] (read-only) | The tabs the token exposes, once data has loaded. |
features | object (read-only) | The features the token enables, once data has loaded. |
Properties set before the element is defined (a framework that renders first and loads this package later) are picked up when it is.
Events
All events are CustomEvents that bubble and cross the shadow boundary (composed: true).
apitraffic:ready | { tabs, features } | The first page loaded: on first load, and again after a new token, api-url or page-size reloads the list. |
apitraffic:error | { code, message } | An API call failed (code is the API's error code, when it sent one), the page handed back an expired token (TOKEN_EXPIRED), or Copy request could not use the clipboard (CLIPBOARD_UNAVAILABLE, CLIPBOARD_FAILED). |
apitraffic:token-expired | { reason } | 'expiring' about 30 s before the token's exp; 'expired' once it has passed and the page has not replaced it; 'unauthorized' when the API answered 401. Set a new token. |
apitraffic:request-click | { requestSid } | A row was expanded. Not fired when a row is collapsed, or when show-detail="false". |
const el = document.querySelector('apitraffic-request-log');
el.addEventListener('apitraffic:ready', (e) => {
console.log('Tabs:', e.detail.tabs);
console.log('Features:', e.detail.features);
});
el.addEventListener('apitraffic:error', (e) => console.warn(e.detail.code, e.detail.message));
el.addEventListener('apitraffic:request-click', (e) => console.log('Opened', e.detail.requestSid));
Token refresh
Listen for apitraffic:token-expired, fetch a new token from your backend and set it on the element. The component re-fetches by itself.
The component asks; it never loops:
- Clock skew. A browser cannot read the server's clock, so the component corrects for the visitor's clock only when the token itself proves it wrong, using the token's
iat (the server's time when it was minted). A clock that is behind by more than a few seconds (iat in the future) is corrected for, so the token is not held past its real expiry. A clock that is ahead by more than the time the token has left makes it look expired on arrival; that token is read as fresh as of its iat instead of triggering an immediate, repeating "expired". A clock ahead by less than that is not corrected: the token is treated as expiring that much early, so apitraffic:token-expired fires sooner (once, with the usual backoff), never in a loop.
- The API decides. A
401 from the API is treated as the token's real end, whatever the clock says.
- An expired token handed back. If the page answers
apitraffic:token-expired with a token that has already expired (the same one, or another just as old), the log shows "Token refresh returned an expired token." and fires apitraffic:error with code: 'TOKEN_EXPIRED', instead of an empty list.
- Backoff. While no load succeeds, each further
apitraffic:token-expired waits twice as long as the last (1 s, 2 s, 4 s … up to 5 minutes).
Tabs
Clicking a row expands the same request detail you see in the ApiTraffic app. The token's tabs decide which of these the embed shows. A tab that is not on the token is not rendered and its data is not returned by the API: nothing is merely hidden client-side.
request | Request line, headers and query string. |
response | Status, headers and content type. |
connection | Per-phase connection timings. |
waf | What the application firewall did with the request. |
related | Other requests sharing this request's context. |
workflows | Workflow executions the request triggered. |
tags | The request's tags. |
waterfall | APM spans, drawn as a waterfall. |
traces | Trace identifiers carried on the request. |
trace | Every request sharing this request's distributed trace. |
concurrency | What else the request's server was doing at the time. |
ai | A fix prompt for the failure, with correlated deploys. Shown only on failed (4xx/5xx) requests. |
Default (when tabs is omitted): request, response, connection, workflows, tags, traces, ai, the tabs that describe the request itself. tabs: [] leaves a read-only list of rows.
Some tabs only appear when they apply: connection needs per-phase timings, ai is offered only on 4xx/5xx, and captures (requests with no response) show only request.
Four tabs answer with data from beyond the request being viewed. Think about each before granting it to an audience that is not your own team:
related and trace follow the request into other traffic in the same bucket, including requests the token's criteria would otherwise exclude.
concurrency lists requests that ran on the same server, which can include traffic from your other buckets.
ai (in the default set) includes the deploys correlated with the failure: repository names, branches and commit messages from your git integration.
Features
Behaviours that are not tabs, passed as features on the token:
copyRequest | false | Adds a Copy request button that generates the request as code: cURL, JavaScript (fetch and axios), Python, PHP, Go, Ruby, Java and C#. Exposes the request headers and query string even when the request tab is not shown. Captured values are never spliced into code: every value goes through its language's literal syntax. |
includeBodies | false | Returns request and response bodies for the request and response tabs. |
Bodies are withheld even on the request and response tabs unless includeBodies is set: they are the likeliest place for something an embed's audience should not see. Your account's redaction rules still apply, so bodies reach the embed already redacted. When shown, a JSON body renders as a collapsible tree (the first two levels open) with a Raw toggle for selecting and copying the text; other bodies render as text.
Theming
theme="light" (the default), theme="dark", or theme="auto" to follow the viewer's prefers-color-scheme. Beyond that, the component's Shadow DOM reads CSS custom properties that you set on the element (or any ancestor):
These values apply whatever the theme. To keep theme="dark" and theme="auto" readable, scope colour overrides to the light theme, as below (the default, so match "not dark or auto") or set dark values too; fonts, radius and height are safe to set unconditionally.
apitraffic-request-log:not([theme="dark"]):not([theme="auto"]) {
--at-font-family: 'Inter', sans-serif;
--at-font-size: 13px;
--at-bg-color: #ffffff;
--at-text-color: #1f2937;
--at-text-muted: #6b7280;
--at-accent-color: #3b82f6;
--at-danger-color: #ef4444;
--at-border-color: #e5e7eb;
--at-border-radius: 6px;
--at-row-hover-bg: #f9fafb;
--at-max-height: 600px;
}
--at-font-family | System font stack | Font family |
--at-font-size | 13px | Base font size |
--at-bg-color | #ffffff | Container background |
--at-text-color | #1f2937 | Primary text |
--at-text-muted | #6b7280 | Secondary text |
--at-accent-color | #3b82f6 | Links, buttons, accents |
--at-danger-color | #ef4444 | Errors |
--at-border-color | #e5e7eb | Borders |
--at-border-radius | 6px | Container corner radius |
--at-row-hover-bg | #f9fafb | Row hover background |
--at-detail-row-border | rgba(0,0,0,0.04) | Dividers inside the request detail |
--at-focus-color | --at-accent-color | Keyboard focus rings |
--at-max-height | none | Maximum height; set it to make the log scroll |
The dark theme (dark, or auto on a dark OS) supplies its own values for --at-bg-color, --at-text-color, --at-text-muted, --at-border-color and --at-row-hover-bg. Values you set on the element take precedence over them, with one exception: in dark mode the outer container's background and border are fixed at #111827 and #374151.
Set custom properties on the apitraffic-request-log element itself, not inside its shadow root: your page's other styles do not reach into the component.
The report and dashboard embeds are drawn by the ApiTraffic app inside their iframe, and themed by the token's theme, not by these properties.
Reports and dashboards
<apitraffic-report> and <apitraffic-dashboard> (from 1.1.0) show one of your saved reports or dashboards to your own customers: "your API calls this month", "your error rate". Your server signs a filter, typically the customer's id, into the token; the embed ANDs it onto the report's own filter on ApiTraffic's side every time. The viewer can pick a time range and nothing else.
1. Allow embedding
In the ApiTraffic app, open the report or dashboard (it must be shared with Everyone in the account), click Embed and turn on Allow embedding (owners and admins; anyone who can edit it can turn it off). The dialog shows its ID (Report ID or Dashboard ID) and a preview. Embed Setup (account settings, Integration → Embed) lists everything that allows embedding. Some reports cannot be embedded, for example one grouped by IP address, by firewall policy or rule, or by a catalogue attribute of a Personal tag; the dialog gives the reason. See Embedded Reports and Dashboards for the full rules.
2. Mint a token
const { token } = await apitraffic.embedToken({
view: 'dashboard',
dashboardSid: 'dash_...',
filters: { tags: { customerId: req.user.customerId } },
timeRanges: ['last_24h', 'last_7d', 'last_30d'],
timeRange: { relative: 'last_30d' },
ttl: 900
});
Over HTTP, the same endpoint takes the filter as a string: "filters": { "filter": "tag:customerId == \"org_8f2a\"" }.
view | 'report' or 'dashboard'. |
reportSid / dashboardSid | What the embed shows. It must allow embedding. |
filters.filter | A filter in the report filter language, ANDed with the report's (and dashboard's) own, at most 4000 characters. With the Node SDK, filters.tags builds and escapes it for you. A tag classified Secret is refused. |
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 | { relative }: the range the embed opens with, one of timeRanges. |
ttl | Lifetime in seconds, 60 to 86400 (default 3600). |
theme | 'light', 'dark' or 'auto'. |
The request log's criteria, streamViewSid, tabs and features do not apply to reports and are refused. A token works only with its own view (403 EMBED_VIEW_MISMATCH otherwise).
3. Place the element
<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();
el.addEventListener('apitraffic:token-expired', refresh);
el.addEventListener('apitraffic:error', (e) => console.warn(e.detail.code, e.detail.message));
</script>
Use <apitraffic-report> for a single report. Load version 1.1.0 or later.
Unlike the request log, these elements wrap an iframe of the ApiTraffic app's /embed page, so charts look exactly as they do in the app, your page's styles never reach them, and the frame never reads your page. The element hands the frame its token by postMessage (to the app's origin only; never in a URL), sizes the frame to its content, and reads messages only from its own frame and the app's origin.
token | - | The embed JWT. Prefer the token property, which keeps it out of the DOM. Setting a new one hands it to the frame; el.token = null clears the embed. |
app-url | https://app.apitraffic.io | The ApiTraffic app the frame loads from (self-hosted or staging). Only its origin and path are used: the frame loads <origin><path>/embed, and a query string or fragment is dropped. Changing it loads a new frame. A value that is not an http(s) URL is an apitraffic:error (INVALID_APP_URL) until a valid one is set. |
frame-title | ApiTraffic report / ApiTraffic dashboard | The frame's accessible name: say what it shows. A title set on the element is moved here (and removed from the element), so the name is not announced twice. |
token | As above. |
appUrl | Reflects app-url. |
appOrigin (read-only) | The origin the frame is loaded from, and the only one its messages are accepted from. |
embedSrc (read-only) | The frame's address, or null for an app-url that is not http(s). |
The theme, the locked filter and the time ranges offered are all in the token, so there is no attribute for them.
Events follow the request log's token contract (all bubble and are composed):
apitraffic:ready | {} | The report or dashboard loaded. |
apitraffic:error | { code, message } | Something went wrong, for example EMBED_NOT_AVAILABLE (embedding turned off, or the item was deleted or made private), INVALID_EMBED_TOKEN, EMBED_VIEW_MISMATCH, FEATURE_NOT_IN_PLAN, INVALID_REPORT (the report could not run; the viewer sees a plain message), RATE_LIMITED, INVALID_APP_URL, or TOKEN_EXPIRED (the page handed back the same token after apitraffic:token-expired, and it has expired). |
apitraffic:token-expired | { reason } | 'expiring' about 30 s before the token's exp (read against the server's clock, from the token's iat), then 'expired' at exp if the token was not replaced, or whenever the API refused it as expired. Repeated notices without a successful load back off (1 s, 2 s, 4 s … up to 5 minutes). |
Pages with a Content Security Policy need frame-src https://app.apitraffic.io (or your app-url's origin). The host page must be served from an http(s) origin: a page opened from file: or an app-specific scheme (app://, capacitor://, …) cannot frame the embed.
Headless usage and the programmatic API
Render your own UI with @apitraffic/embed/headless: the API client and the token helpers, without any Web Component. Importing it registers no custom element and touches no DOM API, so it is safe anywhere, including a server render. (@apitraffic/embed-react's useApiTrafficEmbed hook is built on it.)
import { EmbedApi, clockOffsetFor, secondsUntilExpiry } from '@apitraffic/embed/headless';
const api = new EmbedApi('https://api.apitraffic.io/v1', token);
const controller = new AbortController();
const page = await api.getRequests({ limit: 25, signal: controller.signal });
if (page.hasMore) {
const next = await api.getRequests({ from: page.from, limit: 25 });
}
const offset = clockOffsetFor(token);
console.log(secondsUntilExpiry(token, offset));
Records only carry the fields the token's tabs allow, so check config.tabs before rendering anything beyond the row itself.
The main entry (@apitraffic/embed) exports everything the headless entry does, plus the elements and their tag names.
Exports
EmbedApi | both | The embed API client (below). |
decodeToken(token) | both | The JWT's payload, without verifying it, or null when it is not a JWT. |
isTokenExpired(token, bufferSeconds = 30, clockOffsetSeconds = 0) | both | Whether the token has expired, or expires within bufferSeconds. A token without exp counts as expired. |
secondsUntilExpiry(token, clockOffsetSeconds = 0) | both | Seconds until exp, never negative. |
clockOffsetFor(token, bufferSeconds = 30, nowSeconds?) | both | Server time minus local time, in seconds, derived from the token's iat. 0 unless the local clock is provably off (see Token refresh). |
RequestLogElement | main | The <apitraffic-request-log> class. |
EmbedFrameElement | main | The base class of the report and dashboard elements. |
ReportEmbedElement, DashboardEmbedElement | main | The <apitraffic-report> and <apitraffic-dashboard> classes. |
TAG_NAME, REPORT_TAG_NAME, DASHBOARD_TAG_NAME | main | 'apitraffic-request-log', 'apitraffic-report', 'apitraffic-dashboard'. |
The token helpers only read the token. The signature is checked by the API on every request, and a 401 from it is the authoritative "this token is no longer valid".
EmbedApi
new EmbedApi(apiUrl?, token?). apiUrl defaults to https://api.apitraffic.io/v1. Every method takes an optional signal (an AbortSignal) so you can cancel a request you no longer want, because the token changed or the view left the page.
setToken(token) | - | Use a new token for later calls. |
getRequests({ from?, limit?, signal? }) | GET /embed/requests | A page of the log. from is the previous page's cursor; limit defaults to 25 (the API accepts 1 to 100). |
getRequest(requestSid, { signal? }) | GET /embed/requests/{sid} | One request, filtered to the granted tabs. |
getRelated(requestSid, { signal? }) | GET /embed/requests/{sid}/related | Needs the related tab. |
getTrace(requestSid, { signal? }) | GET /embed/requests/{sid}/trace | Needs the trace tab. traceId is null when the request is not part of one. |
getConcurrent(requestSid, { signal? }) | GET /embed/requests/{sid}/concurrent | Needs the concurrency tab. |
getFixPrompt(requestSid, { lookbackMinutes?, signal? }) | GET /embed/requests/{sid}/fix-prompt | Needs the ai tab. lookbackMinutes sets how far back to correlate deploys. |
A non-2xx answer rejects with an Error carrying status (the HTTP status; 401 means the token is expired or invalid) and code (the API's error code, when it sent one). An endpoint whose tab is not granted answers as though the request does not exist. A cancelled call rejects with an AbortError.
TypeScript
Declarations ship with the package, for both import and require, and for both entries. No @types package is needed.
import type { RequestLogElement, RequestLogTokenExpiredDetail, EmbedRequestsPage } from '@apitraffic/embed';
const el = document.querySelector('apitraffic-request-log');
el?.addEventListener('apitraffic:token-expired', (e) => {
console.log(e.detail.reason);
});
Importing the main entry adds the three tags to HTMLElementTagNameMap, and the elements' addEventListener overloads type each apitraffic:* event's detail.
EmbedTab | both | A tab name a token can grant. |
EmbedFeatures | both | { copyRequest?, includeBodies? }. |
EmbedConfig | both | { view, theme, tabs, features }, as resolved by the API. |
EmbedRequestRecord | both | One captured request; every field beyond sid is optional. |
EmbedRequestsPage | both | { records, hasMore, from, config? }. |
EmbedRequestDetail | both | One request, with config. |
EmbedApiError | both | The error EmbedApi rejects with (status, code). |
EmbedRequestOptions, GetRequestsOptions, GetFixPromptOptions | both | Method options. |
EmbedTokenPayload | both | The decoded payload (exp, iat, other claims). |
RequestLogReadyDetail, RequestLogErrorDetail, RequestLogTokenExpiredDetail, RequestLogRequestClickDetail | main | detail of the request log's events. |
RequestLogElementEventMap | main | Event name to event type, for the request log. |
EmbedFrameErrorDetail, EmbedFrameTokenExpiredDetail, EmbedFrameElementEventMap | main | The same, for the report and dashboard elements. |
Server-side rendering
Importing @apitraffic/embed on a server (Next.js, Remix, Nuxt, plain Node) is safe: outside a browser it registers nothing and touches no global, and the elements are registered when the same module loads in the browser. Nothing is fetched until an element is on a page and has a token. @apitraffic/embed/headless has no side effects at all.
Security model
- Signed server-side. The embed JWT is signed with a per-account HMAC-SHA256 secret. Any tampering invalidates the signature.
- Filters are locked into the token. Bucket, environment, criteria and stream view (or a report's filter) come from the verified token; query parameters from the browser are ignored for filtering, and only the pagination cursor (
from, limit) is accepted.
- Tabs are the access boundary. Exposed tabs and features are signed into the token and enforced server-side: the API rebuilds every response from the fields the granted tabs need, so a tab you do not grant contributes no data to the payload at all. The per-tab endpoints (
related, trace, concurrent, fix-prompt) answer as if they do not exist unless their tab is granted, so a viewer cannot probe for what was withheld.
- Every read is re-scoped. A single request is re-checked against the token's bucket and environment, so a guessed request sid outside the embed's scope reads as not found.
- Short-lived. Tokens last
ttl seconds (default one hour, at most 24 hours), with automatic refresh support. A stolen token shows only what the embed was designed to show, and only until it expires.
- Kept out of the DOM. The elements hold the token privately and never reflect it into an attribute, so session-replay tools and other scripts reading the page do not see it. Report and dashboard elements pass it to their frame by
postMessage, to the app's origin only.
- Embed endpoints accept only embed JWTs. Standard API tokens and session tokens are rejected by
/v1/embed/*, and an embed JWT cannot call any other ApiTraffic endpoint.
- Your minting route is your responsibility. It must require your user's sign-in and fix the scope server-side. See Minting embed tokens.
Content Security Policy
The request log works under a strict policy with no 'unsafe-inline':
- Its styles are a constructed stylesheet adopted by its shadow root (shared by every instance on the page), not a
<style> element, and geometry such as waterfall bars is set through the CSSOM rather than style="..." attributes.
- It needs
connect-src https://api.apitraffic.io (or your api-url's origin), plus script-src for wherever the script comes from (https://unpkg.com for the CDN build).
- Browsers without constructable stylesheets (Safari before 16.4) fall back to a
<style> element, which a strict policy only allows with a nonce: set nonce="..." on <apitraffic-request-log> and the component copies it.
- Trusted Types are not supported. The component renders its shadow DOM by assigning HTML strings to
innerHTML (every API value is escaped first), so a page that enforces require-trusted-types-for 'script' will block its rendering. Leave Trusted Types unenforced on pages that host the component, or use the headless entry (@apitraffic/embed/headless, or useApiTrafficEmbed in React) and render the data yourself.
Report and dashboard embeds need frame-src https://app.apitraffic.io (or your app-url's origin).
Accessibility
Rows are keyboard-operable buttons (Enter / Space) that report aria-expanded; the detail tabs are a WAI-ARIA tablist (arrow keys, Home, End); loading, results and errors are announced through a polite live region; and focus rings are kept for keyboard users (--at-focus-color recolors them). Report and dashboard frames take their accessible name from frame-title.
Browser support
Any browser with Custom Elements v1, Shadow DOM and ES2020:
- Chrome 80+
- Edge 80+
- Firefox 74+
- Safari 13.1+
Browsers without constructable stylesheets (Safari before 16.4) use the <style> fallback described under Content Security Policy. Copy request needs the asynchronous Clipboard API (navigator.clipboard.writeText); where it is missing or refused, the component fires apitraffic:error with CLIPBOARD_UNAVAILABLE or CLIPBOARD_FAILED.
Troubleshooting
The token request is refused with 403. Only Administrator API tokens can mint embed tokens; ingest tokens (the one your SDK sends traffic with) and User tokens get 403 with code 2010. Create an Administrator token with no access scopes under Account → API Tokens and set it as API_TRAFFIC_EMBED_TOKEN (a scoped Administrator token gets the same 403, with a message saying its access scopes do not allow the action). A 403 with code 2221 means your plan does not include Embeds.
"No requests found". Check that the token's bucketSid has traffic, and that its filters are not too narrow.
A tab I expected is missing. Check in order: is it on the token (read element.tabs, or e.detail.tabs from apitraffic:ready; the default set excludes waf, related, trace, concurrency and waterfall)? Does it apply to this request (connection needs phase timings, ai only appears on 4xx/5xx, captures show only request)? A granted tab with nothing to show says so rather than disappearing.
Bodies are empty on the Request or Response tab. Set features.includeBodies: true on the token. If they are still empty, check your account's redaction rules and that body capture is on for the bucket.
401 errors in the browser console. The token expired or was rejected. The element fires apitraffic:token-expired with reason: 'unauthorized'; handle it (or onTokenExpired in React). If the 401s continue, your endpoint is probably serving a cached token rather than minting a new one.
"Token refresh returned an expired token." Your apitraffic:token-expired handler set a token that has already expired: usually a cached one. Mint a new token on every call.
Styles are blocked by your Content Security Policy. Current browsers need no style-src allowance. Safari before 16.4 falls back to a <style> element: set nonce="..." on the element.
Styles look wrong. Set CSS custom properties on the apitraffic-request-log element itself. Your page's other styles do not reach into the shadow root, by design.
The element never renders. Make sure the package is loaded: import '@apitraffic/embed' in a bundler, or the <script> tag on the page (check the browser console for an integrity mismatch, which blocks the script). The element upgrades in place whenever the script loads.
A report or dashboard 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 report or dashboard says "This report is no longer available." Embedding was turned off, or the item was made private or deleted (apitraffic:error with EMBED_NOT_AVAILABLE).
More in the docs: request log troubleshooting and report and dashboard troubleshooting.
Sample application
The ApiTraffic source (not the npm package) includes examples/sample-app, an Express server that mints embed tokens behind a sign-in, with a page that renders <apitraffic-request-log> under a strict CSP and live controls for theme, page size, detail rows and height, plus an event log.
npm install
npm run build
cd examples/sample-app
cp .env.example .env
npm install
npm start
Its README lists every setting.
Documentation and related packages
Changelog
Release history is on npm.
License
MIT
Follow us on our Blog and on X.
Chat with the ApiTraffic team and other members on Discord and follow our tutorials and other videos on YouTube.


