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_, which
must bind a location on every request with ?location_id= or the
X-Location-Id header) or a location secret key (sk_, auto-scoped to its
own location). An agency key that binds no location returns
400 location_required; one that names a location it is not linked to returns
403 location_not_linked.
Reads accept either key. 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:
| Field | Why not |
|---|---|
id | Server-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_id | Taken from your key. This is what stops one agency writing into another's clients. |
plan_id | A billing decision. Contact support to change tier. |
status | Use POST /client/v1/status — enabling and disabling tracking is its own operation. |
publishable_key, secret_key | Server-generated. |
gcp_project_id, gcp_credentials | Staff-managed export credentials. |
access_token, refresh_token, company_id, api_metadata | Not 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. The GET
repeats cname_target and dns_instructions, so the record to add is not
lost after the claim. 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.
Connecting GoHighLevel
Link a client to its GoHighLevel sub-account by installing the Mythic GHL app. The install runs in the browser of someone who can log in to that GHL account, so the API hands you a link rather than doing it for you.
Create the link
POST /client/v1/locations/ghl/install returns install_url. Pass
return_url to get the browser back, with mythic_ghl=connected or
mythic_ghl=error&mythic_error=<code> appended. Its origin must be in the
agency's oauth_return_origins, set with
PUT /client/v1/agency/oauth-return-origins.
Install
The person opens the link, logs in to GoHighLevel, and picks one sub-account. An agency-level install is not linked to a client.
Confirm
GET /client/v1/locations/ghl reads status: active with the linked
ghl_location_id. This read also completes the link when the browser never
came back, so a headless caller can poll it.
The link does not expire, so treat it like a credential: anyone who opens it
can link a GHL sub-account they control to this client. A sub-account already
linked to another client is refused with ghl_location_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.