Copy as markdown[View .md](https://docs.funnelfizz.com/ai-agents/browser-handoffs "View the raw markdown for this page")[Open in Claude](https://claude.ai/new?q=Read%20https%3A%2F%2Fdocs.funnelfizz.com%2Fai-agents%2Fbrowser-handoffs.md%20and%20help%20me%20with%20this%20FunnelFizz%20topic%3A%20Browser%20handoffs "Open this page in Claude with context")[Open in ChatGPT](https://chat.openai.com/?q=Read%20https%3A%2F%2Fdocs.funnelfizz.com%2Fai-agents%2Fbrowser-handoffs.md%20and%20help%20me%20with%20this%20FunnelFizz%20topic%3A%20Browser%20handoffs "Open this page in ChatGPT with context")

# 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)[​](#stripe-checkout-subscribing-or-changing-plan "Direct link to Stripe Checkout (subscribing or changing plan)")

### Starting a checkout[​](#starting-a-checkout "Direct link to 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[​](#state-machine "Direct link to State machine")

`billing.poll_checkout` returns one of:

* `pending` — checkout in progress or not yet completed
* `completed` — Stripe webhook fired `checkout.session.completed`
* `failed` — payment was declined or the session errored
* `expired` — 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[​](#idempotency "Direct link to 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)[​](#stripe-customer-portal-managing-payment--invoices "Direct link to 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)[​](#oauth-connecting-providers "Direct link to 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[​](#browser-controlling-agents "Direct link to 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?[​](#why-urls-instead-of-automating "Direct link to 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[​](#related "Direct link to Related")

* [Safety tiers](https://docs.funnelfizz.com/ai-agents/safety-tiers.md) — T0/T1/T2 model.
* [MCP](https://docs.funnelfizz.com/ai-agents/mcp.md) — main MCP overview.
* [Connect Stripe](https://docs.funnelfizz.com/getting-started/connect-stripe.md) — first-time Stripe setup walkthrough.
