Skip to main content
MoonPay Enterprise gives you full visibility into your customer onboarding: clear status at every stage, written feedback from our compliance team when something needs fixing, and real-time insight into your customer’s payment abilities.

Steps to Onboard and Activate a Customer

1

Create a new customer

POST /api/customersCreate a customer record. Present the terms and conditions next, before you create an identification.The status the customer is created in depends on the API version you send:
The version is pinned to the customer when you create it, and it drives that customer’s onboarding for its lifetime. Existing customers keep the behaviour they were created with, so adopting 2026-08-01 only affects customers you create after you start sending it. See API Versioning.
2

Present and sign the terms and conditions

GET /api/terms-and-conditions?country={ISO3}Fetch and present the terms, then record acceptance via POST /api/customers/{id}/signings. The terms contain the data sharing agreement, so your customer must accept them before MoonPay Enterprise collects KYC data. country is a required parameter, but as of the terms update in the changelog it no longer changes which document you get back. See Terms and Conditions for the current behavior and what changed.
For customers created on 2026-08-01 or later you can skip this endpoint entirely and call GET /api/customers/{id}/required-signings instead. It returns the same document, takes no country, and is the same call you need after KYC and for every future document, so one code path serves the whole lifecycle. Use GET /api/terms-and-conditions?country={ISO3} when you need the terms before a customer record exists, or for customers created on an earlier version.
Example response from terms-and-conditions:
Pass the url to your customer for review. The signing request takes content_id (the id from the terms response) and signed: true.
3

Verify the customer's identity

POST /api/customers/{id}/identifications/v2Create an identification using one of these methods:
  • Hosted MoonPay Enterprise KYC link
  • SumSub token sharing
  • KYC/KYB Sharing
This endpoint is not an upsert. Every call creates a new identification record, and the newest record drives the customer’s status. If you create a new identification while another is in flight, the customer resets to IdentificationRequired and the older identification stops counting, even if it is approved later.Create one identification, then wait for it to reach a terminal status (Approved, Declined, or Expired) before creating another. To track progress, subscribe to the identification_status webhook or poll GET /api/customers/{id}/identifications. Never re-call this endpoint to refresh status or to correct a submission.An Idempotency-Key does not protect you here. It stops one request running twice. It does not stop two requests creating two records. See Resubmitting and Correcting a Submission.
4

KYC is approved and the customer is activated

Once approved, MoonPay Enterprise checks whether the customer has signed the current terms. If they have, and nothing else is outstanding, the customer becomes Active and you can move on. Having signed the terms in step 2, that is the normal outcome.Handle the other branch anyway. If anything is outstanding the status is SigningsRequired instead: call GET /api/customers/{id}/required-signings, present each returned document, and mark each as signed via POST /api/customers/{id}/signings. The customer becomes Active once none are left.When nothing is outstanding, the response depends on your API version: 2026-08-01 and later return 200 with an empty list, earlier versions return 409 Conflict.
This is not only an onboarding branch. An Active customer returns to SigningsRequired whenever a new document is published, and cannot transact until they sign it, so the flow you build here needs to stay reachable for the life of the account. See Signing new terms later.
Example response from required-signings:
Pass the url to your customer for review. The signing request takes content_id (the id from required-signings) and signed: true.
5

Check payout rail availability

abilities.fiat_payout is nested by currency, then by rail. Check the specific rail you plan to use, for example abilities.fiat_payout.usd.ach === "Active", before initiating payouts.
Each rail also has a _thirdparty variant (e.g. ach_thirdparty) for payouts to a third party. The abilities object also returns fiat_deposit with the same shape and a currencies array listing the flows (mint, redeem, onramp, offramp, swap) available per currency.Call the endpoint once here, then subscribe to the customer_fiat_abilities webhook and keep your copy current from the deliveries. Rails turn on at times you cannot predict from your own requests, so the webhook replaces polling this endpoint. See Tracking ability changes.
Before an active customer can transact, register their wallet addresses for Travel Rule compliance. Self-hosted wallets register with a signed proof-of-ownership message, or, for US and Rest of World customers, by self-attestation. See the Crypto Addresses guide.
A customer’s status reverts from Active to SigningsRequired or IdentificationRequired when new compliance actions are required (e.g. updated terms and conditions, fraud review, enhanced due diligence). Plan for this: see Signing new terms later.

Terms and Conditions

The terms and conditions contain the data sharing agreement between MoonPay Enterprise and your customer. Your customer must accept them before MoonPay Enterprise collects KYC data, so present the terms right after creating the customer and before creating an identification.

Fetching Terms Before KYC

