Skip to main content

Mutations & writes

The MCP server can do everything you can do in the dashboard — set up workspaces, build funnels, send emails, activate automations, even manage teams and billing. Every write is gated by one of three safety tiers (see Safety tiers for the full model):

  • T0 reads — free use, plan + role filtered at tools/list
  • T1 mutations — require a target_token minted after explicit user confirmation
  • T2 admin — require a 6-digit code emailed to the API-key-holder

This page focuses on T1 — the bulk of write activity.

The T1 flow (funnel-scoped)

User: "Draft an email to US trial users in my Widgets Pro funnel."


Agent: funnel.resolve_by_name({ query: "Widgets Pro" })
│ → matches[]

Agent: shows matches to user, asks them to confirm


User: "Yes, Widgets Pro Main."


Agent: funnel.confirm_target({ funnelId, action: "email.draft.create" })
│ → targetToken (10-min single-use)

Agent: email.draft.create({ targetToken, audience: { stage: "TRIAL", conditions: [...] }, ... })
│ → { campaignId, autoCreated[] }

Agent: tells user the draft is ready, links to dashboard for review

The agent must ask the user to confirm — even when there's a single exact match. There is no auto-confirm code path.

The T1 flow (entity-scoped)

For high-impact non-funnel writes — deleting tracking sites, templates, senders, domains, disconnecting integrations:

1. Agent enumerates entities:
tracking.site.list()
→ user picks which to delete

2. Agent: confirm_target({
targetType: "tracking_site",
targetId: "site-id",
action: "tracking.site.delete"
})
→ targetToken

3. Agent: tracking.site.delete({ id, targetToken })
→ site deleted; token consumed

Tool surface

Each tool's required scope and tier are documented in Tool reference. The categories below cover the writable surface.

Resolution + targeting

ToolWhat it does
funnel.resolve_by_nameFind funnels by free-text query, ranked by match + activity
funnel.confirm_targetMint a single-use 10-min target token for a funnel mutation
confirm_targetGeneralized: mint a token for any entity type (tracking_site, email_template, email_domain, email_sender, integration, workspace)

Funnel structure (scope: write, tier: T1 funnel-token)

ToolNotes
funnel.createOptionally returns a chainTargetToken for an immediate follow-up
funnel.renameBound to action funnel.rename
funnel.archiveSoft-delete; revertible
funnel.split.createMain, nested, extension, nested-extension via flags
funnel.split.archiveRevertible
funnel.extension.addBuilds the full Cons + Trial + Customer extension pipeline in one call
funnel.stage.link_provider · .unlink_providerAttach OAuth providers to stages
funnel.link_tracking_site · funnel.link_stripeWire workspace resources to a funnel
funnel.replay_historyBackfill historical Stripe customers onto stages + tracks (idempotent)
funnel.feature_flags.updateToggle per-funnel feature flags

Email content

ToolNotes
email.draft.createAuto-creates missing splits, attaches DRAFT campaign
email.draft.updateEdit subject/blocks/etc. on a DRAFT
email.draft.archiveDelete a DRAFT
email.draft.send_testSend a test email to a verified address
email.draft.scheduleSchedule the DRAFT to send at a future time
email.draft.send_nowPromote DRAFT to live send. T1 funnel-token. Once sent, can't be unsent (tombstone in change feed).
email.draft.create_dripMulti-step drip in one call
email.template.list · .create · .update · .previewTemplate CRUD (no token for create/update — write scope is enough)
email.template.deleteT1 entity-token (only required if linked to active sends)
email.sender.add · .list · .verify_with_code · .update · .resend_confirmation · .get_statusSender lifecycle
email.sender.delete · email.domain.deleteT1 entity-token
email.asset.request_upload_url · .list · .deleteBrand image library
email.campaign.list · .get · .get_analytics · .list_recipientsRead-only

Automations

ToolNotes
automation.draft.create · .list · .archive · .add_step · .remove_stepDraft authoring
automation.activateGoes live. T1 funnel-token. Tombstone (can pause but already-triggered runs continue).
automation.pause · .resumeState transitions on a live automation
automation.canvas.get · automation.canvas.updateRead or replace the canvas state (snapshot saved on update)
automation.list_runs · automation.get_statsRead-only

Tracking & integrations

ToolNotes
tracking.site.add · .check · .list · .get_metricsSetup + read
tracking.site.updatewrite scope, no token
tracking.site.deleteT1 entity-token. Tombstone — events history orphaned.
integration.list · integration.check_connectionRead
integration.start_oauth · integration.poll_oauthBrowser handoff (see Browser handoffs)
integration.configureProvider-specific config (e.g. GSC site URL, brand mentions keywords)
integration.disconnectT1 entity-token
integration.stripe.connect · .get_setup_guide · .get_connection · .list_products · .sync_catalog · .list_catalogStripe-specific

Workspace settings

ToolNotes
workspace.update_brand · workspace.update_profilewrite scope, no token (snapshot in mcp_changes.previousState)
workspace.complete_onboardingIdempotent
workspace.custom_event.register · .update · .archiveWorkspace event registry

Admin (T2 code-paste — see Admin actions)

Tool
team.invite_member · team.remove_member · team.change_role
billing.cancel_subscription · billing.resume_subscription · billing.change_plan
api_key.create · api_key.revoke
workspace.transfer_ownership · workspace.delete

Plan tiers + quotas

FREEHOBBYHOBBY-trialPROPRO-trial
Read scope grantable
Write scope grantable
Admin scope grantable✅ (role-gated)✅ (role-gated)✅ (role-gated)
Mutations / day05025500100
Mutations / month05001505,000500
Mutations / minute015106030

