# Embeds

## What is an embed?

An embed shows a dashboard, chat, skill or notebook inside a frame on another site. It always shows
the whole object, view-only. You can't embed one chart on its own, so a dashboard embed shows every
chart on the dashboard. Embeds work on every plan.

You turn one on in the object's **Settings → Embed**. Flip the switch to **Enabled**, then pick who
sees it. The frame's address is the object's own address under `/embed/`, such as
`/embed/dashboards/{id}`.

## Who sees an embed?

The **Embed** card offers three audiences, and one is on at a time.

| Audience | Who sees it | What you set up |
|---|---|---|
| **Anyone** | Anyone with the address, in the frame or outside it | Nothing. This is the object's public access |
| **Anyone who can access your website** | Whoever your site lets in, with no Vistazo account | A key, and a few lines on your server that sign each page load |
| **Anyone who can access this dashboard** (or chat, skill, notebook) | Only people who can already open the object in Vistazo | Nothing, or a key if your own sign-in names the reader |

Flipping the switch on picks **Anyone who can access this dashboard**, so enabling an embed never
makes anything public. **Anyone** is the same setting as **Anyone with the link** in the Sharing
pane ([How sharing works](/docs/sharing/overview#how-does-public-access-work)). Turn public access on there and the
embed moves to **Anyone**. Turn it off there and an embed at **Anyone** goes off with it.

> [!WARNING]
> While an embed is at **Anyone**, the object is public. Anyone who finds its address can open it,
> with or without your site.

You need edit access to manage an embed. On a chat, only its author can. Guests never see the Embed
pane. Your agent sets the same choice with the `set_sharing` tool's `embed`.

## What does the frame show?

The frame has no filters, no refresh and no built-in assistant. A reader can't change the values a
dashboard shows.

A frame whose object isn't public asks its reader to sign in, with **Sign in to see this dashboard if
you have access.** Someone signed in who can't open it reads **You don't have access to this, or it
doesn't exist.** They get **Open in Vistazo** and **Sign in again**, which signs the frame in afresh
for a workspace that asks for a recent sign-in. Neither message says whether the object exists, so
you can hand out the code for a private object safely.

A page your server signed that the embed won't serve shows **This embed isn't available**, with no
link out.

## What is in the frame's header?

The frame opens with a header. It carries the Vistazo logo, the object's name, when it last
refreshed and a button that opens it in Vistazo. On a chat or skill the time reads as last updated.

To hide the header, turn off **Show header** in the **Code** card. Or add `header=0` to the frame's
address yourself, as in `/embed/dashboards/{id}?header=0` or `…?t={token}&header=0`. The frame
shrinks to fit and reports its new height to your page.

## How does a website embed work?

A website embed shows the object to whoever your site lets in, while its public access stays off.
Your server signs a short-lived token on each page load and adds it to the frame's address. Anyone
who loads that page sees the object, with no Vistazo account.

### Embed keys

Your server signs with an embed key. In the object's **Settings → Embed**, press **New key** in the
**Keys** card and give it a name. Vistazo shows the secret once. Copy it into your server's
environment as `VISTAZO_EMBED_SECRET` straight away, because it never appears again.

Each key's row then shows the last four characters of its secret. That lets you check which secret
your server holds. The row's menu rotates a key, which issues a new secret while the old one keeps
working for 24 hours. Your server can switch over in that window without a gap.

Deleting a key stops every page signed with it. Switching the embed to **Anyone who can access this
dashboard** also stops these pages. Switching doesn't delete the keys, so switching back to **Anyone
who can access your website** makes the same pages show again.

### Signing a token

The **Code** card writes the code for the object you're on. Pick Node or Python for your server and
HTML or React for the frame. In Node, with [`jose`](https://github.com/panva/jose):

```js
import { SignJWT } from 'jose'

const token = await new SignJWT({ dashboardId: '{dashboard id}' })
  .setProtectedHeader({ alg: 'HS256', kid: '{key id}' })
  .setExpirationTime('10m')
  .sign(new TextEncoder().encode(process.env.VISTAZO_EMBED_SECRET))

const src = `https://app.vistazo.ai/embed/dashboards/{dashboard id}?t=${token}`
```

In Python, with [PyJWT](https://pyjwt.readthedocs.io):

```python
import os, time, jwt

token = jwt.encode(
    {"dashboardId": "{dashboard id}", "exp": int(time.time()) + 600},
    os.environ["VISTAZO_EMBED_SECRET"],
    algorithm="HS256",
    headers={"kid": "{key id}"},
)

src = "https://app.vistazo.ai/embed/dashboards/{dashboard id}?t=" + token
```

Set `src` as the iframe's address. Any JWT library works. The token is signed with HS256, its header's
`kid` is the key's ID, and its payload names the object and an expiry. The claim that names the
object depends on its kind.

| Object | Claim | Frame address |
|---|---|---|
| Dashboard | `dashboardId` | `/embed/dashboards/{id}` |
| Chat | `chatId` | `/embed/chats/{id}` |
| Skill | `skillId` | `/embed/skills/{id}` |
| Notebook | `notebookId` | `/embed/notebooks/{id}` |

Vistazo refuses a token that expires more than 10 minutes out rather than shortening it. The frame
loads the object once when it opens and keeps showing it after the token expires. To show newer
data, your page loads again with a fresh token.

### Filter values

A dashboard with filters opens at its published defaults. To show something else, such as each
customer's own region, add `params` to the token. Each entry is a filter's key and a value of that
filter's type. Your server signs them, so the reader can't change them.

```js
const token = await new SignJWT({
  dashboardId: '{dashboard id}',
  params: { region: 'EMEA', countries: ['US', 'DE'], window: { start: 'today-30', end: 'today' } },
})
```

```python
{"dashboardId": "{dashboard id}", "params": {"region": "EMEA"}, "exp": int(time.time()) + 600}
```

For a dashboard with filters, the **Code** card adds a `params` line set to its first filter's
default. Only dashboards take `params`. A chat, skill or notebook token that carries them shows
**This embed isn't available**.

Vistazo checks every value against the dashboard's filters, by type and, for a list, against its
options. One unknown key or bad value shows **This embed isn't available**. Vistazo never drops the
value and shows a wider view instead. To test a set of values, open the dashboard in Vistazo and pick
the same ones in its filters.

A value equal to its filter's default counts as the default. Relative dates resolve in UTC, so
`today` turns over at midnight UTC. A text value that would land inside a warehouse query is refused,
because the query runs as the dashboard's owner. Text that only fills a title is fine.

The token rides in the frame's address. Anyone who can see your page's source, the browser's history
or a log of addresses can read the values, so never put a secret in them.

### Who pays for filter values

The defaults, and any combination of values already shown, cost nothing and run no new queries. Each new
combination runs the dashboard's queries on its owner's connections, as the owner, and lands on the
owner's warehouse bill.

A dashboard runs up to 500 new combinations a day. After that, a new combination shows **This embed
isn't available** until the day is up, while the defaults and earlier combinations keep showing. The
limit counts runs rather than their cost. To cap spend, use your warehouse's own quotas, such as
BigQuery's [custom query quotas](https://cloud.google.com/bigquery/docs/custom-quotas).

## How do you embed for people with access?

This audience shows the object on any site, to the people who can already open it in Vistazo. Anyone
else sees nothing of it, including a member it isn't shared with. The **Sign-in** choice in the
**Code** card sets how the frame learns who is reading.

### Vistazo sign-in

**Vistazo** is the default. The code is the object's address, as a link, an
HTML snippet or a React snippet. You need no key and no server code.

A reader who isn't signed in presses **Sign in** in the frame and signs in to Vistazo in a small
window. The first time, Vistazo asks whether to show itself on that site. The answer covers every
page on the site, because browsers keep one sign-in per site. Each reader sees the sites they
allowed under **Settings → Security → Authorized apps**, and can revoke any of them there.

Your workspace's single sign-on and two-factor rules apply in the frame, as in the app. If the site
blocks pop-up windows, the frame offers **Open in Vistazo** in a new tab. With no token there are no
filter values, so a dashboard shows its published defaults.

### SSO

With **SSO**, your server signs a token that names the object and the person reading. It can be picked
only in a workspace with single sign-on available, and elsewhere it is greyed out. Pages show once
the workspace has verified at least one domain.

Sign as in [Signing a token](#signing-a-token) and add the person as `viewer`. One set of keys
serves both website embeds and these.

```js
const token = await new SignJWT({ dashboardId: '{dashboard id}', viewer: { email: user.email } })
  .setProtectedHeader({ alg: 'HS256', kid: '{key id}' })
  .setExpirationTime('10m')
  .sign(new TextEncoder().encode(process.env.VISTAZO_EMBED_SECRET))
```

```python
token = jwt.encode(
    {"dashboardId": "{dashboard id}", "viewer": {"email": user_email}, "exp": int(time.time()) + 600},
    os.environ["VISTAZO_EMBED_SECRET"],
    algorithm="HS256",
    headers={"kid": "{key id}"},
)
```

An email must be on one of the workspace's verified domains. To name someone by your identity
provider's user ID instead, use `viewer: { sub: '{user id}' }`. Read the email from the session your
server verified, never from the page or the request. The **Code** card shows that line for Okta,
Microsoft Entra, Google Workspace and WorkOS.

These tokens take `params` under the same rules, and the queries still run as the dashboard's owner.
`viewer` decides who may open the frame and has no say in which rows they see.

Your workspace's single sign-on and two-factor rules don't apply in this frame, because your site has
already signed the person in. If the workspace loses single sign-on or its last verified domain,
these pages stop showing. **Vistazo** sign-in keeps working.

## Who manages embed keys?

Whoever can edit an object manages its keys, and on a chat that is its author. When someone else
creates, rotates, renames or deletes a key, the object's owner gets a notification. Deleting an
object deletes its keys.

Workspace admins find every embed key in the workspace under **Settings → General →
Authentication**, in the **Embed keys** card. Each row shows the object, the key's name, who created
it and when it was last used. **Revoke** there stops the key.

## What happens when public links are off?

An admin can turn off the workspace's **Public links** switch. Embeds at **Anyone** and **Anyone who
can access your website** then stop showing, and both audiences read **Disabled for this workspace**.
Each object keeps its choice, so turning public links back on brings its embed back. Embeds for
people with access keep working, because their readers can already open the object.

## How does a pasted link become an embed?

Some pages turn a pasted link into an embedded view using [oEmbed](https://oembed.com), an open
standard for exactly that. Paste an object's address into any page that accepts an oEmbed link and
the page shows the object's frame, as the audience you picked in **Settings → Embed** allows. Pasting
the address of one chart on a dashboard shows the whole dashboard. On a page that accepts an iframe
instead, paste the **HTML** snippet from the **Code** card.

The endpoint those pages call is public:

```
GET https://api.vistazo.ai/api/oembed?url={address}&format=json
```

`url` is the object's address as it appears in the browser, such as
`https://app.vistazo.ai/dashboards/{id}`. The answer is the `rich` type, and its `html` is the
object's `/embed/` frame. An object with public access also returns its title. Every other address
gets the same answer without one, so the endpoint gives nothing away about a private object.

Every object's page also advertises the endpoint, so a site that looks for it finds it from the
pasted address alone.

## What are an embed's limits?

| Limit | Value |
|---|---|
| Keys per object | 5 |
| Token lifetime | 10 minutes at most |
| Token length | 8,192 characters, room for about 200 short codes. `@vistazo/embed` won't sign a longer one |
| Old secret after a rotation | Keeps working for 24 hours |
| New filter combinations per dashboard | 500 a day |
