OverviewIntroduction

Client Provisioning

Create your own clients, get their install snippet, attach a first-party tracking hostname, and confirm events are arriving — end to end on your agency key, with no request to Mythic in the middle.

Overview

Onboarding a client has four steps, and this surface covers all of them:

Create the client

POST /client/v1/locations returns the client's id and its publishable key. Needs an agency-wide key.

Install tracking

GET /client/v1/locations/loader returns the exact <script> block to paste into the client's site.

Optionally go first-party

PUT /client/v1/locations/hostname claims a tracking hostname on the client's own domain and returns the CNAME to configure.

Confirm it works

GET /client/v1/locations/last-event says whether events are arriving; GET /client/v1/locations/events shows the individual events when they are not.

Everything after step one is per-client. Step one is agency-wide, because the client does not exist yet and the agency comes from the key.

Base URL

https://mythic-analytics.gulp.workers.dev/client/v1/locations

Authentication

The same key system as the rest of /client/v1: an agency key (ak_, bind a location with ?location_id= or the X-Location-Id header) or a location secret key (sk_, auto-scoped to its own location). Reads accept either. Writes require an agency key — same rule as Client Config, Settings, and Destinations.

Creating a client needs an agency-wide key specifically. A key bound to one client cannot create siblings.

This surface deliberately serves no CORS headers. ak_ and sk_ keys are server-side credentials — never call it from browser JavaScript.

What you may set, and what you may not

A client row carries fields that are yours and fields that are ours. The settable set is:

name, domain, email, phone, website, timezone, global_name, ghl_location_id, proxy_api_key

The last two behave unusually and are covered below. Everything else is rejected rather than ignored:

FieldWhy not
idServer-generated. A caller-chosen id would let one agency squat another's id namespace, and the resulting conflict would leak that the id exists.
agency_idTaken from your key. This is what stops one agency writing into another's clients.
plan_idA billing decision. Contact support to change tier.
statusUse POST /client/v1/status — enabling and disabling tracking is its own operation.
publishable_key, secret_keyServer-generated.
gcp_project_id, gcp_credentialsStaff-managed export credentials.
access_token, refresh_token, company_id, api_metadataNot settable through this API.

Rejecting beats ignoring here. A caller that sends plan_id: "unlimited" and gets a 200 would reasonably believe it had changed the client's tier. It gets a 400 naming the field instead.

A typo in a settable field name is a separate error — unknown_field, whose message lists what is settable — so a mistyped tiemzone is findable rather than silently dropped.

Two fields that behave unusually

proxy_api_key is write-only. It is your GoHighLevel API key, which Mythic forwards as X-API-Key when proxying to GoHighLevel for this client. You set it; no endpoint ever returns it. Reads carry proxy_api_key_set: true|false instead, so rotating means sending the new value — there is no read-back to compare against.

ghl_location_id is a lookup key, not a label. One GoHighLevel sub-account maps to exactly one client, because Mythic resolves a client from that id when proxying. Claiming one already mapped elsewhere returns 409 ghl_location_taken — clear it on the other client first.

Before this was enforced, six GoHighLevel ids were shared across fourteen clients, and the GoHighLevel proxy failed for all fourteen — the lookup became ambiguous, and the error reported a missing proxy_api_key for clients that had one set. If you manage the same GoHighLevel account across several Mythic clients, map it to one and leave the others unmapped.

kv_sync — why writes report their edge sync

The SDK does not read the database. It runs from the client's edge config, and ingestion routes by edge entries too. A client that exists in the database but not at the edge rejects every event, and nothing about the database row would tell you.

So writes on this surface sync the edge inline and report the outcome:

{
  "success": true,
  "data": { "id": "…", "publishable_key": "pk_…" },
  "kv_sync": "synced"
}

synced means the edge agrees with the database. Anything starting failed: means the row was written but the edge was not — retry the write. Check this before handing the snippet to the client's site.

Custom tracking hostnames

A custom hostname makes tracking first-party: the client's site sends events to analytics.theirdomain.com instead of the shared host, which survives third-party cookie and script blocking.

Claiming one is a two-phase operation, and the phases matter:

Claim

PUT /client/v1/locations/hostname registers the hostname and returns a CNAME target. kv_sync reads pending_validation — the hostname is reserved but routes nothing yet.

Validate

Configure the CNAME in DNS, then poll GET /client/v1/locations/hostname until cf_status is active. That poll is what activates the hostname: events start routing on the first read that sees Cloudflare confirm the CNAME.

A hostname starts routing events only once Cloudflare confirms its CNAME, which is proof you control the DNS. Until then it is reserved and inert. This is deliberate: a hostname that routed on the claim alone would let one agency bind a domain another operates, and the real owner's events would land in the wrong client the moment their DNS pointed here.

A hostname already attached to another location is refused with hostname_taken.

Confirming an install

Two operations, for two different questions.

GET /client/v1/locations/last-event answers "is it working" — one event count and one timestamp, cheap enough to poll on an onboarding screen. A null last_event_at means nothing arrived in the window.

GET /client/v1/locations/events answers "why isn't it working" — the individual events, newest first, filterable by event name, session, or distinct id. This is a debugging view, not an analytics one; for counts, trends, and breakdowns use the Data API.

Pass exclude_event_type=$replay_summary when a client has session replay enabled. The exclusion is applied before the row limit, so replay telemetry cannot crowd the events you are looking for off the page.

Limits

Requests are limited to 60 per minute per location. An agency has a ceiling on how many clients it may create; hitting it returns location_ceiling, and support can raise it.

Session replay

Replay capture is configured through GET/PUT /client/v1/settings/session-replay (under Settings in the Data API), not here — it is a tracking setting rather than a provisioning field. Keys are merged rather than replaced on write, so a partial body cannot silently un-mask the fields that keep personal data out of a recording.

The same operations as MCP tools

Every route here has an MCP tool on the Settings Server: create_location, get_location, update_location, get_location_loader, get_location_hostname, set_location_hostname, delete_location_hostname, get_location_last_event, and list_location_events.

Scopes split along the grain boundary. locations:read and locations:write cover one client's own setup. provisioning:write is what creates a new client, and it is agency-wide only — a key bound to a single client can never hold it.