Client Config
Manage the configuration the SDK runs from — tags, snippets, and event transformers — over REST, with every write synced to the edge and the sync outcome reported back as kv_sync.
Overview
Three things the Mythic SDK executes on a client's site are configured per location:
| Resource | What it is | Table |
|---|---|---|
| Tags | GTM-like tag assignments (a template id plus params) fired by the SDK | tenant_tags |
| Snippets | Custom JavaScript (code) injected by the SDK | tenant_snippets |
| Transformers | Event transforms (map field mappings or code) applied by the SDK to every captured event before it is sent, in priority order | transformers |
The Client Config API is CRUD over those three on /client/v1/config — 14
routes. The same operations exist as MCP tools on the
Settings Server (tags, snippets, transformers
scope families); this is their headless REST form.
Base URL
https://mythic-analytics.gulp.workers.dev/client/v1/config
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 Settings,
Destinations, and Event Contracts.
This surface deliberately serves no CORS headers. ak_ and sk_ keys
are server-side credentials — never call it from browser JavaScript.
kv_sync — why every write reports its edge sync
The SDK does not read the database. It runs from the location's edge config (a Cloudflare KV entry), and tenant-config writes are not synced to it by database triggers. A write that lands in the database but never reaches the edge is the classic failure mode here: the API says "saved", and the site keeps running the old tags forever.
So every write on this surface rebuilds the edge config inline, and the response tells you how that went:
{ "success": true, "data": { "...": "..." }, "kv_sync": "synced" }
"synced"— the edge config was rebuilt from database truth. The SDK picks the change up on its next config fetch."failed: <reason>"— the database write landed, but the edge is still serving the previous config. Retry the write (any successful write re-syncs the whole location config), or rebuild via the admin API.
Treat a failed sync as the write not having taken effect yet — because for
the SDK, it hasn't.
Quickstart
Assign a tag
curl -X POST "$BASE/client/v1/config/tags?location_id=$LOC" \
-H "Authorization: Bearer $AK" -H "Content-Type: application/json" \
-d '{
"tag_id": "meta-pixel",
"params": { "pixel_id": "1234567890" },
"trigger_rules": { "url_contains": "/checkout" }
}'
enabled defaults to true and params to {}. Tags are keyed by
tenant_key + tag_id, so updates and deletes address the assignment by
its tag_id.
Add a snippet
curl -X POST "$BASE/client/v1/config/snippets?location_id=$LOC" \
-H "Authorization: Bearer $AK" -H "Content-Type: application/json" \
-d '{
"code": "console.log("hello from mythic");",
"description": "Debug logger",
"enabled": true
}'
code is required. The list endpoint omits code bodies — GET /snippets/{id} returns the full row.
Create a transformer
curl -X POST "$BASE/client/v1/config/transformers?location_id=$LOC" \
-H "Authorization: Bearer $AK" -H "Content-Type: application/json" \
-d '{
"name": "Add page title",
"type": "map",
"config": { "from": "document.title", "as": "page_title" },
"priority": 50
}'
type is map (evaluate config.from, a JS expression, and store the
result at config.as in the event properties) or code (run
config.code with (data, mythic) — mutate data and return it, or
return null to drop the event). The config must be complete for the
type — an incomplete one is rejected with 400 bad_config instead of
silently no-oping at capture time. Transformers run on every captured
event in priority order, lowest first; they are unconditional (there are
no trigger rules — gate inside code when you need conditions). Like
snippets, the list omits config.
Transformers execute arbitrary JavaScript in the browser, exactly like
snippets — transformers:write on a key is code-execution trust. They
are not loaded at all when the SDK runs in HIPAA mode or with
disable_remote_config.
Update and verify
curl -X PATCH "$BASE/client/v1/config/tags/meta-pixel?location_id=$LOC" \
-H "Authorization: Bearer $AK" -H "Content-Type: application/json" \
-d '{ "enabled": false }'
{
"success": true,
"data": {
"tenant_key": "loc_abc123",
"tag_id": "meta-pixel",
"enabled": false,
"params": { "pixel_id": "1234567890" },
"trigger_rules": { "url_contains": "/checkout" }
},
"kv_sync": "synced"
}
PATCH is partial — only the fields you pass change. A PATCH with no
recognized fields returns 400 nothing_to_update. Always check
kv_sync before considering a write done.
Errors and limits
| Status | Code | Meaning |
|---|---|---|
400 | bad_json, missing_field, bad_type, bad_config, bad_priority, nothing_to_update | Malformed body, a required field missing, transformer type not map/code, an incomplete type-specific transformer config, a priority outside 1–100, or an empty PATCH |
401 | unauthorized | Missing/invalid key, or an ak_ with no location bound |
403 | agency_required | Write attempted with an sk_ key |
404 | not_found | No such tag assignment, snippet, or transformer for this location |
429 | rate_limited | 120 requests/min per location — retry after Retry-After |
Deletes are idempotent for tags (removing an absent assignment succeeds);
snippet and transformer deletes verify ownership first and return 404 for a
row that is not this location's.
The install snippet prefetches this endpoint
The published install snippet calls /decide itself, during HTML parse, before
the loader script is even requested — then parks the in-flight promise on
window.__mythicCfgPrefetch and the SDK adopts it instead of issuing its
own request:
var u = "https://api.adberserk.com/decide?key=pk_xxx&v=2",
r = fetch(u, { cache: "default" });
r.catch(function() {});
window.__mythicCfgPrefetch = { url: u, response: r };
Measured on a cold visit, this starts config at ~12ms instead of ~300-500ms, because it no longer waits for the loader to download and execute. Config is what gates tags, snippets and platform pixels, so the whole server-defined layer lands earlier on the page.
Three things matter if you hand-roll the snippet rather than using the one from the API or the builder:
- The URL must match exactly, including
&v=2. The SDK adopts the parked response only when the URL equals the one it was going to request; anything else and it fetches again, giving you two/decidecalls per page. - The handle is single-use, and it lives on its own window key rather than
on
window.mythic— the core replaces that global, which would drop it. - Keep the
.catch(). Without it a failed prefetch is an unhandled rejection on the customer's page. The SDK handles the failure when it adopts.
Omit the prefetch entirely for HIPAA-mode and disable_remote_config tenants.
Their SDK makes no config request at all, so prefetching would send the exact
request those modes exist to prevent.
respect_dnt is the same rule with a runtime condition. The SDK checks Do Not
Track and Global Privacy Control during init and disables itself before it
requests config, so on those tenants a DNT visitor generates no requests at all
— and the snippet, which runs earlier than any of that, has to repeat the test:
if (!(window.doNotTrack === "1" || navigator.doNotTrack === "1"
|| navigator.doNotTrack === "yes" || window.msDoNotTrack === "1"
|| navigator.globalPrivacyControl)) {
/* prefetch */
}
The generated snippet emits this automatically when respect_dnt: true is in
the init config, and the loader applies the same test before starting its own
prefetch. Match the signal list exactly if you hand-roll it: a gate that misses
one lets the request through for a visitor the SDK would have honoured.
Snippet execution context
Snippets run in a function scope with two arguments:
mythic— the SDK instance (same object as the page global), so a snippet can callmythic.capture(...), read config, etc.event— only for event-triggered snippets (a snippet whosetrigger_rulescontain amythic_eventrule):{ name, properties, uuid }for the event that fired it.
// Event-triggered snippet: forward a custom conversion with the same
// event id the server destinations use, for platform-side dedup.
fbq('trackSingle', PIXEL_ID, 'Purchase', event.properties, { eventID: event.uuid });
event.uuid is the Mythic event id (UUIDv7) that the SDK generated for that
event — it is the same value the server-side destinations send as Meta's
event_id and Google's transaction_id. Use it as your dedup key whenever a
snippet sends a conversion to a platform that also receives the server-side
delivery.
Two execution rules to know:
- An event-triggered snippet fires once — on the first matching event. It does not re-run for later matches; use a platform destination for per-event forwarding.
- A snippet with no trigger rules runs at load time with
eventundefined.
Snippets execute arbitrary JavaScript in the browser — snippets:write on a
key is code-execution trust, exactly like transformers. Nothing server-defined
(tags, snippets, transformers) loads at all in HIPAA mode or with
disable_remote_config.
Managed platform tags
Two tag types are provisioned by the Platform Integrations API
rather than this CRUD surface: meta_pixel and google_tag. They carry no
code — the behaviour lives in the SDK (loaded as an on-demand chunk, so sites
without them never download it), and the per-location config (pixel id,
event mapping) lives in tenant_tags.params.
They are listed here because you will see them through these endpoints and the
tags scope: each platform has one global template row (named
Mythic · Meta Pixel / Mythic · Google Tag), and a location's assignment
carries its config. Don't hand-edit those rows — reconnect through the
integrations API (or the connect_meta_integration / connect_google_ads_integration
MCP tools), which keeps the browser tag and the server destination in sync and
preserves the shared-id dedup between them.