For a customer created on 2026-08-01 or later, GET /api/customers/{id}/required-signings returns the outstanding document at any point in their lifecycle, including before identification. Prefer it: no country parameter, and the same call serves onboarding and every document afterwards. The endpoint below is still the way to fetch the terms before a customer record exists, and the only way for customers created on an earlier version. GET /api/terms-and-conditions?country={ISO3} MoonPay Enterprise serves a single global document, the same for every customer regardless of country. country is still a required parameter (pass any valid ISO 3166-1 alpha-3 code), but its value no longer changes which document you get back. The response is a list with id, url, and display_name, the same shape as required-signings. Present the url to your customer, then record acceptance via POST /api/customers/{id}/signings with content_id and signed: true. A malformed country code returns 400 with a plain string body:
This is the current behavior, in effect since the terms update noted in the changelog. Earlier API versions still resolve a region-specific document from country, and compare the signed terms against the region KYC verifies. See the changelog entry for that behavior if you’re integrating against an earlier version.

Validation After KYC

After the identification is approved, MoonPay Enterprise checks that the customer has signed the current terms document. If they have, and nothing else is outstanding, they’re Active; otherwise GET /api/customers/{id}/required-signings returns the document to present and sign.

Signing New Terms Later

Signing terms is not a one-time onboarding step. When MoonPay Enterprise publishes a new document, the previous signing no longer satisfies the requirement: affected customers move from Active back to SigningsRequired, and GET /api/customers/{id}/required-signings returns the new document. They cannot transact until they sign it. This is how new capabilities reach your customers. Access to an additional currency, a new payment rail, or a new product often depends on terms your customer has not seen yet. The faster they can sign, the faster you can turn those features on for them. So treat the signing flow you build during onboarding as a permanent, reusable path rather than a step you run once:
  • Poll or subscribe. Subscribe to the customer_status webhook and treat a transition into SigningsRequired as a prompt to act, rather than discovering it from a failed transaction.
  • Reuse one code path. For customers created on 2026-08-01 or later, required-signings returns whatever is currently outstanding at any point in the lifecycle. The same function that handles onboarding handles every later document.
  • Never cache “terms signed” as a permanent flag. Match on the content_id that required-signings returns. A customer who signed last year’s document has a signing on file but still owes the current one.
  • Keep it reachable in your UI. Your customer should be able to sign from wherever they already are, without contacting your support team or restarting onboarding.
Signings are kept for audit, never replaced. GET /api/customers/{id}/signings returns the full history, so an earlier document and the current one both appear. Presence of a signing does not mean the current document is signed. Compare against required-signings.

Handling Missing Information

If an identification is incomplete, the customer’s status is IdentificationRequired and a url is returned on the Identification object. Redirect your customer to this URL. It opens a hosted step-up flow that collects only the missing data. This occurs when:
  • A data point is found to be invalid, expired, or inconsistent
  • A limit triggers additional due diligence requirements
  • A Business submission registered outside the US is created without all required documents (see Incomplete Submissions). US businesses are rejected instead of held pending when documents are missing.

Checking Outstanding Requirements

GET /api/customers/{id}/requirements The customer status tells you which stage a customer is at. This endpoint tells you what specifically is outstanding. MoonPay Enterprise computes it on demand from the customer’s identifications, so there is no ingestion step and nothing to subscribe to. The response sorts every requirement into four buckets, plus a coarse summary:
array
Categories with at least one non-complete item, deduplicated and sorted. Values are kyc and kyb. Empty for a fully onboarded customer. Check this first, then drill into the buckets only when it is non-empty.
array
Requirements the customer has satisfied.
array
Requirements the customer has submitted that are awaiting review.
array
Requirements the customer has never submitted.
array
Requirements that were submitted and rejected, or that could not be processed. Read retryable to tell a correctable issue from a terminal one.
All four buckets are always present and any of them can be empty.

Requirement Items

Each bucket holds items with this shape:
string
required
Stable typed identifier. Branch on this. One of kyc_selfie, kyc_identity, kyc_proof_of_residence, kyc_questionnaire, kyb_company_data, kyb_company_beneficiaries, kyb_company_documents, kyb_questionnaire, identification_kyc, identification_kyb, kyc_data_missing, kyc_data_invalid, kyb_data_missing, kyb_data_invalid.
string
Human-readable reason from the verification provider. Can be null.
boolean
true when the customer can submit again to clear the item. false for terminal issues. null when it does not apply, which is the case for everything in complete and pending.
string
The identification that produced the item, when applicable.
array
Coded rejection reasons for the step, for example SCREENSHOTS or UNSATISFACTORY_PHOTOS. Empty when the step carries none.
array
The same reasons paired with human-readable text. Use these for display and keep reject_labels as the stable set to switch on.
array
Required questionnaire questions the customer left unanswered, with the question text they saw. Populated for kyc_questionnaire and kyb_questionnaire only.
Switch on code, and on reject_labels where you need detail. The description field is verbatim text from the verification provider and changes without notice. Do not parse it or match on it.

