Embedding
Overview
Embedding lets your application show Polinode networks inside its own pages and drive them from your code: open a saved view, filter, focus a node, read what is on screen and save a view.
Every person who views an embedded network is a real Polinode user that your integration provisions and manages through the API (a managed user). You act as the identity provider for those users only: you authenticate them in your application, and you ask Polinode for a short-lived session for each one.
Embedding is off by default. It is enabled per organization by Polinode (contact support), which also sets the monthly active users your plan includes (see Usage). After that, an organization admin configures it under Organization Settings → Embedding:
- Allowed host origins: the exact origins of the pages that may frame Polinode, such as
https://app.example.comorhttps://app.example.com:8443. Wildcards and paths are not accepted. - Embeddable networks: the only networks that can be granted to managed users or opened in an embedded session. By default All owned networks is on: your integration can grant any network owned by a member of your organization. Turn that off to choose specific networks instead.
- Data disclosure: making a network embeddable authorizes your integration to receive the network's full content, including node and edge attributes and its structure, and to pass it to its own providers, including AI providers.
How it Works
- Your backend provisions a managed user and grants it access to one or more embeddable networks.
- When that person opens the page in your application, your backend creates a session for them and receives a launch token (valid for 60 seconds, single use).
- Your page loads
https://app.polinode.com/embed/<sessionId>in an iframe and uses the@polinode/embedSDK. The SDK and the iframe pair over a privateMessageChannel, and the SDK hands the iframe a launch token by calling yourgetLaunchTokencallback. - The iframe exchanges the launch token for an access token (valid for 15 minutes) that it keeps in memory. No cookies are used. Before the access token expires, and only if the viewer or your code has been active, the iframe asks the SDK for a new launch token.
- Your code drives the viewer through the SDK's commands.
Managed Users
All embedding API calls use an organization API credential that has the Embedding scope. "Full API access" credentials include it; a credential with custom scopes needs it selected when you create the credential. The scope has no effect until embedding is enabled for your organization. Any credential of your organization with this scope can manage all of your organization's managed users.
A managed user:
- has no password, cannot sign in to Polinode directly, and never receives product emails;
- has a placeholder email address and is shown by its display name;
- appears in your organization's user list with an Embedded badge, and does not use your ordinary user seats;
- has a fixed organization permission (
Access-only,Simplified Access-onlyorStandard User, optionally read-only), set when it is first provisioned; - is subject to your organization's AI Networks Chat settings and the per-user AI chat toggles in the user list.
Create or Update a Managed User
PUT /api/v2/embed/users/:externalId
externalId is your own identifier for the person (1–128 characters of letters, digits and . _ : @ | -). The request is idempotent: the first call creates the user; later calls update it.
| Field | Required | Description |
|---|---|---|
displayName | on create | The name shown in Polinode. |
chatAccess | no | off, presetOnly (preconfigured questions only) or full. Defaults to full for a new user. |
orgPermission | no | Access-only (default), Simplified Access-only or Standard User. Fixed after creation. |
readOnly | no | true for a read-only user (View access only). Fixed after creation. |
Returns 201 on create and 200 on update, with the user's externalId, displayName, status, chatAccess, networks and activity dates. Returns 402 with code EMBEDDED_SEAT_LIMIT if your organization has an embedded user limit and it is reached, and 409 with code EMBED_MEMBERSHIP_IMMUTABLE if you try to change orgPermission or readOnly.
Calling it for a deactivated user reactivates the user.
Grant or Remove Network Access
PUT /api/v2/embed/users/:externalId/networks/:networkId with { "permission": "View" | "Edit" }
DELETE /api/v2/embed/users/:externalId/networks/:networkId
The network must be one of your embeddable networks. Edit is needed to save or update views. Removing access takes effect on the viewer's next request.
Deactivate a Managed User
POST /api/v2/embed/users/:externalId/deactivate
Deactivating a user ends all of its sessions, removes all of its network access and frees its place under any embedded user limit. Views the user saved remain on their networks. Deactivation is how you offboard a person; a deactivated user can be reactivated later with the same externalId.
Delete a Managed User
DELETE /api/v2/embed/users/:externalId
Deleting a user permanently removes it, for example to meet an erasure request. It works on an active or a deactivated user, first doing everything deactivation does, then deleting the user, its personal template views and its AI chats. Views the user saved on a network remain on that network, because other users of the network can see them. Your organization's audit log keeps its records of the user's actions, which identify it by externalId, and a user who opened a session this month still counts towards this month's monthly active users.
Returns 200 with { externalId, userId, deleted: true }, or 404 if there is no such user. A deleted user cannot be restored; calling PUT with the same externalId afterwards creates a new user.
List Managed Users
GET /api/v2/embed/users?status=active|deactivated&limit=100&offset=0
Returns { total, offset, limit, users } for reconciliation with your own records. limit is at most 500.
Sessions
Create a Session
POST /api/v2/embed/sessions
| Field | Required | Description |
|---|---|---|
externalId | yes | An active managed user. |
networkIds | yes | Exactly one network id: an embeddable network the user has access to. |
origin | yes | The origin of the page that will frame the viewer; must exactly match an allowed host origin. |
maxMinutes | no | Session lifetime, 1–720 minutes (default 60). |
ui | no | Omit it to show everything. Otherwise { menu?, aiChat? } to hide parts of the viewer; see Viewer Options. |
header | no | none (default) or title: a slim strip showing the network and view names. |
Returns 201 with { sessionId, launchToken, launchTokenExpiresAt, expiresAt, ui, header }, where ui is the normalized form of what you sent.
Viewer Options
By default an embedded viewer shows everything: every Explore left-menu item and AI Networks Chat. The optional ui object only hides things, and anything you leave out stays shown:
| Field | Description |
|---|---|
menu | Omitted: every item is shown. To show only some, list them from layout, open, save, templates, labels, layers, tables, metrics, color, size, filter, shapes, rollup, export and settings. []: no left menu at all, leaving the network, legend and node details, for experiences driven entirely by your code. Items always appear in Explore's own order, whatever order you list them in. |
aiChat | Omitted or true: AI Networks Chat is shown. false hides both its floating button and its menu item. |
{ "menu": ["open", "filter", "color", "size"], "aiChat": false }
An unknown key or menu item, or a value of the wrong type, fails with 400 and code INVALID_ARGS. The viewer's session.info() reports the session's ui.
Hiding items only changes what is on screen. What the viewer can do is still decided by its network access (View or Edit) and its AI chat access, so do not rely on a hidden menu item to restrict a user.
The embedded Explore page has no Back button, since there is no network list inside an embed. With header: 'title', a slim strip above the network shows the network and view names.
Issue a New Launch Token
POST /api/v2/embed/sessions/:sessionId/launch-tokens returns { launchToken }. The SDK asks for one when the viewer loads, reloads or needs to refresh its access token.
Revoke a Session
DELETE /api/v2/embed/sessions/:sessionId
Revoke the session when your user logs out of your application. Simply stopping refreshes is not revocation.
A session also ends when it reaches maxMinutes, after 30 minutes without any viewer or bridge activity (a viewer with no activity ends sooner, see Expiry and Recovery), when the managed user is deactivated, when embedding is disabled, or when its origin is removed from the allowed host origins. Disabling embedding or removing an origin also stops the embed page itself from loading in a frame on that origin straight away. After a revocation, new requests fail immediately; signed network data links already issued stay valid for up to 5 minutes, and data already in the browser stays there until you remove the iframe.
Expiry and Recovery
A viewer's access token lasts 15 minutes. About 2 minutes before it runs out, the viewer asks your getLaunchToken for a new launch token, but only if the viewer (clicking, typing or scrolling in the viewer) or your code (a command that changes something, or session.keepAlive()) has been active since the last refresh. An untouched tab therefore ends within about 15 minutes, which is deliberate: an unattended screen does not stay signed in. Independently, the session itself ends at maxMinutes however active the viewer is.
Before a session ends
About 2 minutes before a session ends, the SDK emits session.expiring { reason, expiresAt }, so you can act before anything disappears:
reason: 'idle': nothing has happened in the viewer since the last refresh, so its access token is about to lapse. Any activity in the viewer beforeexpiresAtkeeps the session going. The viewer may be working elsewhere in your application, so this is the moment to ask, in your own UI, whether they are still there. When they answer, callpn.session.keepAlive(), which counts as activity. Only ever call it in response to the viewer: calling it on a timer would keep an unattended screen signed in.reason: 'maxAge': the session reaches itsmaxMinutes, which is never extended. If the viewer is active, callpn.recover()to move them to a new session now (this needscreateSession); otherwise let it end.
pn.on("session.expiring", ({ reason }) => {
if (reason === "idle") showStillTherePrompt({ onYes: () => pn.session.keepAlive() });
if (reason === "maxAge" && viewerIsActiveInMyApp()) pn.recover();
});
When a session ends
When a session ends, the viewer shows "This embedded view has ended." and the SDK emits session.expired. If you pass createSession to the SDK, the viewer also shows a Continue button. Clicking it calls your createSession, which asks your backend for a new session for the same viewer; the SDK then reloads the viewer on it and emits session.recovered { sessionId, restored }. The old session is never extended.
The new viewer picks up where the old one left off: the open view, including layer changes that were not saved, the camera and the selected nodes (restored: true). This state stays inside the Polinode frame and never passes through your page. The AI Networks Chat conversation is not reopened; it remains in the viewer's chat history. Pass preserveState: false to create() to start each new session afresh instead, for example to reopen a view of your own choosing in your session.recovered handler.
Recovery happens only when the viewer clicks Continue, or when your code calls pn.recover(): from a user action, or on session.expiring { reason: 'maxAge' } for an active viewer. It never happens in the background. It ends for good, with session.expired { final: true }, when createSession fails or when the session ends again within a minute of recovering. At that point show your own message or sign-in.
Host Responsibilities
- Launch-token callback. Your
getLaunchToken(sessionId)must call your own backend, which must identify the viewer from its own authenticated session and check that this viewer ownssessionIdbefore requesting a launch token. A session id on its own must never yield a launch token. - Session callback (optional). Your
createSession()must also call your own backend, which must identify the viewer from its own authenticated session (never from anything the browser sends) and create a new session for that viewer. Protect that endpoint like any other state-changing request (CSRF protection), and record which viewer owns the new session sogetLaunchTokencan check it. Recovery is only as strict as your own session policy: if your user's session in your application has ended,createSessionshould fail. - Logout. Revoke the session and remove the iframe when your user logs out.
- Framing. Use an iframe such as:
<iframe
src="https://app.polinode.com/embed/SESSION_ID"
sandbox="allow-scripts allow-same-origin allow-popups allow-popups-to-escape-sandbox"
referrerpolicy="no-referrer"
style="border: 0; width: 100%; height: 700px"
></iframe>
- Identity. You are responsible for how your users authenticate, including whether that meets your organization's SSO or MFA expectations; your organization's MFA requirement does not apply to managed users. Polinode's audit log records the identity your integration asserted, not proof that a particular person acted.
- Your page. Code running on your page, and privileged browser extensions, can use everything the bridge exposes.
The SDK
Install
The SDK is the @polinode/embed package on npm. It has no runtime dependencies and includes TypeScript types.
npm install @polinode/embed
Without a bundler, load the browser build from a CDN, pinned to an exact version with its integrity hash; it defines a global PolinodeEmbed:
<script src="https://cdn.jsdelivr.net/npm/@polinode/embed@1.1.0/dist/polinode-embed.min.js"
integrity="sha384-EGkNEwf8CjmLXjJ/yeZIPV6FC2y8UY6z5DUoyO7i1izE7oG4X2czMROK5QQxNBvQ" crossorigin="anonymous"></script>
Usage
import PolinodeEmbed from "@polinode/embed";
const pn = await PolinodeEmbed.create(document.getElementById("polinode"), {
sessionId,
getLaunchToken: (id) => fetch(`/my-backend/polinode/launch-token/${id}`).then((r) => r.json()).then((b) => b.launchToken),
// Optional: lets an ended view offer Continue, and lets recover() move the
// viewer to a new session, keeping what is on screen (see Expiry and Recovery).
createSession: () => fetch("/my-backend/polinode/session", { method: "POST" }).then((r) => r.json()).then((b) => b.sessionId),
});
await pn.views.open(landscapeViewId);
await pn.layers.filter({ attribute: "External intent", min: 0.7 });
await pn.nodes.focus({ label: "Programme Aurora" }, { zoom: 2 });
const [aurora] = await pn.nodes.get([{ label: "Programme Aurora" }]);
pn.on("node.clicked", ({ nodeId }) => console.log("clicked", nodeId));
pn.on("session.expiring", ({ reason }) => reason === "idle" && showStillTherePrompt({ onYes: () => pn.session.keepAlive() }));
pn.on("session.expired", ({ final }) => final && showSignedOutMessage());
create() resolves once the viewer has loaded the network. Every command returns a promise that resolves only when the change has been applied (including any camera animation), or rejects with a PolinodeEmbedError carrying a code.
Commands
| Command | Description |
|---|---|
session.info() | Display name, capabilities, network scope, viewer options (ui) and expiry. |
session.keepAlive() | The viewer is still there: counts as activity, so an access token about to lapse is refreshed. Returns { sessionExpiresAt }. Call it only in response to the viewer; it cannot extend maxMinutes. See Expiry and Recovery. |
state.get() | { networkId, viewId, dirty, layers, selectedNodeIds, visibleNodeCount, revision }. |
network.info() | Name, node and edge counts, node attribute names and types. |
views.list() / views.open(viewId) | The network's saved views; open one. |
views.save({ name, description? }) / views.update() | Save the current state as a new view, or onto the open one. Needs Edit access; saved views are visible to the network's other users. |
layers.filter({ attribute, values }) / layers.filter({ attribute, min, max }) | Filter nodes by a categorical or numeric attribute. Returns { layerId }. |
layers.color({ attribute }) / layers.size({ attribute }) | Color or size nodes by an attribute. Returns { layerId }. |
layers.remove(layerId) / layers.clear() | Remove one layer, or all of them. |
nodes.find({ attribute, equals | contains | min / max, limit?, cursor? }) | Visible nodes matching a condition, 50 per page by default (at most 200), with coverage and a cursor for the next page. |
nodes.get(refs) | Look up to 100 nodes; each result reports resolved, ambiguous, fuzzy, hidden or not_found. |
nodes.focus(ref, { zoom? }) / nodes.select(refs) / nodes.clearSelection() | Select nodes and move the camera. |
camera.fit(refs?) | Frame the given nodes, or the whole network. |
chat.ask(message, { newChat? }) | Ask AI Networks Chat a question on the viewer's behalf (up to 4,000 characters). The viewer's chat panel opens on the conversation, so the viewer sees what was asked. Resolves as soon as the question is sent; the answer arrives as a chat.reply event. RATE_LIMITED while another question is being answered. |
chat.messages() | The current conversation: [{ id, role, content, isError }]. |
chat.open() / chat.close() / chat.new() / chat.cancel() | Open or close the chat panel, start a new conversation, or stop the answer in progress. |
cancel(requestId) | Cancel a queued or running command. |
A node reference is { id } or { label }. Commands that change something need a reference that identifies exactly one visible node: an ambiguous or approximate label fails with AMBIGUOUS_NODE and up to 10 candidates, and a missing or filtered-out node fails with NOT_FOUND.
Changes run one at a time, in order, together with the viewer's own clicks. At most 20 can be pending (RATE_LIMITED beyond that), and each has a deadline (30 seconds by default, TIMEOUT after it).
Every reply and event carries a revision that increases whenever what is on screen changes, whether your code or the viewer changed it. Pass { ifRevision } to a command to have it fail with STALE_STATE if the viewer has moved on since you last looked.
Events
ready, busy { on }, state.changed { revision }, selection.changed { selectedNodeIds }, node.clicked { nodeId }, chat.question { messageId, content, source }, chat.reply { messageId, content, isError }, error, session.expiring { reason, expiresAt } (reason is idle or maxAge), session.expired (with final: true once no recovery will follow), session.recovered { sessionId, restored }.
chat.question and chat.reply report every question and answer in the viewer's chat, whether your code or the viewer asked it (source is host or viewer); a cancelled answer arrives as chat.reply with cancelled: true. Answers are Markdown.
AI Networks Chat
chat.ask behaves exactly as if the viewer had typed the question: it uses the viewer's chat access (chatAccess), counts against your organization's AI allowance, and is refused with NOT_PERMITTED when the session's ui.aiChat is false or the viewer has no chat. A viewer with chatAccess: 'presetOnly' is offered only the network's preset questions, and chat.ask is held to them too. This shapes what the viewer is offered rather than being a hard content boundary: a viewer who scripts requests with their own session could still steer an answer. Their chat stays confined to the networks you granted and counts against your AI allowance either way, so use off when a viewer must not reach the AI at all. The AI may add layers or open views while answering; those changes raise state.changed like any other.
pn.on("chat.reply", ({ content, isError }) => showAnswerInMyApp(content, isError));
await pn.chat.ask("Which division is most isolated from the others?", { newChat: true });
Error Codes
UNKNOWN_COMMAND, INVALID_ARGS, NOT_PERMITTED, NOT_FOUND, AMBIGUOUS_NODE, STALE_STATE, CANCELLED, TIMEOUT, RATE_LIMITED, SESSION_EXPIRED, INTERNAL.
EmbedView v1
Views are described in a versioned format:
{
"version": 1,
"id": "…",
"name": "Landscape",
"description": "",
"layers": [
{ "id": "L1", "operation": "Filter", "appliedTo": "nodes", "attribute": "Region", "attType": "string", "hidden": false, "values": ["EMEA"] },
{ "id": "L2", "operation": "Color", "appliedTo": "nodes", "attribute": "External intent", "attType": "number", "hidden": false, "min": 0, "max": 1 },
{ "id": "L3", "operation": "Calculate", "summary": "Calculate: Betweenness Centrality on nodes", "readOnly": true }
],
"camera": { "x": 0.5, "y": 0.5, "ratio": 1, "angle": 0 },
"display": { "backgroundColor": "rgba(51, 51, 51, 1)", "showLabels": "always", "showEdges": true }
}
Filter, Color and Size layers on node attributes are described in full. When the left menu is shown, the viewer can add any other kind of layer through the Explore page; those appear as read-only entries with a summary, so your code always sees what is on screen.
Limits
- Provisioning, grants and session creation: 600 requests per hour per organization.
- Launch tokens: 6,000 per hour per organization (roughly the number of embedded page loads).
- Metric calculations in embedded sessions: 3,000 per hour per organization.
- Launch token redemptions: 60 per hour per session, plus a fixed ceiling per IP address.
- If your integration needs different organization or per-session limits, contact Polinode support to have them adjusted for your organization.
- Each session can run one metric calculation and one AI Networks Chat request at a time.
- AI Networks Chat in embedded sessions uses your organization's AI budget. Requests already in progress count against the budget, so many simultaneous conversations cannot exceed it.
Usage
Embedding usage is measured in monthly active users: the managed users who open at least one embedded session in a calendar month (UTC). A user counts once per month however many sessions they open, and a user who is deactivated later in the month still counts for that month. Creating a user or a session does not count on its own; the viewer has to open it.
The Usage tab under Organization Settings shows this month's monthly active users against the number your plan includes, and last month's total. Going over the included number never blocks anyone; additional users are billed as set out in your agreement.
By default there is no limit on how many embedded users you can create. Polinode can set one for your organization if needed; while it is in place, creating or reactivating a user beyond it returns 402 with code EMBEDDED_SEAT_LIMIT, and deactivating a user frees a place.
Branding
Embedded viewers have no Polinode navigation bar. A small Polinode icon is shown at the bottom right, beside the AI Networks Chat button (or in its place when chat is hidden). Hovering over it shows "Powered by Polinode", and clicking it opens www.polinode.com in a new tab. White-labelled organizations have no Polinode mark, and their embedded viewers use the organization's own colors.
Node images hosted on any https site are displayed in embedded viewers.