OverviewIntroduction

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:

RouteOne row perAnswers
GET /agency/locationslocationThe roster — every client linked to this agency
GET /agency/statslocationEvent volume, unique users, unique sessions over a date window, plus the all-time live person count
GET /agency/destinations/healthlocation × destinationDelivery success rate, failure counts, and the last error over a trailing window — sorted worst-first
GET /agency/contracts/summarylocationDeclared event contracts, whether the daily alert webhook is wired, and when the last alert fired
GET /agency/usageday × location × surface × routeThe agency's own API + MCP request telemetry — the same data Mythic meters on
GET /agency/quotaquota in forceThe 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 keySurface
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
mcpthe 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" } }.

StatusCodeMeaning
401unauthorizedMissing or invalid key
403agency_requiredThe key is an sk_ — this surface takes agency keys only
403location_not_linkedA location_ids entry is not linked to this agency (the message names it)
404no_locationsNo locations are linked to this agency
422too_many_locationsMore than 100 locations in scope — batch with location_ids
429rate_limited60 requests/min per agency — retry after Retry-After
402feature_not_in_planThe feature is not included in this account's plan (returned by the surface you called, and by /client/v1/settings; never by this one)
502stats_failed, health_failed, usage_failed, quota_failed, plan_failedThe 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).