REST API

A small, Bearer-authenticated JSON API for programmatic access to this app.

Pointing an AI agent at this API?

Hand it the LLM-ready Markdown version — self-contained instructions an agent can follow with just a URL and an API key.

Open /docs/llm/api.md

Getting started

Base URL

All endpoints live under `https://tracetap.net/api/v1`. `https://tracetap.net` is the origin you were given (scheme + host, e.g. `https://example.com`). Do not add a trailing slash.

Authentication

Every endpoint requires an API key sent as a Bearer token: `Authorization: Bearer sk_your_key_here`. Keys always start with `sk_`. A missing or invalid key returns `401 { "error": "Invalid or missing API key" }`. Create keys in the app under Profile → API Keys.

Content type

Responses are JSON unless noted (file download returns raw bytes). Request bodies are JSON (`Content-Type: application/json`) except file upload, which is `multipart/form-data`.

Probe endpoints

The `/api/v1/tracetap/*` endpoints are called by installed TraceTap probes, not by users, and use different credentials: `Authorization: Bearer pt_...` (the probe key) together with `X-TraceTap-Project: pk_...` (the project key). An `sk_` key does not work on them, and a probe key does not work anywhere else. Request bodies may additionally be sealed with the project communication key (`ck_...`) using AES-256-GCM.

Rate limiting

Requests are rate limited per API key. When you exceed a limit you get `429` (or `403` if the limit is configured to block) with an `error` message and, when applicable, a `Retry-After` header (seconds). Back off and retry.

Errors

Errors are JSON with an `error` string and a matching HTTP status (`400` bad input, `401` unauthenticated, `403` forbidden, `404` not found, `413` payload too large, `429` rate limited, `500` server error).

Your base URL is https://tracetap.net. Create API keys under Profile → API Keys.

Endpoints

GET
/api/v1/health

Health check

Confirms the API is up and your key is valid. Handy as a first call to verify credentials and connectivity.

Auth: Bearer tokenAccess: Any valid API key.

Request

curl https://tracetap.net/api/v1/health \
  -H "Authorization: Bearer sk_your_key_here"

Response

{
  "status": "healthy",
  "timestamp": "2026-07-19T12:00:00.000Z",
  "uptime": 1234.56,
  "version": "1.0.0",
  "apiKey": "My key",
  "userId": "usr_...",
  "message": "API is running successfully"
}
GET
/api/v1/stats

Account & API usage stats

Returns the calling user together with API-usage counters (requests today / this week / this month, error rate, API-key count).

Auth: Bearer tokenAccess: Any valid API key (scoped to the key owner).

Request

curl https://tracetap.net/api/v1/stats \
  -H "Authorization: Bearer sk_your_key_here"

Response

{
  "user": { "id": "usr_...", "email": "you@example.com", "name": "You", "role": "user", "createdAt": "..." },
  "apiStats": {
    "totalApiKeys": 2,
    "requestsToday": 14,
    "requestsThisWeek": 98,
    "requestsThisMonth": 412,
    "errorRate": "1.20%",
    "errorCount": 5
  },
  "meta": { "timestamp": "...", "apiKey": "My key" }
}
GET
/api/v1/users

List users

Lists users. A regular key returns only its own user record; an admin key returns all users with pagination.

Auth: Bearer tokenAccess: Any valid API key (admin keys see all users; others see themselves).
NameInTypeReq.Description
limitqueryintegernoPage size, 1–100 (default 10). Admin only; ignored for non-admins.
offsetqueryintegernoRows to skip (default 0). Admin only.

Request

curl "https://tracetap.net/api/v1/users?limit=20&offset=0" \
  -H "Authorization: Bearer sk_your_key_here"

Response

{
  "users": [
    { "id": "usr_...", "email": "you@example.com", "name": "You", "role": "user", "emailVerified": null, "createdAt": "..." }
  ],
  "meta": { "limit": 20, "offset": 0, "total": 1, "apiKey": "My key" }
}
POST
/api/v1/users

Create user (scaffold)

Admin-only endpoint scaffold for creating a user. Ships as a stub in this starter — it validates input and echoes it back rather than persisting. Fill in real creation logic before relying on it.

