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

# Customer API

> Look up a customer's KYC status, submit requirements, and export their verified identity, keyed on customerId.

The Customer API lets you check a customer's KYC status and submit what's missing, keyed on `customerId` instead of a separate identity resource. Use it once you have a connected customer to answer three questions, server-side or client-side: is this customer KYC'd, what's missing, and how do you submit it. It also lets you export a customer's verified identity to another system, with their consent.

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

## Prerequisites

* A connected customer. See [Connect a customer](/platform/guides/connect-a-customer). You need the customer's `id` from an active connection.
* 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 the customer. Secret-key calls are scoped to your customers; access-token calls are scoped to the token's own customer.

## How it works

1. You get the customer's `id` from an active connection.
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.

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

```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). |

## Export a customer's identity

Export a customer's verified MoonPay identity to another system, with their consent, so they don't have to verify again elsewhere.

### Prerequisites

* Customer export enabled on your partner account. Contact your MoonPay account team.
* A customer with an approved KYC record with MoonPay.
* A server that can call the export endpoint with your secret key.
* The customer's consent, captured by rendering a MoonPay-hosted consent frame on their device. See [Capture consent](#capture-consent).

### Capture consent

Render the consent frame with `client.setupCustomerExport()`, the same way
you'd render the [Auth frame](/platform/sdk-reference/web/setup-auth). The
customer reviews what's being shared and authorizes the export inside the
MoonPay-hosted frame.

```ts Capture consent theme={null}
import {
  createClient,
  type CustomerExportEvent,
} from "@moonpay/platform-sdk-web";

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

const exportResult = await client.setupCustomerExport({
  container: document.querySelector("#exportContainer"),
  onEvent: (event: CustomerExportEvent) => {
    switch (event.kind) {
      case "ready":
        // The consent UI is rendered. Reveal the container if you hide it while loading.
        break;
      case "complete":
        // Forward event.payload.token to your backend now. It expires at event.payload.tokenExpiresAt.
        console.log(event.payload.token, event.payload.tokenExpiresAt);
        break;
      case "error":
        console.error(event.payload);
        break;
    }
  },
});

if (!exportResult.ok) {
  console.error(exportResult.error.kind, exportResult.error.message);
} else {
  // Remove the frame from the DOM now that the flow has completed:
  exportResult.value.dispose();
}
```

The frame delivers the consent token on its `complete` event. Forward it to
your backend and call the export endpoint within its 5-minute validity
window.

```ts Export a customer's identity theme={null}
const res = await fetch(
  "https://api.moonpay.com/platform/v1/customers/export",
  {
    method: "POST",
    headers: {
      "X-Api-Key": "sk_test_123",
      Authorization: `Bearer ${consentToken}`,
    },
  },
);

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

```json Result theme={null}
{
  "data": {
    "basicDetails": {
      "firstName": "Jane",
      "lastName": "Doe",
      "dateOfBirth": "1990-01-15",
      "nationality": "USA"
    },
    "residentialAddress": {
      "country": "USA",
      "administrativeArea": "NY",
      "locality": "New York",
      "street": "350 Fifth Avenue",
      "postalCode": "10118"
    },
    "phoneNumber": { "number": "+14155551234" },
    "taxIdentifiers": [{ "type": "ssn", "value": "123-45-6789" }],
    "files": [
      {
        "id": "file_abc123",
        "type": "passport",
        "uploadedAt": "2026-06-01T12:00:00Z",
        "downloadUrl": "https://files.moonpay.com/..."
      }
    ]
  }
}
```

Fields you don't hold data for are `null` or omitted. `taxIdentifiers` is included when the customer has one on file; each entry's `type` is `tin`, `ssn`, or `cpf`, with `country` present only for `tin`. `residentialAddress.subStreet` and each file's `side` (`front` or `back`) are included only when applicable, for example a two-sided ID document. Each file's `downloadUrl` is pre-signed and expires after 60 minutes; if it expires before you fetch the file, get a fresh consent token and call the export endpoint again.

The consent token is consumed on the first call, whether it succeeds or not. Notable responses: `403` if the token isn't bound to a customer, `409` if the customer has no approved KYC to export, and `429` (with a `Retry-After` header) if you've exceeded the rate limit.

## Next steps

<CardGroup cols={2}>
  <Card title="Connect a customer" href="/platform/guides/connect-a-customer">
    Connect a customer's MoonPay account to get the `id` this API is keyed on.
  </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>

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