A "mutation" = one top-level tool call with write scope, regardless of fan-out. email.draft.create that auto-creates 2 splits + 1 draft = 1 mutation. Charge for the intent, not the implementation.

Admin (T2) actions don't count against mutation quotas — the code-paste step is sufficient throttling on its own.

Safety model

  1. Target-bound. Every T1 mutation requires a target_token validated against (api key, target, action). Wrong key, wrong action, wrong target, expired, or already-consumed → 400 invalid_request with a structured tokenStatus reason. Failed token validation does not consume mutation quota.
  2. Code-paste for admin. T2 actions (team, billing, keys, workspace transfer/delete) require a 6-digit code emailed to the API-key-holder. See Admin actions.
  3. 24-hour revert. Every successful revertible mutation creates an mcp_changes row. The agent can call mcp.revert_change within 24h. Irreversible actions write tombstone rows with revertible: false. See Revert changes.
  4. Audit log. Every /api/public/* call writes an ApiAuditLog row with PII-redacted args, scope, tool, status, and duration. Surfaced in Settings → Developer → API keys → Activity.
  5. Per-workspace advisory lock. Concurrent writes within a workspace serialize via Postgres advisory lock — UI and agent edits can't race.
  6. Plan/role downgrade. A plan downgrade or role demotion silently revokes scopes mid-key — the next call returns 403 forbidden_scope without burning quota.

Walkthrough: drafting a US-desktop trial campaign

User prompt:

"Send an email to everyone who signed up for a free trial from the US who entered through desktop in my Widgets Pro funnel. Use template 1, image1.png as the banner. Don't send — draft it for me."

// 1. Find the funnel.
funnel.resolve_by_name({ query: "Widgets Pro", intent: "mutate" })
// → { matches: [...], suggestion: "disambiguate" }

// 2. Show matches to the user. Wait for explicit confirmation.

// 3. Mint a token.
funnel.confirm_target({ funnelId: "fn_abc", action: "email.draft.create" })
// → { targetToken: "fft_…", expiresAt: "..." }

// 4. (Optional) Upload the banner image.
email.asset.request_upload_url({ mimeType: "image/png", sizeBytes: 84210, slot: "banner" })
// → { uploadUrl, r2Key, publicUrl }

// 5. Find a verified sender + the template id.
email.sender.list() // → senderId
email.template.list() // → template id for "Template 1"

// 6. Draft.
email.draft.create({
targetToken: "fft_…",
audience: {
stage: "TRIAL",
conditions: [
{ condition: "geography", value: "US" },
{ condition: "device_type", value: "Desktop" },
],
},
senderId,
subject: "Quick tip for your US desktop trial",
bodyMode: "TEMPLATE",
templateId,
blocks: { /* template variables incl. banner image url */ },
})
// → { campaignId, autoCreated: [...], funnelId, trackId }

If Trial / geography=US / device_type=Desktop doesn't yet exist as a track combination on the funnel, the server creates the missing tracks inside the same transaction as the draft and returns them in autoCreated. The agent surfaces this to the user.

Walkthrough: 3-step drip on Hobby→Pro upgrade

funnel.resolve_by_name({ query: "Widgets Pro" })
funnel.confirm_target({ funnelId, action: "email.draft.create_drip" })

email.sender.list()
email.template.list()

email.draft.create_drip({
targetToken,
triggerStage: "CUSTOMER",
triggerType: "subscription_paid",
name: "Hobby → Pro upgrade nurture",
emails: [
{ subject: "Welcome to Pro!", templateId, bodyMode: "TEMPLATE", delayDays: 0 },
{ subject: "Top 3 Pro features", templateId, bodyMode: "TEMPLATE", delayDays: 3 },
{ subject: "Ready to share with your team?", templateId, bodyMode: "TEMPLATE", delayDays: 3 },
],
})
// Status: DRAFT. To go live: automation.activate({ id, targetToken }).

Walkthrough: sending a draft live

// Drafted earlier; now ready to send.
funnel.resolve_by_name({ query: "Widgets Pro" })
funnel.confirm_target({ funnelId, action: "email.draft.send_now" })

email.draft.send_now({ draftId, targetToken })
// → { campaignId, recipientsCount, sentAt }
// Tombstone row added to mcp_changes (revertible: false).

Content engine

FunnelFizz does not become a CMS. The model is MCP-to-MCP federation: your agent has FunnelFizz MCP installed alongside Notion / Drive / GitHub MCPs. Brand voice + copy lives wherever you already manage it; the agent reads it via those other MCPs and hands the rendered content to FunnelFizz MCP via email.draft.create.

The only "content" stored in FunnelFizz is the email image library (uploaded via email.asset.request_upload_url).

Errors you'll see

CodeMeaning
forbidden_scopePlan/role doesn't grant this scope (filtered from tools/list if no scope path applies)
plan_requiredPlan-tier feature gate (e.g. profile search on HOBBY)
invalid_request + tokenStatus: "missing"Pass X-MCP-Target-Token header on the mutation request
invalid_request + tokenStatus: "expired"Token older than 10 min — mint a new one
invalid_request + tokenStatus: "consumed"Token already used — mint a fresh one for the next mutation
invalid_request + tokenStatus: "wrong_action"Token's action doesn't match this tool
invalid_request + tokenStatus: "wrong_key"Token belongs to a different API key
invalid_request + tokenStatus: "wrong_target"Entity-token bound to a different target type/id
missing_admin_tokenT2 tool called without an admin token — see Admin actions
monthly_quota_exceededHit a daily/monthly mutation cap — Retry-After header indicates reset

All errors are non-destructive — the underlying state is unchanged, the audit log captures the attempt, and failed token-validation calls do not consume mutation quota.