Embed a question, a dashboard, or an AI agent
Embed tokens are self-verifiable signed JWTs carrying a resource type, a resource id, locked params and an expiry, signed with a per-organisation key. The render surface takes no authentication. And because Qrly is priced on revenue rather than per viewer, embedding to ten thousand end users does not change the bill.
Three separate ways to put Qrly inside something else
They are deliberately not one abstraction. Each has its own trust model, and choosing wrongly is the usual cause of an embedded-analytics security incident.
Signed, scoped, expiring — for a question or a dashboard
- Self-verifiable signed JWT — no server-side session lookup
- Carries resource type, resource id, locked params and an expiry
- Locked params are always emitted, defaulting to
{} - Signed with a per-organisation key
- The shipped placeholder key is rejected
- Configurable default and maximum token lifetimes
- Create, list and revoke from the admin surface
The simpler mechanism, for a report
- A share token mints an unauthenticated viewer URL
- Separate from embed tokens, with its own viewer route
- Its own spreadsheet route for spreadsheet reports
- Generate, copy, revoke — from report settings
- Dashboards have their own public share link too
- No locked params, no SDK, no parameter surface
An AI agent as a drop-in script tag
- Token, endpoint and container — one script tag
- Token carries agent type and locked params
- Allow-lists for connections, questions and tools
- Per-session message and tool-call caps
- Per-day session cap
- Greeting message, brand logo and colour
- Its own signing key and audience, active flag, revoked timestamp
What an embed token actually carries
The claim set is small on purpose. Everything a viewer is allowed to do is decided at issue time, on your server, and then signed.
| Claim or control | ShippedQrly embed tokenSelf-verifiable signed JWT | Where it is enforcedAnd what it means in practice |
|---|---|---|
| Resource type | Question or dashboard | A question token cannot render a dashboard |
| Resource id | One resource, per token | Signature covers it — no id substitution |
| Locked params | Always emitted | Defaults to {} — the claim is never simply absent |
| Expiry | On every token | Configurable default and maximum lifetime |
| Signing key | Per organisation | One organisation cannot mint another's tokens |
| Distribution placeholder key | Rejected | You cannot ship to production still using it |
| Render surface authentication | None required | /embed/{token} — the token is the credential |
| Qrly attribution in the frame | Hideable | A brand flag hides "powered by" in embeds |
| Iframe height management | Auto-resize | postMessage, re-broadcast as a DOM event |
| Per-viewer licence cost | €0 | Qrly is priced on revenue, not on seats or viewers |
| Row-level security / data sandboxing | Not implemented | Locked params + embed-agent allow-lists are the shipped equivalent |
Two calls: init and embed
The SDK is served from the application itself — there is no CDN dependency and no external asset request from an embedded page.
Mint the token on your server
Your application already knows who the viewer is. Issue the token there, with the tenant identifier locked in, and hand the browser nothing else. This is the whole of the multi-tenant story.
curl -X POST https://bi.example.com/api/embed/tokens \
-H "Authorization: Bearer $QRLY_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"resourceType": "DASHBOARD",
"resourceId": 412,
"lockedParams": { "tenant_id": "acme-gmbh" },
"expiresInSeconds": 900
}'
Render it in your page
Qrly.init takes the base URL of your installation, the token you just minted and a theme. Qrly.embed takes a selector and either a questionId or a dashboardId, plus any params that are not locked. The iframe reports its own height back over postMessage, which the SDK re-broadcasts as a DOM event so your layout code can react without listening on the raw message channel.
<div id="usage-dashboard"></div>
<script src="https://bi.example.com/embed/qrly-sdk.js"></script>
<script>
Qrly.init({
baseUrl: 'https://bi.example.com',
token: EMBED_TOKEN, // minted server-side, short-lived
theme: 'dark'
});
Qrly.embed('#usage-dashboard', {
dashboardId: 412,
params: { period: 'last_30_days' } // viewer-supplied, never security-bearing
});
// iframe auto-resize is re-broadcast as a DOM event
document.addEventListener('qrly:resize', (e) => {
console.log('embed height', e.detail.height);
});
</script>
The rule that matters
Anything in lockedParams is covered by the signature and is decided by your server. Anything in params is supplied by the browser and must be treated as untrusted — it is for periods, groupings and presentation, not for deciding whose rows come back. Qrly applies the same discipline to dashboard filters generally: a viewer can only supply a filter id and a value, never a column or an operator, and the binding operator comes from an allow-list.
An embeddable AI agent, not just a chart
Every embedded-analytics vendor can put an iframe in your product. Qrly puts a constrained agent in it — with allow-lists and caps written into the token rather than trusted to the front end.
One script tag, three attributes
The admin page issues an embed-agent token and hands back a copyable snippet. The token, the endpoint and the container element are the entire integration surface.
<div id="qrly-agent"></div> <script src="https://bi.example.com/embed/qrly-agent.js" data-token="<embed-agent token>" data-endpoint="https://bi.example.com" data-container="#qrly-agent"> </script>
What the agent token carries
All of the following live inside the signed token, which has its own signing key and its own audience — separate from the question and dashboard embed keys, so revoking one does not disturb the other.
- Agent type. Which of Qrly's agent personas the widget runs as. The embedded widget is not a general-purpose console.
- Allowed connections. An explicit list. A connection that is not named cannot be reached, whatever the conversation asks for.
- Allowed questions. An explicit list of saved questions the agent may run.
- Allowed tools. An explicit list drawn from the agent runtime's tool set. Anything not listed is unavailable for that widget.
- Locked params. The same mechanism as dashboard embedding — the customer scoping is signed in, not negotiated at runtime.
- Per-session message cap and tool-call cap. A bounded conversation and a bounded amount of work per conversation.
- Per-day session cap. Bounded exposure per token, per day, which is what makes a public-facing widget a defensible cost.
- Greeting message, brand logo and brand colour. So the widget reads as part of your product rather than as a visitor from ours.
- Active flag and revoked timestamp. Turn one widget off without touching the others.
Note what is not in that list: no way for the embedded page to widen its own scope. The allow-lists are claims in a signed token, and a browser that edits them invalidates the signature.
Four things that change when you embed with Qrly
Written for the engineer who has already priced an embedded-analytics vendor once and did not enjoy it.
One dashboard, every customer, their rows only
The multi-tenant SaaS case is concrete. You build one dashboard against one connection, parameterised on the tenant column. You then mint one embed token per customer with {"tenant_id": "…"} in lockedParams. Every customer opens the same dashboard id and sees only their own rows, because the scoping is in a claim your server signed, not in a query-string value their browser could edit.
This scales the way you want it to: adding your five-hundredth customer is minting a five-hundredth token, not authoring a five-hundredth dashboard. And when you change the dashboard, all five hundred customers get the change at once, because there is only ever one of it.
Ten thousand end users does not change the bill
Embedded analytics is where per-seat pricing turns hostile. The people looking at the embedded dashboard are your customers' users — a population you do not control, cannot cap and are not going to charge individually. A per-viewer licence turns your own growth into a variable cost, and finance eventually asks you to gate the feature to slow it down.
Qrly is priced on revenue rather than on seats or viewers. The bill responds to how big your company is, not to how many people opened a chart last Tuesday. Embedding therefore stays a product decision rather than a licensing negotiation, and you never end up hiding analytics behind a plan tier for reasons that have nothing to do with your customers.
Constraints in the token, not in the front end
Putting a chatbot with database access into a customer-facing product is normally the point where security review stops the project — and reasonably so, because the usual design puts the guardrails in the front-end code that the customer's browser is running.
Qrly's embed-agent token carries the allow-lists itself: agent type, allowed connections, allowed questions, allowed tools, locked params, per-session message and tool-call caps, a per-day session cap. Those are signed claims verified server-side on every call. The reviewable question stops being "what could the widget be talked into" and becomes "what did we put in the token", which is a question with a written answer.
What the locked-param mechanism is not
Row-level security and data sandboxing as general platform features are not implemented in Qrly. There is no policy language that filters every query for every user everywhere in the product. If your evaluation depends on that, you should know it now rather than in week three of a proof of concept.
What ships is narrower and, for the embedded case, sufficient: locked params in a signed token, and the embed-agent allow-lists. Inside the application, access is governed by collection, data and project permission levels resolved most-permissive-wins across a user's groups, plus always-applied model filters on OLAP models, which act as a security boundary for model-driven queries. Those are different mechanisms with different reach, and we would rather name them than blur them into a marketing word.
Embedding questions
How does Qrly stop one embedded customer seeing another customer's rows?
Does Qrly support row-level security?
What does embedding cost per end user?
Does the embedded page require my users to log in to Qrly?
/embed/{token} takes no authentication at all — the token is the credential and it verifies itself. Your application issues the token server-side with your own session already established, and the browser only ever sees a short-lived signed value scoped to one resource.