Auth: Bearer tokenAccess: Admin API keys only (others get 403).
NameInTypeReq.Description
emailbodystringyesNew user email.
namebodystringyesNew user display name.
rolebodystringno'user' (default) or 'admin'.

Request

curl -X POST https://tracetap.net/api/v1/users \
  -H "Authorization: Bearer sk_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{"email":"new@example.com","name":"New User","role":"user"}'

Response

{
  "message": "User creation endpoint - implementation needed",
  "requestedData": { "email": "new@example.com", "name": "New User", "role": "user" },
  "apiKey": "My key"
}
  • This is a template stub — no user is actually created yet.
POST
/api/v1/files

Upload a file

Uploads a file and stores its raw bytes. Use this instead of a form/Server Action for any real upload (Server Actions cap the body at ~1MB; this endpoint does not). Send `multipart/form-data` with a single `file` field.

Auth: Bearer tokenAccess: Any valid API key (the file is owned by the key owner).
NameInTypeReq.Description
fileformfileyesThe file to upload (multipart field name must be "file").

Request

curl -X POST https://tracetap.net/api/v1/files \
  -H "Authorization: Bearer sk_your_key_here" \
  -F "file=@./photo.png"

Response

{
  "id": "fil_...",
  "filename": "photo.png",
  "url": "/api/v1/files/fil_..."
}
  • Default max size is 100MB (configurable via MAX_FILE_SIZE). Oversized uploads return 413.
  • The returned `url` is the Bearer-gated download endpoint below.
GET
/api/v1/files/:id

Download / preview a file

Streams the raw file bytes with the stored Content-Type. Because it is Bearer-gated you cannot put it directly in an `<img src>`; fetch it with the token and build an object URL client-side.

Auth: Bearer tokenAccess: Any valid API key.
NameInTypeReq.Description
idpathstringyesFile id returned by the upload endpoint.

Request

curl https://tracetap.net/api/v1/files/fil_your_file_id \
  -H "Authorization: Bearer sk_your_key_here" \
  --output downloaded-file

Response

Raw binary body with the stored `Content-Type` and `Content-Disposition: inline; filename="..."`. Returns `404 { "error": "File not found" }` if unknown.
DELETE
/api/v1/files/:id

Delete a file

Deletes a file owned by the calling key.

Auth: Bearer tokenAccess: Any valid API key (only the owner may delete).
NameInTypeReq.Description
idpathstringyesFile id to delete.

Request

curl -X DELETE https://tracetap.net/api/v1/files/fil_your_file_id \
  -H "Authorization: Bearer sk_your_key_here"

Response

{ "deleted": true }   // { "deleted": false } with status 404 if not found / not owned
POST
/api/v1/tracetap/ingest

Submit sampled traffic (probe)

Where an installed probe posts the messages it sampled. Bodies have already been transformed by the project rules inside the probe, so nothing arrives here that the rules did not allow through. The body is either plain JSON or a sealed envelope encrypted with the project communication key.

Auth: Probe key + project headerAccess: A probe key belonging to the project named in X-TraceTap-Project.
NameInTypeReq.Description
X-TraceTap-ProjectheaderstringyesThe project key (pk_...).
messagesbodyarrayyesCaptured messages. Each may carry direction, peer, method, path, statusCode, durationMs, protocol, operation, requestHeaders, responseHeaders, requestBody, responseBody, error and rulesVersion. A call that never got an answer has statusCode null and an error that starts with its class ("ECONNREFUSED: connect ECONNREFUSED 10.0.0.1:443").
connectionIdbodystringnoPins these messages to one connection on the topology.
probeVersionbodystringnoVersion of the probe, recorded for support.
statsbodyobjectnoTraffic counters since the last report: { observed, sampled, withheld, errors, bytesObserved, since, peers? }. This is the only way TraceTap can know real traffic volume — stored samples are 1 in N by design, so counting them measures the sampling rate, not the link. `peers` (probe 1.2.0+) counts every outbound call per target host: { "sharefiles.eu": { calls, failures, classes: { "ECONNREFUSED": 12 } } } — dependency health and the dependency-failing alert are computed from it.

Request

