Event Log Embed (Beta)¶
The Event Log Embed lets you drop your workspace's unified Event Log — inbound arrivals, record changes, and outbound webhook deliveries — into your own internal tooling as an <iframe>, without building any of that UI yourself.
You mint a short-lived token on your server, hand it to your frontend, and render an <iframe> pointed at https://embed.centrali.io/v1/event-log. The iframe is hardened: tokens are passed in the URL fragment, which keeps them out of HTTP request lines, server logs, and Referer headers; the iframe also scrubs the fragment immediately after parsing it. The token is scoped to your workspace, so the iframe shows that workspace's full event log.
The embed shows your whole workspace
An embed token grants read access to the entire workspace event log — every inbound arrival, record change, and outbound delivery, across all of your customers. It is an internal / admin observability surface. Do not hand an embed token (or an iframe rendered with one) to an individual end-customer: there is no per-customer filter, so doing so would expose every other customer's events. Embed it only in tooling your own team uses.
Beta
The Event Log Embed is in beta. The token surface, postMessage contract, and capability vocabulary are subject to change before GA. We will call out breaking changes in the migration guides and the changelog.
When to use this
Use the embed when you want your workspace's Event Log inside your own admin console, internal ops dashboard, or support tool — anywhere outside the Centrali console. For debugging from within Centrali, use the console UI's built-in Event Log directly; the embed renders the same workspace-scoped view for embedding elsewhere.
How it works¶
your team member's browser
├─ your internal dashboard (e.g. admin.yourproduct.com)
│ └─ <iframe src="https://embed.centrali.io/v1/event-log#token=ev_…&o=https://admin.yourproduct.com">
│ └─ fetches → api.centrali.io (validated by IAM, scoped to the token's workspace)
│
└─ your backend
└─ mints `ev_…` token via SDK or HTTP → IAM
- Your backend asks Centrali to mint a workspace-scoped embed token.
- Your frontend renders the embed iframe with the token in the URL fragment (not the query string — fragments don't leak into HTTP logs).
- The iframe scrubs the token from
location.hrefimmediately, then queries Centrali's API with it. Every query is confined to the token's workspace by IAM. - The iframe talks back to your page via
postMessage(auto-resize, "please mint me a fresh token", "replay completed").
Prerequisites¶
You need a service account in your Centrali workspace with these capabilities:
event-lineage:issue-embed-token— required to mint any embed token.event-lineage:list+event-lineage:retrieve— required because every minted token implicitly grantsevents:read.webhook-subscriptions:replay— only required if you want to grant the iframe theevents:replaycapability (so an operator can click "Replay" on a failed delivery).
The SDK uses your existing service-account clientId + clientSecret to mint a separate, IAM-audienced token under the hood. Publishable keys and dev tokens are rejected by IAM for this endpoint — embed minting must come from a confidential credential.
See Service Accounts for how to provision one.
Step 1 — Mint a token on your server¶
import { CentraliSDK } from '@centrali-io/centrali-sdk';
const centrali = new CentraliSDK({
baseUrl: 'https://centrali.io',
workspaceId: 'your-workspace-slug',
clientId: process.env.CENTRALI_CLIENT_ID!,
clientSecret: process.env.CENTRALI_CLIENT_SECRET!,
});
// In your "give me an embed URL" route:
const { token, sessionId, expiresAt } = await centrali.embed.eventLog.issueToken({
capabilities: ['events:read'], // omit for read-only; add 'events:replay' to enable replay
ttlSeconds: 600, // optional, defaults to 600s, max 3600s
});
return { token, sessionId, expiresAt };
Both fields are optional — issueToken() with no arguments mints a read-only token with the default TTL.
First exchange your service-account clientId + clientSecret for a workspace JWT audienced for IAM:
curl -X POST https://auth.centrali.io/token \
-H 'Content-Type: application/x-www-form-urlencoded' \
-d "grant_type=client_credentials" \
-d "client_id=$CENTRALI_CLIENT_ID" \
-d "client_secret=$CENTRALI_CLIENT_SECRET" \
-d "resource=https://auth.centrali.io"
Warning
The resource parameter must be the auth URL, not the API URL. The embed endpoint rejects data-audienced tokens with audience_mismatch.
Then mint the embed token. The body is optional — every field defaults — so a bodyless POST mints a read-only token with the default TTL:
curl -X POST https://auth.centrali.io/workspace/your-workspace-slug/embed/event-log/issue-token \
-H "Authorization: Bearer $WORKSPACE_JWT" \
-H 'Content-Type: application/json' \
-d '{
"capabilities": ["events:read"],
"ttlSeconds": 600
}'
Response:
Note
tenantId is not accepted. The embed is workspace-scoped; a request including tenantId is rejected with a 400. (Earlier betas were tenant-scoped — if you have old minting code, drop the field.)
Tip
Never ship clientSecret to a browser. Mint tokens in a server route your frontend calls — the response is what's safe to give your frontend.
Step 2 — Render the iframe¶
Drop the token into the URL fragment with your parent origin as o:
<iframe
src="https://embed.centrali.io/v1/event-log#token=ev_…&o=https://admin.yourproduct.com&theme=auto&accent=%23E45A2B"
style="width:100%; border:none;"
title="Event log"
></iframe>
| Fragment key | Required | Description |
|---|---|---|
token | Yes | The ev_… token returned by Step 1. The iframe scrubs it from location.href once it's parsed. |
o | Yes | Your parent origin (scheme + host + port). The iframe validates postMessage source against this and will not respond to messages from any other origin. |
theme | No | auto (default), light, or dark. auto follows the user's prefers-color-scheme. |
accent | No | URL-encoded hex color (#RGB or #RRGGBB) used for primary buttons and focus rings. Invalid colors fall back to Centrali's default accent. |
Step 3 — Listen for postMessage events¶
The iframe sends a small set of structured messages back to the parent. Always validate event.origin before reacting.
const EMBED_ORIGIN = 'https://embed.centrali.io';
const iframe = document.querySelector<HTMLIFrameElement>('iframe.event-log')!;
window.addEventListener('message', async (event) => {
if (event.origin !== EMBED_ORIGIN) return;
switch (event.data?.type) {
case 'cev:ready':
// First paint complete.
break;
case 'cev:resize':
// Auto-grow: the iframe is telling you how tall its body is.
iframe.style.height = `${event.data.height}px`;
break;
case 'cev:auth-stale': {
// The current token expired or was revoked. Mint a fresh one on your
// server, then post it back. The iframe swaps tokens without reloading.
const fresh = await fetch('/api/embed/event-log/token', { method: 'POST' });
const { token } = await fresh.json();
iframe.contentWindow?.postMessage({ type: 'cev:token', token }, EMBED_ORIGIN);
break;
}
case 'cev:replay-completed':
// An operator clicked "Replay" and the new delivery was queued.
// event.data.deliveryId / event.data.replayedFrom are available.
toast.success('Delivery replayed.');
break;
case 'cev:error':
// Anything else worth surfacing (event.data.code, event.data.message).
console.warn('Event log embed error:', event.data);
break;
}
});
| Message | Direction | When it fires |
|---|---|---|
cev:ready | iframe → parent | After first paint completes. |
cev:resize | iframe → parent | Whenever the iframe's body changes height. |
cev:auth-stale | iframe → parent | The token's exp has fired, or the session was revoked. |
cev:replay-completed | iframe → parent | A "Replay" action returned 202 from the data plane. |
cev:error | iframe → parent | Generic error surface for anything else (e.g. network failure). |
cev:token | parent → iframe | Hand the iframe a fresh token, typically in response to cev:auth-stale. Payload: { token: string }. |
cev:theme | parent → iframe | Update the iframe's theme and/or accent without reloading. Payload: { theme?: 'auto'\|'light'\|'dark'; accent?: '#RRGGBB' \| null }. Useful if your dashboard has a runtime theme toggle. |
cev:refresh | parent → iframe | Ask the iframe to refetch the event-log list, typically after your app observes a new delivery. |
cev:destroy | parent → iframe | Tell the iframe the host is unmounting it so in-flight requests can be cancelled during teardown. |
Step 4 (optional) — Revoke on logout¶
If you mint embed tokens per-session, you can revoke every token sharing a sessionId when an operator signs out. The token can still be exchanged for one full denylist TTL (max 60 minutes) of dead-on-arrival, but the data plane rejects all of them.
If you don't supply your own sessionId at mint time, the server generates one and returns it in the response — store it alongside whatever you persist for the session.
Capability reference¶
| Capability | What it grants |
|---|---|
events:read | List + retrieve the workspace's event log rows. Always granted; cannot be opted out of. |
events:replay | Allow the iframe's "Replay" action on a failed delivery. The issuing service account must also hold webhook-subscriptions:replay. |
If you omit capabilities entirely, the iframe gets ['events:read'] only — no replay button.
Token lifecycle¶
- TTL — default 600 seconds (10 minutes), capped at 3600 (1 hour). Tune via
ttlSeconds. - Rate limits — both issuance and revoke are bucketed per service account. Steady minting is fine; minting in a tight loop will surface 429s.
- Audience — the embed JWT carries
aud=embed.centrali.io. Centrali's data plane is the only consumer; you should never need to decode or inspect it.
What the iframe can't do¶
- It cannot read any data outside the token's workspace.
- It cannot mint new tokens or escalate capabilities.
- It cannot navigate the parent page (it's iframed cross-origin).
- It cannot persist user state across token rotations — each token rotation drops the prior token's cached query data in the same render commit, so swapping tokens never paints stale rows.
Troubleshooting¶
audience_mismatch when minting: your workspace JWT was minted with resource=<data URL>. The embed endpoint requires resource=<IAM URL> (https://auth.centrali.io). The Node SDK handles this automatically; if you're using raw HTTP, see Step 1.
tenantId is no longer accepted (400): you're sending a tenantId in the issue-token body from an older integration. The embed is workspace-scoped now — remove the field.
iframe shows "Authentication required": the token's exp has fired, the token's sessionId has been revoked, or the parent never posted a fresh token after cev:auth-stale. Confirm your cev:auth-stale handler is posting cev:token to the iframe with the correct origin.
iframe is the wrong height: confirm you're handling cev:resize and applying event.data.height to the iframe's style.height. The iframe does not size itself.
Replay button doesn't appear: the token was minted without events:replay capability, or the issuing service account lacks webhook-subscriptions:replay. Both are required.
Related¶
- Outbound Webhooks — what's being logged
- Webhook Ingestion — the other end of the pipe
- Service Accounts — how to provision the credential that mints embed tokens