Api Reference

9 min read

TamperTrail — API Reference

Base URL: http://your-host (all traffic enters via Nginx on port 80) API prefix: Every endpoint starts with /v1


Table of Contents

  1. Authentication
  2. Log API — POST /v1/log
  3. Health Check — GET /health
  4. HTTP Status Code Reference
  5. Rate Limits

1. Authentication

TamperTrail uses two separate authentication systems. Never mix them up.

API Key — For Sending Logs (this is what you need)

Create a key in the dashboard under API Keys → Create Key. The raw key is shown only once.

Pass it as an HTTP header on every request:

X-API-Key: vl_a1b2c3d4e5f6...

Keys are stored Argon2-hashed. A lost key cannot be recovered — revoke it and create a new one.

Never hardcode an API key. Use an environment variable: TAMPERTRAIL_API_KEY=vl_...

JWT Session — For the Dashboard

The browser dashboard handles this automatically via an HTTPOnly cookie (tampertrail_token) set at login. Tokens expire after 24 hours. One active session per user — logging in from a new location ends the previous session. You don't need to worry about this for log integration.


2. Log API — POST /v1/log

Purpose: Ingest a single audit log entry. Auth: X-API-Key header Response: 202 Accepted — log is written to the WAL on disk and queued for batch DB insertion. The response does not wait for the DB write.

2.1 Payload Schema

Every log entry sent to TamperTrail can contain up to 12 fields. Only 2 are required.

FieldRequiredTypeConstraintDescription
actorYESstringmax 255 charsWho performed the action. Use a consistent prefix format: "user:alice@acme.com", "service:billing-worker", "cron:nightly-sync"
actionYESstringmax 255 charsWhat happened. Use "resource.verb" convention: "invoice.created", "user.login.failed", "file.deleted"
levelnostringone of: DEBUG, INFO, WARN, ERROR, CRITICALExplicit severity. Case-insensitive. Overrides auto-detection. If omitted, TamperTrail derives it from keywords in action (see Severity Auto-Derivation).
messagenostringmax 1,000 charsHuman-readable description of the event. This is the first thing someone reads when investigating a log.
target_typenostringmax 255 charsType of resource affected. Appears in the "Target" column: "order", "invoice", "user", "document"
target_idnostringmax 255 charsUnique ID of that resource: "ORD-1001", "inv_9f2a3b4c", "usr_alice_8821"
statusnostringmax 50 charsOutcome. Accepts HTTP codes ("200", "404") or descriptive strings ("success", "failed", "timeout"). Default: "200"
environmentnostringmax 100 charsDeployment environment. Default: "production". Options: "production", "staging", "test", "development"
source_ipnostringvalid IPv4/IPv6End-user's IP address. If omitted, TamperTrail uses the API caller's IP automatically (respects X-Forwarded-For from Nginx).
request_idnostringmax 255 charsYour trace/correlation ID. Also used as an idempotency key — duplicate request_id within 10 minutes is silently dropped.
tagsnoobjectany JSON objectSearchable, plaintext key-value pairs. GIN-indexed JSONB. Visible in the dashboard. Use for data you want to filter and display.
metadatanoobjectany JSON objectEncrypted key-value payload. Fernet AES-128 encrypted on arrival. Never returned by the API. Never shown in the UI. Use for PII, credentials, stack traces.

Field-by-Field Examples

actor — who did it:

"user:alice@acme.com"       — a logged-in user
"service:billing-worker"    — a backend service/microservice
"cron:nightly-sync"         — a scheduled job
"api:mobile-app"            — an API consumer
"admin:superadmin"          — an admin performing a privileged action

action — what happened:

"order.created"             — resource.verb format
"payment.refunded"          — financial event
"user.login.failed"         — nested context
"file.deleted"              — destructive action (auto-detected as critical)
"api_key.revoked"           — security event

message — write for humans:

✅ "Order ORD-1001 created — AirPods Pro worth ₹24,900 shipping from Mumbai to Delhi"
✅ "Payment of $149.00 processed via Stripe for Pro plan upgrade"
❌ "order created"         — too vague, not useful for investigation
❌ "{json blob here}"      — put structured data in tags/metadata, not message

target_type + target_id — identify the resource:

target_type: "order"        target_id: "ORD-1001"
target_type: "invoice"      target_id: "inv_9f2a3b4c"
target_type: "user"         target_id: "usr_alice_8821"
target_type: "document"     target_id: "doc_annual_report_2025"
target_type: "api_key"      target_id: "vl_a1b2"

status — what happened:

"success"     — action completed
"failed"      — action did not complete
"timeout"     — action timed out
"200"         — HTTP 200 OK
"404"         — resource not found
"500"         — server error

environment — deployment context:

"production"  — live system
"staging"     — pre-production testing
"test"        — automated test environment
"development" — local development

2.2 The tags vs. metadata Decision

This is the most important design decision when integrating TamperTrail.

tagsmetadata
StoragePlaintext JSONBFernet AES-128 encrypted BYTEA
Searchable✅ Yes — GIN-indexed, fast❌ No — encrypted blob
Visible in dashboard✅ Yes — shown in Tags column❌ Never — not in any UI
Returned by API✅ Yes — in GET /v1/logs❌ Never — no API returns it
Use forContext you want to filter and displaySensitive forensic data

Decision rule — ask yourself:

  • Would you be comfortable showing this on a team dashboard? → tags
  • Does it contain PII, secrets, credentials, stack traces, or internal details? → metadata

tags examples (searchable, visible):

