Quickstart for agents
This page is executable. Every step is a copy-paste curl with the expected response
shown, so you can verify before proceeding. Set two variables your human gives you, then
run top to bottom. GA4 and GTM can be connected with zero human clicks if you
already have Admin on the client’s property/container (service-account path below).
If you do not, hand the human an OAuth URL — they never enter the FunnelKeeper
dashboard. The poll contract is in Connecting Google sources.
SEMrush stays a paste-a-key path.
export FUNNELKEEPER_API="https://api.funnelkeeper.com"
export FK_EMAIL="you@example.com" # the human's email
export FK_PASSWORD="a-long-passphrase" # 10+ characters
1. Create the account
curl -s -X POST "$FUNNELKEEPER_API/auth/signup" \
-H 'content-type: application/json' \
-d "{\"email\":\"$FK_EMAIL\",\"password\":\"$FK_PASSWORD\"}"
# expect: {"ok":true,"status":"verification_sent"}
A verification email goes to the human. Stop and ask them to click it. The response is identical whether or not the email already existed — no enumeration.
2. Log in and mint an API key
Sessions are for browsers; agents should hold an API key.
SESSION=$(curl -s -X POST "$FUNNELKEEPER_API/auth/login" \
-H 'content-type: application/json' \
-d "{\"email\":\"$FK_EMAIL\",\"password\":\"$FK_PASSWORD\"}" | jq -r .token)
curl -s -X POST "$FUNNELKEEPER_API/auth/keys" \
-H "authorization: Bearer $SESSION" \
-H 'content-type: application/json' \
-d '{"name":"agent"}'
# expect: {"id":"…","name":"agent","key":"fk_live_…"}
The key is shown exactly once. Store it:
export FUNNELKEEPER_API_KEY="fk_live_…"
3. Create a product
curl -s -X POST "$FUNNELKEEPER_API/products" \
-H "authorization: Bearer $FUNNELKEEPER_API_KEY" \
-H 'content-type: application/json' \
-d '{"name":"Demo Product","slug":"demo-product","currency":"USD"}'
# expect: {"id":"…","slug":"demo-product","name":"Demo Product","currency":"USD","stage":"validation"}
A 409 means the slug is taken — pick another.
4. Connect GA4 and GTM (service account — zero clicks)
If you have Admin on the client’s GA4 property and/or GTM account, this is the whole grant. No consent screen, no FunnelKeeper dashboard.
curl -s -X POST "$FUNNELKEEPER_API/connect/google/service-account/start" \
-H "authorization: Bearer $FUNNELKEEPER_API_KEY" \
-H 'content-type: application/json' \
-d '{"product_slug":"demo-product","kinds":["ga4","gtm"]}'
# expect: {"sa_email":"fk-…@….iam.gserviceaccount.com","kinds":["ga4","gtm"],"instructions":{…}}
export FK_SA_EMAIL="fk-…@….iam.gserviceaccount.com"
Grant that email on Google’s side using your Google access (not FunnelKeeper’s). Replace the placeholders with the client’s property id and GTM account/container ids:
# GA4 — Viewer on the property
curl -s -X POST "https://analyticsadmin.googleapis.com/v1beta/properties/${GA4_PROPERTY_ID}/accessBindings" \
-H "authorization: Bearer $GOOGLE_ACCESS_TOKEN" \
-H 'content-type: application/json' \
-d "{\"user\":\"$FK_SA_EMAIL\",\"roles\":[\"predefinedRoles/viewer\"]}"
# GTM — Publish on the container (Read is enough for the tag audit; Publish enables snippet deploy)
curl -s -X POST "https://tagmanager.googleapis.com/tagmanager/v2/accounts/${GTM_ACCOUNT_ID}/user_permissions" \
-H "authorization: Bearer $GOOGLE_ACCESS_TOKEN" \
-H 'content-type: application/json' \
-d "{\"emailAddress\":\"$FK_SA_EMAIL\",\"accountAccess\":{\"permission\":\"accountPermissionUnspecified\"},\"containerAccess\":[{\"containerId\":\"${GTM_CONTAINER_ID}\",\"permission\":\"publish\"}]}"
Then prove the grant landed. Without ids this lists what the service account can now see; with ids it activates and starts the backfill:
curl -s -X POST "$FUNNELKEEPER_API/connect/google/service-account/verify" \
-H "authorization: Bearer $FUNNELKEEPER_API_KEY" \
-H 'content-type: application/json' \
-d "{\"product_slug\":\"demo-product\",\"ga4_property_id\":\"$GA4_PROPERTY_ID\",\"gtm_container_path\":\"accounts/${GTM_ACCOUNT_ID}/containers/${GTM_CONTAINER_ID}\"}"
# expect: {"ok":true,"activated":["ga4","gtm"],"sa_email":"fk-…@…"}
No Google admin? Hand the human auth_url from POST /connect/google/start
(kinds: ["ga4","gtm"]) and poll /connect/google/status — they only ever see
Google’s consent page. MCP: connect_service_account / verify_service_account,
or connect_source_start for the OAuth fallback. CLI: fk connect google-sa --product demo-product.
5. Connect SEMrush
The human’s SEMrush API key (SEMrush → Profile → API). Stored encrypted; never test-called, because every SEMrush call burns their units.
curl -s -X POST "$FUNNELKEEPER_API/products/demo-product/connections/semrush" \
-H "authorization: Bearer $FUNNELKEEPER_API_KEY" \
-H 'content-type: application/json' \
-d '{"api_key":"'"$SEMRUSH_API_KEY"'","domain":"example.com"}'
# expect: {"ok":true,"connection_id":"…"}
6. Verify and read
curl -s -X POST "$FUNNELKEEPER_API/products/demo-product/connections/semrush/test" \
-H "authorization: Bearer $FUNNELKEEPER_API_KEY"
# expect: {"ok":true,"detail":"Key stored. SEMrush is never test-called — every call burns your units; the weekly snapshot is the live test."}
curl -s "$FUNNELKEEPER_API/portfolio" \
-H "authorization: Bearer $FUNNELKEEPER_API_KEY"
# expect: [{"slug":"demo-product","spend_30d_cents":0,"revenue_30d_cents":0,"cac_cents":null, …}]
Zeros are honest zeros — no data has synced yet. cac_cents is null, not 0, because
no customers exist to divide by. The Health endpoint (GET /health) tells you which
sources have delivered and when.
What an agent must never do
Queue cards (GET /queue) are resolved by humans. If you hold an approval tool (MCP:
approve_card), it exists to relay a decision your human just made — the actor
field is their name, not yours. The server’s policy engine enforces spend caps and
network walls regardless of what any client asks for.