Telltale for APIs

View as Markdown

Some agents skip your pages and your MCP server and call your API directly. Telltale for APIs records those calls, per account and per route, beside the agents in your pages and the agents using your MCP server: one record of AI agent activity across every account and every channel.

It's one middleware in the server you already run: Node's http server, Express, Next.js route handlers, Hono or Fastify. Your routes, handlers and responses stay as they are. Telltale never receives a request's content.

What you get

Install

Node 18+ or any runtime with fetch and Web Crypto. The middleware has no dependencies.

  1. Download the adapter, check its SHA-256 against the adapters table, and put it beside your server code. It is an ES module: in a project whose package.json says "type": "commonjs" (what npm init writes), save the two files as telltale-api.mjs and telltale-api.d.mts and import ./telltale-api.mjs:

    Shell
    curl -sSo telltale-api.js https://app.trytelltale.dev/adapters/v1/api.js
    curl -sSo telltale-api.d.ts https://app.trytelltale.dev/adapters/v1/api.d.ts
  2. Create the client once per process, with the Site's server key (Settings › Channels › Server keys). The key stays on your server:

    JavaScript
    import { createTelltaleApi } from './telltale-api.js';
    
    export const telltale = createTelltaleApi({
      endpoint: 'https://app.trytelltale.dev',
      serverKey: process.env.TELLTALE_SERVER_KEY,
      identify: (req) => ({ accountId: req.user?.accountId, userId: req.user?.id }),   // your own opaque IDs
    });
    
    process.on('SIGTERM', async () => { await telltale.close(); process.exit(0); });
  3. Add it to your framework (below), then make one request to your API. Settings › Channels › API › Is it working? turns to "Working" with the time of the last batch, usually within five seconds.

Node http

JavaScript
import http from 'node:http';
http.createServer(telltale.node((req, res) => { /* your handler */ })).listen(3000);

The Node server has no route templates of its own, so the built-in normaliser (below) or your route function names the route.

Express (4 and 5)

JavaScript
app.use(telltale.express());                                          // early: records every request
app.post('/v1/invoices', requireAuth, telltale.protect(), createInvoice);   // a protected route, after your auth

The route template is read from Express (req.baseUrl + req.route.path, for example /v1/invoices/:id) when the response finishes.

Next.js route handlers (App Router)

JavaScript
// app/api/invoices/[id]/route.js
import { telltale } from '@/lib/telltale';
export const GET = telltale.next(async (request, context) => Response.json(await loadInvoice(context.params)), { route: '/api/invoices/[id]' });

Pass the file's route as route; a route handler doesn't know its own template. On Vercel, set trustProxy: 1 so the client address comes from X-Forwarded-For. In a serverless function, call await telltale.flush() before it returns, or hand it to the platform (waitUntil(telltale.flush())).

Hono

JavaScript
app.use(telltale.hono());
app.post('/v1/invoices', requireAuth, telltale.honoProtect(), createInvoice);

On Cloudflare Workers, flush with c.executionCtx.waitUntil(telltale.flush()).

Fastify (4 and 5)

JavaScript
await fastify.register(telltale.fastify);

The plugin applies to the whole instance. The route template is Fastify's (request.routeOptions.url). Protected routes are checked in a preHandler hook.

Route templates

Telltale stores the route template, never the path a request used and never a query string.

  1. Your own route: req => '/v1/invoices/:id' function wins, when you give one.
  2. Otherwise the framework's route (Express, Fastify, Hono, or route for Next.js), cleaned to the characters Telltale accepts (regular-expression groups such as :id(\\d+) are removed).
  3. Otherwise the built-in normaliser: the query and fragment are dropped, and a segment is kept only when it reads like a word (a letter first, at most two digits, not a hex run, not a prefixed ID). Numbers, UUIDs, ULIDs, hashes, prefixed IDs such as inv_0 or cus_NffrFeUfNV2Hib, email addresses, slugs with digits and encoded text become :id. So /v1/invoices/12345?secret=x and /v1/invoices/inv_0 are both recorded as /v1/invoices/:id. At most 16 segments.

    Changed in @telltale/api 1.0.2: prefixed IDs (a short word, an underscore, then a value with a digit or mixed case) and UUIDs written with few digits now become :id; 1.0.1 kept them, so a route such as /v1/invoices/inv_0 was recorded once per ID. Every other route is recorded exactly as before. Routes already recorded keep their old name; new calls from 1.0.2 are recorded under the template.