{
  "payment_provider": "stripe",
  "amount_usd": "149.00",
  "plan": "pro",
  "region": "eu-west-1",
  "browser": "Chrome"
}

metadata examples (encrypted, never shown):

{
  "card_last4": "4242",
  "stripe_charge": "ch_3abc123def",
  "billing_email": "alice@acme.com",
  "full_request_body": { "..." },
  "stack_trace": "Traceback (most recent call last)..."
}

2.3 Severity Auto-Derivation

If you don't provide a level field, TamperTrail automatically derives severity by scanning your action string for keywords.

How level maps to dashboard severity:

level you sendDashboard badgeColor
DEBUGinfoblue
INFOinfoblue
WARNwarningamber
ERRORcriticalred
CRITICALcriticalred

When level is omitted — auto-detection kicks in:

TamperTrail scans the action string for these keywords:

Auto-derived severityTriggered when action contains any of...
critical (red)delete, destroy, revoke, drop, purge, wipe
warning (amber)update, edit, modify, change, patch, rename
info (blue)everything else

Examples:

action: "user.deleted"      → auto-derived: critical (contains "delete")
action: "profile.updated"   → auto-derived: warning  (contains "update")
action: "order.created"     → auto-derived: info     (no trigger words)
action: "api_key.revoked"   → auto-derived: critical (contains "revoke")

Explicit level always wins. If you send "level": "INFO" with "action": "user.deleted", the log will be info, not critical.


2.4 Auto-Captured Fields

These fields are captured by the server automatically. You do not need to provide them.

FieldSourceDescription
source_ipHTTP connectionClient IP address. Reads X-Forwarded-For from Nginx. You can override it by passing source_ip in the body.
user_agentUser-Agent headerRaw user-agent string of the API caller (e.g. "python-httpx/0.27", "Mozilla/5.0 Chrome/...")
device_typeParsed from user-agentAutomatically classified: "desktop", "mobile", "tablet", "bot", or null
created_atServer clock (UTC)Timestamp of ingestion. Clock-skew protected — always monotonically increasing per tenant.
idServer-generatedUUID v4, unique identifier for this log entry
hashServer-computedSHA-256 hash of this entry, chained from the previous entry's hash

2.5 Requests & Responses

Minimal Request — Only the 2 required fields

{
  "actor": "user:alice@acme.com",
  "action": "document.downloaded"
}

Full Request — Every field TamperTrail accepts

{
  "actor":       "user:alice@acme.com",
  "action":      "payment.processed",
  "level":       "INFO",
  "message":     "Payment of $149.00 processed via Stripe for Pro plan upgrade.",
  "target_type": "invoice",
  "target_id":   "inv_9f2a3b4c",
  "status":      "success",
  "environment": "production",
  "source_ip":   "203.0.113.42",
  "request_id":  "req_trace_abc123",
  "tags": {
    "payment_provider": "stripe",
    "amount_usd":       "149.00",
    "plan":             "pro"
  },
  "metadata": {
    "card_last4":    "4242",
    "stripe_charge": "ch_3abc123def",
    "billing_email": "alice@acme.com"
  }
}

Success Response

{ "status": "accepted", "message": "Log queued for processing" }

Error Responses

CodeCause
401Missing or invalid X-API-Key
403API key is revoked
422actor or action missing, or any field exceeds its max length
429Rate limit exceeded — 100 req/min per IP
503WAL disk full. Free disk space and retry.

2.6 Integration Examples

While you can write standard HTTP requests directly to /v1/log, we recommend using our official drop-in snippets to ensure connection pooling and non-blocking execution.

👉 View Integration Guide


3. Health Check — GET /health

Purpose: Returns the live status of the server, database, and ingestion queue. No authentication required. Suitable for load balancer health probes.

Auth: None

Response: 200 OK

{
  "status": "ok",
  "db": "ok",
  "queue_depth": 0,
  "wal_entries": 0
}

If the database is unreachable, "db" will be "error" and the status code will be 503.


4. HTTP Status Code Reference

CodeMeaningCommon Cause
200 OKSuccessStandard successful response
201 CreatedResource createdNew user, key, or checkpoint
202 AcceptedQueuedLog entry accepted by WAL, not yet written to DB
400 Bad RequestInvalid request logicBusiness rule violation (e.g. deleting yourself)
401 UnauthorizedAuth failedMissing/invalid API key or wrong password
403 ForbiddenAccess deniedRevoked key, deactivated account, or license limit
404 Not FoundResource missingUser/key with given ID does not exist
409 ConflictDuplicateSetup already done, username already exists
422 Unprocessable EntitySchema errorMissing required field, field too long, bad enum value
429 Too Many RequestsRate limitedExceeded per-IP request quota (see Rate Limits)
503 Service UnavailableServer errorDatabase unreachable, or WAL disk full

5. Rate Limits

Rate limiting is enforced by Nginx before requests reach the application server. Limits are per IP address.

Endpoint(s)LimitBurst
POST /v1/auth/login5 req/min3
POST /v1/log100 req/min20
All other /v1/*200 req/min50

When a limit is exceeded, Nginx returns 429 Too Many Requests with a Retry-After header.

For high-volume ingestion above 100 req/min, batch your logs client-side into arrays and send them in fewer, larger requests — or contact us about a Pro ingestion upgrade.

Share this doc𝕏 Twitterin LinkedIn

Help improve these docs

See a typo or outdated information? Open a GitHub issue and we'll update it.

View Raw Markdown on GitHub