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

# The connection

> Consent, the held question, and the automatic replay.

A money question with no connection is not an error. `run()` answers `needs_connection`, holds the question, and includes the URL to offer:

```ts theme={null}
const result = await payman.forUser(userId).run(req.body.text);
// { kind: "needs_connection", reason: "no_grant",
//   connectUrl: "...", stashed: true }   // the question is already held
res.json(result);
```

Render the button from that result:

```tsx theme={null}
import "@paymanai/connect-react/styles.css";
import { PaymanProvider, ConnectButton, ResumeNotice } from "@paymanai/connect-react";

<PaymanProvider autoResume onResume={(result) => addToTranscript(result)}>
  <ResumeNotice />
  {result.kind === "needs_connection" && <ConnectButton result={result} />}
</PaymanProvider>
```

## The click, start to finish

1. `<ConnectButton>` opens Payman's consent page in a popup. It renders a real link as the blocked-popup fallback and retires itself once connected.
2. The customer signs in, picks a provider, and approves access to their Payman account.
3. Payman redirects to the mounted callback, which verifies `state`, exchanges the code, and stores the connection.
4. Before redirecting back, the callback starts the held question server-side. The reply is already being produced while the browser navigates.
5. The page lands with `?payman=connected` (plus `resume=1` when a replay started). `<PaymanProvider autoResume>` opens one stream on `{basePath}/resume/stream` and the answer arrives with nothing retyped.

While the connection holds (90 days), the same call is an ordinary turn and the button does not render. A denial lands with `?payman=denied`. Treat it as an answer, not an error, and offer again when a later turn needs it.

<Tip>
  Ask at the moment of need, not at onboarding. Offer the button in the turn that actually needed money or account data; the question is already held, so the answer follows the consent straight back. Your agent is what decides: give it the [Connect tool](/build/tool-integration) and let it pick the turns that need the connection.
</Tip>

## Watching the replay

```tsx theme={null}
const { state, result, liveText, progress } = usePaymanResume();
```

| Field      | Holds                                                                                   |
| ---------- | --------------------------------------------------------------------------------------- |
| `state`    | `"idle"` (nothing to resume, or the stream failed), `"resuming"`, or `"done"`           |
| `result`   | the turn's normal result, set at `"done"`; the provider's `onResume` fires once with it |
| `progress` | the current step: `{ phase: "started" \| "completed" \| "waiting", label }`             |
| `liveText` | the reply text accumulating as it streams                                               |

Progress frames carry a phase and the platform's own label, nothing else. Show `progress.label` while the turn runs rather than the raw `liveText`; `<ResumeNotice>` does exactly that and hides once output lands.

## Refresh and multiple instances

The resume stream attaches instead of starting. A refresh mid-replay catches up on buffered frames and follows live, and the held question can never run twice. When the callback landed on another instance, the stream falls back to the shared store, so multi-instance deployments need the shared store they already chose (see [any framework](/frameworks)).

Off the browser there is no page to attach. After handling your callback, call `resumePending()` to run the held question yourself.

<Warning>
  Record the resumed turn by having your server re-read the SDK's record of it, never by trusting what the page posted. A browser that can write "the payment went through" into your transcript can lie about money.
</Warning>