curl -X POST https://tracetap.net/api/v1/tracetap/ingest \
  -H "X-TraceTap-Project: pk_your_project_key" \
  -H "Authorization: Bearer pt_your_probe_key" \
  -H "Content-Type: application/json" \
  -d '{
    "messages": [{
      "method": "POST",
      "path": "/api/customers",
      "statusCode": 201,
      "requestBody": "{\"email\":\"generated@example.com\"}",
      "responseBody": "{\"id\":42}"
    }]
  }'

Response

{
  "accepted": 1,
  "divergences": 1,
  "rulesVersion": 3
}
  • To encrypt, send { "v": 1, "iv": "...", "ct": "...", "tag": "..." } — AES-256-GCM under the project communication key, wrapping the JSON body above.
  • At most 100 messages are processed per request; the rest are ignored.
  • `rulesVersion` in the response tells the probe whether a newer rules document exists.
  • `stats` counters are deltas since the probe last reported, not running totals. Send them again if a request fails, or that window is lost.
  • The counters are plain numbers — no payload content — so reporting them does not weaken the guarantee that nothing sensitive leaves the probe.
GET
/api/v1/tracetap/rules

Fetch the deterministic generation rules (probe)

Returns the rules document a probe enforces locally: which fields are personal data or secrets, which generator replaces each one, and what to do with anything unclassified. Pass `since` with the version you already have to get 204 when nothing changed.

Auth: Probe key + project headerAccess: A probe key belonging to the project named in X-TraceTap-Project.
NameInTypeReq.Description
X-TraceTap-ProjectheaderstringyesThe project key (pk_...).
sincequeryintegernoRules version the probe already holds. Omit, or pass 0, to always receive the document.

Request

curl "https://tracetap.net/api/v1/tracetap/rules?since=0" \
  -H "X-TraceTap-Project: pk_your_project_key" \
  -H "Authorization: Bearer pt_your_probe_key"

Response

{
  "format": 1,
  "projectKey": "pk_...",
  "version": 3,
  "publishedAt": "2026-08-24T10:00:00.000Z",
  "sampleEveryN": 20,
  "unknownFieldPolicy": "mask",
  "generators": [
    { "id": "gen_...", "name": "email", "kind": "builtin", "builtinId": "email", "config": {} }
  ],
  "fields": [
    { "endpoint": "POST /api/customers", "direction": "request", "section": "body",
      "pointer": "$.email", "classification": "pii", "piiKind": "email",
      "action": "generate", "generatorId": "gen_..." },
    { "endpoint": "GET /api/lists/:id/rows", "direction": "response", "section": "body",
      "pointer": "$.data.content[].f{n}", "pattern": "$.data.content[].f{n}",
      "classification": "internal", "action": "keep" }
  ],
  "redactHeaders": ["authorization", "cookie", "set-cookie", "x-api-key"]
}
  • Returns 204 with no body when `since` is 1 or higher and already matches the current version.
  • A field carrying `pattern` is a field pattern: it applies to every pointer the pattern matches (`{n}` = digits, `*` = any characters within one key segment, `**` = any number of segments, `re:` = an anchored regex). An exact `pointer` rule always wins over a pattern; then an endpoint rule beats a `*` one; then the most specific pattern. Probes older than 1.4.0 ignore pattern rules (`pointer` repeats the pattern text, which no real pointer equals), so those fields fall to `unknownFieldPolicy` — masked by default.
  • `unknownFieldPolicy` is what a probe applies to any field not listed: the default, `mask`, is why an unreviewed field never leaves in the clear.
POST
/api/v1/tracetap/heartbeat

Probe liveness and error reporting (probe)

Marks the probe as alive and, optionally, reports a local problem it cannot fix itself. The response states which rules version the probe should be running and how often it should sample.

Auth: Probe key + project headerAccess: A probe key belonging to the project named in X-TraceTap-Project.
NameInTypeReq.Description
X-TraceTap-ProjectheaderstringyesThe project key (pk_...).
versionbodystringnoProbe version string.
errorbodystringnoA local failure worth surfacing in the Pulse feed.
statsbodyobjectnoThe same traffic counters the ingest endpoint accepts. Sent here too because a probe sampling 1 in 1000 on a quiet link may go a long time without an ingest call, and its traffic volume still has to arrive.
metabodyobjectnoFree-form diagnostics stored with the event.

Request

