# Record calls to your own API (Telltale for APIs)

`POST /api/v1/api-calls` · operationId `recordApiCalls`

For developers: your server sends this request with a server key; it is not a button in the dashboard. To see the same information without code, open **Tools › All tools** in the dashboard.

Shapes only: unknown fields are refused, never dropped, so no body, query value, header or credential can be stored. A call with agent evidence (agent token, Web Bot Auth signature, a declared name checked against its operator's IP list, an AI agent provider's published IP range, or a declared name) is one approved-route call with channel api, once per eventId; every other call is Unknown (never Human) and only counted per route and day. Sent by the API middleware (/adapters/v1/api.js; see [Telltale for APIs](/docs/api-channel)).

## Authentication

Send `Authorization: Bearer YOUR_SERVER_KEY` (siteServerKey: Site server key in Authorization: Bearer <key> (the scheme is case-insensitive and required; a bare key is 401). Shown once on creation or rotation. Publishable keys cannot authenticate). Keep the server key in server-only configuration; never send it to a browser.

## Request body

Content-Type: application/json (required)

- `batchId` (string, optional) · pattern `^[A-Za-z0-9_-]{8,128}$`: Kept across retries: a resent batch is a replay as a whole.
- `sdk` (object, optional)
- `sdk.name` (string, required)
- `sdk.version` (string, optional)
- `framework` ("node" | "express" | "next" | "hono" | "fastify" | "other", optional)
- `dropped` (integer, optional) · minimum 0
- `calls` (object[], required)
- `calls[].eventId` (string, required) · pattern `^[A-Za-z0-9_-]{8,128}$`
- `calls[].method` ("GET" | "HEAD" | "POST" | "PUT" | "PATCH" | "DELETE" | "OPTIONS", required)
- `calls[].route` (string, required) · pattern `^/[A-Za-z0-9_.~:{}\[\]*+/-]{0,255}$`: The route template (/v1/invoices/:id), never a raw path or a query string. A segment that looks like a value (three or more digits, a UUID, a long hex run) is refused.
- `calls[].status` (integer, required) · minimum 100, maximum 599
- `calls[].startedAt` (integer, required)
- `calls[].endedAt` (integer, required)
- `calls[].responseBytes` (integer | null, optional) · minimum 0, maximum 1000000000000
- `calls[].responseTokens` (integer | null, optional) · minimum 0, maximum 1000000000000
- `calls[].size` ("empty" | "under_1kb" | "1kb_10kb" | "10kb_100kb" | "100kb_1mb" | "over_1mb" | "unknown", optional)
- `calls[].accountId` (string, optional) · maxLength 512
- `calls[].userId` (string, optional) · maxLength 512
- `calls[].sampleRate` (number, optional) · maximum 1
- `calls[].decisionId` (string, optional)
- `calls[].refusedBy` ("deny" | "hold" | "unreachable", optional)
- `calls[].evidence` (object, optional): Transient agent-evidence material: never stored, logged or returned.
- `calls[].evidence.ip` (string, optional): Used once for the provider and declared-name checks, never stored.
- `calls[].evidence.secure` (boolean, optional)
- `calls[].evidence.signature` (object, optional): A Web Bot Auth signature's own headers, verified once and never stored.
- `calls[].evidence.declared` (object, optional)
- `calls[].evidence.agentToken` (string, optional) · pattern `^[a-f0-9]{64}$`: SHA-256 hex of "agent-token:" + the Telltale agent token; the token never leaves your server.
- `calls[].tags` (object, optional): Categories only: keys that suggest personal data or secrets, identifier-like values and free text are dropped and counted in tagsDropped.

```json
{
  "framework": "express",
  "calls": [
    {
      "eventId": "call_00000001",
      "method": "GET",
      "route": "/v1/invoices/:id",
      "status": 200,
      "startedAt": 1794268800000,
      "endedAt": 1794268800085,
      "size": "1kb_10kb",
      "accountId": "acct_456"
    }
  ]
}
```

## Responses

- **200**: Success · body `object`
- **400**: bad_request: the request is invalid; message says what to fix and field names the request field · body `Error`
- **401**: unauthorized (missing or invalid server key, missing Bearer scheme) · body `Error`
- **403**: forbidden: the call came from a browser page or another Site; call from your server with this Site’s server key · body `Error`
- **404**: not_found: unknown path, or an ID in the path that is not on this Site · body `Error`
- **409**: conflict: batchId or an eventId was already used with different facts · body `Error`
- **413**: payload_too_large: the request body exceeds 131072 bytes · body `Error`
- **429**: rate_limited: wait for Retry-After · body `Error` · headers `Retry-After`
- **500**: internal_error: fail the protected action; do not retry automatically · body `Error`
- **503**: service_unavailable: temporarily unavailable; only verify retries · body `Error`

### Success body fields

- `accepted` (integer, required)
- `replayed` (integer, required)
- `agentCalls` (integer, required)
- `rejected` (object[], required)
- `rejected[].index` (integer, required)
- `rejected[].field` (string | null, required)
- `rejected[].error` (string, required)

Failures are not verdicts. Error bodies follow the `Error` schema: `code` is one of `bad_request`, `unauthorized`, `session_reinit_required`, `forbidden`, `not_found`, `conflict`, `identity_required`, `payload_too_large`, `rate_limited`, `internal_error`, `service_unavailable`, `request_failed`, `approval_invalid`, `approval_used`, `approval_expired`, `approval_mismatch`, `step_up_not_held`, `step_up_mismatch`, `step_up_settled`, `step_up_expired`, `step_up_superseded`, `agent_tokens_disabled`, `tool_not_approved`, `tool_not_read_only`. [Errors and retries](/docs/errors) explains each code and what to do.

## Example

```sh tab=curl
curl -sS -X POST 'https://app.trytelltale.dev/api/v1/api-calls' \
  -H "Authorization: Bearer $TELLTALE_SERVER_KEY" \
  -H 'Content-Type: application/json' \
  --data '{"framework":"express","calls":[{"eventId":"call_00000001","method":"GET","route":"/v1/invoices/:id","status":200,"startedAt":1794268800000,"endedAt":1794268800085,"size":"1kb_10kb","accountId":"acct_456"}]}'
```

```js tab=fetch
const response = await fetch("https://app.trytelltale.dev/api/v1/api-calls", {
  method: "POST",
  headers: {
    Authorization: 'Bearer ' + process.env.TELLTALE_SERVER_KEY,
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    "framework": "express",
    "calls": [
      {
        "eventId": "call_00000001",
        "method": "GET",
        "route": "/v1/invoices/:id",
        "status": 200,
        "startedAt": 1794268800000,
        "endedAt": 1794268800085,
        "size": "1kb_10kb",
        "accountId": "acct_456"
      }
    ]
  }),
  signal: AbortSignal.timeout(5000)
});
const result = await response.json();
```

Machine-readable contract: [openapi.json](https://app.trytelltale.dev/openapi.json). Request builder: the [API explorer](/docs/api-explorer) shows every operation as curl; it sends requests only on a local dashboard.
