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

# Render the Identity Verification Step in Your Own UI

> Render the identity verification step in your own view instead of inside the embedded checkout, for more control over when and where verification happens.

By default, when an onramp order requires identity verification (KYC), Crossmint collects it inside the embedded checkout component. If you would like more control over when and where the verification step shows up, you can decouple it: create the order via API, read the verification credentials that Crossmint already returns on the order, and render the step yourself with Crossmint's `CrossmintIdentityVerification` component.

This mostly affects where the verification step lives in your UI. You can surface it as its own screen, a modal, or a step in your flow, rather than having it appear inside the checkout iframe. The underlying flow stays Crossmint's, and the credentials on the order are enough to render it.

<Note>
  Crossmint runs the underlying identity verification provider on your behalf, and your application only ever integrates with Crossmint's component.

  If you instead want to run verification with your own UI **and** your own identity verification provider, see the [Identity quickstart](/identity/quickstart) for sharing the resulting KYC data with Crossmint.
</Note>

## How it works

<Steps>
  <Step title="Create the order and check the payment status">
    Create an onramp order via API. If the buyer needs to verify their identity, it comes back with `payment.status: "requires-kyc"`.
  </Step>

  <Step title="Read the verification credentials from the order">
    The `requires-kyc` response includes `payment.preparation.kyc`, the verification session Crossmint has prepared for this buyer.
  </Step>

  <Step title="Render the verification step in your own view">
    Pass those credentials to `CrossmintIdentityVerification` and render it wherever you want in your app.
  </Step>

  <Step title="Poll the order until verification resolves">
    Once the buyer finishes verifying, poll the order until the status moves off `requires-kyc`, then continue with payment as usual.
  </Step>
</Steps>

## 1. Prerequisites

Install the Crossmint React SDK, along with the base package that exports the shared types. No provider SDK is needed, since the verification UI is rendered by Crossmint:

<CodeGroup>
  ```bash npm theme={null}
  npm install @crossmint/client-sdk-react-ui @crossmint/client-sdk-base
  ```

  ```bash pnpm theme={null}
  pnpm add @crossmint/client-sdk-react-ui @crossmint/client-sdk-base
  ```

  ```bash yarn theme={null}
  yarn add @crossmint/client-sdk-react-ui @crossmint/client-sdk-base
  ```

  ```bash bun theme={null}
  bun add @crossmint/client-sdk-react-ui @crossmint/client-sdk-base
  ```
</CodeGroup>

<Note>
  Be sure to use the latest SDK versions.
</Note>

## 2. Create the order and detect the verification requirement

Create the order server-side as you normally would (see the [onramp quickstart](/onramp/quickstarts/react)). When the response comes back, inspect `payment.status`. If it is `requires-kyc`, read the credentials from `payment.preparation.kyc`:

```json Example order response theme={null}
{
    "orderIdentifier": "123",
    // ...
    "payment": {
        "method": "card",
        "currency": "usd",
        "status": "requires-kyc",
        "preparation": {
            "kyc": {
                "provider": "persona",
                "inquiryId": "inq_ANKvpcJxyJS5Gp2Supt9szsVm6a394"
            }
        }
    }
}
```

Pass the `payment.preparation.kyc` object straight to the component, not the whole order.

<Note>
  If you only want to preview what an order would look like, you can pass `state: "draft"` on creation. A draft order is not persisted, so it cannot be polled or paid: create the order for real (omit `state`, or pass `state: "create"`) and use that order's credentials and `orderId` for the verification step and the polling below.
</Note>

## 3. Render the verification step in your own view

Instead of letting the checkout collect the verification, render `CrossmintIdentityVerification` yourself with those credentials. You can place it wherever suits your app, such as a dedicated route, a modal, or a step in your onboarding. The component must be rendered inside `CrossmintProvider`:

```tsx components/VerificationStep.tsx theme={null}
"use client";

import { CrossmintIdentityVerification } from "@crossmint/client-sdk-react-ui";
import type { IdentityVerificationCredentials } from "@crossmint/client-sdk-base";

interface VerificationStepProps {
    credentials: IdentityVerificationCredentials;
    onComplete: () => void;
}

export function VerificationStep({ credentials, onComplete }: VerificationStepProps) {
    return (
        <div className="my-custom-verification-container">
            {/* Your own heading, copy, progress indicator, etc. */}
            <CrossmintIdentityVerification
                credentials={credentials}
                locale="en-US"
                onReady={() => console.log("Verification UI loaded")}
                onComplete={() => onComplete()}
                onCancel={() => console.log("The buyer left the verification")}
                onError={({ retriable, reason, message }) => {
                    console.error("Verification error", reason, message);
                    if (!retriable) {
                        // The buyer cannot finish this attempt. Show a terminal state.
                    }
                }}
            />
        </div>
    );
}
```

Since you own the surrounding view, the verification tends to feel more like a native step in your product than something embedded inside the checkout. The component renders a Crossmint-hosted iframe that resizes itself to its content, so your container controls the width and placement, and the height follows the current verification screen.

### Verification outcomes

