Browser handoffs
A few "user actions" fundamentally need a real browser:
- Stripe Checkout — collecting card details on Stripe's PCI-compliant hosted page
- Stripe Customer Portal — managing payment method, viewing invoices
- OAuth callbacks — providers redirect back to a callback URL with state tokens
For these, MCP returns a { url, sessionId?, instructions } shape. Your agent renders the URL, the user clicks (or a browser-controlling agent navigates), and MCP exposes a poll endpoint so the agent knows when the action completes.
Stripe Checkout (subscribing or changing plan)
Starting a checkout
1. Agent reads available plans:
billing.list_plans()
→ [{ priceId, plan: "HOBBY"|"PRO", interval: "month"|"year", amount }]
2. User picks a plan in chat.
3. Agent starts checkout:
billing.start_checkout({ priceId: "price_..." })
→ {
url: "https://checkout.stripe.com/c/pay/...",
sessionId: "abc-123",
instructions: "Open this URL, complete payment with your card,
then return here. I'll detect when it's done."
}
4. Agent renders the URL.
- Text-only agents tell the user to click it.
- Browser-controlling agents navigate to it directly.
5. User completes Stripe Checkout.
6. Agent polls every ~5 seconds:
billing.poll_checkout({ sessionId: "abc-123" })
→ { state: "pending" }
... (Stripe webhook fires when checkout completes) ...
→ { state: "completed", completedAt: "...", plan: "PRO" }
7. Agent: "✓ Plan upgraded to PRO. You now have access to A/B testing,
profile search, and the AI funnel reports."
State machine
billing.poll_checkout returns one of:
pending— checkout in progress or not yet completedcompleted— Stripe webhook firedcheckout.session.completedfailed— payment was declined or the session erroredexpired— user abandoned; session expired after 24 hours
The Stripe webhook (/api/webhooks/stripe) is the source of truth. Poll is just reading that webhook's persisted state from BillingCheckoutSession.
Idempotency
The poll endpoint is safe to call as often as needed. Once the state is non-pending, it's final.
Stripe Customer Portal (managing payment + invoices)
For users already on a paid plan who want to update card, download invoices, or cancel via Stripe:
1. Agent: billing.open_portal()
→ {
url: "https://billing.stripe.com/p/session/...",
instructions: "Open this portal to manage your subscription,
payment method, and invoices. Changes you make
will be reflected here within a few seconds."
}
2. User clicks, makes changes in the portal.
3. Stripe webhooks update workspace state (subscription status, invoice history)
via the existing /api/webhooks/stripe handler. No poll needed — local state
is up-to-date by the time the user finishes.
The portal URL is single-use; calling billing.open_portal() again issues a fresh URL.
OAuth (connecting providers)
Provider connections (X, YouTube, LinkedIn, Instagram, TikTok, Reddit, GSC, Google Ads) require the user to authorize FunnelFizz in the provider's UI.
1. Agent: integration.start_oauth({
provider: "google_search_console",
funnelId?: "...", // optional auto-link on success
stage?: "awareness"
})
→ {
url: "https://accounts.google.com/o/oauth2/v2/auth?...",
pendingId: "xyz-789",
instructions: "Sign in with the Google account that owns the GSC
property and grant read access."
}
2. Agent renders URL. User authorizes.
3. Provider redirects to FunnelFizz, server stores tokens.
4. Agent polls:
integration.poll_oauth({ pendingId: "xyz-789" })
→ { state: "pending" | "connected" | "failed" | "needs_reconnect" }
5. When connected, the integration is linked. If `funnelId + stage` were
passed, it's also auto-attached to that stage.
Browser-controlling agents
Agents with computer-use capabilities (Claude with computer use, OpenClaw) can drive the browser directly:
- Open the returned URL in a tab
- Fill in card details (using the user's saved info or asking)
- Complete the flow
- Read the success page
For these agents, the instructions string is a hint, not a contract. Provider UIs change; the agent should be resilient to that and surface failures clearly.
Why URLs instead of automating?
Stripe Checkout and the customer portal are PCI-compliant hosted pages. We cannot legally proxy the card form through our own UI without re-undertaking PCI scope. The hosted pages are the right primitive.
For OAuth, the callback URL must be registered with each provider; we can't tunnel that through the MCP transport.
Related
- Safety tiers — T0/T1/T2 model.
- MCP — main MCP overview.
- Connect Stripe — first-time Stripe setup walkthrough.