On Node's own http server, pass route. Plain node:http has no router, so without route every path goes through the normaliser, and a short ID that reads like a word stays as it is: /v1/reports/r1 is recorded as /v1/reports/r1, not /v1/reports/:id. Such a route then never matches a rule you saved for GET /v1/reports/:id (the call is not checked) and each ID shows as its own route. Give telltale.node(handler) a route function (route: req => matchMyRoutes(req.url)) that returns your template. The dashboard warns in Tools › All tools when a Node route looks raw.

Each process keeps at most maxRoutes distinct templates (default 300); a new one after that is recorded as /:other. The server refuses a route that still looks like it carries a value (three or more digits in a row, a UUID, a long hex run, a long token with digits) and reports it in Is it working?.

Identity

identify(req) returns { accountId, userId } from your own authentication. Send opaque IDs only, never an email address or a name.

Privacy: what is and isn't sent

Sent to POST /api/v1/api-calls (server key) in batches. The batch:

Privacy: what is and isn't sent
FieldWhat it is
batchIdrandom, 8 to 128 letters, digits, _ or -; kept across retries so a resent batch is counted once
sdk{name, version} of the middleware, printable ASCII, at most 64 characters each
frameworknode, express, next, hono, fastify or other
droppedwhole number of calls the middleware dropped because its queue was full
calls1 to 200 calls

Each call:

Privacy: what is and isn't sent, table 2
FieldWhat it is
eventIdrandom per call, 8 to 128 letters, digits, _ or -
methodGET, HEAD, POST, PUT, PATCH, DELETE or OPTIONS
routethe route template: a leading /, then at most 255 letters, digits or _ . ~ : { } [ ] * + / -; no query, fragment, %, =, @ or spaces
statusthe HTTP status code you answered (100 to 599)
startedAt, endedAtmilliseconds; within the last 7 days, at most a day long
sizethe response size bucket: empty, under_1kb, 1kb_10kb, 10kb_100kb, 100kb_1mb, over_1mb or unknown
responseBytes, responseTokensmiddleware 1.1.0 and later, when the size is known (Content-Length or the bytes written): the response's byte count and an estimated token count (bytes / 4, rounded up), counted by the middleware. Used for Oversized responses in Issues. Two numbers; the body is never read for this
accountId, userIdwhat your identify returned, at most 512 characters each
sampleRateonly on a sampled call: the share kept (above 0, at most 1)
decisionId, refusedBythe rule decision behind a protected call; deny, hold or unreachable when the middleware refused it
evidenceagent evidence material, used once and never stored (below)
capturedonly with Deep insights on (owner-enabled, off by default) and a call with agent evidence: the body and query fields you listed for that route

Telltale refuses any other field (per call, reported back as rejected; the rest of the batch is kept).

Never sent (unless you turn on Deep insights and list body or query fields, and even then never headers or credentials): request or response bodies, query values, the raw path, other headers, cookies, Authorization values, your own tokens and session IDs, and the user-agent string. The middleware sends no diagnostics about itself anywhere and never writes to stdout; debug: true (or TELLTALE_DEBUG=1) writes shapes-only lines to stderr or to your own function (debug mode).

Agent evidence material: used once, never stored