curl -X POST https://tracetap.net/api/v1/tracetap/heartbeat \
  -H "X-TraceTap-Project: pk_your_project_key" \
  -H "Authorization: Bearer pt_your_probe_key" \
  -H "Content-Type: application/json" \
  -d '{
    "version": "1.0.0",
    "stats": {
      "observed": 4210, "sampled": 210, "withheld": 0,
      "errors": 7, "bytesObserved": 8402331,
      "since": "2026-08-24T12:00:00.000Z"
    }
  }'

Response

{
  "ok": true,
  "rulesVersion": 3,
  "sampleEveryN": 20
}
GET
/api/v1/tracetap/apis

Search the API inventory across all your projects

Every outbound API (host) that any system in any TraceTap project you can see calls: which project, environment and system calls it, how often (total, last 24 h, hourly), which keys it used (masked, e.g. `sk-or-v1-c02…8ca` — computed inside the app, the key never reaches TraceTap), its status and the fleet rule that decides it. `q` matches host, system or project name and masked key fragments; learned endpoint paths that match are returned too. Empty `q` lists everything, what needs a decision first.

Auth: Bearer tokenAccess: Projects you own, projects shared with you (every environment), everything for admins.
NameInTypeReq.Description
qquerystringnoe.g. `openrouter`, `VA5`, `8ca`. Do NOT put a full key here (query strings are logged) — use POST /api/v1/tracetap/apis/search.
statusquerystringnoComma-separated: learned, new, approved, blocked, forbidden.
limitquerynumbernoMax API rows (default 300, max 1000).

Request

curl "https://tracetap.net/api/v1/tracetap/apis?q=openrouter" \
  -H "Authorization: Bearer sk_your_api_key"

Response

{
  "query": "openrouter",
  "credentialSearch": false,
  "environmentsSearched": 16,
  "enforcement": "record-only",
  "apis": [
    {
      "id": "k3j…",
      "host": "openrouter.ai",
      "status": "learned",
      "forbidden": true,
      "rule": { "id": "r1…", "pattern": "openrouter.ai", "verdict": "deny", "note": "All AI via GluedEasy" },
      "project": { "rootName": "AppSalad platform", "environment": "production", "apiMode": "learning" },
      "system": { "id": "4ca8…", "name": "VA5" },
      "calls": 11, "calls24h": 0, "failures": 0, "sampledOnly": false,
      "hourly": [0, 0, 3, 8, 0],
      "credentials": [{ "display": "sk-or-v1-c02…8ca", "calls": 11, "lastSeenAt": "2026-10-03T21:12:00.000Z" }],
      "firstSeenAt": "2026-10-03T19:40:00.000Z",
      "lastSeenAt": "2026-10-03T21:12:00.000Z"
    }
  ],
  "endpoints": [
    { "method": "POST", "pathPattern": "/api/v1/chat/completions", "connection": "VA5 → openrouter.ai" }
  ]
}
  • status: learned (seen while the environment was learning), new (first seen after it was locked), approved, blocked. `forbidden` is true when a fleet deny rule matches.
  • "Blocked" is recorded and alerted on every call; enforcing it at the network edge is coming soon (`enforcement: "record-only"`).
  • Counts with `sampledOnly: true` come from a probe older than 1.2.0, which only reports sampled calls.
POST
/api/v1/tracetap/apis/search

Search the API inventory (query in the body; find where a key is used)

The same search as GET /api/v1/tracetap/apis, with the query in the JSON body. Paste a FULL key as `q` to find every API call made with exactly that key: it is matched by fingerprint (an HMAC per environment) and is never stored or logged.

Auth: Bearer tokenAccess: As GET /api/v1/tracetap/apis.
NameInTypeReq.Description
qbodystringnoSearch text, or a full key.
statusbodystring[]nolearned, new, approved, blocked, forbidden.
limitbodynumbernoMax API rows.

Request

curl -X POST https://tracetap.net/api/v1/tracetap/apis/search \
  -H "Authorization: Bearer sk_your_api_key" \
  -H "Content-Type: application/json" \
  -d '{"q": "sk-or-v1-…the full key…"}'

Response

{ "query": "…", "credentialSearch": true, "apis": [ { "host": "openrouter.ai", "credentials": [ { "display": "sk-or-v1-c02…8ca", "matched": true } ] } ] }
POST
/api/v1/tracetap/apis/:id/decision

