HUB.KI DocsCustom apps
HUB.KI Docs
Custom apps

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.

View as Markdown

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()
PurposeProves to your backend who the user is.Calls the HUB.KI REST API as the user.
Verified byYour backend, against HUB.KI's JWKS.HUB.KI, on every request.
token_usecustom_appcustom_app_api
audurn:hub:custom-app:<organizationId>:<appId>urn:hub:public-api
Requiresapi:accessapi:access and an approved REST API scope
RenewalonToken(handler)onApiToken(handler)

Both expire after 5 minutes.

What you get

const { coreUrl, iamUrl, organizationId, token } = await hub.getConnection();
FieldUse
iamUrlBase URL of the identity service — its JWKS verifies the token.
organizationIdThe active organization.
tokenA short‑lived, app‑scoped bearer token (see below).
coreUrlBase 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 Origin is null. The sandbox has no stable origin, so your backend must CORS-allow Origin: null (e.g. reply Access-Control-Allow-Origin: null). Use bearer tokens, not cookies — credentialed requests can't use a null allow-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:

ClaimExpected value
token_usecustom_app
audurn:hub:custom-app:<organizationId>:<appId>
app_idyour app's appId
subthe user's stable id
tenant_idthe organization id
scopespace‑delimited scopes the org approved for your app
email, namepresent 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/aud are 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:

  1. List the REST API scopes it needs in the manifest's apiScopes, next to api:access in scopes.
  2. Have an organization admin approve them.
  3. In the app, read the token with hub.getApiToken() or subscribe with hub.onApiToken(handler), and send it to your backend with each request.
  4. In the backend, send it as Authorization: Bearer <token> to coreUrl from hub.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:

ClaimValue
token_usecustom_app_api
audurn:hub:public-api
app_idyour app's appId
subthe user's stable id
tenant_idthe organization id
scopespace‑delimited REST API scopes approved for your app when it was issued
exp5 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 with 403 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/responses is not available with this token. Run AI through hub.responses in 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 / getToken and onApiToken / getApiToken when you need one.
  • api:access is opt‑in: an organization admin must approve it before getConnection returns 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.