Agency API
Cross-location rollups for the whole agency — the roster, per-client event/user/person stats, destination delivery health, event-contract coverage, and the agency's own API usage — on /client/v1/agency, with an agency key only.
Overview
Every other /client/v1 surface answers questions about one client. The
Agency API answers the agency-grain question — "how are all my clients
doing?" — in one call, without iterating a location list yourself:
| Route | One row per | Answers |
|---|---|---|
GET /agency/locations | location | The roster — every client linked to this agency |
GET /agency/stats | location | Event volume, unique users, unique sessions over a date window, plus the all-time live person count |
GET /agency/destinations/health | location × destination | Delivery success rate, failure counts, and the last error over a trailing window — sorted worst-first |
GET /agency/contracts/summary | location | Declared event contracts, whether the daily alert webhook is wired, and when the last alert fired |
GET /agency/usage | day × location × surface × route | The agency's own API + MCP request telemetry — the same data Mythic meters on |
GET /agency/quota | quota in force | The plan, the features it includes, and month-to-date ingested events against the quota |
The same rollups exist as MCP tools on the
Settings Server (get_agency_stats,
get_agency_destination_health, get_agency_contracts_summary,
get_agency_usage, get_agency_quota); this is their headless REST form.
Base URL
https://mythic-analytics.gulp.workers.dev/client/v1/agency
Authentication
Agency key (ak_), or an agency-wide scoped key (mcp_) holding
agency:read — and no location parameter — the agency itself is the scope.
Unlike the rest of /client/v1, a location secret key (sk_) is rejected with
403 agency_required, and a client-scoped mcp_ key with
403 agency_key_required: both belong to one location, and this surface reads
across all of them.
curl "https://mythic-analytics.gulp.workers.dev/client/v1/agency/stats" \
-H "Authorization: Bearer $AK"
This surface deliberately serves no CORS headers. ak_ keys are
server-side credentials — never call it from browser JavaScript.
The roster and location_ids
Tenancy is resolved server-side: the roster is every location whose
agency_id is the key's agency, and every query runs against exactly that
set. The pipes never decide tenancy.
?location_ids=a,b,c narrows a request to a subset — and the subset is
validated against the roster. Requesting a location that is not linked to
your agency is a hard 403 location_not_linked naming the foreign ids, never
a silent empty result, so a typo can't read as "no data".
The 100-location cap
Rollups are capped at 100 locations per request. An agency with more gets
422 too_many_locations:
{
"success": false,
"error": {
"code": "too_many_locations",
"message": "This agency has 240 locations; rollups are capped at 100 per request. Pass ?location_ids= with batches of up to 100."
}
}
Batch it: fetch the full roster from GET /agency/locations (which is never
capped), then call the rollup routes with ?location_ids= chunks of up to
100.
Zero-filled stats
GET /agency/stats zero-fills from the roster: a location that sent
nothing in the window still appears, as a row of zeros. A silent client is
a visible zero row, never an omission — which is exactly the client an
agency-wide sweep exists to catch.
{
"success": true,
"data": [
{
"location_id": "loc_abc123",
"name": "Acme Dental",
"active": true,
"total_events": 48210,
"unique_users": 6120,
"unique_sessions": 9344,
"people": 12873
},
{
"location_id": "loc_def456",
"name": "Quiet Client Co",
"active": true,
"total_events": 0,
"unique_users": 0,
"unique_sessions": 0,
"people": 0
}
]
}
total_events, unique_users, and unique_sessions cover the date window
(date_from defaults to 30 days ago; date_to is an inclusive day);
people is the all-time live person count (merge tombstones excluded).
Plan and event quota
GET /agency/quota reports the tier this account is on, the features it
includes, and how many events have been ingested this calendar month against
what was bought.
curl -H "Authorization: Bearer ak_..." \
"https://mythic-analytics.gulp.workers.dev/client/v1/agency/quota"
{
"success": true,
"data": {
"month_offset": 0,
"plan": {
"id": "growth",
"name": "Growth",
"features": ["autocapture", "error_tracking", "data_api", "session_replay", "heatmaps", "experiments", "contracts", "destinations"],
"unlimited": false,
"monthly_event_quota": 10000000
},
"buckets": [
{ "scope": "agency", "plan_id": "growth", "locations": 12, "metered": true, "used": 8412663, "quota": 10000000, "ratio": 0.841, "over": false, "threshold": 0.8 }
],
"locations": 12,
"soft_cap": true
}
}
The quota is a soft cap. Going over it is a billing signal, not a stop: ingestion continues, no events are dropped, and no endpoint starts failing. Analytics you lose at your busiest moment cannot be recovered later, so the cap alerts and bills rather than blocking.
Why buckets is a list
The tier normally lives on the agency and every location draws from one pooled
quota — that is the single scope: "agency" bucket above.
A location can be put on its own plan, and then it is metered separately:
it gets its own scope: "location" bucket against its own quota, and its
events leave the agency pool. Without that exclusion the same events would
count against two quotas at once. A location override replaces the agency
plan rather than adding to it, so a location on a smaller plan than its agency
really does get the smaller quota and the smaller feature set.
month_offset selects the period: 0 is the current calendar month (the
default), 1 the previous one, up to 12. Numbers are measured live at
request time, so a check right after a traffic spike reflects the spike.
Features and your plan
plan.features is the authoritative list of what this account can use. A
feature outside it is refused in three places, and the same 402
feature_not_in_plan comes back from all of them — the key is valid and
authorized, the tier is what is missing.
Turning one on. PUT /client/v1/settings/... refuses to enable a feature
the plan excludes. Turning a feature off and reading its current state work
on every tier, so an account that changes plan can always see and disable what
it no longer pays for.
Calling one. Whole API surfaces are sold by tier, and a request to one the plan excludes is refused before any query runs:
| Feature key | Surface |
|---|---|
data_api | /client/v1/data |
bigquery_export | /client/v1/data/export |
contracts | /client/v1/contracts |
destinations | /client/v1/destinations |
constraints | /client/v1/constraints |
correlation | /client/v1/correlation |
incrementality | /client/v1/incrementality |
airbyte | /client/v1/airbyte |
mcp | the MCP server |
Reads are refused as well as writes. A settings toggle is a switch you own, so turning it off stays open on every tier; an API surface is the product itself, and there is no equivalent of "off" to leave open.
Serving one to the browser. A feature outside the plan is switched off in the configuration served to the SDK, so a plan change reaches live pages on their next config fetch rather than waiting for someone to toggle it.
A plan change takes up to a minute to apply everywhere — entitlements are cached briefly on each edge node.
Errors and limits
Errors are always { "success": false, "error": { "code", "message" } }.
| Status | Code | Meaning |
|---|---|---|
401 | unauthorized | Missing or invalid key |
403 | agency_required | The key is an sk_ — this surface takes agency keys only |
403 | location_not_linked | A location_ids entry is not linked to this agency (the message names it) |
404 | no_locations | No locations are linked to this agency |
422 | too_many_locations | More than 100 locations in scope — batch with location_ids |
429 | rate_limited | 60 requests/min per agency — retry after Retry-After |
402 | feature_not_in_plan | The feature is not included in this account's plan (returned by the surface you called, and by /client/v1/settings; never by this one) |
502 | stats_failed, health_failed, usage_failed, quota_failed, plan_failed | The upstream analytics query failed — retry |
GET /agency/locations is exempt from the cap (it is the batching tool)
and GET /agency/usage takes no location_ids at all — usage is already the
agency's own slice, filtered by day window (days_back, default 30, max 90).