Approve, block or reset one API

Decides on one API row (one system of one environment calling one host). approve resolves its open "new API" and "blocked" alerts; block marks it blocked — every further call raises an alert (enforcement integrations are coming soon); reset puts it back to learned.

Auth: Bearer tokenAccess: Owner, admin or editor of the project.
NameInTypeReq.Description
idpathstringyesThe API row id from the search.
decisionbodystringyesapprove | block | reset
notebodystringnoWhy — shown next to the status.

Request

curl -X POST https://tracetap.net/api/v1/tracetap/apis/API_ID/decision \
  -H "Authorization: Bearer sk_your_api_key" \
  -H "Content-Type: application/json" \
  -d '{"decision": "block", "note": "AI must go through GluedEasy"}'

Response

{ "ok": true, "id": "k3j…", "status": "blocked", "enforcement": "record-only" }
GET
/api/v1/tracetap/api-rules

List fleet API rules

Hosts forbidden (deny) or known-good (allow) across every project of their owner, with the projects each rule does not apply to.

Auth: Bearer tokenAccess: Your own rules, plus those of the owners of projects shared with you.

Request

curl https://tracetap.net/api/v1/tracetap/api-rules -H "Authorization: Bearer sk_your_api_key"

Response

{
  "rules": [
    { "id": "r1…", "pattern": "openrouter.ai", "verdict": "deny", "note": "All AI via GluedEasy",
      "except": [{ "id": "p9…", "name": "viberun" }], "mine": true }
  ]
}
POST
/api/v1/tracetap/api-rules

Forbid or allow a host across all your projects

deny: any call to the host from any of your projects (except the listed ones) raises an `api-forbidden` alert at once, in learning and locked environments alike. allow: never flagged as a new API. `openrouter.ai` also covers its subdomains; `*.example.com` only subdomains. PATCH /api/v1/tracetap/api-rules/:id replaces a rule, DELETE removes it.

Auth: Bearer tokenAccess: Any user; the rule applies to the projects you own.
NameInTypeReq.Description
patternbodystringyesHost, host:port or *.domain.
verdictbodystringyesdeny | allow
notebodystringnoWhy.
exceptRootIdsbodystring[]noRoot project ids the rule does not apply to.

Request

curl -X POST https://tracetap.net/api/v1/tracetap/api-rules \
  -H "Authorization: Bearer sk_your_api_key" \
  -H "Content-Type: application/json" \
  -d '{"pattern": "openrouter.ai", "verdict": "deny", "note": "All AI via GluedEasy"}'

Response

{ "rule": { "id": "r1…", "pattern": "openrouter.ai", "verdict": "deny", "except": [] } }
  • MCP: the same search, decisions and rules are tools on POST /api/mcp (streamable HTTP, Bearer sk_ key): search_apis, list_flagged_apis, decide_api, list_api_rules, add_api_rule, delete_api_rule.
GET
/api/v1/tracetap/projects/:projectId/field-patterns

List the field patterns of a project environment

Field patterns resolve data-driven keys with one decision: `$.data.content[].f{n}` stands for `f2350441`, `f2350451`, … A pointer a pattern matches raises no new-field divergence and gets no field of its own; its type is still checked. Syntax: the pointer text is literal except `*` (any characters within ONE key segment), `{n}` (one or more digits; `(\d+)` and `\d+` mean the same) and `**` (any number of segments, only in the middle). `re:<regex>` is a raw, anchored regex (max 200 chars; nested quantifiers, alternations inside quantified groups, backreferences and lookarounds are refused). Literal fields always win; then an endpoint pattern beats a project-wide one; then the most specific.

Auth: Bearer tokenAccess: Anyone who can see the project (owner, members, admins).
NameInTypeReq.Description
projectIdpathstringyesThe environment (project) id.

Request

curl https://tracetap.net/api/v1/tracetap/projects/PROJECT_ID/field-patterns -H "Authorization: Bearer sk_your_api_key"

Response

