> For the complete documentation index, see [llms.txt](https://developers.felt.com/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://developers.felt.com/js-sdk/authenticated-embeds.md).

# Authenticated embeds

An authenticated embed shows a **private** Felt map inside your own application, to people who don't have Felt accounts. Your backend mints a short-lived embed token for the map, and your page passes it to `Felt.embed`. Your users sign in to your app the way they already do. Felt never sees their passwords, you don't create Felt accounts for them, and you don't sync users or permissions between the two systems: your app decides who gets a token.

{% hint style="info" %}
Requires the Platform add-on, and included in the free trial. See [Platform](/plans-access-and-limits.md#platform).

Public and unlisted maps don't need a token, and standard embeds don't need Platform.
{% endhint %}

## How it works

1. A signed-in user of your app opens a page with a map.
2. Your server decides which Felt map that user may see, then calls the Felt API to mint an embed token for that map.
3. Your page calls `Felt.embed` with the map ID and the token.
4. Felt renders the map, with its styling, legend, popups and components, and keeps it live: edits made to the map in Felt appear in the embed.

## Step 1: mint a token on your server

Call `POST /api/v2/maps/{map_id}/embed_token` with your [API token](/rest-api/authentication.md). The `user_email` **query parameter** is required. Pass the email of the person who will view the map; Felt uses it to attribute views. That person doesn't need a Felt account.

{% tabs %}
{% tab title="curl" %}

```bash
curl -X POST \
  -H "Authorization: Bearer ${FELT_API_TOKEN}" \
  "https://felt.com/api/v2/maps/${MAP_ID}/embed_token?user_email=jane%40example.com"
```

{% endtab %}

{% tab title="Node.js" %}

```javascript
// Express route: mint a token for the signed-in user's map.
app.get("/api/felt-token", requireLogin, async (req, res) => {
  const mapId = mapIdForCustomer(req.user.customerId); // your own lookup
  const url = new URL(`https://felt.com/api/v2/maps/${mapId}/embed_token`);
  url.searchParams.set("user_email", req.user.email);

  const r = await fetch(url, {
    method: "POST",
    headers: { Authorization: `Bearer ${process.env.FELT_API_TOKEN}` },
  });
  if (!r.ok) {
    console.error(r.status, await r.text());
    return res.status(502).json({ error: "Could not load the map" });
  }
  const { token, expires_at } = await r.json();
  res.json({ mapId, token, expiresAt: expires_at });
});
```

{% endtab %}

{% tab title="Python" %}

```python
import os
import requests

def mint_embed_token(map_id: str, user_email: str) -> dict:
    r = requests.post(
        f"https://felt.com/api/v2/maps/{map_id}/embed_token",
        params={"user_email": user_email},
        headers={"Authorization": f"Bearer {os.environ['FELT_API_TOKEN']}"},
    )
    r.raise_for_status()
    return r.json()  # {"token": "...", "expires_at": "..."}
```

{% endtab %}
{% endtabs %}

A successful response looks like this:

```json
{
  "token": "felt_et_eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
  "expires_at": "2026-10-06T21:04:11"
}
```

Never call this endpoint from the browser: it would expose your API token. Only the embed token goes to the client.

A Team workspace with Platform can call this endpoint even though the Team plan doesn't include the rest of the REST API.

## Step 2: embed the map with the token

```typescript
import { Felt } from "@feltmaps/js-sdk";

const { mapId, token } = await fetch("/api/felt-token").then((r) => r.json());

const felt = await Felt.embed(document.getElementById("map"), mapId, {
  token,
  uiControls: { cooperativeGestures: false },
});

// The full SDK is available: listen to events and control the map.
felt.onSelectionChange({
  handler: ({ selection }) => showDetailsInMyApp(selection),
});
```

Give the container an explicit height; a zero-height `div` renders nothing. If you're not using the SDK, you can put the token on an iframe URL instead:

```html
<iframe src="https://felt.com/embed/map/MAP_ID?token=TOKEN" width="100%" height="600"></iframe>
```

## Token lifetime and authorization

* **Mint a fresh token for each page load.** A token lasts 12 hours, which covers a working session in one tab, reconnects included. When it expires, the embed asks the visitor to refresh, and your page mints a new one.
* **A token grants view access to one map.** Anyone holding it can view that map until it expires, so treat it like a session credential and only hand it to the user you minted it for.
* **Tokens can't be revoked individually.** An open embed re-checks every 15 minutes that the workspace still has access to authenticated embeds. To cut off a user, stop minting tokens for them; their existing token expires within 12 hours. Deleting the map ends access to it.
* **Viewers can't edit.** Token viewers see the map read-only. To let them download layer data, turn on **Viewer permissions: Export data** in the map's settings.

## Serving each customer their own data

The token is the security boundary, and it's scoped to a map. To keep one customer's data away from another's, **give each customer their own map** and mint tokens only for the map that customer is allowed to see. A common pattern is a template map that you duplicate per customer with the REST API (`POST /api/v2/maps/{map_id}/duplicate`), then point at that customer's data.

Filters set with [`setLayerFilters`](/js-sdk/layer-filters.md) on a shared map change what's *displayed*, not what's *accessible*. Use them to focus a view, never to hide one customer's data from another.

## Usage and limits

* **Every embed load counts as a map view**, authenticated or not, against your plan's monthly allowance. When the workspace reaches its map-view limit, embeds show an over-limit page until the month resets or you add capacity. See [Plans, access and limits](/plans-access-and-limits.md#monthly-limits).
* Token viewers don't use viewer spots and need no seats.

## Troubleshooting

| Symptom                                                                 | Cause                                                                                                                                                               | Fix                                                                                            |
| ----------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------- |
| `401` with "Embed tokens need the Platform add-on" when minting         | The workspace doesn't have Platform.                                                                                                                                | Add Platform in your workspace's billing settings.                                             |
| `401 invalid_access_token` when minting                                 | The API token is missing, revoked, or from a different workspace than the map.                                                                                      | Use a token created in the map's workspace. See [Authentication](/rest-api/authentication.md). |
| `Felt.embed` rejects after a few seconds with "Failed to load Felt map" | Either the workspace has no SDK access (the map appears but can't be controlled), or the workspace is over its map-view limit (the embed shows an over-limit page). | Check that Platform is active and that the workspace is under its map-view limit.              |
| The embed asks the visitor to refresh                                   | The token expired.                                                                                                                                                  | Mint a new token on page load.                                                                 |
| A blank area where the map should be                                    | The container has no height, or the iframe URL uses `/map/` instead of `/embed/map/`.                                                                               | Set a height. Only `/embed/map/…` URLs can be framed by other sites.                           |


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation by asking a question.

Perform an HTTP GET request on the following URL with the `ask` and `goal` query parameters:

```
GET https://developers.felt.com/js-sdk/authenticated-embeds.md?ask=<question>&goal=<user_goal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is what the user is ultimately trying to achieve, the reason they need the answer. Sharing it helps GitBook give you a better, more relevant answer. A goal is most helpful when it describes the outcome the user wants rather than restating the question. For example, with `ask=how do I create an API token`, a goal like `automate deployments from our CI pipeline` lets GitBook tailor the answer to that use case.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
