OverviewIntroduction

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:

ResourceWhat it isTable
TagsGTM-like tag assignments (a template id plus params) fired by the SDKtenant_tags
SnippetsCustom JavaScript (code) injected by the SDKtenant_snippets
TransformersEvent transforms (map field mappings or code) applied by the SDK to every captured event before it is sent, in priority ordertransformers

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/&#123;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

StatusCodeMeaning
400bad_json, missing_field, bad_type, bad_config, bad_priority, nothing_to_updateMalformed 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
401unauthorizedMissing/invalid key, or an ak_ with no location bound
403agency_requiredWrite attempted with an sk_ key
404not_foundNo such tag assignment, snippet, or transformer for this location
429rate_limited120 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 /decide calls 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 call mythic.capture(...), read config, etc.
  • eventonly for event-triggered snippets (a snippet whose trigger_rules contain a mythic_event rule): { 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 event undefined.

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.