`onComplete` reports one of `verified`, `pending-review`, `pending-manual-review`, `declined`, `expired`, `failed`, or `unknown`. Treat `unknown` as an unresolved outcome rather than a success, and fall back to the order status.

### If you also use the embedded checkout for payment

If the buyer pays through the embedded checkout rather than a fully headless payment flow, tell the checkout that your application owns the verification step by passing `identityVerificationHandling="external"`. The checkout then renders nothing for verification and keeps polling the order in the background, so it picks the flow back up at the payment step on its own:

```tsx components/OnrampCheckout.tsx theme={null}
"use client";

import {
    CrossmintProvider,
    CrossmintCheckoutProvider,
    CrossmintEmbeddedCheckout,
    useIdentityVerificationCredentials,
} from "@crossmint/client-sdk-react-ui";
import { VerificationStep } from "./VerificationStep";

export function OnrampCheckout({ orderId, clientSecret }: { orderId: string; clientSecret: string }) {
    return (
        <CrossmintProvider apiKey="YOUR_CLIENT_API_KEY">
            <CrossmintCheckoutProvider>
                <CrossmintEmbeddedCheckout
                    orderId={orderId}
                    clientSecret={clientSecret}
                    identityVerificationHandling="external"
                    payment={{
                        receiptEmail: "YOUR_USER_EMAIL",
                        crypto: { enabled: false },
                        fiat: { enabled: true },
                        defaultMethod: "fiat",
                    }}
                />
                <DecoupledVerification />
            </CrossmintCheckoutProvider>
        </CrossmintProvider>
    );
}

function DecoupledVerification() {
    // The checkout emits order updates to this hook, so the credentials arrive
    // without a second API call. Undefined until verification is required.
    const credentials = useIdentityVerificationCredentials();

    if (credentials == null) {
        return null;
    }

    return <VerificationStep credentials={credentials} onComplete={() => console.log("Verification submitted")} />;
}
```

For an order that does not come from the checkout context, the plain `getIdentityVerificationCredentials(order)` function is also exported from `@crossmint/client-sdk-react-ui`.

<Warning>
  Passing `identityVerificationHandling="external"` without rendering `CrossmintIdentityVerification` leaves the buyer with no way to finish the order. The checkout suppresses the verification step and all of its outcome screens, on the assumption that your application shows them instead.
</Warning>

## 4. Poll the order until verification resolves

The component callbacks fire when the buyer finishes, but the order status is the source of truth. After `onComplete`, poll the order until `payment.status` moves off `requires-kyc`:

```ts theme={null}
async function waitForVerification(orderId: string, maxAttempts: number = 60): Promise<string> {
    for (let attempt = 0; attempt < maxAttempts; attempt++) {
        const order = await getOrder(orderId); // GET /2022-06-09/orders/{orderId}
        const status = order.payment?.status;

        if (status !== "requires-kyc") {
            return status; // "awaiting-payment", "manual-kyc", "failed-kyc", ...
        }

        await new Promise((r) => setTimeout(r, 2000));
    }

    throw new Error("Verification did not resolve within the expected time");
}
```

Verification usually resolves within 15 seconds, so the loop above is generous on purpose: it allows for about two minutes before giving up. Adjust `maxAttempts` or add your own deadline to fit your UX. If you use the embedded checkout with `identityVerificationHandling="external"`, the checkout already polls for you, and `useCrossmintCheckout` returns a fresh order instead.

The resulting status tells you what to do next:

| Status               | Meaning                                               | What to do                                                                                 |
| :------------------- | :---------------------------------------------------- | :----------------------------------------------------------------------------------------- |
| `awaiting-payment`   | Verification passed and the order is ready to be paid | Continue to the payment step (embedded component or your headless payment flow).           |
| `pending-kyc-review` | The submission is being reviewed                      | Show a pending state and keep polling.                                                     |
| `manual-kyc`         | Verification needs manual review                      | Show a "Crossmint is reviewing your identity" state. The buyer is notified of the outcome. |
| `failed-kyc`         | Verification failed                                   | The buyer cannot proceed with this order. Show a terminal state.                           |

Because the checkout suppresses its own verification screens when your application owns the step, the pending, review, and rejected states are yours to render as well.

<Note>
  See the [Status Codes](/payments/headless/guides/status-codes) page for the authoritative list of order statuses.
</Note>

## 5. Continue with payment

Once the order reaches `awaiting-payment`, continue as you would in the standard flow, rendering the embedded checkout for payment or driving it yourself if you are fully headless. Decoupling verification only affects that step; the rest of the order lifecycle stays the same.

## Related

<CardGroup cols={2}>
  <Card title="User Onboarding and KYC" icon="id-card" iconType="duotone" href="/onramp/introduction/user-onboarding" />

  <Card title="Import KYC Data" icon="file-import" iconType="duotone" href="/onramp/guides/import-user-kyc-data" />

  <Card title="Status Codes" icon="list-check" iconType="duotone" href="/payments/headless/guides/status-codes" />

  <Card title="Onramp Quickstart (React)" icon="react" iconType="duotone" href="/onramp/quickstarts/react" />
</CardGroup>
