Python SDK · Minter
Use the minter when your application's frontend wants to display events directly. See Embed Tokens for the full model. Construct once at boot, mint per request:
from everscribe import minter
m = minter.Client(project_id, api_key)
The token returned from mint_token is a JWT. Hand it to your frontend via your normal page-render path. Never expose your project API key to the browser.
Set
Cache-Control: no-storeon every token response. This is defense in depth. The embed component POSTs this endpoint, and a POST response is not cached without an explicit opt-in, so forgetting the header is no longer a leak by default. Set it anyway: a handler that also answersGET, or a CDN or proxy configured to cache POSTs, puts a per-user tenant-scoped token back in a shared cache, which is a cross-tenant leak. The handlers below set it. Keep it if you rewrite them.
Single-tenant
Use when your project has one customer per project, or when you're embedding the UI in your own internal tooling. The token has no tenant_id claim, so the embed shows every event in the project.
from everscribe.minter import TokenOptions
@app.post("/everscribe/embed-token")
async def embed_token(request: Request):
user = await authenticate(request)
if user is None:
return PlainTextResponse("unauthorized", status_code=401)
try:
token = m.mint_token(TokenOptions(
# No tenant_id. The project has one customer (or is internal),
# so the token doesn't need to filter by tenant.
expires_in=60 * 60, # 1 hour, in seconds
allowed_columns=["occurred_at", "action", "actor", "target"],
))
# Never let a proxy or CDN cache a per-user token.
return JSONResponse(
{"token": token},
headers={"Cache-Control": "no-store"},
)
except Exception:
return PlainTextResponse("mint failed", status_code=500)
Multi-tenant
Use when your project partitions events by tenant_id and each customer should only see their own. Mint a token whose tenant_id matches the signed-in user's tenant. The server enforces the scope on every read.
@app.post("/everscribe/embed-token")
async def embed_token(request: Request):
user = await authenticate(request)
if user is None:
return PlainTextResponse("unauthorized", status_code=401)
try:
token = m.mint_token(TokenOptions(
# tenant_id set to the signed-in user's tenant, so reads are
# filtered server-side. For an admin view (your internal tool that
# should see every tenant), use the same minter but omit tenant_id.
tenant_id=user.tenant_id,
expires_in=60 * 60,
allowed_columns=["occurred_at", "action", "actor", "target"],
allowed_actions=["user.*", "billing.invoice.created"],
))
# Never let a proxy or CDN cache a per-user token.
return JSONResponse(
{"token": token},
headers={"Cache-Control": "no-store"},
)
except Exception:
return PlainTextResponse("mint failed", status_code=500)
Multi-tenant products often expose two endpoints:
| Endpoint | Token | Used for |
|---|---|---|
/everscribe/embed-token/customer |
tenant_id = signed-in user's tenant |
the end-customer's view of their own audit trail |
/everscribe/embed-token/admin |
tenant_id omitted |
internal admin / support tool, shows every tenant |
Same minter, different scoping rules.
Mounting the route
@app.post(...) registers the route when the module is imported, which means it registers only if something imports the module. Put the handler in a new module nobody imports and the decorator never runs: the embed component's fetch gets a 404 and the whole embed is dead. Nothing fails at startup, which is what makes it worth stating.
If the handler lives in the module that creates app, the examples above are complete and there is nothing more to do. Otherwise put it on an APIRouter and include it:
# routes/everscribe.py
from fastapi import APIRouter, Request
from everscribe import minter
from everscribe.minter import TokenOptions
m = minter.Client(project_id, api_key)
router = APIRouter()
@router.post("/everscribe/embed-token")
async def embed_token(request: Request):
... # exactly the handler body from above
# main.py
from routes.everscribe import router as embed_token_router
app.include_router(embed_token_router) # without this the route does not exist
Flask and Django are the same two steps under different names: a Blueprint plus app.register_blueprint(bp) for Flask, and a view plus an entry in urlpatterns for Django.
Two things about the route itself:
- The path is yours to pick. It only has to match the
tokenEndpointyou hand the embed component./everscribe/embed-tokenis what Everscribe's own tooling assumes, so prefer it unless you have a reason not to. - The verb is
POST. The component POSTstokenEndpointwith no body, so aGET-only route answers 405 and the embed never gets a token. Minting issues a bearer credential and consumes quota, so it is not a safe idempotent read.
Mount it behind the same authentication as the rest of your app. The handler is what decides the caller's scope, so an unauthenticated mint route hands your whole audit trail to anyone who asks for it.
TokenOptions
| Field | Type | Notes |
|---|---|---|
tenant_id |
str |
Optional. Scopes reads to events with matching tenant_id. Trimmed; ≤ 256 chars. Omit for unscoped (single-tenant or admin view). |
expires_in |
float / timedelta |
Token lifetime in seconds (or a timedelta). Rejected client-side outside [MIN_EXPIRES_IN, MAX_EXPIRES_IN], before any HTTP call. Omit or pass 0 for the server default (1h). |
allowed_columns |
list[str] |
Optional whitelist of Event field names. None for no restriction; empty list rejected. |
allowed_actions |
list[str] |
Exact match or suffix wildcard (user.*). None for no restriction; empty list rejected. |
allowed_fields |
list[str] |
Restricts which catalog fields the token's DSL and NLP queries may reference. None for no restriction; empty list rejected. Entries are validated server-side. |
allow_dsl_input |
bool |
Unlocks the Query (advanced DSL) tab in the embed and ?q= on the read API. Default False. |
allow_nlp |
bool |
Unlocks the AI ("Ask in plain English") tab and the NLP endpoint. Default False. |
The SDK exports MIN_EXPIRES_IN, MAX_EXPIRES_IN, and ALLOWED_COLUMNS constants if you want to validate before calling, plus MinterError for inspecting a non-2xx mint response.