> ## Documentation Index
> Fetch the complete documentation index at: https://developer.kodexa.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Mint an analytics embed token

Exchange the caller's own Kodexa credential (typically a Profile → API token) for a short-lived, tenant-scoped RS256 JWT that the standalone analytics-server accepts on its query API. Use this to embed dashboards or run analytics queries on a user's behalf without ever handing out a long-lived analytics credential.

A successful response returns:

```json theme={null}
{
  "token": "eyJhbGciOiJSUzI1Ni...",
  "expiresAt": "2026-07-24T14:15:00Z",
  "models": ["invoices", "documents"],
  "orgs": ["acme-corp"]
}
```

* **`token`** — the signed embed JWT. Present it to analytics-server as a bearer credential.
* **`expiresAt`** — RFC 3339 expiry. Token lifetime is `tokenTtlSeconds`, defaulting to and **hard-capped at 15 minutes** (900s) — the analytics-server ceiling. Mint a fresh token when it expires; do not cache it.
* **`models`** — the closed model allowlist baked into the token.
* **`orgs`** — the organization slug(s) the token is scoped to.

## Organization scope is server-derived

The caller can **never** request an organization scope. Scope is derived server-side from the authenticated user's team/org assignments, and every granted dataset is row-filtered to exactly those orgs. The minter attaches a per-object row filter — `{ field: "org_slug", op: "in", value: [<caller's slugs>] }` — to every object in the token's allowlist, so an object granted without a filter cannot leak rows. A caller who belongs to no organization is rejected.

The `models` and `objects` allowlists are **closed**: analytics-server denies anything not explicitly listed, so an object added to the warehouse but omitted from the config fails closed (denied), never exposed.

## Enabling the minter

The endpoint is opt-in through the `analyticsEmbed` block in the kodexa-api config. Everything in this block is non-secret:

```yaml theme={null}
analyticsEmbed:
  enabled: true
  issuer: kodexa-api                 # JWT `iss`; must match an analytics-server auth.embed.issuers entry
  audience: analytics-server         # JWT `aud`
  keyId: embed-2026-07               # JWT `kid` header, for verifier key rotation
  tokenTtlSeconds: 900               # token lifetime; defaults to and capped at 900 (15m)
  models:                            # closed model allowlist baked into every token
    - invoices
    - documents
  objects:                           # closed object allowlist; each gets a row filter
    - document_facts
    - invoice_facts
  rowFilterField: org_slug           # tenant column on every object; defaults to org_slug
```

The RS256 **signing private key is not part of this config** — it is supplied at runtime via the `ANALYTICS_EMBED_SIGNING_KEY_PEM` environment variable. The `keyId` above is emitted as the token's `kid` header so verifiers can rotate keys.

<Note>
  The endpoint returns **503 Service Unavailable** when the minter is disabled (`enabled: false`) or when it is enabled but `ANALYTICS_EMBED_SIGNING_KEY_PEM` is empty — in that case the signer is never wired up and the minter stays disabled. Set both to bring the endpoint online.
</Note>
