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

# Onboarding Lifecycle

> End-to-end onboarding flow for partners and customers. Covers customer creation, terms and conditions, identification, status tracking, and activation.

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

<Steps>
  <Step title="Create a new customer">
    `POST /api/customers`

    Create 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:

    | `X-API-Version` | Created status |
    | - | - |
    | `2026-08-01` or later | `SigningsRequired`. Terms are the first step |
    | Earlier, or header omitted | `IdentificationRequired` |

    <Note>
      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 behavior they were created with, so adopting `2026-08-01` only affects customers you create after you start sending it. See [API Versioning](/versioning).
    </Note>

    <Accordion title="API Example">
      <CodeGroup>
        ```bash theme={null}
        curl --request POST \
             --header 'content-type: application/json; charset=utf-8' \
             --header 'idempotency-key: <unique-request-id>' \
             --header 'x-api-key: <your-api-key>' \
             --data '{"name": "new_amazing_customer", "email": "customer@example.com", "customer_type": "Person"}' \
             --url 'https://api.sandbox.iron.xyz/api/customers'
        ```
      </CodeGroup>
    </Accordion>
  </Step>

  <Step title="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](/changelog#2026-08-01) it no longer changes which document you get back. See [Terms and Conditions](#terms-and-conditions) for the current behavior and what changed.

    <Tip>
      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](#signing-new-terms-later), 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.
    </Tip>

    <Accordion title="API Example">
      <CodeGroup>
        ```bash Get terms for a country theme={null}
        curl --request GET \
             --header 'accept: application/json; charset=utf-8' \
             --header 'x-api-key: <your-api-key>' \
             --url 'https://api.sandbox.iron.xyz/api/terms-and-conditions?country=DEU'
        ```

        ```bash Record the signing theme={null}
        curl --request POST \
             --header 'accept: application/json; charset=utf-8' \
             --header 'content-type: application/json; charset=utf-8' \
             --header 'idempotency-key: <unique-request-id>' \
             --header 'x-api-key: <your-api-key>' \
             --data '{
               "content_id": "<id_from_terms_response>",
               "signed": true
             }' \
             --url 'https://api.sandbox.iron.xyz/api/customers/<customer_id>/signings'
        ```
      </CodeGroup>

      **Example response from terms-and-conditions:**

      <CodeGroup>
        ```json theme={null}
        [
          {
            "display_name": "Terms and Conditions",
            "id": "019ababb-ddd6-7f02-8f5d-7469a0e8afb6",
            "url": "https://example.com/terms-and-conditions"
          }
        ]
        ```
      </CodeGroup>

      Pass the `url` to your customer for review. The signing request takes `content_id` (the `id` from the terms response) and `signed: true`.
    </Accordion>
  </Step>

  <Step title="Verify the customer's identity">
    `POST /api/customers/{id}/identifications/v2`

    Create an identification using one of these methods:

    * Hosted MoonPay Enterprise KYC link
    * SumSub token sharing
    * KYC/KYB Sharing

    <Warning>
      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](/kyb#resubmitting-and-correcting-a-submission).
    </Warning>

    <Accordion title="API Example">
      <CodeGroup>
        ```bash theme={null}
        curl --request POST \
             --header 'accept: application/json; charset=utf-8' \
             --header 'content-type: application/json; charset=utf-8' \
             --header 'idempotency-key: <unique-request-id>' \
             --header 'x-api-key: <your-api-key>' \
             --url 'https://api.sandbox.iron.xyz/api/customers/<customer_id>/identifications/v2' \
             --data '{"type": "Link"}'
        ```
      </CodeGroup>
    </Accordion>
  </Step>

  <Step title="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`.

    <Note>
      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](#signing-new-terms-later).
    </Note>

    <Accordion title="API Example">
      <CodeGroup>
        ```bash Retrieve signings theme={null}
        curl --request GET \
             --header 'accept: application/json; charset=utf-8' \
             --header 'x-api-key: <your-api-key>' \
             --url 'https://api.sandbox.iron.xyz/api/customers/<customer_id>/required-signings'
        ```

        ```bash Sign a document theme={null}
        curl --request POST \
             --header 'accept: application/json; charset=utf-8' \
             --header 'content-type: application/json; charset=utf-8' \
             --header 'idempotency-key: <unique-request-id>' \
             --header 'x-api-key: <your-api-key>' \
             --data '{
               "content_id": "<id_from_required_signings>",
               "signed": true
             }' \
             --url 'https://api.sandbox.iron.xyz/api/customers/<customer_id>/signings'
        ```
      </CodeGroup>

      **Example response from required-signings:**

      <CodeGroup>
        ```json theme={null}
        [
          {
            "display_name": "Terms and Conditions",
            "id": "019ababb-ddd6-7f02-8f5d-7469a0e8afb6",
            "url": "https://example.com/terms-and-conditions"
          }
        ]
        ```
      </CodeGroup>

      Pass the `url` to your customer for review. The signing request takes `content_id` (the `id` from `required-signings`) and `signed: true`.
    </Accordion>
  </Step>

  <Step title="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.

    ```json theme={null}
    {
      "fiat_payout": {
        "usd": { "ach": "Active", "wire": "Active", "rtp": "Unavailable", "fednow": "Unavailable", "swift": "Active" },
        "eur": { "sepa": "Active", "swift": "Unavailable" },
        "gbp": { "fps": "Active", "chaps": "Active", "swift": "Unavailable" },
        "brl": { "pix": "Unavailable" }
      }
    }
    ```

    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](/webhooks) 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](#tracking-ability-changes).
  </Step>
</Steps>

<Note>
  Before an active customer can transact, register their wallet addresses for [Travel Rule](/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](/crypto-addresses#register-a-self-attested-crypto-address). See the [Crypto Addresses guide](/crypto-addresses).
</Note>

<Note>
  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](#signing-new-terms-later).
</Note>

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

| Parameter | Required | Description |
| - | - | - |
| `country` | Yes | ISO 3166-1 alpha-3 code of the customer's country (e.g. `DEU`) |

MoonPay Enterprise serves a single global document, the same for every customer regardless of country. `country` is still a required parameter, but its value no longer changes which document you get back. Pass the customer's ISO 3166-1 alpha-3 code. A 3-letter code MoonPay Enterprise doesn't map to a region (for example `MCO`) returns `500`, so don't retry it. 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:

```json theme={null}
"Invalid country code. Expected a 3-letter ISO 3166-1 alpha-3 code."
```

<Note>
  This is the current behavior, in effect since the [terms update noted in the changelog](/changelog#2026-08-01). 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.
</Note>

### 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](/webhooks) 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.

<Note>
  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`.
</Note>

## 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 missing required documents never reaches this state. It is rejected with `400` at creation, in every country, and no identification is created. See [Incomplete Submissions](/kyb#incomplete-submissions).

## Checking Outstanding Requirements

`GET /api/customers/{id}/requirements`

The customer `status` tells you the stage of onboarding. This endpoint tells you which verification requirements are satisfied, awaiting review, or need attention. Use `X-API-Version: 2026-10-01` or later for the response described here. See the [API Reference](/reference/customer/get-customer-requirements) for the selected version's schema.

<ResponseField name="requirements_due" type="array" required>
  Verification categories with outstanding requirements: `kyc`, `kyb`, or an empty array. Includes requirements awaiting review, so a non-empty array does not always mean the customer needs to act. An empty array does not confirm that terms are signed or payment rails are active.
</ResponseField>

<ResponseField name="complete" type="array" required>
  Requirements the customer has satisfied. Each item has `code` and `description`.
</ResponseField>

<ResponseField name="pendingReview" type="array" required>
  Requirements awaiting review. Each item has `code` and `description`. Show a waiting state for these items.
</ResponseField>

<ResponseField name="pending" type="array" required>
  Requirements awaiting customer input. Each item has `code` and `description`. Questionnaire items also include `missing_questions`, with each unanswered question's `id`, `title`, and nullable `section_id`.
</ResponseField>

<ResponseField name="issues" type="array" required>
  Requirements with review issues. Each item includes `code`, `description`, nullable `review_comment` and `retryable`, `reject_reasons`, and rejected document `steps`. See [Handling Review Issues](#handling-review-issues).
</ResponseField>

<ResponseField name="identification_id" type="string | null" required>
  The identification associated with the response. This is a top-level field, not a field on each requirement. It is `null` when no identification is associated with the response.
</ResponseField>

<ResponseField name="general_review_comment" type="string | null" required>
  Feedback about the overall submission, when available for requirements awaiting review or with issues. Show it alongside the more specific requirement and document comments. See [Displaying Onboarding Comments to Your Customer](#displaying-onboarding-comments-to-your-customer).
</ResponseField>

<ResponseField name="preflight_summary" type="object | null" required>
  Preliminary assessment, when available. Contains `status`, `missingInputs`, `reasons`, and `workflowErrors`. Its `status` is `likely_pass`, `fail`, `likely_fail`, or `inconclusive`. This is an early signal, not a verification approval or a replacement for the requirement buckets.
</ResponseField>

All four buckets are always present, including when empty. `pendingReview` uses that exact casing.

<Note>
  Earlier API versions and requests without `X-API-Version` keep the legacy format: `missing` means awaiting input, and `pending` means awaiting review. From `2026-10-01`, use `pending` for input and `pendingReview` for review, and read `reject_reasons[].error_code` instead of `reject_labels`. See [API Versions](/versioning).
</Note>

### Requirement Items

Use each item's `code` to identify the requirement. Verification codes are `kyc_selfie`, `kyc_identity`, `kyc_applicant_data`, `kyc_proof_of_residence`, `kyc_questionnaire`, `kyb_company_data`, `kyb_company_beneficiaries`, `kyb_company_documents`, `kyb_questionnaire`, `identification_kyc`, and `identification_kyb`.

`description` explains what the requirement is. It is not the reviewer's feedback or a stable value to match on. Read the comment and rejection fields for what needs fixing. Only pending questionnaire items contain `missing_questions`; ordinary pending items and completed items do not carry that field.

### Handling Review Issues

Review problems are returned inside a successful `200` response. Read each `issues` item even when the HTTP request succeeds. These are separate from the validation errors returned when an identification submission fails with `400`.

<ResponseField name="review_comment" type="string | null" required>
  Customer-facing feedback for this requirement. A `null` value does not mean there is no issue: also read `general_review_comment`, the rejection reasons, and document-step comments.
</ResponseField>

<ResponseField name="retryable" type="boolean | null" required>
  `true` means the customer can correct and resubmit to clear the issue. `false` means the issue is terminal. `null` means retryability is unknown; do not treat it as permission to retry. Follow the correction instructions for your integration, and contact [support](/support) if the next action is unclear.
</ResponseField>

<ResponseField name="reject_reasons" type="array" required>
  Review rejection reasons for the requirement.

  <Expandable title="reason properties">
    <ResponseField name="error_code" type="string" required>
      Stable code to use in application logic. See [Verification rejection codes](/errors#verification-rejection-codes) for the meanings.
    </ResponseField>

    <ResponseField name="description" type="string" required>
      Explanation of the code. The text can change; do not parse it or match on it.
    </ResponseField>
  </Expandable>
</ResponseField>

<ResponseField name="steps" type="array" required>
  Rejected document steps for this requirement. Several steps can have the same document type; keep each step's `id` to distinguish them.

  <Expandable title="step properties">
    <ResponseField name="id" type="string" required>
      UUID identifying the document step.
    </ResponseField>

    <ResponseField name="name" type="string" required>
      Document type, such as `PASSPORT` or `SHAREHOLDER_REGISTRY`.
    </ResponseField>

    <ResponseField name="description" type="string" required>
      Explanation of the document type.
    </ResponseField>

    <ResponseField name="review_comment" type="string | null" required>
      Customer-facing instructions for this specific document.
    </ResponseField>

    <ResponseField name="reject_reasons" type="array" required>
      This document's rejection reasons, each with `error_code` and `description`.
    </ResponseField>
  </Expandable>
</ResponseField>

For example, `verification_document_expired` means the document is outside its accepted validity period. `verification_screenshots` means an original image is required instead of a screenshot. Check both `issues[].reject_reasons[].error_code` and `issues[].steps[].reject_reasons[].error_code`, and use the [full rejection-code list](/errors#verification-rejection-codes) for their meanings.

Handle `verification_unknown_rejection` and codes your app does not recognize with a general feedback message. An empty `reject_reasons` array does not mean approval: screening, fraud, and policy reasons are not exposed by this endpoint. Keep the issue visible and use the available comments and `retryable` value to guide the next action.

### Reading the Buckets Correctly

Read all four buckets together. For example, company data can be `complete`, beneficiaries can be in `pendingReview`, the questionnaire can be in `pending`, and company documents can be in `issues` at the same time. Show the questionnaire and document corrections alongside the review progress; do not hide actionable work behind a waiting screen.

* **`complete`:** No action is needed for that requirement.
* **`pendingReview`:** Show that the item is awaiting review. Do not ask the customer to upload it again just because review is unfinished.
* **`pending`:** Guide the customer to supply the requested information. For questionnaires, use `missing_questions` to identify unanswered questions.
* **`issues`:** Read the comments and rejection codes, then check `retryable` before offering a correction or resubmission.

When detailed requirements are not yet available, `identification_kyc` or `identification_kyb` can represent the identification as a whole. In `pending`, it means the identification flow still needs input or has not started. In `pendingReview`, it means the submission is being processed or reviewed. Use top-level `identification_id` to find the associated identification. If it is `null`, there is no associated identification to resume; start the initial identification after completing required signings.

<Warning>
  A requirement remaining outstanding is not a reason to create another identification. Continue the existing flow or follow the correction process for your integration. See [Resubmitting and Correcting a Submission](/kyb#resubmitting-and-correcting-a-submission) for business submissions.
</Warning>

The following example shows all four buckets for a business customer. Beneficiaries are awaiting review, while the customer still needs to answer questionnaire questions and correct a shareholder registry.

```bash theme={null}
curl --request GET \
     --header 'Accept: application/json; charset=utf-8' \
     --header 'X-API-Key: <your-api-key>' \
     --header 'X-API-Version: 2026-10-01' \
     --url 'https://api.sandbox.iron.xyz/api/customers/<customer_id>/requirements'
```

<Accordion title="Example 200 response">
  ```json theme={null}
  {
    "identification_id": "01a08c3e-97fd-73d0-991a-3f8a3293037b",
    "complete": [
      {
        "code": "kyb_company_data",
        "description": "Company information required for business verification."
      }
    ],
    "pendingReview": [
      {
        "code": "kyb_company_beneficiaries",
        "description": "Associated-person information required for business verification."
      }
    ],
    "pending": [
      {
        "code": "kyb_questionnaire",
        "description": "Business information required for verification.",
        "missing_questions": [
          {
            "id": "sourceOfFunds",
            "title": "What is the source of the company's funds?",
            "section_id": "businessInformation"
          },
          {
            "id": "expectedMonthlyVolume",
            "title": "What monthly volume do you expect?",
            "section_id": null
          }
        ]
      }
    ],
    "issues": [
      {
        "code": "kyb_company_documents",
        "description": "Company documents required for business verification.",
        "review_comment": "Please correct the rejected company documents.",
        "retryable": true,
        "reject_reasons": [
          {
            "error_code": "verification_document_unsuitable",
            "description": "The document type or certification does not meet requirements."
          }
        ],
        "steps": [
          {
            "id": "10000000-0000-5000-8000-000000000001",
            "name": "SHAREHOLDER_REGISTRY",
            "description": "A document identifying the company's current shareholders.",
            "review_comment": "Provide a signed shareholder registry within the accepted validity period.",
            "reject_reasons": [
              {
                "error_code": "verification_document_unsuitable",
                "description": "The document type or certification does not meet requirements."
              },
              {
                "error_code": "verification_document_expired",
                "description": "The document is outside the accepted validity period."
              }
            ]
          }
        ]
      }
    ],
    "requirements_due": [
      "kyb"
    ],
    "preflight_summary": null,
    "general_review_comment": "Please address the outstanding company document issues."
  }
  ```
</Accordion>

A request for a customer outside your account returns `404`:

```json theme={null}
"Customer not found for the provided ID."
```

## Mapping Onboarding Statuses in Your App

Use the customer's `status` for lifecycle gates, then use the requirements response to decide what verification work to show. Apply the lifecycle gates first: a suspended or archived customer must not see a retry or ready-to-transact screen just because a requirement permits a retry.

| Customer `status` | What to show | What to check |
| - | - | - |
| `UserRequired` | Partner Area user setup | Create the required user before continuing |
| `SigningsRequired` | Terms signing | Call `GET /api/customers/{id}/required-signings`, present the returned documents, and record acceptance |
| `IdentificationRequired` | Verification tasks and review progress | Read `pending`, `issues`, and `pendingReview` as described below |
| `Active` | Customer account, with payment availability per rail | Check `GET /api/customers/{id}/abilities` for the specific rail before offering a transaction |
| `Suspended` | Account unavailable | Contact support; do not offer a verification retry as a way to remove the suspension |
| `Archived` | Account permanently deactivated | Do not offer transactions or restart onboarding for this customer |

For customers in `IdentificationRequired`, combine the buckets rather than picking one screen from a single identification status:

| Requirements state | In your UI |
| - | - |
| `pending` has items | Show the requested input or resume the existing hosted flow |
| `issues` has items with `retryable: true` | Show the feedback and the supported correction flow |
| An issue has `retryable: false` | Show the available feedback without a retry action for that issue |
| An issue has `retryable: null` | Show the available feedback; resolve the next action before offering a retry |
| `pendingReview` has items | Show review progress alongside any input or corrections still needed |
| Only `pendingReview` has outstanding items | Show a waiting state without requesting another submission |
| No outstanding items | Re-read customer status and required signings; do not infer activation from empty buckets |

<Note>
  The customer object's `identification_status` is deprecated and returns `null`. When you need the identification's own `status` or hosted `url`, use `GET /api/identifications/{id}` with the requirements response's `identification_id`. If that ID is `null`, no identification is associated with the response; do not interpret this as an approval or rejection.
</Note>

Use identification status for lifecycle context: `Pending` means the flow is in progress, `Processed` and `PendingReview` mean the submission is being processed or reviewed, and `Approved` means verification is approved. For `Declined`, check the requirement issues and retryability before offering another attempt. For `Expired`, follow the restart flow for your integration. `Archived` is terminal. Use requirement buckets for the individual tasks within that lifecycle.

Customers can return to `SigningsRequired` when new terms are due. For customers created with an API version earlier than `2026-08-01`, present terms before collecting identification data even if their initial status is `IdentificationRequired`. See [Terms and Conditions](#terms-and-conditions).

Subscribe to the `identification_status` [webhook](/webhooks) to track identification changes and re-fetch requirements. Track rail availability with the `customer_fiat_abilities` webhook. `Active` on the customer does not guarantee that a particular payment rail is active.

### 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](/limits-and-minimum#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:

| `status` | `with_edd` | Meaning |
| - | - | - |
| `Pending` | `null` / `false` | Standard KYC in progress |
| `Pending` | `true` | EDD flow in progress |
| `PendingReview` | `null` / `false` | Standard KYC under manual review |
| `PendingReview` | `true` | EDD complete at verification provider, awaiting compliance review |
| `Approved` | `null` / `false` | Standard KYC approved, no EDD |
| `Approved` | `true` | Approved through EDD |
| `Declined` | `null` / `false` | Standard KYC rejected |
| `Declined` | `true` | Rejected after EDD review |

### Edge Cases

| `status` | Meaning |
| - | - |
| `Suspended` | Blocked. Can happen at any stage. Contact support. |
| `UserRequired` | Partner-area user must be created first. Only applies if `requires_user` is enabled. |

### Displaying Onboarding Comments to Your Customer

Use the requirements response to tell your customer what needs attention in KYC or KYB. Read feedback at all three levels:

* **Overall submission:** `general_review_comment`.
* **Requirement:** `issues[].review_comment`.
* **Document:** `issues[].steps[].review_comment`, kept with the document's `id`, `name`, and `description`.

Show non-empty comments whenever the response contains them, including while other requirements are awaiting review. Do not restrict feedback to a customer's or identification's status. A `null` comment does not clear the issue: check the rejection reasons and the other comment levels.

Keep document instructions beside the document they refer to. Deduplicate repeated text within the same context, but keep comments for different documents separate even when the wording matches. An issue comment that repeats `general_review_comment` is returned as `null`; document comments can still repeat other feedback.

The [example response above](#reading-the-buckets-correctly) contains an overall comment, a company-document comment, and instructions for the rejected shareholder registry. This helper extracts all three levels and retains the requirement and document identifiers for your UI:

```typescript theme={null}
type RequirementsFeedback = {
  general_review_comment: string | null;
  issues: Array<{
    code: string;
    review_comment: string | null;
    steps: Array<{
      id: string;
      name: string;
      description: string;
      review_comment: string | null;
    }>;
  }>;
};

type VerificationMessage = {
  message: string;
  requirement_code?: string;
  step_id?: string;
  document_name?: string;
  document_description?: string;
};

function getVerificationMessages(
  requirements: RequirementsFeedback,
): VerificationMessage[] {
  const messages: VerificationMessage[] = [];
  const seen = new Set<string>();
  const general = requirements.general_review_comment?.trim();

  function add(comment: string | null, context: Omit<VerificationMessage, "message">) {
    const message = comment?.trim();
    if (!message) return;
    const key = JSON.stringify([context.requirement_code, context.step_id, message]);
    if (seen.has(key)) return;
    seen.add(key);
    messages.push({ message, ...context });
  }

  add(requirements.general_review_comment, {});
  for (const issue of requirements.issues) {
    if (issue.review_comment?.trim() !== general) {
      add(issue.review_comment, { requirement_code: issue.code });
    }
    for (const step of issue.steps) {
      add(step.review_comment, {
        requirement_code: issue.code,
        step_id: step.id,
        document_name: step.name,
        document_description: step.description,
      });
    }
  }
  return messages;
}
```

For an issue without comments, show an explanation based on the recognized rejection codes, or a general message if no code is available. Read both requirement and document `reject_reasons[].error_code`; see [Verification rejection codes](/errors#verification-rejection-codes) for their meanings. Check the issue's `retryable` value separately before showing a correction action.

<Warning>
  `step_status` on the identification is deprecated for reading customer requirements from `2026-10-01`. Use the requirements buckets and their feedback fields to build this UI. Do not infer required work from null legacy step results or display raw step verdicts as correction instructions.
</Warning>

***

## API Endpoints

| Endpoint | Purpose |
| - | - |
| `POST /api/customers` | Create the customer record |
| `GET /api/customers/{id}` | Customer lifecycle `status` and identification IDs |
| `POST /api/customers/{id}/identifications/v2` | Create an identification (`Link`, `Token`, `Person`, or `Business`) |
| `GET /api/customers/{id}/identifications` | Identifications with their own `status` and hosted `url`, when available |
| `GET /api/identifications/{id}` | One identification, using the top-level `identification_id` from requirements |
| `GET /api/customers/{id}/required-signings` | Documents the customer currently owes, at any point in their lifecycle. Empty list when nothing is outstanding (`409` before `2026-08-01`) |
| `POST /api/customers/{id}/signings` | Record acceptance of one document |
| `GET /api/customers/{id}/signings` | Full signing history, kept for audit. Not a substitute for `required-signings` |
| `GET /api/customers/{id}/requirements` | Verification work in `complete`, `pendingReview`, `pending`, and `issues` (`2026-10-01` or later) |
| `GET /api/customers/{id}/abilities` | Customer capabilities: `fiat_deposit` and `fiat_payout` (nested by currency then rail) plus a `currencies` array. Read once, then track changes with the `customer_fiat_abilities` webhook |
| `GET /api/terms-and-conditions?country={ISO3}` | The current terms document. Use before an identification exists |

<Note>
  `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](/idempotency).
</Note>

## Status Reference

### Customer Status

Returned on the customer object.

| Status | Description |
| - | - |
| `IdentificationRequired` | Must complete KYC/KYB |
| `SigningsRequired` | Must sign required documents. Reached before identification on `2026-08-01`, and also after approval if terms are outstanding |
| `Active` | Customer account active. Check payment abilities for the rail you need |
| `Suspended` | Blocked for compliance or fraud reasons |
| `UserRequired` | Partner-area user must be created first |
| `Archived` | Terminal. Set by `PUT /api/customers/{id}/archive`. The customer and all their identifications are permanently blocked from every service |

### Identification Status

Returned on each identification object.

| Status | Description |
| - | - |
| `Pending` | Customer has not started, or a business submission is still being processed by the verification provider |
| `Processed` | Documents submitted, awaiting review |
| `PendingReview` | Under compliance review |
| `Approved` | Approved |
| `Declined` | Rejected |
| `Expired` | Expired due to inactivity |
| `Archived` | Terminal. Set when the customer is archived. The record no longer drives customer status |

**Typical Flow:** `Pending` → `Processed` → `PendingReview` → `Approved` / `Declined`

The identification object also includes `with_edd` (boolean, nullable) to indicate whether EDD was applied. See [Tracking EDD Status](#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`).

| Status | Description |
| - | - |
| `Active` | Available for use |
| `Pending` | Activation in progress |
| `Unavailable` | Not offered for this customer |
| `Blocked` | Blocked for this customer |
| `Maintenance` | Temporarily unavailable |

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](/webhooks#example-payloads).

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

<Note>
  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.
</Note>
