Tawa docs

Analytics API

Pull aggregate spend, call volume, and decision metrics into Grafana, Datadog, or another BI tool.

The Analytics API returns aggregate metrics for an app — call volume, decisions (allow/block breakdown), latency, character usage, and organization-wide spend — not individual job or scan data. If you need to inspect specific scans, use the dashboard or the job detail pages; this API exists to feed a BI tool, not to list jobs.

Growth plan and above. See pricing.

Authentication

The Analytics API uses a separate key from your regular scan keys — one per app, created from Settings → API keys → Analytics API in the dashboard. It's read-only: it cannot call /scan, /rag/*, or /agent/*, and a regular scan key can't call anything under /analytics/*. Each key kind rejects the other with a 403.

Auth works the same as everywhere else — bearer token or x-api-key:

curl "https://gateway.tawaguard.com/v1/analytics/summary" \
  -H "Authorization: Bearer $TAWA_ANALYTICS_KEY"

Endpoints

EndpointUse it for
GET /v1/analytics/summaryTotals for a period — a stat panel or a single BI pull
GET /v1/analytics/timeseriesBucketed series for a chart
GET /v1/analytics/metricsPrometheus text-exposition format — a native Grafana/Datadog scrape target

Summary

start/end (ISO 8601, given together or not at all) default to the org's current billing period. include_preview (boolean, default false) includes scans run from the dashboard (free preview scans); leave it off so figures reflect production traffic only.

curl "https://gateway.tawaguard.com/v1/analytics/summary?start=2026-08-01T00:00:00Z&end=2026-08-08T00:00:00Z" \
  -H "Authorization: Bearer $TAWA_ANALYTICS_KEY"
response (trimmed)
{
  "appId": "app_abc123",
  "generatedAt": "2026-08-08T00:03:11.482Z",
  "period": { "start": "2026-08-01T00:00:00.000Z", "end": "2026-08-08T00:00:00.000Z" },
  "volume": { "total": 12453, "completed": 12401, "failed": 12, "inflight": 40, "errorRate": 0.0009 },
  "decisions": {
    "allow": 12100, "softBlock": 210, "review": 60, "needsReview": 60, "sandbox": 5, "hardBlock": 26,
    "classified": 12401, "blockRate": 0.0242, "avgConfidence": 0.91
  },
  "latency": { "avgMs": 82.4, "p50Ms": 61, "p95Ms": 210 },
  "chars": { "used": 1245300 },
  "byCategory": [
    { "jobCategory": "prompt", "total": 11800, "blocked": 280 },
    { "jobCategory": "rag", "total": 600, "blocked": 20 },
    { "jobCategory": "agent", "total": 53, "blocked": 1 },
    { "jobCategory": "unknown", "total": 0, "blocked": 0 }
  ],
  "byKeyType": [
    { "keyType": "dev", "total": 340, "charsUsed": 34000 },
    { "keyType": "staging", "total": 0, "charsUsed": 0 },
    { "keyType": "prod", "total": 12113, "charsUsed": 1211300 }
  ],
  "org": {
    "periodStart": "2026-08-01T00:00:00.000Z",
    "periodEnd": "2026-09-01T00:00:00.000Z",
    "charsUsed": 21300000, "charsLimit": 20500000,
    "monthlyAllowance": 500000, "grantRemaining": 20000000,
    "grantExpiresAt": "2027-08-01T00:00:00.000Z",
    "overageCharsUsed": 800000,
    "allowOverage": true, "overageSpentCents": 40, "spendCapCents": null,
    "currency": "GBP"
  }
}

decisions is the same 5-state verdict ladder the dashboard uses (ALLOW / SOFT_BLOCK / REVIEW / SANDBOX / HARD_BLOCK) — blockRate is the share of classified decisions that weren't ALLOW.

These counts use the engine's persisted action, not the ALLOW or BLOCK that the scan response enforced. A scan the gateway collapsed to allow or block still counts under review, which makes review the number of flagged scans. needsReview is an alias of review with the same value, named to match the needs_review flag on scan results. The two are returned side by side so existing readers of review keep working. org is always the organization's current billing period regardless of period above: spend and the free allowance are pooled at the org level, not per app, so every app's analytics key reports the same org numbers — the same totals your billing page shows.

Read the three allowance fields together. charsLimit is everything spendable for free this period, and it is not a monthly number: it is monthlyAllowance (the figure that repeats every period) plus grantRemaining (what is left of the one-time signup grant, which expires at grantExpiresAt and does not renew). Budget dashboards and alerts want monthlyAllowance; charsLimit is only the boundary at which this period starts billing overage.

Timeseries

Adds interval (hour or day, default day) and include_preview (boolean, default false; includes dashboard preview scans when set). Buckets are zero-filled: one point per interval even when nothing happened. Bounded to 90 days and 1000 buckets per request; interval=hour over more than ~41 days exceeds that, so use interval=day for longer ranges.

curl "https://gateway.tawaguard.com/v1/analytics/timeseries?interval=day" \
  -H "Authorization: Bearer $TAWA_ANALYTICS_KEY"
response (trimmed)
{
  "appId": "app_abc123",
  "interval": "day",
  "period": { "start": "2026-08-01T00:00:00.000Z", "end": "2026-08-08T00:00:00.000Z" },
  "buckets": [
    {
      "bucketStart": "2026-08-01T00:00:00.000Z",
      "total": 1802, "completed": 1795, "failed": 2, "blocked": 41,
      "charsUsed": 180200, "avgLatencyMs": 79.1
    }
  ]
}

Prometheus scrape endpoint

GET /v1/analytics/metrics always covers the current billing period, with one optional query parameter, include_preview (boolean, default false), to include dashboard preview scans. Returns text/plain; version=0.0.4:

# HELP tawa_jobs_total Jobs in the current billing period, by status.
# TYPE tawa_jobs_total counter
tawa_jobs_total{status="completed"} 12401
tawa_jobs_total{status="failed"} 12
# HELP tawa_jobs_needs_review_total Decisions whose engine verdict was REVIEW in the current billing period. Callers saw ALLOW or BLOCK; this is the flagged count.
# TYPE tawa_jobs_needs_review_total counter
tawa_jobs_needs_review_total 60
# HELP tawa_block_rate Fraction of classified decisions that weren't ALLOW.
# TYPE tawa_block_rate gauge
tawa_block_rate 0.0242
# HELP tawa_chars_used_total App characters billed in the current billing period.
# TYPE tawa_chars_used_total counter
tawa_chars_used_total 1245300
# HELP tawa_org_chars_used Organization-wide characters billed this period (pooled across all workspaces sharing the billing identity).
# TYPE tawa_org_chars_used gauge
tawa_org_chars_used 21300000
# HELP tawa_org_chars_limit Total free characters this period: the monthly allowance plus any remaining one-time signup grant. NOT a monthly figure -- alert on tawa_org_monthly_allowance for that.
# TYPE tawa_org_chars_limit gauge
tawa_org_chars_limit 20500000
# HELP tawa_org_monthly_allowance Recurring free characters per period. This is the figure that repeats every month.
# TYPE tawa_org_monthly_allowance gauge
tawa_org_monthly_allowance 500000
# HELP tawa_org_grant_remaining Remaining one-time signup grant, 0 once spent or expired.
# TYPE tawa_org_grant_remaining gauge
tawa_org_grant_remaining 20000000

Alert on the monthly allowance, not the limit

tawa_org_chars_limit includes whatever is left of the one-time signup grant, so on a new account it reads roughly 20.5M and then drops to 500k once the grant is spent or expires. A budget alert built on it will look fine for months and then fire constantly. Use tawa_org_monthly_allowance for anything that assumes a recurring figure.

Counters reset at the billing-period boundary

Metric names ending in _total are Prometheus counters scoped to the current billing period — they reset to 0 at the next period, not at midnight or a rolling window. Grafana's rate()/increase() already treat a decrease as a reset event, so this is safe, but don't build a panel that assumes a counter only ever goes up forever.

Grafana — point a Prometheus data source's scrape config at this URL with a bearer token:

prometheus.yml
scrape_configs:
  - job_name: tawa-analytics
    scrape_interval: 30s
    metrics_path: /v1/analytics/metrics
    scheme: https
    authorization:
      credentials_file: /etc/prometheus/token/analytics-key.txt
    static_configs:
      - targets: ["gateway.tawaguard.com"]

Datadog — the OpenMetrics check can scrape the same URL directly; set Authorization: Bearer <key> as a custom header on the check config.

Rate limits and retries

Analytics keys are limited to 120 requests/minute — generous for BI polling (Grafana/Datadog typically scrape every 15-60s). This is separate from, and doesn't share a bucket with, your app's regular scan-key limits.

Errors, retries, and backoff follow the same contract as the rest of the API — see Best practices for the full retry/jitter algorithm. 429 responses include Retry-After.

Errors

StatusCodeMeaning
400INVALID_DATE, RANGE_INVERTED, RANGE_TOO_LARGE, TOO_MANY_BUCKETS, INVALID_INTERVALBad start/end/interval
401MISSING_API_KEY, INVALID_API_KEYNo key, or the key doesn't verify
403NOT_ANALYTICS_KEYA regular scan key was used here
403ANALYTICS_KEY_NOT_ALLOWEDAn analytics key was used against a scan/job endpoint
403PLAN_UPGRADE_REQUIREDThe org is below Growth (checked live on every request, not just at key creation)
429RATE_LIMITEDKey-level rate limit — honor Retry-After

On this page