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

# Embed the payment form

> Take card payments on your own checkout page

Put Natural's payment form on your own checkout page. Your server creates a payment intent. The form takes the card and runs the payment.

<Snippet file="shared/prerequisites.mdx" />

Start in Sandbox. There the form loads on any site, so you can build on localhost. Set `NATURAL_API_KEY` to your Sandbox API key and call `https://api.sandbox.natural.com`. Before you take real payments, [add your site](#go-live).

<Snippet file="shared/cents-note.mdx" />

## Create the intent

Create the intent on your server with [`POST /payment-intents`](/api-reference/payment-intents/create-payment-intent).

<Snippet file="shared/instance-id-note.mdx" />

<CodeGroup>
  ```python Python theme={null}
  import uuid
  from naturalpay import Natural

  client = Natural(
      base_url="https://api.sandbox.natural.com",
      instance_id=str(uuid.uuid4()),
  )
  intent = client.payment_intents.create(
      amount=3920,
      currency="USD",
      description="Order 8842",
      idempotency_key="order-8842",
  )
  ```

  ```typescript TypeScript theme={null}
  import Natural from "@naturalpay/sdk";

  const client = new Natural({ baseUrl: "https://api.sandbox.natural.com" });
  const intent = await client.paymentIntents.create(
    {
      amount: 3920,
      currency: "USD",
      description: "Order 8842",
      idempotencyKey: "order-8842",
    },
    { instanceId: crypto.randomUUID() },
  );
  ```

  ```bash CLI theme={null}
  export NATURAL_BASE_URL=https://api.sandbox.natural.com

  natural payment-intents create \
    --amount 3920 \
    --currency USD \
    --description "Order 8842" \
    --x-instance-id "$(uuidgen)" \
    --idempotency-key order-8842
  ```

  ```bash cURL theme={null}
  curl -X POST https://api.sandbox.natural.com/payment-intents \
    -H "Authorization: Bearer $NATURAL_API_KEY" \
    -H "X-Instance-ID: $(uuidgen)" \
    -H "Idempotency-Key: order-8842" \
    -H "Content-Type: application/json" \
    -d '{"data":{"attributes":{"amount":3920,"currency":"USD","description":"Order 8842"}}}'
  ```
</CodeGroup>

Use the order as the idempotency key, so a retry within 48 hours returns the same intent instead of creating a second one.

The response includes `data.attributes.clientSecret`. Store `data.id` with the order, and send the secret to the browser. Treat it like a password. Don't log it or put it in a URL.

<Snippet file="api-examples/paymentIntents.create.response.mdx" />

## Add the form to your page

```bash theme={null}
npm install @naturalpay/react
```

```tsx theme={null}
import { PaymentForm } from "@naturalpay/react";

export function Checkout({ clientSecret }: { clientSecret: Promise<string> }) {
  return (
    <PaymentForm
      clientSecret={clientSecret}
      onComplete={({ status }) => showConfirmation(status)}
      // error.code and error.message are for you. Show the payer your own words.
      onError={(error) => console.error(error.code, error.message)}
    />
  );
}
```

Create the `clientSecret` Promise once, outside render. A new `clientSecret` or `theme` mounts a new form. `PaymentForm` removes the form when it unmounts. It works with server rendering, Strict Mode, and the Next.js app router.

| Prop | What it is |
| - | - |
| `clientSecret` | The intent's `clientSecret`, or a Promise of it. Required |
| `theme` | `"light"`, the default, or `"dark"`. See [Match your page](#match-your-page) |
| `onComplete` | Called once when the payer is done. See [Confirm the payment](#confirm-the-payment) |
| `onError` | Called when the form can't load or stops working. Without it, errors go to the console |
| `className` | Set on the `div` that holds the form |

## Confirm the payment

When the payer is done, `onComplete` gets `{ paymentIntentId, status }`. It fires once. If the intent had already finished when the form mounted, it fires right away, so a payer who presses Back to your checkout page sees your confirmation again.

| `status` | Meaning |
| - | - |
| `completed` | The payment succeeded |
| `processing` | The payment went in but isn't final yet. The form shows "Payment processing" |

Show your confirmation on `onComplete`. Don't ship the order from there. Ship it when the `paymentIntent.completed` [webhook](/guides/webhooks/receive-events) arrives on your server. The webhook arrives even if the payer closes the page.

A `processing` payment can still fail. If it does, you get a `cardPayment.failed` webhook and the intent goes back to `open`. The form keeps showing "Payment processing". To let the payer try again, render `PaymentForm` with a new `key`.

## Handle errors

When `onError` fires, the form is done. It can't take a payment anymore. To try again, render `PaymentForm` with a new `key`.

An error is a plain `{ code, message }` object. Match on `code`. We never rename or remove a code, but we do add new ones, so handle a code you don't know. The `message` is for your logs. Its wording can change.

A problem the payer can fix, like a declined card, shows inside the form. It doesn't call `onError`.

| Code | Meaning |
| - | - |
| `invalid_client_secret` | The secret is missing, isn't a `clientSecret`, or matches no intent |
| `client_secret_failed` | The `clientSecret` Promise rejected |
| `payment_intent_unavailable` | The intent is canceled, expired, or declined. The form stays to tell the payer |
| `domain_not_allowed` | Live mode on a site you haven't listed. See [Go live](#go-live) |
| `invalid_origin` | The page has no origin, like a sandboxed frame or a `file://` page |
| `invalid_option` | A `theme` other than `light` or `dark` |
| `frame_blocked` | The form didn't start within 15 seconds, or your [CSP](#content-security-policy) blocked it |
| `load_failed` | The form didn't load within 30 seconds, couldn't load the intent, or stopped working |
| `unsupported_protocol` | The form doesn't support this version of `natural.js`. Reload the page |

## Show the order on your page

The form shows the total, your business name, and your policies. Make sure your page shows:

* Each item, any tax or fee, and the total with its currency, matching the intent's `amount`
* Your country, and an email address or phone number for support
* Your delivery policy, if you ship goods

To change the order, [update the intent](/api-reference/payment-intents/update-payment-intent) on your server. The next time the payer presses Pay, the form shows the new total and asks them to press Pay again.

## Match your page

The form has no background of its own. Its Pay button uses the brand color from your Natural account. On a dark page, pass `theme="dark"`:

```tsx theme={null}
<PaymentForm clientSecret={clientSecret} theme="dark" />
```

Popups use the same theme.

## Place the form

* The form grows to fit what it shows. Don't give `PaymentForm` or its parents a fixed height with `overflow: hidden`.
* Size and position the form with `className` on `PaymentForm`. `natural.js` sets the size of its own frames.
* Popups, such as a 3-D Secure challenge or a sign-in code, open over your whole page.
* Serve your page over HTTPS. The form doesn't load on plain `http`, except on `localhost` in Sandbox.

## Test in Sandbox

Your page code doesn't change between Sandbox and live. The secret tells the form which one it's in. Pay with the [test cards](/guides/accept/test-in-sandbox#2-pay-with-a-test-card). When the form asks for a code, to save a card or to sign in, enter `000000`.

Sandbox skips the 3-D Secure challenge popup, Apple Pay, Google Pay, and the receipt email.

## Go live

Create the intent with your production API key at `https://api.natural.com`. Then list every site that shows the form. Open **Developers**, then **API keys**, and add them under **Checkout domains**.

A site is a scheme, host, and port, like `https://shop.example.com`. Browsers treat `example.com` and `www.example.com` as two sites, so list both if you use both. If you're a platform, your sites cover the intents you create for your merchants. On a site you haven't listed, `onError` gets `domain_not_allowed`.

## Content Security Policy

If your page sets a Content Security Policy, allow Natural's checkout origin in `script-src` and `frame-src`:

```text theme={null}
script-src https://checkout.natural.com;
frame-src https://checkout.natural.com;
```

That's the whole list. `natural.js` makes no network requests from your page and adds no styles. `PaymentForm` adds the `natural.js` script tag without a nonce. If your policy uses nonces, keep the source above or add `'strict-dynamic'`. If your page enforces Trusted Types, allow the one policy `PaymentForm` creates:

```text theme={null}
trusted-types natural;
```

## Browser support

| Browser | Supported from |
| - | - |
| Chrome and Edge | 111 |
| Safari and iOS | 16.4 |
| Firefox | 128 |

The form opens no popup windows, so it works in webviews and in-app browsers. Apple Pay and Google Pay may not appear there.


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