To tell whether an agent made a call, the middleware forwards a little more, inside evidence. Telltale uses it while it records the batch and keeps only the result (the agent's name, how we know, the confidence and a network label such as "cloud provider"):

Agent evidence material: used once, never stored
FieldWhat it is
ipthe client address (the socket, or X-Forwarded-For with trustProxy, or your clientIp function)
securetrue when the request arrived over HTTPS
signatureonly when the request carries a Web Bot Auth signature (tag="web-bot-auth") that covers nothing but @authority, @method, @path, @scheme and signature-agent: {authority, path?, headers: {signature, signature-input, signature-agent}}. The path is sent only when the signature covers it, and never the query
declared{id, via}: the ID of a declared agent name the middleware matched in the user agent or a header you listed in declaredHeaders, such as chatgpt-user. Never the header value itself
agentTokenthe SHA-256 hash of a Telltale agent token the request carried (by default the Telltale-Agent-Token header, or Authorization: Bearer tt_agent_…). Never the token

The address, the signature headers and the hash are never stored, logged or returned, as with Telltale's own traffic.

Data stays with you

To keep the full requests for your own debugging, pass them to your own logs or OpenTelemetry. Telltale gets none of it:

JavaScript
createTelltaleApi({ /* … */, dataStaysWithYou: (call) => logger.info({ route: call.route, status: call.status, ms: call.durationMs }) });

Deep insights (opt in, owner-enabled)

Off by default. With deepInsights: {routes: {'POST /v1/invoices': {body: ['amount'], query: ['expand']}}} and the site owner's switch on, the middleware sends the fields you list for calls that showed agent evidence; Telltale stores them only for AI agent calls, for 7 to 30 days. Headers, cookies, Authorization, tokens and other credentials are never sent or stored, whatever you list. See Deep insights.

Agents known only by their network address are not captured. "Agent evidence" here means a signal the middleware can see on the request itself: a Web Bot Auth signature, a declared agent name in the user agent or a header, or a Telltale agent token. An agent that Telltale recognises only by its address (an AI provider's published egress range, "Published IP range") is identified on Telltale's side after the call has been sent, so the middleware never had a reason to send that call's fields, and none are captured for it. This is deliberate: the middleware cannot tell such a call from a person's, and people's request bodies stay on your server. The call itself is still recorded, counted and shown as an AI agent call.

How Telltale knows it was an agent

Strongest first. The first that applies names the call:

How Telltale knows it was an agent
EvidenceShown asConfidence
A valid agent token your backend issuedVerified by tokenHigh
A valid Web Bot Auth signature from a reviewed operatorVerifiedHigh
A declared agent name whose operator publishes an IP list, and the address is in itVerified (published IP range)High
The address is in an AI agent provider's published egress list (OpenAI, Anthropic, Perplexity, Google)Published IP rangeHigh
A declared agent name we could not checkDeclaredMedium
None of theseUnknown

Verified (published IP range) means the call named its agent and its address is in that agent's own published list; Published IP range means only the address matched an AI provider's list, with no name to check.

Sampling high-volume APIs

JavaScript
createTelltaleApi({ /* … */, sampleRate: 0.1 });

Sampling applies only to calls with no agent signal the middleware can see (no signature, no declared name, no agent token) and no rule check. Those calls are kept at the rate you set and carry it, so the route counts in the dashboard are estimates. Every call with a visible signal, and every protected call, is always sent. A sampled call that Telltale later names by its network is recorded but never chargeable, because its count is an estimate. Leave out health checks and static files with skip: req => ….

Rules for protected routes

Name the routes that should ask for your rule (Control › Rules) before your handler runs:

JavaScript
createTelltaleApi({
  /* … */,
  protect: {
    routes: [
      'POST /v1/invoices',                                                        // a write: fails closed when Telltale can't be reached
      { route: 'GET /v1/reports/:id', whenUnreachable: 'closed' },
      { route: 'DELETE /v1/invoices/:id', actionId: 'delete_invoice' },           // your own rule name
    ],
    // whenUnreachable: 'open' | 'closed' | { read, write }, decideTimeoutMs: 1500
  },
  onHold: async ({ decisionId, method, route, accountId, userId, req }) => {
    const ok = await askThePersonInYourApp(userId, route);   // your own step-up
    return ok ? { result: 'passed', method: 'own_approval' } : 'failed';
  },
});

The decide and step-up endpoints

The middleware calls these for you; any language can call them with the server key:

Billing

An API call is chargeable when all of these hold: your server (or an agent token) named the account, the evidence is high confidence, the call ran (not refused) and it was not sampled. Every other agent call is counted and shown, never chargeable. Telltale's own plans count identified users a month, once across channels, never calls (Pricing).

Batching and outages

Calls are queued in memory and sent in batches of up to 200 every five seconds. Network errors, 408, 429 and 5xx answers are retried with growing waits (up to a minute, honouring Retry-After), with the same batchId, so a batch whose answer was lost is never counted twice. Any other refusal drops the batch, logged once with debug: true. The queue holds 20,000 calls; past that the oldest are dropped and counted ("dropped" in Is it working?). A Telltale outage never slows or fails a request. Only protected routes wait, and only up to their timeout.

Call close() on shutdown and flush() at the end of a serverless invocation; calls still queued when a process is killed are lost (nothing is kept on disk). The server accepts up to 1,200 batches and 12,000 rule checks a minute per Site; a 429 just makes the middleware wait.

Where it shows in the dashboard

Troubleshooting

See Approved route for agents for agent tokens, Telltale for MCP servers for the MCP channel, and Agent-usage ledger for how calls are counted.