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
| Endpoint | Use it for |
|---|---|
GET /v1/analytics/summary | Totals for a period — a stat panel or a single BI pull |
GET /v1/analytics/timeseries | Bucketed series for a chart |
GET /v1/analytics/metrics | Prometheus 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"{
"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"{
"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 20000000Alert 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:
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
| Status | Code | Meaning |
|---|---|---|
400 | INVALID_DATE, RANGE_INVERTED, RANGE_TOO_LARGE, TOO_MANY_BUCKETS, INVALID_INTERVAL | Bad start/end/interval |
401 | MISSING_API_KEY, INVALID_API_KEY | No key, or the key doesn't verify |
403 | NOT_ANALYTICS_KEY | A regular scan key was used here |
403 | ANALYTICS_KEY_NOT_ALLOWED | An analytics key was used against a scan/job endpoint |
403 | PLAN_UPGRADE_REQUIRED | The org is below Growth (checked live on every request, not just at key creation) |
429 | RATE_LIMITED | Key-level rate limit — honor Retry-After |
Bucketed metrics for charting GET
Call volume, decisions, credits, and latency bucketed by hour or day, zero-filled so every interval has a point — for a Grafana JSON-API panel or a Datadog custom HTTP check. Bounded to 90 days and 1000 buckets per request.
Scan a proposed tool call POST
Inspect a proposed tool call before execution.