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

# Approvals

> Render payment approvals and receipts

An action that moves money stops for the customer's approval. The result comes back as `kind: "approval"`, Payman collects the approval from the customer on its own page, and your app renders two components:

```tsx theme={null}
import { ApprovalButton, PaymanStatusChip } from "@paymanai/connect-react";

{result.kind === "approval" && (
  <>
    <ApprovalButton
      approval={result.approval}
      onSettled={(receipt) => addMessage(`Sent. Ref ${receipt.transactionRef}.`)}
    />
    <PaymanStatusChip approval={result.approval} />
  </>
)}
```

`<ApprovalButton>` opens Payman's approval page in a popup and follows the approval until it ends. `<PaymanStatusChip>` narrates the state beside it. `onSettled` fires exactly once, with the receipt, when the payment posts.

Your app never approves. The SDK has no `approve()` and no type with a field that could carry an approval code.

<Warning>
  **Never build an input for the approval code.** Payman sends it to the customer and refuses it from apps; a code field in your UI is a phishing form.
</Warning>

## The approval object

`result.approval` carries everything the components need:

| Field       | Value                                               |
| ----------- | --------------------------------------------------- |
| `ref`       | Identifies the approval. Not a secret.              |
| `channel`   | `"payman_hosted"` or `"payman_console"`             |
| `url`       | Payman's approval page. `null` on `payman_console`. |
| `expiresAt` | When the approval lapses, about 15 minutes out.     |

When `url` is null the components render a notice instead of a button: the customer approves from their Payman account. It is still a real approval, so keep it rendered and watching.

## States

The chip shows each state; `onStateChange` on the button reports the same.

| State                  | Meaning                                                                        |
| ---------------------- | ------------------------------------------------------------------------------ |
| `pending`              | Waiting for the customer                                                       |
| `approved`             | The customer said yes. Money has not moved yet.                                |
| `settling`             | Payman is completing the payment                                               |
| `posted`               | Done. Carries `receipt: { transactionRef, postedAt }`.                         |
| `declined` / `expired` | Done, no payment                                                               |
| `unknown`              | The outcome could not be confirmed. Send the customer to their Payman account. |

`approved` is not terminal: wait for `posted` before saying a payment went out. Terminal states are `posted`, `declined`, `expired` and `unknown`.

Components watching one approval share a single stream, and a page reload re-subscribes on its own. `disconnect()` does not cancel a pending approval.

## Recording the payment

Record a payment from your server, never from what the browser posts. `approvals.watch(ref)` yields the same states server-side; surfaces without a browser use it in place of the components.

```ts theme={null}
const watcher = payman.forUser(userId).approvals.watch(ref);
for await (const event of watcher) {
  if (event.state === "posted") await recordPayment(event.receipt);
}
```

## Styling

The components are headless: real markup, a default class name, no styles of their own. One import styles them:

```ts theme={null}
import "@paymanai/connect-react/styles.css";
```

Passing `className` replaces the default class, so the stylesheet stops applying to that element.
