Quickstart
Protect one action in about five minutes: install the SDK on a signed-in page, send its session token with the protected request, and verify that token on your backend before the work runs.
Your progress
0 of 5 done
- Install the browser SDK
- Attach identity from your own authentication
- Send the session token with the protected request
- Verify on your backend, then decide
- Check it
Ticks are saved in this browser only. The dashboard’s Settings › Channels tab shows when the first event arrives.
Not a developer? You only need the tag: paste it from Settings › Channels into your site's head (or use the WordPress, Shopify, Webflow or Google Tag Manager guide), then open Your agent report. The steps below are for the developer who protects an action.
What you need
- A Site in the dashboard (Settings › Channels), registered for your exact application origin, for example
https://app.example.com. - The Site's publishable key (
ao_pk_...) for the browser and its server key (ao_sk_...) for your backend. The server key is shown once; keep it in server-only configuration. - One authenticated backend route that performs the protected action.
- The address of your Telltale server. The examples on this page already use the one that serves this page,
https://app.trytelltale.dev. HTTP localhost works for development only.
See it working on localhost
Add your development address (for example http://localhost:3000) as its own Site in the dashboard and use that Site's keys while you build, so test traffic never mixes with your production numbers. Do the same for staging.
1. Install the browser SDK
Pick one way to install per page. Settings › Channels shows when the first activity arrives for each path.
- Hosted tag: one script element (below). Step-by-step pages for your platform: Next.js, React (Vite or Create React App), WordPress, Shopify and Webflow.
- Google Tag Manager: see the Google Tag Manager guide (the template, or a Custom HTML tag).
- Cloudflare Zaraz: see Install with Cloudflare.
- Code: load
/sdk/v1.jsand callinit()yourself (Browser SDK guide).
The hosted tag starts recording on page load. If your site shows a cookie or consent banner, hold it until the banner decides (below). Settings › Channels asks whether your site shows a consent banner and writes the tag for your answer.
<script src="https://app.trytelltale.dev/sdk/v1.js" data-telltale-key="YOUR_SITE_PUBLISHABLE_KEY"></script>/sdk/v1.js is always the current v1 build (cached for five minutes). To pin an exact build with Subresource Integrity, use the immutable URL and integrity value. For the build this Telltale server serves now (the Content Security Policy guide explains both options):
<script src="https://app.trytelltale.dev/sdk/v1.8cefa3306b522996.js" integrity="sha384-CkbHPkBjjOqi54cng1gnrGPJKK1pEdkMAN7GZ9mznQ9vpJNrRoUD3dPHG0YGh2ei" crossorigin="anonymous" data-telltale-key="YOUR_SITE_PUBLISHABLE_KEY"></script>This tag does not discover agent tools. Discovery is opt-in per Site; see the Agent tools (WebMCP) guide.
Does your site show a cookie or consent banner?
No. Keep the tag as it is: it records from the first page view. EU and UK sites usually need a consent banner for this kind of measurement; if yours has none, check with whoever owns your privacy notice before you rely on the default.
Yes. Add data-telltale-wait-for-consent to the tag, so it waits and sends nothing until the banner allows measurement:
<script src="https://app.trytelltale.dev/sdk/v1.js" data-telltale-key="YOUR_SITE_PUBLISHABLE_KEY" data-telltale-wait-for-consent></script>Then apply the banner's current decision from a module script placed after the tag. Module scripts run after the classic tag has initialized the client, and they allow top-level await. optIn() starts collecting and optOut() stops it; call them whenever the decision changes. For a consent manager, use the Consent bridge guide and the OneTrust, Cookiebot or Sourcepoint recipe.
<script type="module" src="/telltale-start.js"></script>// /telltale-start.js (a module served from your own origin)
const client = window.Telltale.getClient();
// Call optIn() only when your banner permits collection, and optOut() when the visitor withdraws.
// optIn() resolves even while the Telltale server is unreachable; the SDK keeps starting in the background.
await client.optIn();Do not remove data-telltale-wait-for-consent to skip a consent manager you have.
2. Attach identity from your own authentication
Identity has two parts. The server part is the one your backend can trust: after sign-in, your backend associates the session with the user and account it authenticated. The browser part is only a label.
From your server (recommended). Send the session token (step 3 shows how to read it) to an authenticated route once after sign-in, and associate it there with the server adapter from step 4:
// POST /api/telltale/identify: your route, behind your own authentication
export async function identifySession(request, user) {
const { sessionToken } = await request.json();
if (typeof sessionToken !== 'string' || !sessionToken) return new Response(null, { status: 204 }); // paused until consent, or unreachable: nothing to associate
await telltale.identify({ sessionToken, userId: user.id, accountId: user.accountId });
return new Response(null, { status: 204 });
}Settings › Channels › Install health shows whether server identify is in use and how many sessions have a server-confirmed identity. verify() returns that identity, so your backend can compare it with the user it authenticated (Server API).
In the browser (optional label). In the same module, once your own authentication knows who is signed in:
try {
await client.identify({ userId: 'usr_123', accountId: 'acct_456' });
} catch (error) {
// Paused until consent (NotAllowedError) or an unreachable Telltale server (code network_unavailable): the label is skipped.
}Use stable internal IDs. Never send names, e-mail addresses you are not permitted to share, tokens or secrets. Browser identity labels telemetry only; it is not authentication. Call await client.reset() on logout or account switch.
3. Send the session token with the protected request
Still in a module (or any async function), read a fresh token just before the protected request:
let sessionToken = null;
try {
sessionToken = await client.getSessionToken({ timeoutMs: 3000 });
} catch (error) {
// NotAllowedError: collection is paused until consent, or withdrawn.
// error.code === 'network_unavailable': the Telltale server is unreachable or slow (client.status() says which).
console.warn('No Telltale token', error.name, error.code);
}
const response = await fetch('/api/export', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ sessionToken, reportId: 'rep_42' })
});getSessionToken() rejects with NotAllowedError while collection is paused until consent, or withdrawn, and with code network_unavailable when the Telltale server cannot be reached in time. Decide what the protected action does without a token (your backend sees a missing token); do not treat it as human. Keep these calls inside try/catch so an Telltale server outage never stops the rest of your page script.
4. Verify on your backend, then decide
Download the server adapter and its companion into your backend. server.js imports ./identifiers.js, so both files go in the same folder. The Server adapters guide lists each file's SHA-256 integrity to check.
mkdir -p lib/telltale && cd lib/telltale
curl -fsSO https://app.trytelltale.dev/adapters/v1/server.js
curl -fsSO https://app.trytelltale.dev/adapters/v1/identifiers.js
printf '{ "type": "module" }\n' > package.jsonThe adapters are ES modules. The package.json line marks that folder as ES modules, so they load in a CommonJS project too (what npm init writes): import them from an ES module (a .mjs file, or any file in a "type": "module" project), or require() them on Node 22.12 or later.
Authenticate your user and validate input first, then verify with the adapter. The adapter applies a five-second deadline, validates the response, and retries only after a valid 503 (or once after a 408 or 429 whose Retry-After fits the deadline).
import { createTelltaleServer } from './lib/telltale/server.js';
const telltale = createTelltaleServer({
endpoint: 'https://app.trytelltale.dev',
serverKey: process.env.TELLTALE_SERVER_KEY
});
export async function exportReport(request, user) {
const { sessionToken, reportId } = await request.json();
// No token (paused until consent, withdrawn, or the Telltale server was unreachable): there is nothing to verify, and verify()
// would throw a TypeError. Decide yourself; a missing token is not a human verdict.
if (typeof sessionToken !== 'string' || !sessionToken) return holdForReview(user, reportId, null);
// targetId binds the check (and any approval of it) to this report; redeemApproval below must name the same one.
const evidence = await telltale.verify({ sessionToken, actionId: 'export_report', targetId: reportId });
if (evidence.decision === 'allow') return runExport(user, reportId);
if (evidence.decision === 'review') return holdForReview(user, reportId, evidence);
return Response.json({ status: 'denied', verdict: evidence.verdict }, { status: 403 });
}decision (allow, review or deny) comes from the rule you set for export_report in Control › Rules. Your own authorization still decides whether the user may do the work.
Verdicts are human, agent or unknown. Scores are categories, not probabilities, and unknown is never human. A verification failure is not a verdict: never run or replay the protected action because verification failed.
Verdicts and authorization explains what each verdict means. Server API and recovery lists every response field.
Held actions approved in the dashboard
When your rule for export_report says Hold for review for agents, verify() answers review and the request waits in Control › Approvals › Waiting. Approving there lets this one request through once, within 15 minutes, if your app asks again. Your app still checks permissions.
When the agent (or your retry) asks again, read the approval and redeem it on your backend before doing the work:
const { approvalToken } = await telltale.getDecision(held.decisionId);
if (!approvalToken) return Response.json({ status: 'waiting' }, { status: 202 }); // not approved yet, rejected or expired
await telltale.redeemApproval(approvalToken, { sessionToken, actionId: 'export_report', targetId: reportId });
// Your own authorization still decides: the user still has the role, the report still exists.
if (!(await canExport(user, reportId))) return Response.json({ status: 'denied' }, { status: 403 });
await runExport(user, reportId);
await telltale.reportOutcome(held.decisionId, { outcome: 'performed' });An approval is one-time and bound to the session, the action and the targetId. A second redemption fails with approval_used, except an identical one (same session, action and targetId) within 60 seconds, which answers the original success so a retry after a dropped connection is safe; and after 15 minutes with approval_expired (Errors and retries). Server API and recovery covers approvals, outcomes and the review queue.
5. Check it
- Open Settings › Channels and leave the Is it working? check open, then open your site in another tab (Open my site to test it). The check moves through Script loaded on your address, First session received and, once identity is set up, Signed-in user linked. Initialization alone does not count as a session received. Each install page has a Check it worked section for its platform: Next.js, React, WordPress, Shopify, Webflow, Google Tag Manager.
- Stuck on a step? It's not working goes through each symptom: nothing arrives, the address is not on the allowed list, a wrong key, a tag waiting for consent, a Content Security Policy block, and sessions without a signed-in user.
- Change the rule for
export_reportin Control › Rules to see allow, review and deny. Also test an expired token (session_reinit_required; see Errors and retries) and a page that is paused until consent.
Your API and MCP server
Agents that call your API directly, or work through your MCP server, never load your pages. Add Telltale for APIs (one middleware for Node http, Express, Next.js, Hono or Fastify) or Telltale for MCP servers to see them per account too. Both send shapes only, never content.
Next
- Verdicts and authorization: what human, agent and unknown mean, and why a verdict never authorizes an action.
- Errors and retries: every error code, what it means and what to do.
- Authentication and keys: key rotation and the Better Auth adapter.
- Server API and recovery: identify, metering and the compatibility API (/v1).
- Verify evidence before a protected action: the API reference page for
POST /api/v1/verify, with a request you can try.