If you run a Nevermined organization, you can embed Nevermined-hosted UI flows directly into your site instead of redirecting customers to nevermined.app. Your end-users stay on your domain; the iframe handles auth, payments, and card delegations against the Nevermined backend.Documentation Index
Fetch the complete documentation index at: https://docs.nevermined.app/llms.txt
Use this file to discover all available pages before exploring further.
This guide is for organization admins. If you haven’t upgraded to an organization yet, see Platform Partners first.
What you can embed
Checkout
Sell an agent’s plans — or a standalone plan with no agent — from your own pricing page.
Card enrollment
Let users add a payment method without leaving your site. Returns the resulting
paymentMethodId.Card delegation
List enrolled cards, create delegations, and revoke either — without redirecting to nevermined.app.
How it works
{ email, orgId, rawSecret } to the Nevermined API server-to-server; the API hashes the candidate and constant-time compares against the SHA-256 of the keys you generated in the dashboard, then returns a 2-hour WidgetSession your backend forwards verbatim to the browser SDK.
Setup
Generate a widget key
Open Settings > Organization in the Nevermined App and scroll to the Widget Integration section.
Click Generate widget key and fill in the dialog:

-
Name — a descriptive label, e.g.
Production website. -
Allowed origins — every domain where the widget will be mounted, e.g.
https://yourcompany.comandhttps://staging.yourcompany.com. At least one origin is required. Use Add origin to add more rows. No path, query, fragment, or trailing slash.
parentOrigin doesn’t match this allowlist — it’s your second line of defense if the secret ever leaks. You can edit the list later from the key’s actions menu without rotating the secret.Click Generate key. The raw secret is shown once. Copy it and store it as an environment variable on your server (for example, NVM_WIDGET_SECRET). You won’t see it again.
Mint the widget session on your server
Install the server SDK on your backend and expose a small endpoint that mints a The call is the same regardless of framework:Pick the framework that matches your stack:
WidgetSession for the currently logged-in end-user.- Vite middleware
- Express
- Next.js Route Handler
email identifies the end-user of your site, not the org admin. Resolve it from your auth session — Nevermined uses it as the canonical identity for the widget user (the same email logging in directly at nevermined.app resolves to the same Privy user, wallet and profile).Mount the widget in the browser
Install the browser SDK on your frontend.Fetch the Then mount whichever widget you need into a container element on your page:Each
session from your endpoint with a POST (the server side mints a bearer credential — GET would risk caching/prefetching it) and pass it straight to the SDK. The same nvm instance can mount any of the available widgets.onSuccess receives a single event object: event.result holds the flow’s result fields and event.handle.close() dismisses the iframe. The widget stays mounted on success (showing its success state) until you call handle.close() — there is no auto-dismiss.- Checkout (agent)
- Checkout (plan only)
- Enroll a card
- List & delegate
Sell a specific agent’s plans. Pass the agent
did; planId is optional — omit it to show the plan carousel.Every iframe-mounting method also accepts optional
width / height (pixels) for inline sizing — the SDK clamps them to a supported range; omit them for the default overlay. Checkout, enroll, and list also accept onAuthMismatch / onAuthSwitchRequest for the multi-org identity case — see Handling identity mismatches.Available widgets
| Method | Mounts iframe | Result |
|---|---|---|
nvm.checkout.start({ did, planId?, container, ... }) | yes | onSuccess → event.result: { did, planId?, txHash? } |
nvm.checkout.startPlan({ planId, container, ... }) | yes | onSuccess → event.result: { planId, txHash? } |
nvm.delegations.enrollCard({ container, provider?, ... }) | yes | onSuccess → event.result: { paymentMethodId } |
nvm.delegations.listCards({ container, onCardAction, ... }) | yes | onCardAction: { action: 'delegate' | 'revoked', paymentMethodId } |
nvm.delegations.createDelegation({ paymentMethodId, container, ... }) | yes | onSuccess → event.result: { delegationId, paymentMethodId } |
await nvm.delegations.revokeCard(paymentMethodId) | no — direct API call | resolves on 2xx |
await nvm.delegations.revokeDelegation(delegationId) | no — direct API call | resolves on 2xx |
| Callback | When |
|---|---|
onBooted | iframe DOM mounted (auth not yet validated) |
onReady | session validated, widget interactive |
onSuccess(event) | flow succeeded — the widget stays mounted in its success state; call event.handle.close() to dismiss it |
onError | terminal error — widget destroys itself |
onClose | widget dismissed (the user closed it, or you called event.handle.close()) — instance destroyed |
A widget instance is single-use. Once it reaches a terminal state — you call
event.handle.close() after success, or it fires onError / onClose — calling its method again throws. Construct a fresh widget via the same nvm instance to mount another flow.Redirect / CLI mode (no iframe)
For integrations that aren’t an in-page iframe — a CLI tool, or any flow where you’d rather hand the user to a full-page Nevermined screen and get them back via a callback — open the embed route as a top-level browser navigation instead of mounting it through the SDK. You pass asessionToken and a returnUrl on the query string. The page runs the same flow and, on success, redirects the browser to returnUrl with the result IDs (plus your state echo) appended as query parameters — the same shape as an OAuth callback.
Supported on the card flows and plan-only checkout. The agent checkout (/embed/checkout/$did) is iframe-only.
| Route | Result params appended to returnUrl |
|---|---|
/embed/cards/setup | paymentMethodId, delegationId |
/embed/cards/enroll | paymentMethodId |
/embed/cards/delegate | delegationId |
/embed/checkout/plan/<planId> | planId, paymentIntent |
Minting a session for redirect mode
- CLI / logged-in Nevermined user —
POST {apiBaseUrl}/api/v1/widgets/session/selfwith the user’s Nevermined credential and theorgId(the user must be a member of the org). Self-mint sessions only accept localhostreturnUrls — the CLI listens on a local port. - Hosted site — mint with the same server-side
createWidgetSessionas the iframe flow; thereturnUrlmust belong to the widget key’sallowedOrigins.
Example
Handling identity mismatches
A widget session is minted for a specific email (the end-user you passed tocreateWidgetSession). If the host browser already has a Nevermined (Privy) login for a different email, the iframe detects the mismatch and:
- fires
onAuthMismatch(detail)—detailcarries the expected vs. host emails, and the iframe shows an in-place prompt (mirror it in your own UI if you want); - if the user chooses “Continue as
<host email>”, firesonAuthSwitchRequest(detail)— re-mint a widget session bound todetail.requestedEmail(call your session endpoint again with that email) and reopen the widget.
Environments
Pick the environment that matches the Nevermined dashboard you used to mint the widget key.| Environment | API base | Webapp base | When to use |
|---|---|---|---|
sandbox | https://api.sandbox.nevermined.app | https://nevermined.app | Testing on Base Sepolia with sandbox credentials |
live | https://api.live.nevermined.app | https://nevermined.app | Production on Base mainnet |
Production checklist
Server-side secrets only
Keep
NVM_WIDGET_SECRET in your runtime env vars. Never commit it, never ship it to the browser, never put it in NEXT_PUBLIC_* or VITE_* variables.Allowed origins populated
Add every domain where you mount the widget. The backend rejects sessions from any other origin.
HTTPS only
Serve both your site and your widget-session endpoint over HTTPS in production.
parentOrigin includes the scheme, so origins must match exactly.Real email per session
Pass the actual end-user email from your auth — never a placeholder, never the same value for every visitor. It’s the canonical identity used across Nevermined.
Rotate on suspicion
If the secret might have leaked, revoke the key from the dashboard and generate a new one. Update your env var and redeploy. The API only ever stores its SHA-256, so the only recovery path is rotation.
Short session TTLs
Widget sessions expire after 2 hours; the SDK auto-refreshes them in-place. Don’t cache or share session objects across users.
Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
onError: UNAUTHORIZED, apiCode BCK.WIDGET_SESSION.0001 | rawSecret doesn’t match any active key for the org — or the org has no active key at all (one code covers both cases, by design, so attackers can’t probe which) | Confirm the secret in your env matches an active key in the dashboard; generate one if the org has none; rotate if leaked |
onError: UNAUTHORIZED, apiCode BCK.WIDGET_SESSION.0005 | Session token expired (default 2h) | The SDK auto-refreshes; if it fails persistently, re-fetch a fresh session from your server |
onError: UNAUTHORIZED, apiCode BCK.WIDGET_SESSION.0011 | The widget key was revoked while the session was alive | Re-call createWidgetSession with the new key’s rawSecret |
Widget mounts but never reaches onReady → onError apiCode BCK.WIDGET_SESSION.0012 (403) | The mount origin isn’t in the key’s allowedOrigins | Add your site’s origin to the key’s allowlist (mind the scheme: https:// vs http://) |
onError: UNAUTHORIZED, apiCode BCK.WIDGET_SESSION.0020 (403) | The did / planId you’re checking out belongs to a different organization than the widget session | Embed only agents/plans owned by the org the session was minted for |
onError: PAYMENT_NOT_CONFIRMED | Came back from a Stripe redirect, but the backend couldn’t confirm the payment intent | Don’t trust the redirect_status query flag alone; if the user was actually charged, reconcile via your Stripe dashboard / support |
onError: NETWORK | Fetch failed before getting a response | Check your network, CORS, and that the environment matches the API you minted the key against |
[NeverminedWidgets] initialize: invalid environment | environment is not a supported value | Use 'sandbox' or 'live' |
Related
Platform Partners
Background on Nevermined organizations and how to upgrade your account.
Card enrollment
How Nevermined card enrollment works under the hood — the widget surfaces this same flow.