> ## 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.

# Kernel

> Check out in a Kernel cloud browser with a Natural browser token, without your agent seeing the real card number

[Kernel](https://www.kernel.sh) runs Chromium browsers in the cloud for agents. This guide shows how a Kernel 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. The real card number never reaches your agent, your servers or Kernel.

You need:

* A Natural [agent key](/guides/concepts/agent-keys) with `wallets.read`, `payments.read`, `payments.create` and `saved_cards.use`
* A Kernel API key in `KERNEL_API_KEY`. Custom proxies come with Kernel's [Start-Up and Enterprise plans](https://www.kernel.sh/docs/info/pricing).
* `@onkernel/sdk` and `playwright` (`npm install @onkernel/sdk playwright`)

## How it fits together

1. **Shop.** The agent browses in a Kernel 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. **Swap the proxy.** Create a Kernel custom proxy from the claim, with Natural's proxy certificate, and switch the running browser onto it.
5. **Pay.** The agent fills in the stand-in card and submits.

The agent stays in the same browser the whole time, so the cart and checkout state are kept.

## 1. Shop

```ts theme={null}
import Kernel from "@onkernel/sdk";
import { chromium } from "playwright";

const kernel = new Kernel();

const kernelBrowser = await kernel.browsers.create({ stealth: true, timeout_seconds: 1800 });
const browser = await chromium.connectOverCDP(kernelBrowser.cdp_ws_url);
const context = browser.contexts()[0] ?? (await browser.newContext());
const page = context.pages()[0] ?? (await context.newPage());

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

### Find the processor host

`allowedHosts` must name the host the checkout sends the card to, not the merchant's website. Wildcards aren't accepted. To find it, watch the checkout's requests:

```ts theme={null}
page.on("request", (request) => {
  if (request.method() === "POST") console.log(new URL(request.url()).hostname);
});
```

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

```ts theme={null}
const natural = async (path: string, init: RequestInit = {}) => {
  const response = await fetch(`https://api.natural.com${path}`, {
    ...init,
    headers: {
      Authorization: `Bearer ${process.env.NATURAL_API_KEY}`,
      "X-Instance-ID": instanceId,
      "Content-Type": "application/json",
      ...init.headers,
    },
  });
  const body = await response.json();
  if (!response.ok) throw new Error(`Natural API ${response.status}: ${JSON.stringify(body)}`);
  return body;
};

const session = await natural("/card-sessions", {
  method: "POST",
  headers: { "Idempotency-Key": `card-session-${taskId}` },
  body: JSON.stringify({
    data: {
      attributes: {
        cardId, // eac_* from GET /card-external-accounts
        allowedHosts: ["api.stripe.com"],
        purchase: { merchantName: "Acme Store", amount: 4999, currency: "USD" },
        reference: taskId,
      },
    },
  }),
});

await sendToUser(session.approvalUrl);

let status = session.status;
while (status === "pending_approval") {
  await new Promise((resolve) => setTimeout(resolve, 3000));
  ({ status } = await natural(`/card-sessions/${session.sessionId}`));
}
if (status !== "approved") throw new Error(`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 create a Kernel proxy

Create a [Kernel custom proxy](https://www.kernel.sh/docs/proxies/custom) from the claim. Pass the certificate at `caBundleUrl` as `ca_bundle` so the browser's traffic trusts Natural's proxy.

```ts theme={null}
const claim = await natural(`/card-sessions/${session.sessionId}/claim`, {
  method: "POST",
  headers: { "Idempotency-Key": `card-claim-${taskId}` },
});

const caBundle = await fetch(claim.proxy.caBundleUrl).then((response) => response.text());

const proxy = await kernel.proxies.create({
  type: "custom",
  name: `natural-${session.sessionId}`,
  protocol: claim.proxy.protocol,
  config: {
    host: claim.proxy.host,
    port: claim.proxy.port,
    username: claim.proxy.username,
    password: claim.proxy.password,
    ca_bundle: caBundle,
  },
});
```

Create a new Kernel proxy for every claim. The host and login differ between claims, and a Kernel proxy's settings can't be edited after it's created.

Kernel checks the connection when you create a proxy, so a wrong or expired login shows up here, before anything touches the browser. If you lose the claim response, retry the claim with the same `Idempotency-Key` for a fresh login: see [retrying a claim](/guides/concepts/browser-tokens#retrying-a-claim).

## 4. Swap the proxy

```ts theme={null}
await kernel.browsers.update(kernelBrowser.session_id, { proxy: { id: proxy.id! } });
```

The swap takes under a second. Your CDP connection, open pages and their state stay as they are, but requests in flight during the swap may fail, so swap while the payment page is idle, before the agent fills in the card. While the proxy is attached, all of the browser's traffic goes through it.

Kernel applies `ca_bundle` in its own network layer, so there's nothing to install in Chrome. Make sure you pass it: without it, HTTPS requests fail once the browser is on the proxy.

## 5. Check the route, then pay

Load every allowed host first. If the connection through the proxy doesn't work, for example because the login expired, Kernel returns a `5xx` status with an `X-Forwarder-Error` or `X-Kernel-Proxy-Error` header. A `4xx` from the host itself is fine: `api.stripe.com/` answers `404`.

Wrap the check and the payment in `try`/`finally` so the browser leaves the proxy, and the proxy is deleted, even when either one fails:

```ts theme={null}
try {
  const probe = await context.newPage();
  try {
    for (const host of claim.allowedHosts) {
      const response = await probe.goto(`https://${host}/`);
      const headers = response?.headers() ?? {};
      if (
        !response ||
        response.status() >= 500 ||
        headers["x-kernel-proxy-error"] ||
        headers["x-forwarder-error"]
      ) {
        throw new Error(`Proxy routing to ${host} failed. Don't submit the card.`);
      }
    }
  } finally {
    await probe.close();
  }

  // Your agent fills in claim.card, claim.card.holderName and claim.billingAddress
  // on the payment page, then submits once.
} finally {
  // Back to Kernel's default egress, and remove the proxy.
  await kernel.browsers.update(kernelBrowser.session_id, { proxy: { mode: "default" } });
  await kernel.proxies.delete(proxy.id!);
}
```

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 cardholder name and billing address.

## 6. Clean up

The `finally` block switches the browser back to Kernel's default egress and deletes the proxy. 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. 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, Kernel | 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 stays in your backend. Kernel only ever holds a proxy login for one purchase.


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