The drop-in widgets
Three React components your users see on your site, in your brand — API keys, users & groups, and AI chat with your API. They all follow one pattern: a page with the component, and a server route on your backend that holds the APIblaze credential and answers one question — who is signed in? The browser never holds a secret.
| Widget | Your users… | Server credential |
|---|---|---|
| <ChatWidget/> | chat with your API — book, query, act in plain English | APIBLAZE_CHAT_DP_KEY (an API key for your proxy) |
| <ApiKeyWidget/> | mint, rotate & revoke their own API keys | APIBLAZE_CP_KEY (widget key) |
| <UsersGroupsWidget/> | manage their users & nested groups (admins) | the SAME APIBLAZE_CP_KEY |
Everything below is copy-paste; the values you supply are «highlighted». One npm install apiblaze ships all three.
AI chat with your API — on your own site
A floating chat bubble (or an inline card) where your signed-in users book, query and act on your API by typing a sentence. The AI runs server-side behind your proxy — it can only use your API's published tools, every call is authorized and metered like any other request, and the browser never holds a credential or an AI key.
First, one environment variable
The chat widget calls your API as a consumer, so it needs a plain API key for your proxy — the same kind your own users mint. (The keys & groups widgets below use a different, producer-side widget key.) npx apiblaze create does not print one — callers sign in by default — so mint it: npx apiblaze apikeys mint --tenant «acme» (no --for: the widget tells the proxy who is chatting), or grab one from your <ApiKeyWidget/> page or your proxy's hosted dev portal. Put it in .env:
APIBLAZE_CHAT_DP_KEY=sk_prod_…
This key also decides the tenant. Every APIblaze key is minted inside a tenant (a customer workspace), and the chat endpoint derives the tenant from the key — never from a URL — so the whole conversation (identity, groups, authorization rules, billing) runs in that tenant's world. Mint the key in the tenant you want the chat to live in.
How the pieces fit — two files
- 1The component
<ChatWidget/>renders the chat (a bottom-right bubble by default). It streams every reply live — including “Checking availability…” progress lines while the AI calls your API — from one URL on your site. - 2The server route answers it.
createApiblazeChat(...)returns{ handler }you export asPOST. It holds the proxy key, asks yourgetUserwho is chatting, and pipes the answer stream straight through.
getUser here is even smaller than the other widgets' — just { userId }. Return null when nobody is signed in and the widget simply isn't offered chat on your dime.
Step 1 — the server route. Pick your login, as before:
import { createApiblazeChat } from 'apiblaze/server';
import { auth } from '@/auth';
const chat = createApiblazeChat({
project: 'acme', // ① your proxy's name
apiKey: process.env.APIBLAZE_CHAT_DP_KEY!, // ② an API key for that proxy
getUser: async () => {
const session = await auth();
if (!session?.user) return null; // signed out → no chat
return { userId: session.user.id }; // ③ which person is chatting
},
});
export const POST = chat.handler;Step 2 — the page. Mount it once (e.g. in your layout) and the bubble follows your users everywhere:
import { ChatWidget } from 'apiblaze/react';
export default function Page() {
return (
<ChatWidget
endpoint="/api/apiblaze/chat"
title="Chat with Acme" avatar="⚡"
welcome="Hi! Ask me anything about your account."
suggestions={['What can you do?', 'Show my recent orders']}
storageKey={`apiblaze-chat:acme:${user.id}`} // per-user transcript (shared machines!)
theme={{ accent: '#7C3AED' }}
/>
);
}No key, no server route — your users' own login is the credential
Your site already logs people in and holds a session JWT. Register that issuer on your proxy once, and the widget sends each user's own token straight to the chat endpoint. Every user then chats as themselves: per-person daily allowances, per-person authorization rules, and no one-at-a-time bottleneck — because there is no shared key.
Step 1 — register your login's issuer on the proxy (one time). Three values from your auth provider: iss, aud, jwks_url. In the dashboard: your proxy → Authentication → third-party JWTs — or as configuration:
"requests_auth": {
"mode": "authenticate",
"methods": ["jwt"],
"jwt": {
"allowed_pairs": [{
"iss": "https://login.acme.com/",
"aud": "acme-api",
"jwks_url": "https://login.acme.com/.well-known/jwks.json"
}]
}
}The end-user identity defaults to the token's sub claim (a different claim or a mapping is one extra field — see Authentication). Already created the proxy? npx apiblaze config acme opens every setting, including this one.
Step 2 — the page. That's the whole integration — project +getToken instead of endpoint, no API route anywhere:
'use client';
import { ChatWidget } from 'apiblaze/react';
import { useSession } from '@/lib/auth'; // however YOUR app exposes the session
export default function Page() {
const session = useSession();
if (!session) return null; // signed out → no chat bubble
return (
<ChatWidget
project="acme" // your proxy's name
getToken={() => session.token} // the user's own JWT, per message
title="Chat with Acme" avatar="⚡"
welcome="Hi! Ask me anything about your account."
storageKey={`apiblaze-chat:acme:${session.userId}`}
theme={{ accent: '#7C3AED' }}
/>
);
}- •
getTokenis called per message, so refreshed tokens win automatically. The token'saudmust match what you registered. - • The proxy verifies the signature against your JWKS on every request and handles CORS — the widget works from any origin you serve it on.
- • Chats are funded and capped exactly the same way (your LLM tab settings) — just metered per person instead of per shared key.
The props you'll actually use
| mode | "bubble" (floating launcher, the default) or "inline" (a card in your layout). |
| title · avatar · welcome | The chrome: header text, emoji, and the first message shown before any exchange. |
| suggestions | Up to 4 tappable prompt chips shown on an empty chat — seed the questions you want asked. |
| storageKey | Where the transcript lives (sessionStorage — survives page hops, dies with the tab). Make it per-user whenever your page has logins, or the next person at a shared machine can read the previous chat. |
| theme | Full white-label: accent, bubbles, launcher, radius, fonts — same philosophy as the other widgets. |
Steer the assistant — from the dashboard, not from code
Dashboard → your proxy → LLM → Steer the assistant: a short note like “always suggest the daily special”. It's injected server-side beneath the assistant's safety rules (tools only, no invented endpoints), and applies to every chat surface on the proxy — this widget, the dev portal's Chat tab, and npx apiblaze apichat.
Who pays, and the honest limits
- • You fund the AI by default, with guardrails you set on the same LLM tab: per-person and per-day spending caps, chats per person per day, and a proxy-wide daily ceiling. When a limit is hit the widget shows a friendly “come back tomorrow” — never an error.
- • With an API key, one relay key = one shared allowance and one chat at a time across your site — ideal for a demo or a low-traffic page. For per-person allowances, per-person permissions and real concurrency, flip the selector above to Auth with login (OAuth): each user chats as themselves.
- • Building your own chat UI? You don't need the widget: the chat endpoint speaks the standard Vercel AI SDK stream protocol, so
useChat, AI Elements and assistant-ui work against it directly.
“Get an API key” on your own site
The same two-file pattern: your signed-in users mint, rotate and revoke their own API keys — on your page, in your brand. The browser talks only to your backend; your backend holds the APIblaze credential. You write exactly three values — everything else is paste-as-is.
First, one environment variable
APIblaze Dashboard → Developers → create a Widget key (not a full admin key), then put it in .env:
APIBLAZE_CP_KEY=sk_prod_…
The same key powers both widgets — you never need a second one.
How the pieces fit — two files
- 1The component
<ApiKeyWidget/>renders the keys UI. When it needs data it makes onefetch()to a URL on your site (default/api/apiblaze/keys) — it never sees your APIblaze key and never calls APIblaze directly. - 2The server route answers that fetch.
createApiblazeKeys(...)returns{ handler }— a normal request handler you export asGET/POST. It holds your secretcpKeyand calls the one function you write —getUser— to learn who is asking, then talks to APIblaze server-to-server on their behalf (listing / minting / revoking only that user's keys).
The widget finds your route by its URL, not by any variable name — so const apiblazeKeys = … below is just your local name (call it whatever you like). Moving the route? Set the widget's endpoint prop to the new path. getUser is the only code you actually write.
Step 1 — the server route. Pick whatever you already use to log people in:
import { createApiblazeKeys } from 'apiblaze/server';
import { auth } from '@/auth'; // your NextAuth config file
const apiblazeKeys = createApiblazeKeys({
cpKey: process.env.APIBLAZE_CP_KEY!,
getUser: async () => {
const session = await auth();
if (!session?.user) return null; // not signed in → widget asks them to
return {
tenant: session.user.companyId, // ① your org/team id — or session.user.id
userId: session.user.id, // ②
email: session.user.email ?? undefined, // ③
};
},
});
export const GET = apiblazeKeys.handler;
export const POST = apiblazeKeys.handler;Step 2 — the page. Identical for everyone, nothing to fill in:
import { ApiKeyWidget } from 'apiblaze/react';
export default function Page() {
return <ApiKeyWidget theme={{ accent: '#7C3AED' }} />;
}The only three values you supply
| ① tenant | Which of your customers this user belongs to. It is the wall between customers — two tenants never see each other's keys, users or groups. Use your company / team / workspace / organisation id. No such concept in your app? Pass the user's own id — then every user simply gets their own private space. |
| ② userId | Which person is signed in. They own the keys they create. Your user's primary key. Must be stable, and never reused for a different human. |
| ③ email | Optional, but recommended — it puts a real identity on this user. Sent once (never on every call) to link userId ↔ email, so the same person is recognised across surfaces — e.g. when they later sign into your API's dev portal with that email, or when an admin adds them by email.It's also the hook for admin rights: an email on the tenant's admin allowlist becomes a tenant admin in the Users & Groups widget. Omit it and the user still works — they're just identified only by userId. |
Where those come from, whatever you use to log people in
| Your login | tenant | userId |
|---|---|---|
| NextAuth | session.user.organizationId | session.user.id |
| Clerk | orgId (from auth()) | userId |
| Auth0 / WorkOS | org_id claim | sub |
| Your own database | the account / company row id | the user row id |
| No teams at all | the same value as userId | your user id |
“Organisation”, “org”, “workspace”, “team”, “account” and “company” all mean the same thing here: the customer you bill. That is your tenant.
- • Eligibility is decided on your server, from your session — never in the browser. Return
keyTypes: falsefromgetUserto deny a user API access entirely; a list of two or more types shows a picker. - • Keys are durable by default (no silent expiry); set
keyExpiresInSecondsfor expiring, re-revealable keys. Durable secrets are shown once, masked afterwards. - • Everything is white-label through the
themeprop: accent, radius, fonts.
Users & groups on your own site
Literally the same thing again with one word changed: same widget key, same getUser, one more route. Your customers' admins then manage their own users and (nested) groups from your page — and group membership drives authorization at the proxy.
getUser, same APIBLAZE_CP_KEY — only createApiblazeKeys → createApiblazeGroups. Compare the tab below to your keys route: identical but the one word. (Running both widgets and don't want to write getUser twice? Move it to a shared file and import it in both — optional.)import { createApiblazeGroups } from 'apiblaze/server'; // ← the only change vs. the keys route
import { auth } from '@/auth';
const apiblazeGroups = createApiblazeGroups({
cpKey: process.env.APIBLAZE_CP_KEY!, // the SAME widget key
getUser: async () => { // the SAME getUser
const session = await auth();
if (!session?.user) return null;
return {
tenant: session.user.companyId, // ① which CUSTOMER
userId: session.user.id, // ② which PERSON
email: session.user.email ?? undefined, // ③ links identity (dev-portal, admin rights)
};
},
});
export const GET = apiblazeGroups.handler;
export const POST = apiblazeGroups.handler;- • Groups nest, and rules walk the whole tree (OpenFGA underneath) —
maria ∈ reservationists ⊂ adminpasses anadmincheck. - • Identities seen in real traffic surface in the widget so admins can pull them into groups.
- • Resolved membership is forwarded to your backend as
abz.groupson every request. - • To enforce access from group membership, add a rule — see Authorization rules.