> ## Documentation Index
> Fetch the complete documentation index at: https://docs.natural.com/llms.txt
> Use this file to discover all available pages before exploring further.

> ## Agent Instructions
> When building application code, use `@naturalpay/sdk` for TypeScript or `naturalpay` for Python.
> When searching Natural documentation, use the docs MCP at `https://docs.natural.com/mcp`.
> Use the Natural CLI for terminal and CI automation.
> Start integration development and testing in the sandbox. Do not use production unless the user explicitly requests it.
> For sandbox requests that require `customerPartyId`, list customers first. Reuse only the intended fixture; otherwise create a customer.
> REST and SDK amounts use integer minor units. Payments MCP amounts use decimal strings with a required currency code.

# Browser Use

> Check out in a Browser Use cloud browser with a Natural browser token by routing only the card request through Natural's proxy

[Browser Use](https://browser-use.com) runs cloud browsers and browser agents. This guide shows how a Browser Use browser pays at a merchant's checkout with a Natural [browser token](/guides/concepts/browser-tokens): the agent types a stand-in card, and Natural's proxy swaps in the real card on the way to the payment processor.

<Warning>
  Don't put Natural's proxy in Browser Use's `customProxy`. It can't connect, and the browser fails with `ERR_TUNNEL_CONNECTION_FAILED` or hangs before the page loads:

  * Natural's proxy only accepts TLS connections (`protocol: "https"`, port 443). `customProxy` has no protocol field and connects over plain HTTP.
  * The proxy password from a claim is about 900 characters. `customProxy` accepts up to 255.
  * The browser has to trust Natural's proxy certificate for the allowed hosts. `customProxy` has no way to add a CA, and `ignoreCertErrors` turns off certificate checks for every site.
</Warning>

Instead, keep the browser on Browser Use's residential proxy and have your backend send only the card request through Natural's proxy. Your backend connects to the browser over CDP, pauses requests to `allowedHosts`, resends them through the proxy and hands the response back to the page. Everything else, including the merchant's own pages, stays on Browser Use's proxy.

You need:

* A Natural [agent key](/guides/concepts/agent-keys) with `wallets.read`, `payments.read`, `payments.create` and `saved_cards.use`
* A Browser Use API key in `BROWSER_USE_API_KEY`
* Python with `playwright`, `httpx` and `certifi` (`pip install playwright httpx certifi`). No local browser download is needed.

## How it fits together

1. **Shop.** The agent browses in a Browser Use browser and gets to the merchant's payment step.
2. **Open a session.** Your backend calls [`POST /card-sessions`](/api-reference/card-sessions/create-card-session) with the purchase and the processor host that receives the card, then sends the user the `approvalUrl`.
3. **Claim.** Once the user approves, [`POST /card-sessions/{sessionId}/claim`](/api-reference/card-sessions/claim-card-session) returns a stand-in card and a proxy login.
4. **Route the card request.** Your backend connects to the browser's `cdpUrl` and routes requests to `allowedHosts` through Natural's proxy.
5. **Pay.** The agent fills in the stand-in card and submits.

## 1. Shop

Create a browser, or use the one your V4 agent run is already using. Both have a `cdpUrl`:

```python theme={null}
import os, httpx

BU = httpx.AsyncClient(
    base_url="https://api.browser-use.com/api/v4",
    headers={"X-Browser-Use-API-Key": os.environ["BROWSER_USE_API_KEY"]},
)

browser_session = (await BU.post("/browsers", json={"proxyCountryCode": "us"})).json()
# For an agent run's browser: (await BU.get(f"/browsers/{browser_id}")).json()
cdp_url = browser_session["cdpUrl"]

# Your agent shops and stops at the payment step.
```

Treat `cdpUrl` as a credential: anyone who has it can control the browser.

### Find the processor host

`allowedHosts` must name the host the checkout sends the card number to, not the merchant's website. Wildcards aren't accepted. Do a dry run of the checkout with DevTools open, or log the checkout's `POST` requests over CDP, and look for the request whose body holds the card number.

Common hosts include `api.stripe.com`, `payments.braintree-api.com` and `pci-connect.squareup.com`. If the payment form encrypts the card in the browser, the proxy can't swap it in: see [when the proxy can't reach the checkout](#when-the-proxy-cant-reach-the-checkout).

## 2. Open a session and wait for approval

```python theme={null}
import asyncio

NATURAL = httpx.AsyncClient(
    base_url="https://api.natural.com",
    headers={"Authorization": f"Bearer {os.environ['NATURAL_API_KEY']}", "X-Instance-ID": instance_id},
)

async def natural(method, path, **kwargs):
    response = await NATURAL.request(method, path, **kwargs)
    response.raise_for_status()
    return response.json()

session = await natural(
    "POST",
    "/card-sessions",
    headers={"Idempotency-Key": f"card-session-{task_id}"},
    json={
        "data": {
            "attributes": {
                "cardId": card_id,  # eac_* from GET /card-external-accounts
                "allowedHosts": ["api.stripe.com"],
                "purchase": {"merchantName": "Acme Store", "amount": 4999, "currency": "USD"},
                "reference": task_id,
            }
        }
    },
)
if session.get("warning"):
    raise RuntimeError(session["warning"])  # Known checkout that encrypts the card in the browser

await send_to_user(session["approvalUrl"])

status = session["status"]
while status == "pending_approval":
    await asyncio.sleep(3)
    status = (await natural("GET", f"/card-sessions/{session['sessionId']}"))["status"]
if status != "approved":
    raise RuntimeError(f"Card session {status}")
```

The user has to approve, and you have to claim, before the session's `claimExpiresAt`: 10 minutes after you open it. Open the session when the agent is at the payment step, not earlier.

## 3. Claim and build a proxy client

```python theme={null}
import ssl, certifi

claim = await natural(
    "POST",
    f"/card-sessions/{session['sessionId']}/claim",
    headers={"Idempotency-Key": f"card-claim-{task_id}"},
)

ca_bundle = certifi.contents() + (await httpx.AsyncClient().get(claim["proxy"]["caBundleUrl"])).text
tls = ssl.create_default_context(cadata=ca_bundle)

p = claim["proxy"]
relay = httpx.AsyncClient(
    proxy=httpx.Proxy(f"https://{p['username']}:{p['password']}@{p['host']}:{p['port']}", ssl_context=tls),
    verify=tls,
    timeout=30,
)
```

Build a new client for every claim. The host and login differ between claims. The CA bundle keeps the system certificates and adds Natural's proxy certificate, so certificate checks stay on.

## 4. Route the card request

Register the routes on the browser context, not a page, so cross-origin iframes such as hosted card fields are covered too:

```python theme={null}
from playwright.async_api import async_playwright

DROP_REQUEST = {"host", "content-length", "connection", "accept-encoding"}
DROP_RESPONSE = {"content-encoding", "content-length", "transfer-encoding", "connection"}

async def through_natural(route):
    request = route.request
    if request.method == "OPTIONS":  # CORS preflight, no card data
        return await route.continue_()
    headers = {
        k: v for k, v in (await request.all_headers()).items()
        if k.lower() not in DROP_REQUEST and not k.startswith(":")
    }
    response = await relay.request(request.method, request.url, headers=headers, content=request.post_data_buffer)
    response_headers = {k: v for k, v in response.headers.items() if k.lower() not in DROP_RESPONSE}
    if cookies := response.headers.get_list("set-cookie"):
        response_headers["set-cookie"] = "\n".join(cookies)  # Playwright splits Set-Cookie on newlines
    await route.fulfill(status=response.status_code, headers=response_headers, body=response.content)

pw = await async_playwright().start()
browser = await pw.chromium.connect_over_cdp(cdp_url)
context = browser.contexts[0]
for host in claim["allowedHosts"]:
    await context.route(f"https://{host}/**", through_natural)
```

Keep the asyncio event loop running for the rest of the checkout. If it's blocked, for example by `time.sleep`, the paused requests never go out.

The card request leaves from Natural's proxy instead of Browser Use's residential IP. That's normal for a payment processor's API, but if the allowed host is the merchant's own domain behind bot protection, it may block the request.

## 5. Check the route, then pay

Send one request to each allowed host through `relay` first. A `4xx` from the host itself is fine: `api.stripe.com/` answers `404`. A proxy or TLS error means the route doesn't work.

```python theme={null}
try:
    for host in claim["allowedHosts"]:
        try:
            await relay.get(f"https://{host}/")
        except httpx.HTTPError as error:
            raise RuntimeError(f"Proxy routing to {host} failed. Don't submit the card.") from error

    # Your agent fills in claim["card"], claim["card"]["holderName"] and claim["billingAddress"]
    # on the payment page, then submits once.
finally:
    for host in claim["allowedHosts"]:
        await context.unroute(f"https://{host}/**")
    await relay.aclose()
    await pw.stop()  # Disconnects CDP; the Browser Use browser keeps running
```

If routing fails, stop. Never send the stand-in card around the proxy, and before retrying, check that the earlier attempt didn't go through.

Treat the claim as a secret: don't log or store any of it, including the request bodies your handler resends, the proxy URL, the cardholder name and the billing address.

## 6. Clean up

The `finally` block removes the routes, closes the proxy client and disconnects from the browser. Disconnecting doesn't stop a Browser Use browser: stop it with `PATCH /api/v4/browsers/{id}` and `{"action": "stop"}` when the agent is done. The proxy login stops working at the claim's `expiresAt` either way, 10 minutes after the claim by default. Set `ttlSeconds` on the session, up to 900, if checkout needs longer.

## When the proxy can't reach the checkout

Some checkouts encrypt the card in the browser or inside the processor's own iframe, so the stand-in number never appears in a request the proxy can see. `POST /card-sessions` returns a `warning` when an allowed host is a checkout known to do this. For those, open the session with `requestAgentToken: true`. When the user approves, they can also set up agent payments on the card, and the session returns a `mandateId`. Then pay with an [agent token](/guides/concepts/agent-tokens): it's a tokenized card the agent types in directly, with no proxy and no swap.

## Customers' cards

To pay with a [Connect](/guides/concepts/customers) customer's saved card, pass `customerPartyId` in `data.attributes` when you open the session, and as a query parameter when you get or claim it. See [pay with a customer's saved card](/guides/concepts/browser-tokens#pay-with-a-customers-saved-card).

## What each party sees

| Party | Sees |
| - | - |
| Your agent, your servers, Browser Use | The stand-in card and the proxy login, which stop working at `expiresAt` |
| Natural's proxy | The real card, sent only to `allowedHosts` |
| The merchant's payment processor | The real card, as in any checkout |

Your Natural agent key and the proxy login stay in your backend. Browser Use never sees the proxy login.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.