Skip to main content
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 section for requirements you must meet before taking this integration to production.

Prerequisites

  • A connected customer. See Connect a customer. You need the customer’s id from an active connection.
  • Either a secret key or an 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.
Get a customer
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);
Result
{
  "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.
Submit KYC data
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.
Get an upload URL
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();
Upload the file
await fetch(uploadUrl.url, {
  method: "PUT",
  headers: uploadUrl.headers,
  body: passportImageBlob,
});
Confirm the upload
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.
Poll for the outcome
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 for the full flow.
Error codes include verification_rejected on a terminal rejection, and country_mismatch when submitted data conflicts with the declared country.

KYC status

ValueDescription
not_createdNo KYC session exists yet for this customer with your account.
collectingThe customer has outstanding requirements to submit.
verifyingMoonPay is processing the submitted data. No action is needed from you or the customer.
activeKYC is complete. The customer is in good standing.
unavailableKYC 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.
Render the consent frame with client.setupCustomerExport(), the same way you’d render the Auth frame. The customer reviews what’s being shared and authorizes the export inside the MoonPay-hosted frame.
Capture consent
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.
Export a customer's identity
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();
Result
{
  "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

Connect a customer

Connect a customer’s MoonPay account to get the id this API is keyed on.

API and SDK credentials

Understand the secret keys and access tokens this API accepts.

Handle challenges

Render the challenge frame when verification needs extra steps.