{
  "patterns": [
    { "id": "fp_…", "pattern": "$.data.content[].f{n}", "endpointId": "ep_…", "endpointLabel": "GET /api/lists/:id/rows",
      "direction": "response", "section": "body", "jsonType": "number|string",
      "classification": "internal", "action": "keep", "generatorId": null, "status": "approved",
      "source": "user", "matchCount": 812, "foldedFields": 3 }
  ]
}
POST
/api/v1/tracetap/projects/:projectId/field-patterns

Create a field pattern ("resolve with pattern")

Creates the pattern, resolves every open/acknowledged new-field divergence in its scope whose pointer it matches (resolved by you), and folds the matching literal fields into it (retired; restored when the pattern is deleted). Literal fields that were already approved keep their own decision. Pass `"dryRun": true` to only get the matches. Publish the rules afterwards (dashboard) so probes 1.4.0+ apply it.

Auth: Bearer tokenAccess: Owner, admin or editor of the project.
NameInTypeReq.Description
projectIdpathstringyesThe environment (project) id.
patternbodystringyese.g. `$.data.content[].f{n}`, `$.byId.*`, `re:^\$\.data\.f\d+$`.
endpointIdbodystring|nullnoLimit to one endpoint; omit/null for every endpoint (`*`).
directionbodystringyesrequest | response
sectionbodystringnobody (default) | headers | query
jsonTypebodystringnoExpected type, may be a union (`number|string`); empty = any. Values of another type raise type-change.
classificationbodystringyesunknown | public | internal | pii | secret
actionbodystringyeskeep | mask | generate | drop
generatorIdbodystringnoGenerator of this project, required for `generate`.
labelbodystringnoPlain-language description.
dryRunbodybooleannoOnly preview what it matches.

Request

curl -X POST https://tracetap.net/api/v1/tracetap/projects/PROJECT_ID/field-patterns \
  -H "Authorization: Bearer sk_your_api_key" \
  -H "Content-Type: application/json" \
  -d '{"pattern": "$.data.content[].f{n}", "endpointId": "ENDPOINT_ID", "direction": "response", "classification": "internal", "action": "keep", "jsonType": "number|string"}'

Response

{
  "pattern": { "id": "fp_…", "pattern": "$.data.content[].f{n}", "status": "approved", "foldedFields": 2 },
  "resolvedDivergences": 14,
  "foldedFields": 2,
  "keptFields": 0
}
  • dryRun answers `{ "preview": { "error": null, "divergenceCount": 14, "divergences": […], "fieldCount": 2, "fields": […], "suggestedType": "number|string" } }`.
  • Errors (bad syntax, unsafe regex, unknown endpoint/generator, duplicate) answer 400 with `{ "error": "…" }`.
PATCH
/api/v1/tracetap/projects/:projectId/field-patterns/:id

Change a field pattern

Replaces the pattern text, scope and decision (same body as POST, without dryRun) and re-applies it: newly matching divergences are resolved, newly matching fields folded, fields it no longer matches restored.

Auth: Bearer tokenAccess: Owner, admin or editor of the project.
NameInTypeReq.Description
projectIdpathstringyesThe environment (project) id.
idpathstringyesThe pattern id.

Request

curl -X PATCH https://tracetap.net/api/v1/tracetap/projects/PROJECT_ID/field-patterns/PATTERN_ID \
  -H "Authorization: Bearer sk_your_api_key" \
  -H "Content-Type: application/json" \
  -d '{"pattern": "$.data.content[].f{n}", "direction": "response", "classification": "pii", "action": "mask"}'

Response

{ "pattern": { "id": "fp_…", "classification": "pii", "action": "mask" }, "resolvedDivergences": 0, "foldedFields": 0, "keptFields": 0 }
DELETE
/api/v1/tracetap/projects/:projectId/field-patterns/:id

Delete a field pattern

The fields it folded come back; any other pointer it matched becomes learnable again from the next sample on (a new-field divergence when it is next seen). Nothing is raised for past data.

Auth: Bearer tokenAccess: Owner, admin or editor of the project.
NameInTypeReq.Description
projectIdpathstringyesThe environment (project) id.
idpathstringyesThe pattern id.

Request

curl -X DELETE https://tracetap.net/api/v1/tracetap/projects/PROJECT_ID/field-patterns/PATTERN_ID -H "Authorization: Bearer sk_your_api_key"

Response

{ "ok": true, "restoredFields": 2 }
API Documentation · TraceTap