Reading the Buckets Correctly

A code sitting in missing means MoonPay Enterprise has not received that item. It does not mean your submission failed to arrive.
identification_kyb in missing while a KYB identification exists and is Pending is consistent, not a contradiction. A submission missing required documents is rejected with 400 at creation and never reaches this state — a Pending business identification here means verification is still being processed, not that you need to resend the business anywhere. See Incomplete Submissions.
Do not create a new identification because a requirement still reads as missing. That adds a competing record and resets the customer’s status. See Resubmitting and Correcting a Submission.
Example response:

Mapping Onboarding Statuses in Your App

Use the customer’s status and identification_status together to decide which screen your app shows. Both fields are returned on the customer object.
Typical compliance review turnaround is 24-48 hours. Customers can re-enter SigningsRequired at any time (e.g. updated terms). Active is not the finish line: confirm the specific banking rail you need is Active (e.g. abilities.fiat_payout.usd.ach). That’s when the customer is truly ready to transact. Subscribe to the customer_fiat_abilities webhook to be told when a rail flips, rather than polling GET /api/customers/{id}/abilities.

Tracking EDD Status

Each identification includes a with_edd field that indicates whether Enhanced Due Diligence was applied. This field is an optional boolean:
  • true: EDD was triggered (either by the partner or automatically by MoonPay Enterprise’s AML checks)
  • false: EDD was explicitly not required
  • null: Identification was created before this feature was available
with_edd can be set in two ways:
  1. Partner-initiated. Pass with_edd: true (Link flow) or include edd_questionnaire (Token/Person flow) when creating the identification. See Proactively Increasing Customer Limits.
  2. Automatically by MoonPay Enterprise. If AML checks determine EDD is required (e.g. customer resides in a high-risk jurisdiction), MoonPay Enterprise sets with_edd to true server-side.
Use status and with_edd together to understand where a customer is in the verification process:

Edge Cases

Displaying Onboarding Comments to Your Customer

When a customer’s KYC submission is incomplete or needs correction, our onboarding team writes feedback explaining what to fix. This feedback is available on the identification object via review_comment and step_status.*.comment fields. Display these comments to your customer so they know exactly what to fix before resubmitting. Show these comments when identification_status is Pending or Declined.
Do not display step_status results directly to the customer. The per-step breakdown (e.g. “identity: Declined, selfie: Approved”) creates confusion and support tickets. Instead, extract the comment fields and combine them into a single message.
Example identification response with feedback:
Extract and display all comments in a single message box:

API Endpoints

Idempotency-Key is required on every POST above (POST /api/customers, POST /api/customers/{id}/identifications/v2, POST /api/customers/{id}/signings). Generate a fresh UUID for each new operation, and reuse that same key when retrying that exact request. A retry sent with a new UUID is treated as a new operation and creates a second record. MoonPay Enterprise stores successful responses only, so retrying after an error runs the request again. See Idempotency.

Status Reference

Customer Status

Returned on the customer object.

Identification Status

Returned on each identification object. Typical Flow: PendingProcessedPendingReviewApproved / Declined The identification object also includes with_edd (boolean, nullable) to indicate whether EDD was applied. See Tracking EDD Status for the full interpretation table.

Ability Status

Returned on each rail leaf under abilities.fiat_payout and abilities.fiat_deposit (e.g. abilities.fiat_payout.usd.ach). Rails available per currency: usd carries ach, wire, rtp, fednow, and swift. eur carries sepa and swift. gbp carries fps, chaps, and swift. brl carries pix. Each has a _thirdparty variant.

Tracking Ability Changes

Abilities move without any request from you. An identification gets approved, your customer signs a new document, compliance changes their review outcome, or a banking provider finishes registering a rail: MoonPay Enterprise recomputes the customer’s abilities and delivers the new snapshot to the customer_fiat_abilities webhook. Subscribe to it and treat the deliveries as the source of truth for which rails are open. GET /api/customers/{id}/abilities is for the first read and for the currencies array, which the webhook does not carry. Polling it on a timer spends rate limit on unchanged responses and still leaves a window where you reject a transaction on a rail that already went Active.
Each delivery is a full snapshot of fiat_deposit and fiat_payout, not a diff, and MoonPay Enterprise does not suppress a snapshot identical to the last one it sent. Diff against your stored copy so a repeat delivery doesn’t re-trigger whatever you do when a rail opens.