Mint an analytics embed token
curl --request POST \
--url https://platform.kodexa.ai/api/analytics/embed-token \
--header 'x-api-key: <api-key>'import requests
url = "https://platform.kodexa.ai/api/analytics/embed-token"
headers = {"x-api-key": "<api-key>"}
response = requests.post(url, headers=headers)
print(response.text)const options = {method: 'POST', headers: {'x-api-key': '<api-key>'}};
fetch('https://platform.kodexa.ai/api/analytics/embed-token', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://platform.kodexa.ai/api/analytics/embed-token",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_HTTPHEADER => [
"x-api-key: <api-key>"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"net/http"
"io"
)
func main() {
url := "https://platform.kodexa.ai/api/analytics/embed-token"
req, _ := http.NewRequest("POST", url, nil)
req.Header.Add("x-api-key", "<api-key>")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://platform.kodexa.ai/api/analytics/embed-token")
.header("x-api-key", "<api-key>")
.asString();require 'uri'
require 'net/http'
url = URI("https://platform.kodexa.ai/api/analytics/embed-token")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["x-api-key"] = '<api-key>'
response = http.request(request)
puts response.read_bodyAnalytics
Mint an analytics embed token
Exchanges the caller’s kodexa credential for a short-lived, per-tenant-scoped RS256 JWT for the standalone analytics-server query API. The org scope is derived server-side from the caller’s team/org assignments.
POST
/
api
/
analytics
/
embed-token
Mint an analytics embed token
curl --request POST \
--url https://platform.kodexa.ai/api/analytics/embed-token \
--header 'x-api-key: <api-key>'import requests
url = "https://platform.kodexa.ai/api/analytics/embed-token"
headers = {"x-api-key": "<api-key>"}
response = requests.post(url, headers=headers)
print(response.text)const options = {method: 'POST', headers: {'x-api-key': '<api-key>'}};
fetch('https://platform.kodexa.ai/api/analytics/embed-token', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://platform.kodexa.ai/api/analytics/embed-token",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_HTTPHEADER => [
"x-api-key: <api-key>"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"net/http"
"io"
)
func main() {
url := "https://platform.kodexa.ai/api/analytics/embed-token"
req, _ := http.NewRequest("POST", url, nil)
req.Header.Add("x-api-key", "<api-key>")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://platform.kodexa.ai/api/analytics/embed-token")
.header("x-api-key", "<api-key>")
.asString();require 'uri'
require 'net/http'
url = URI("https://platform.kodexa.ai/api/analytics/embed-token")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["x-api-key"] = '<api-key>'
response = http.request(request)
puts response.read_bodyExchange 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:
The RS256 signing private key is not part of this config — it is supplied at runtime via the
{
"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 istokenTtlSeconds, 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 theanalyticsEmbed block in the kodexa-api config. Everything in this block is non-secret:
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
ANALYTICS_EMBED_SIGNING_KEY_PEM environment variable. The keyId above is emitted as the token’s kid header so verifiers can rotate keys.
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.Authorizations
x-api-keybearerAuth
API key for authentication. Create one from the Kodexa platform UI under Settings > Access Tokens.
Response
Embed token minted (token, expiresAt, models, orgs).
