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_tokenminted 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
| Tool | What it does |
|---|---|
funnel.resolve_by_name | Find funnels by free-text query, ranked by match + activity |
funnel.confirm_target | Mint a single-use 10-min target token for a funnel mutation |
confirm_target | Generalized: 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)
| Tool | Notes |
|---|---|
funnel.create | Optionally returns a chainTargetToken for an immediate follow-up |
funnel.rename | Bound to action funnel.rename |
funnel.archive | Soft-delete; revertible |
funnel.split.create | Main, nested, extension, nested-extension via flags |
funnel.split.archive | Revertible |
funnel.extension.add | Builds the full Cons + Trial + Customer extension pipeline in one call |
funnel.stage.link_provider · .unlink_provider | Attach OAuth providers to stages |
funnel.link_tracking_site · funnel.link_stripe | Wire workspace resources to a funnel |
funnel.replay_history | Backfill historical Stripe customers onto stages + tracks (idempotent) |
funnel.feature_flags.update | Toggle per-funnel feature flags |
Email content
| Tool | Notes |
|---|---|
email.draft.create | Auto-creates missing splits, attaches DRAFT campaign |
email.draft.update | Edit subject/blocks/etc. on a DRAFT |
email.draft.archive | Delete a DRAFT |
email.draft.send_test | Send a test email to a verified address |
email.draft.schedule | Schedule the DRAFT to send at a future time |
email.draft.send_now | Promote DRAFT to live send. T1 funnel-token. Once sent, can't be unsent (tombstone in change feed). |
email.draft.create_drip | Multi-step drip in one call |
email.template.list · .create · .update · .preview | Template CRUD (no token for create/update — write scope is enough) |
email.template.delete | T1 entity-token (only required if linked to active sends) |
email.sender.add · .list · .verify_with_code · .update · .resend_confirmation · .get_status | Sender lifecycle |
email.sender.delete · email.domain.delete | T1 entity-token |
email.asset.request_upload_url · .list · .delete | Brand image library |
email.campaign.list · .get · .get_analytics · .list_recipients | Read-only |
Automations
| Tool | Notes |
|---|---|
automation.draft.create · .list · .archive · .add_step · .remove_step | Draft authoring |
automation.activate | Goes live. T1 funnel-token. Tombstone (can pause but already-triggered runs continue). |
automation.pause · .resume | State transitions on a live automation |
automation.canvas.get · automation.canvas.update | Read or replace the canvas state (snapshot saved on update) |
automation.list_runs · automation.get_stats | Read-only |
Tracking & integrations
| Tool | Notes |
|---|---|
tracking.site.add · .check · .list · .get_metrics | Setup + read |
tracking.site.update | write scope, no token |
tracking.site.delete | T1 entity-token. Tombstone — events history orphaned. |
integration.list · integration.check_connection | Read |
integration.start_oauth · integration.poll_oauth | Browser handoff (see Browser handoffs) |
integration.configure | Provider-specific config (e.g. GSC site URL, brand mentions keywords) |
integration.disconnect | T1 entity-token |
integration.stripe.connect · .get_setup_guide · .get_connection · .list_products · .sync_catalog · .list_catalog | Stripe-specific |
Workspace settings
| Tool | Notes |
|---|---|
workspace.update_brand · workspace.update_profile | write scope, no token (snapshot in mcp_changes.previousState) |
workspace.complete_onboarding | Idempotent |
workspace.custom_event.register · .update · .archive | Workspace 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
| FREE | HOBBY | HOBBY-trial | PRO | PRO-trial | |
|---|---|---|---|---|---|
| Read scope grantable | ❌ | ✅ | ✅ | ✅ | ✅ |
| Write scope grantable | ❌ | ✅ | ✅ | ✅ | ✅ |
| Admin scope grantable | ✅ (role-gated) | ✅ (role-gated) | ✅ | ✅ (role-gated) | ✅ |
| Mutations / day | 0 | 50 | 25 | 500 | 100 |
| Mutations / month | 0 | 500 | 150 | 5,000 | 500 |
| Mutations / minute | 0 | 15 | 10 | 60 | 30 |
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
- Target-bound. Every T1 mutation requires a
target_tokenvalidated against (api key, target, action). Wrong key, wrong action, wrong target, expired, or already-consumed →400 invalid_requestwith a structuredtokenStatusreason. Failed token validation does not consume mutation quota. - 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.
- 24-hour revert. Every successful revertible mutation creates an
mcp_changesrow. The agent can callmcp.revert_changewithin 24h. Irreversible actions write tombstone rows withrevertible: false. See Revert changes. - Audit log. Every
/api/public/*call writes anApiAuditLogrow with PII-redacted args, scope, tool, status, and duration. Surfaced in Settings → Developer → API keys → Activity. - Per-workspace advisory lock. Concurrent writes within a workspace serialize via Postgres advisory lock — UI and agent edits can't race.
- Plan/role downgrade. A plan downgrade or role demotion silently revokes scopes mid-key — the next call returns
403 forbidden_scopewithout 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
| Code | Meaning |
|---|---|
forbidden_scope | Plan/role doesn't grant this scope (filtered from tools/list if no scope path applies) |
plan_required | Plan-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_token | T2 tool called without an admin token — see Admin actions |
monthly_quota_exceeded | Hit 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.