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

# Onboarding via API

> Onboard customers in your own UI with the Customer API: check KYC status, submit outstanding requirements, and handle verification.

Run KYC in your own UI. With the Customer API you own the verification screens: MoonPay tells you what's required through `kyc.status` and `kyc.requirements`, you capture the data from the customer, and you submit it, keyed on `customerId`. It answers three questions, server-side or client-side: is this customer KYC'd, what's missing, and how do you submit it.

This is the API-driven onboarding path. If you're deciding between this path, the hosted connect flow, and guest checkout, see [Choose an onboarding path](/platform/guides/onboarding-paths).

See the [Going Live](/platform/overview/going-live) section for requirements you must meet before taking this integration to production.

## Prerequisites

* A server that can create session tokens with your secret key.
* A client that can render the Auth frame, with the [SDK](/platform/sdk-reference/web/setup-auth) or [manually](/platform/frames/auth).
* Either a [secret key](/platform/guides/api-and-sdk-credentials#secret-key) or an [access token](/platform/guides/api-and-sdk-credentials#access-token) for API calls. Secret-key calls are scoped to your customers; access-token calls are scoped to the token's own customer.

Customers who arrive through the hosted [connect flow](/platform/guides/connect-a-customer) also work with this API. You need a customer `id` from an authenticated connection, however that connection was established.

## How it works

1. You [authenticate the customer](#authenticate-the-customer): create a session, check the connection with `skipKyc: true`, and render the Auth frame when the customer isn't connected yet. This gives you the customer's `id`.
2. You call `GET /customers/{id}` to read `kyc.status` and `kyc.requirements`.
3. You submit outstanding requirements with `PATCH /customers/{id}/kyc`, and upload any required files. Once every requirement is submitted, verification starts automatically. If MoonPay can't complete verification from the submitted data alone, the response includes a hosted challenge to render.
4. Verification runs asynchronously, so you poll `GET /customers/{id}` until it reaches a terminal status or surfaces new requirements, then handle the result.

## Authenticate the customer

Every Customer API call is keyed on a customer `id` from an authenticated connection. To establish one without the hosted connect flow, create a session, check the connection, and render the Auth frame.

On your server, create a session token with your secret key and send it to your client. Include the customer's `email` when you create the session: the [Auth frame](/platform/frames/auth) identifies the customer by that address and delivers the one-time passcode there. If no MoonPay account exists for the email yet, the OTP step creates one.

<CodeGroup>
  ```ts Create session token theme={null}
  // Server-side code example
  const url = "https://api.moonpay.com/platform/v1/sessions";

  const res = await fetch(url, {
    headers: {
      "Content-Type": "application/json",
      "X-Api-Key": "sk_test_123",
    },
    method: "POST",
    body: JSON.stringify({
      externalCustomerId: "your_user_id",
      deviceIp: "...ip address from client",
    }),
  });

  console.log(await res.json());
  ```

  ```json Result theme={null}
  {
    "sessionToken": "c3N0XzAwMQ=="
  }
  ```
</CodeGroup>

On your client, check for an existing connection with `client.getConnection({ skipKyc: true })`. The `skipKyc` option opts the check out of KYC-based statuses, so an unverified customer isn't blocked with `pending` or `failed` before you submit their data. The check still surfaces legal statuses: `termsAcceptanceRequired` appears when a Terms of Use attestation is outstanding.

When the status is `connectionRequired`, render the Auth frame with `client.setupAuth()`. The Auth frame handles authentication only, with no payment-method setup or KYC steps. On the `complete` event with `status: "active"`, you have the customer's `id` and subsequent SDK calls are authenticated automatically.

```ts Authenticate the customer theme={null}
import { createClient, type AuthEvent } from "@moonpay/platform-sdk-web";

const client = createClient({ sessionToken: "c3N0XzAwMQ==" });

const connectionResult = await client.getConnection({ skipKyc: true });

if (!connectionResult.ok) {
  // Handle error
  return;
}

if (connectionResult.value.status === "connectionRequired") {
  const authResult = await client.setupAuth({
    container: document.querySelector("#authContainer"),
    onEvent: (event: AuthEvent) => {
      switch (event.kind) {
        case "ready":
          // The auth UI is rendered. Reveal the container if you hide it while loading.
          break;
        case "complete":
          if (event.payload.status === "active") {
            // Customer authenticated — SDK calls are now authenticated.
            console.log(event.payload.customer.id);
          }
          break;
        case "error":
          console.error(event.payload);
          break;
      }
    },
  });

  if (!authResult.ok) {
    // Handle error
    console.error(authResult.error.kind, authResult.error.message);
    return;
  }

  // Remove the frame from the DOM now that the flow has completed:
  authResult.value.dispose();
}
```

If the connection check returns `status: "active"`, the customer is already authenticated: read `customer.id` from the result and skip the Auth frame. For the other statuses, including `termsAcceptanceRequired`, see [`client.getConnection()`](/platform/sdk-reference/web/get-connection#connection).

<Callout icon="circle-info" iconType="regular">
  The connection check primes the client with the token the Auth frame needs. If
  you call `setupAuth()` without a prior connection check that resolved with
  `status: "connectionRequired"`, it returns a `configurationError`. See
  [`client.setupAuth()`](/platform/sdk-reference/web/setup-auth) for the full
  event and error reference.
</Callout>

## Get a customer

Returns the customer's KYC standing and outstanding requirements.

```ts Get a customer theme={null}
const res = await fetch(
  `https://api.moonpay.com/platform/v1/customers/${customerId}`,
  {
    headers: { "X-Api-Key": "sk_test_123" },
  },
);

const { data: customer } = await res.json();
console.log(customer.kyc.status, customer.kyc.requirements);
```

```json Result theme={null}
{
  "data": {
    "id": "c1a2b3c4-0000-4000-8000-000000000000",
    "externalCustomerId": "your_user_id",
    "kyc": {
      "status": "collecting",
      "requirements": {
        "basicDetails": { "status": "complete" },
        "residentialAddress": {
          "status": "incomplete",
          "requiredFields": ["street", "locality", "postalCode"]
        }
      }
    }
  }
}
```

## Submit KYC data

Submit one or more outstanding requirement categories. Only send the categories listed as `incomplete` in `kyc.requirements`. For the fields and documents each country requires, see [KYC data requirements](/platform/guides/kyc-data-requirements).

```ts Submit KYC data theme={null}
const res = await fetch(
  `https://api.moonpay.com/platform/v1/customers/${customerId}/kyc`,
  {
    method: "PATCH",
    headers: { "Content-Type": "application/json", "X-Api-Key": "sk_test_123" },
    body: JSON.stringify({
      residentialAddress: {
        street: "123 Main St",
        locality: "San Francisco",
        administrativeArea: "CA",
        postalCode: "94105",
        country: "USA",
      },
    }),
  },
);

const { data: customer } = await res.json();
```

Returns the updated customer, so you can re-check `kyc.requirements` for what's still outstanding.

## Upload a file

For document-based requirements (for example, `identityDocuments`, `selfie`, or `proofOfAddress`), first get a presigned upload URL, `PUT` the file to it, then confirm the upload.

```ts Get an upload URL theme={null}
const uploadUrlRes = await fetch(
  `https://api.moonpay.com/platform/v1/customers/${customerId}/files/upload-url`,
  {
    method: "POST",
    headers: { "Content-Type": "application/json", "X-Api-Key": "sk_test_123" },
    body: JSON.stringify({ fileType: "passport", mimeType: "image/jpeg" }),
  },
);
const { data: uploadUrl } = await uploadUrlRes.json();
```

```ts Upload the file theme={null}
await fetch(uploadUrl.url, {
  method: "PUT",
  headers: uploadUrl.headers,
  body: passportImageBlob,
});
```

```ts Confirm the upload theme={null}
const filesRes = await fetch(
  `https://api.moonpay.com/platform/v1/customers/${customerId}/files`,
  {
    method: "POST",
    headers: { "Content-Type": "application/json", "X-Api-Key": "sk_test_123" },
    body: JSON.stringify({
      files: [{ uploadId: uploadUrl.uploadId, fileType: "passport" }],
    }),
  },
);
```

The upload URL is valid for 15 minutes.

## Poll for the outcome

Submitting the last outstanding requirement starts verification automatically, and it runs asynchronously. Poll `GET /customers/{id}` while `kyc.status` is `verifying`. Once it changes, verification has finished: a terminal value (`active` or `unavailable`) is a final outcome, and `collecting` means MoonPay needs more information from the customer. Submit the newly outstanding requirements with `PATCH /customers/{id}/kyc` to restart verification.

```ts Poll for the outcome theme={null}
async function pollCustomerKyc(customerId: string) {
  while (true) {
    const res = await fetch(
      `https://api.moonpay.com/platform/v1/customers/${customerId}`,
      { headers: { "X-Api-Key": "sk_test_123" } },
    );
    const { data: customer } = await res.json();

    if (customer.kyc.status !== "verifying") {
      return customer;
    }

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

## Handle the hosted challenge

When MoonPay can't complete verification from the submitted data alone, `kyc.challenge` is present on the `PATCH /customers/{id}/kyc` response (`{ url, expiresAt }`). Render this URL for the customer to finish verification; do not resubmit requirements. The challenge can also appear on a later `GET /customers/{id}` while it's still outstanding, so check for it whenever you re-fetch the customer. See [Handle challenges](/platform/guides/handling-challenges) for the full flow.

<Note>
  Error codes include `verification_rejected` on a terminal rejection, and
  `country_mismatch` when submitted data conflicts with the declared country.
</Note>

## KYC status

| Value         | Description                                                                                     |
| ------------- | ----------------------------------------------------------------------------------------------- |
| `not_created` | No KYC session exists yet for this customer with your account.                                  |
| `collecting`  | The customer has outstanding requirements to submit.                                            |
| `verifying`   | MoonPay is processing the submitted data. No action is needed from you or the customer.         |
| `active`      | KYC is complete. The customer is in good standing.                                              |
| `unavailable` | KYC cannot proceed for this customer (for example, an unsupported region, or a closed account). |

## Next steps

<CardGroup cols={2}>
  <Card title="Export customer data" href="/platform/guides/export-customer-data">
    Share a customer's verified KYC data with another system, with their
    consent.
  </Card>

  <Card title="Handle challenges" href="/platform/guides/handling-challenges">
    Render the Challenge frame when verification needs extra steps.
  </Card>

  <Card title="Choose a payment method" href="/platform/guides/payment-methods">
    Compare the payment surfaces you can build once the customer is verified.
  </Card>

  <Card title="API and SDK credentials" href="/platform/guides/api-and-sdk-credentials">
    Understand the secret keys and access tokens this API accepts.
  </Card>
</CardGroup>
