Browse documentation

Event tracking

FunnelKeeper can pull conversions from a database or GA4. It can also receive them. Two paths, one write:

PathAuthSource namespace
POST /products/{slug}/eventsaccount API key (fk_live_…)api:<slug>
POST /t/eventspublishable write key (fk_pub_…) in the bodyweb:<slug>

Both go through the same append-only ingest as the database adapters: identity stitch, channel resolution, optional transaction. Corrections are new rows, never updates.

Event shape

Every event needs a canonical stage (lead, converted, payment, …) and a dedupe_id. Retries with the same id are silent no-ops. The free-form event name is optional and lands in props.event — same convention as mapped GA4 events.

{
  "stage": "converted",
  "dedupe_id": "order-123",
  "name": "purchase",
  "identity": { "email": "buyer@example.com", "external_id": "cust_9" },
  "attribution": { "utm_source": "google", "gclid": "…" },
  "value": { "amount_cents": 9900, "currency": "USD", "kind": "charge" }
}

value.amount_cents is integer cents. Currency defaults to the product’s. Put revenue on exactly one stage per source ref — a second value on the same dedupe_id would share the transaction key and drop.

Browser snippet

Mint a write key from Integrations → Event tracking (or fk tracking-key rotate, or MCP rotate_tracking_key). Then:

<script src="https://api.funnelkeeper.com/fk.js" data-key="fk_pub_…" async></script>
<script>
  fk.identify({ email: "buyer@example.com" });
  fk.track("converted", { name: "purchase", value_cents: 9900, currency: "USD" });
</script>

On load the snippet captures utm_*, gclid, fbclid, and document.referrer. First-touch is persisted in localStorage and a cookie on the registrable domain (so www.example.com → app.example.com keeps the click). Last-touch updates on each landing. A per-browser anonymous id is the external_id fallback so pre-email conversions still stitch. There is no auto-pageview — GA4 already owns traffic.

fk.track / fk.identify may be called before /fk.js finishes loading: install a stub that queues onto fk.q, then the snippet flushes it. Pass dedupe_id when the same fact is also sent server-side so retries collapse.

The key is publishable on purpose. A leaked fk_pub_… can only write events into that product. Rotate it from Integrations if it leaks; the old key dies immediately.

Google Tag Manager

Manual. Integrations copies a Custom HTML tag. In GTM: Tags → New → Custom HTML → paste → trigger All Pages → Submit / Publish.

Auto-deploy. If GTM is connected, the button is on the Google Tag Manager card itself (Deploy tag to container) and under Event tracking (Deploy via Tag Manager); both ask you to confirm first, and both mint a write key with the tag if the product has none. FunnelKeeper creates a workspace, adds the snippet on All Pages, and publishes. This needs tagmanager.edit.containers and tagmanager.publish on the Google grant. A grant minted before those scopes existed returns 403 — reconnect Google (same connect flow) and retry.

The daily tag-audit job then reports whether the snippet is in the live container.

Server-side

Use the account API key. Dashboard, CLI, and MCP share this route.

curl -X POST https://api.funnelkeeper.com/products/demo-product/events \
  -H "Authorization: Bearer fk_live_…" \
  -H "Content-Type: application/json" \
  -d '{"events":[{"stage":"converted","dedupe_id":"order-123","name":"purchase","value":{"amount_cents":9900}}]}'
fk track --product demo-product --stage converted --dedupe order-123 --cents 9900 --name purchase

MCP tool: track_events. Batch cap 500 on the authenticated route, 25 on the browser route.

What this is not

The snippet is not a replacement for GA4. It does not auto-track pageviews or compete with a CDP. It is the push path for funnel facts you already know — a signup, a sale, a car sold — with attribution captured at the moment they happen.