Calling HUB.KI from your backend
Use the app's tokens to authenticate the signed-in user to your own backend and to call the HUB.KI REST API from it.
Some apps need a server of their own — to run privileged logic, hold secrets, or
integrate a third‑party service. With the api:access scope, your app can hand
its backend a short‑lived token that proves who the signed‑in user is, which
organization they're in, and which scopes they granted your app.
This token is scoped to your app — it is not the user's HUB.KI session token and cannot act as the user across HUB.KI. It is an identity + scope assertion for your backend to verify. See What the token can do.
If your backend also needs to read or change data in HUB.KI, the app can hand it a second token for the HUB.KI REST API, limited to the REST API scopes your organization approved — see Calling the HUB.KI REST API.
Identity token: getToken() | REST API token: getApiToken() | |
|---|---|---|
| Purpose | Proves to your backend who the user is. | Calls the HUB.KI REST API as the user. |
| Verified by | Your backend, against HUB.KI's JWKS. | HUB.KI, on every request. |
token_use | custom_app | custom_app_api |
aud | urn:hub:custom-app:<organizationId>:<appId> | urn:hub:public-api |
| Requires | api:access | api:access and an approved REST API scope |
| Renewal | onToken(handler) | onApiToken(handler) |
Both expire after 5 minutes.
What you get
const { coreUrl, iamUrl, organizationId, token } = await hub.getConnection();| Field | Use |
|---|---|
iamUrl | Base URL of the identity service — its JWKS verifies the token. |
organizationId | The active organization. |
token | A short‑lived, app‑scoped bearer token (see below). |
coreUrl | Base URL of the HUB.KI core API. |
Reaching your backend
The sandbox blocks all network requests by default. Before your app can fetch
its backend, list that backend's origin in the manifest's
connectOrigins and have an
organization admin approve it — otherwise the request is blocked by the app's
Content-Security-Policy.
Two more things to know about requests from the sandbox:
- The request's
Originisnull. The sandbox has no stable origin, so your backend must CORS-allowOrigin: null(e.g. replyAccess-Control-Allow-Origin: null). Use bearer tokens, not cookies — credentialed requests can't use anullallow-origin. - Approved origins are for
fetch, not navigation. Only declared, approved origins are reachable; treat approval as the trust boundary for where the app may send data.
The flow
The app sends the token to your backend; your backend verifies it against HUB.KI's identity service and now trusts the user's identity and granted scopes.
sequenceDiagram
participant App as App (iframe)
participant Host as HUB.KI host
participant Backend as Your backend
participant Iam as HUB.KI identity
App->>Host: hub.getConnection()
Host-->>App: iamUrl, organizationId, token
App->>Backend: your request + token + organizationId
Backend->>Iam: fetch JWKS (cached)
Backend->>Backend: verify signature, aud, token_use, exp
Backend-->>App: result (now trusts user + scopes)Typically you validate the token once to establish your own session (a cookie or your own JWT), then serve subsequent requests from that — the HUB.KI token is a bootstrap credential, not your per‑request session.
Validating the token
Verify the JWT against the identity service's JWKS at ${iamUrl}/api/auth/jwks,
then check its claims:
| Claim | Expected value |
|---|---|
token_use | custom_app |
aud | urn:hub:custom-app:<organizationId>:<appId> |
app_id | your app's appId |
sub | the user's stable id |
tenant_id | the organization id |
scope | space‑delimited scopes the org approved for your app |
email, name | present only if user:read was granted |
import { createRemoteJWKSet, jwtVerify } from "jose";
const jwks = createRemoteJWKSet(new URL(`${iamUrl}/api/auth/jwks`));
const { payload } = await jwtVerify(token, jwks, {
audience: `urn:hub:custom-app:${organizationId}:${appId}`,
});
if (payload.token_use !== "custom_app") throw new Error("wrong token type");
// payload.sub, payload.scope, payload.email are now trusted.What the token can do
- It authenticates the user to your services and tells you which scopes the org approved. That's its job.
- It cannot act as the user on HUB.KI's own APIs. Its
token_use/audare app‑scoped, so HUB.KI's user‑facing endpoints reject it. This is deliberate: a leaked app token can't be replayed against the user's account — it only carries the identity + scopes you already see. To call the REST API, use the REST API token.
Keeping the token fresh
Tokens are short‑lived. The host mints a fresh one before expiry and pushes each new value — subscribe once and always hold a current token instead of polling.
sequenceDiagram
participant Host as HUB.KI host
participant App as App (iframe)
App->>Host: hub.onToken(handler)
Host-->>App: current token (immediately)
Note over Host: ~60s before expiry
Host->>Host: mint a fresh app token
Host-->>App: hub/token/changed (new token)let token = await hub.getToken();
hub.onToken((next, error) => {
token = next; // send the fresh token to your backend before it expires
if (next === null) showNotice(error?.message); // no token could be issued
});When no token can be issued — the approval was withdrawn, the user lost access
to the app, or issuing failed — the handler receives null and a HubError
whose code says why (scope_not_granted, access_denied or
token_unavailable; see Backend access). It is
called with null once, and with the next token as soon as one can be issued
again. onApiToken behaves the same way for the REST API token.
Calling the HUB.KI REST API
To let your backend use the HUB.KI REST API (/v1/*) on the user's behalf:
- List the REST API scopes it needs in the manifest's
apiScopes, next toapi:accessinscopes. - Have an organization admin approve them.
- In the app, read the token with
hub.getApiToken()or subscribe withhub.onApiToken(handler), and send it to your backend with each request. - In the backend, send it as
Authorization: Bearer <token>tocoreUrlfromhub.getConnection().
You don't need to verify this token yourself: HUB.KI verifies it on every request. Its claims, if you want to read them:
| Claim | Value |
|---|---|
token_use | custom_app_api |
aud | urn:hub:public-api |
app_id | your app's appId |
sub | the user's stable id |
tenant_id | the organization id |
scope | space‑delimited REST API scopes approved for your app when it was issued |
exp | 5 minutes after it was issued |
createHubApiClient from @hub-ki/app-sdk/api is a typed client for it. That
entry point needs only a global fetch, so it runs on your server too:
import { createHubApiClient } from "@hub-ki/app-sdk/api";
// Per incoming request, with the token and coreUrl the app sent along.
const client = createHubApiClient({ baseUrl: coreUrl, token: apiToken });
const { data } = await client.openapi.GET("/v1/meetings", {
params: { query: { limit: 20 } },
});Error statuses reject with a HubApiError (status, code, message,
requestId); a 429 is retried automatically after Retry-After, up to three
times. If your backend can get a fresh token on its own, pass it as
refreshToken: async () => newToken: it runs when a request answers 401, and
the request is retried once with the token it returns.
What a request may do:
- It acts as the user. Their permissions apply, capped by the approved REST API scopes. Each user of the app has their own identity, so files, jobs, chats and idempotency keys created through the app stay separate between its users.
- Approval is rechecked on every request. Withdrawing a scope, removing the
user's access to the app, or disabling the app takes effect on the next
request: a token that no longer qualifies is answered with
401 invalid_api_key, a request outside the scopes still approved with403 insufficient_scope. - Rate limits: 60 requests per minute per user, and 600 per minute for the
app across all its users in the organization (
429 rate_limit_exceeded). /v1/responsesis not available with this token. Run AI throughhub.responsesin the app instead.
Presigned upload and download URLs returned by /v1/files work from your
backend as usual. The app cannot reach them from the iframe unless the storage
origin is in its approved connectOrigins.
Security
- Both tokens are app‑scoped and short‑lived — treat them as credentials; never log them or expose them to third parties. The REST API token acts as the user within the approved scopes.
- Don't persist them. Re‑read them from
onToken/getTokenandonApiToken/getApiTokenwhen you need one. api:accessis opt‑in: an organization admin must approve it beforegetConnectionreturns a token.- If your backend needs to call HUB.KI while the user is offline, these browser‑delivered tokens cannot help — that requires a separate server‑to‑server credential, which isn't part of this flow.