Quickstart

View as Markdown

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

  1. Install the browser SDK
  2. Attach identity from your own authentication
  3. Send the session token with the protected request
  4. Verify on your backend, then decide
  5. 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

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.

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.

HTML
<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):

HTML
<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.

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:

HTML
<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.

HTML
<script type="module" src="/telltale-start.js"></script>
JavaScript
// /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:

JavaScript
// 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:

JavaScript
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:

JavaScript
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.

Shell
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.json

The 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).

JavaScript
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.

Important

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:

JavaScript
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

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