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

# Customer Onboarding (KYC)

> Set up your organization's wallets and bank accounts, then onboard individual customers with KYC.

Onboarding has two parts: first set up **your organization's** wallets and bank accounts, then onboard **your customers** by collecting their KYC.

## Set up your organization

Before you can create quotes or orders, your organization needs at least one registered wallet and one active bank account. Set these up via the API (shown below), or in the Etherfuse dashboard — wallets ([sandbox](https://sandbox.etherfuse.com/account/wallets) · [production](https://app.etherfuse.com/account/wallets)) and bank accounts ([sandbox](https://sandbox.etherfuse.com/account/banks) · [production](https://app.etherfuse.com/account/banks)).

<Info>
  Wallets and bank accounts here belong to **your organization** (the parent org). To onboard your **customers** and their accounts, see [Onboard your customers](#onboard-your-customers) below.
</Info>

<Warning>
  **Complete KYB before going to production.** We strongly recommend verifying your organization (Know Your Business) with Etherfuse before setting up production accounts. KYB simplifies wallet onboarding by letting you claim ownership at the organization level, can make you eligible for reduced fee tiers, and ensures your accounts are managed under your company's identity rather than individual credentials. Contact your Etherfuse representative to start the KYB process.
</Warning>

### Wallets

<Steps>
  <Step title="Register a Wallet — POST /ramp/wallet">
    <Expandable title="Details">
      Register a wallet for each blockchain you want to transact on via [POST /ramp/wallet](/api-reference/wallets/register-wallet). Supported blockchains: `solana`, `stellar`, `base`, `polygon`, `monad`.

      <CodeGroup>
        ```bash Sandbox theme={null}
        curl -X POST https://api.sand.etherfuse.com/ramp/wallet \
          -H "Authorization: <api_key>" \
          -H "Content-Type: application/json" \
          -d '{
            "publicKey": "<wallet_public_key>",
            "blockchain": "solana",
            "claimOwnership": true
          }'
        ```

        ```bash Production theme={null}
        curl -X POST https://api.etherfuse.com/ramp/wallet \
          -H "Authorization: <api_key>" \
          -H "Content-Type: application/json" \
          -d '{
            "publicKey": "<wallet_public_key>",
            "blockchain": "solana",
            "claimOwnership": true
          }'
        ```
      </CodeGroup>

      <Tip>
        **Set `claimOwnership: true`** — Claiming ownership links the wallet's KYC status to your organization's KYB status, so the wallet is automatically compliant. Without this flag, you'll need to complete individual KYC for each wallet you register.
      </Tip>

      <Info>
        This endpoint is idempotent — registering an already-active wallet returns the existing record. If the wallet was previously soft-deleted, it will be restored.
      </Info>
    </Expandable>
  </Step>

  <Step title="Check Wallet Status — GET /ramp/wallet/{wallet_id}">
    <Expandable title="Details">
      Verify your wallet is registered and check its KYC status via [GET /ramp/wallet/\{wallet\_id}](/api-reference/wallets/get-wallet) or list all wallets via [GET /ramp/wallets](/api-reference/wallets/list-wallets).

      <CodeGroup>
        ```bash Sandbox theme={null}
        curl -H "Authorization: <api_key>" \
          https://api.sand.etherfuse.com/ramp/wallet/<wallet_uuid>
        ```

        ```bash Production theme={null}
        curl -H "Authorization: <api_key>" \
          https://api.etherfuse.com/ramp/wallet/<wallet_uuid>
        ```
      </CodeGroup>

      The response includes `kycStatus` and `claimedOwnership`:

      ```json theme={null}
      {
        "walletId": "5915dfc1-8995-4465-9c04-76470298b8b3",
        "publicKey": "0x18F6bc4028016D5353f9A68D3Fa5F5c03AA527a0",
        "blockchain": "base",
        "kycStatus": "approved",
        "claimedOwnership": true
      }
      ```

      <Info>
        A wallet must have `kycStatus: "approved"` before it can be used in orders. With `claimOwnership: true` and an approved org, this happens automatically.
      </Info>
    </Expandable>
  </Step>

  <Step title="Delete a Wallet — DELETE /ramp/wallet/{wallet_id}">
    <Expandable title="Details">
      Soft-delete a wallet and release ownership via [DELETE /ramp/wallet/\{wallet\_id}](/api-reference/wallets/delete-wallet). The wallet can be re-registered later.
    </Expandable>
  </Step>
</Steps>

### Bank accounts

<Warning>
  **Treat production bank account registration as permanent.** Mexican banking regulations make it extremely difficult to change or replace a bank account once it has been registered under a given RFC. Double-check the CLABE, RFC, and CURP before submitting in production. Errors may require manual intervention with Etherfuse support and can delay your ability to transact.
</Warning>

<Warning>
  **CLABEs with the `646` prefix are not supported.** Please use a CLABE from a different bank.
</Warning>

<Steps>
  <Step title="Register a Bank Account — POST /ramp/customer/{customer_id}/bank-account">
    <Expandable title="Details">
      Register a Mexican bank account (CLABE) via [POST /ramp/customer/\{customer\_id}/bank-account](/api-reference/bank-accounts/create-bank-account-api), using your own organization's ID as the `customer_id`.

      <CodeGroup>
        ```bash Sandbox theme={null}
        curl -X POST https://api.sand.etherfuse.com/ramp/customer/<org_uuid>/bank-account \
          -H "Authorization: <api_key>" \
          -H "Content-Type: application/json" \
          -d '{
            "account": {
              "transactionId": "<uuid>",
              "firstName": "Juan",
              "paternalLastName": "Garcia",
              "maternalLastName": "Lopez",
              "birthDate": "19900515",
              "birthCountryIsoCode": "MX",
              "curp": "GAJU900515HDFRNN09",
              "rfc": "XEXX010101000",
              "clabe": "012345678901234567"
            }
          }'
        ```

        ```bash Production theme={null}
        curl -X POST https://api.etherfuse.com/ramp/customer/<org_uuid>/bank-account \
          -H "Authorization: <api_key>" \
          -H "Content-Type: application/json" \
          -d '{
            "account": {
              "transactionId": "<uuid>",
              "firstName": "Juan",
              "paternalLastName": "Garcia",
              "maternalLastName": "Lopez",
              "birthDate": "19900515",
              "birthCountryIsoCode": "MX",
              "curp": "GAJU900515HDFRNN09",
              "rfc": "GAJU9005156V3",
              "clabe": "012345678901234567"
            }
          }'
        ```
      </CodeGroup>

      <Info>
        **Sandbox uses a placeholder RFC to auto-approve the account.** Submit `XEXX010101000` as the `rfc` in sandbox — this causes Etherfuse to skip SPEI provider registration and mark the account `compliant: true` immediately. Any other RFC in sandbox is sent through to the SPEI provider's sandbox environment and treated as a real registration — it will not auto-approve. The placeholder is rejected in production; use the real RFC there. See the [Sandbox Reference](/test-environment) for details.
      </Info>
    </Expandable>
  </Step>

  <Step title="Check Bank Account Status — GET /ramp/bank-accounts">
    <Expandable title="Details">
      List your organization's bank accounts via [GET /ramp/bank-accounts](/api-reference/bank-accounts/list-bank-accounts) or get a specific one via [GET /ramp/bank-account/\{bank\_account\_id}](/api-reference/bank-accounts/get-bank-account).

      <CodeGroup>
        ```bash Sandbox theme={null}
        curl -H "Authorization: <api_key>" \
          https://api.sand.etherfuse.com/ramp/bank-accounts
        ```

        ```bash Production theme={null}
        curl -H "Authorization: <api_key>" \
          https://api.etherfuse.com/ramp/bank-accounts
        ```
      </CodeGroup>

      <Info>
        A bank account must be `status: "active"` and `compliant: true` before it can be used in orders.
      </Info>
    </Expandable>
  </Step>
</Steps>

## Onboard your customers

Your customers complete **Know Your Customer (KYC)** verification before they can transact — they submit identity data, upload a selfie and a government-issued ID, and sign three agreements (electronic signature, terms and conditions, customer agreement). It works the same in sandbox as in production.

You have two ways to collect that data:

| Approach                      | Best For                                                                                                               | Section                       |
| ----------------------------- | ---------------------------------------------------------------------------------------------------------------------- | ----------------------------- |
| **Hosted UI (Presigned URL)** | Fastest integration — Etherfuse handles identity collection, document upload, and agreement signing in one hosted page | [Hosted UI](#hosted-ui)       |
| **Programmatic**              | White-label experience — collect data in your own UI, submit via API, full control over the customer journey           | [Programmatic](#programmatic) |

<Tip>
  **Start with the Hosted UI** if you want the fastest path to testing. You can migrate to Programmatic later without re-onboarding existing customers.

  In the **Programmatic** flow, the customer completes the required agreements step by being launched into the app to review and sign them. See [Sign agreements](/guides/user-launch-flows#sign-agreements).
</Tip>

<Warning>
  **KYC Requirements (Production)**

  In production, the following must be submitted for each customer:

  1. **Selfie** — A photo of the customer's face
  2. **Government-issued identification** — A valid ID document (passport, driver's license, national ID)
  3. **Proof of address** — Document showing the customer's current address (utility bill, bank statement)

  If the government-issued ID includes the customer's address (e.g., a driver's license), it can satisfy both requirements. After all data is submitted, Etherfuse reviews the information for accuracy before approving the customer.

  In **sandbox**, you can complete KYC using fake data — no real personal information is required.
</Warning>

<Info>
  **How identity is verified.** Etherfuse verifies identity with **face match + liveness** — the selfie is matched against the government-issued ID, plus a liveness check. There's no single mandated document (e.g. no INE requirement specifically); any valid government ID works. A **liveness check is required for Mexican nationals transacting against their own bank account** — a SPEI compliance requirement.

  **Production vs. sandbox collection.** In **production**, agreement signing and the liveness check are completed through a minimal Etherfuse UI step that you can embed (iframe) in your app. In **sandbox**, you can accept agreements programmatically and customers auto-approve — no liveness UI is involved.
</Info>

### Hosted UI

The hosted UI flow is the fastest way to onboard a customer. You generate a presigned URL, redirect the user to Etherfuse's verification page, and they complete identity verification, document upload, bank account registration, and agreement signing in one session.

<Info>
  **Supported blockchains:** The `blockchain` field accepts `solana`, `stellar`, `base`, `polygon`, or `monad`. All examples use Solana, but the flow works identically across chains — just pass the appropriate value and a valid public key for that chain.
</Info>

<Steps>
  <Step title="Generate Presigned URL — POST /ramp/onboarding-url">
    <Expandable title="Details">
      This creates the customer organization and returns a presigned URL valid for **15 minutes**. You generate the `customerId` and `bankAccountId` UUIDs. See [POST /ramp/onboarding-url](/api-reference/onboarding/generate-onboarding-url) for the full schema.

      <CodeGroup>
        ```bash Sandbox theme={null}
        curl -X POST https://api.sand.etherfuse.com/ramp/onboarding-url \
          -H "Authorization: <api_key>" \
          -H "Content-Type: application/json" \
          -d '{
            "customerId": "<customer_uuid>",       # You generate this UUID
            "bankAccountId": "<bank_account_uuid>", # You generate this UUID
            "publicKey": "<wallet_public_key>",
            "blockchain": "solana",
            "userInfo": {
              "email": "ana@example.com",
              "displayName": "Ana García"
            }
          }'
        ```

        ```bash Production theme={null}
        curl -X POST https://api.etherfuse.com/ramp/onboarding-url \
          -H "Authorization: <api_key>" \
          -H "Content-Type: application/json" \
          -d '{
            "customerId": "<customer_uuid>",       # You generate this UUID
            "bankAccountId": "<bank_account_uuid>", # You generate this UUID
            "publicKey": "<wallet_public_key>",
            "blockchain": "solana",
            "userInfo": {
              "email": "ana@example.com",
              "displayName": "Ana García"
            }
          }'
        ```
      </CodeGroup>

      **Response:**

      ```json theme={null}
      {
        "presigned_url": "https://sandbox.etherfuse.com/onboarding?token=..."
      }
      ```

      <Warning>
        **Store these IDs** — you'll need `customerId` for quotes, and `bankAccountId` for orders. The `customerId` must be a new UUID, not your own organization's ID.
      </Warning>

      <Note>
        **Retries reuse the same IDs.** Client generated ids are your idempotency keys. If a later step fails, call this endpoint again with the same `customerId`; you get a fresh URL for the existing customer. Generating a new `customerId` while reusing the same `publicKey` returns a `409` naming the customer that wallet already belongs to.
      </Note>

      <Info>
        **`userInfo` is recommended** and **will eventually be required**. When provided, Etherfuse pre-creates the user record so the customer's eventual sign-in attaches to the right user, and we can email them on status changes (KYC approved/rejected, bank account verified, etc.).
      </Info>
    </Expandable>
  </Step>

  <Step title="Redirect the User">
    <Expandable title="Details">
      Send the customer to the `presigned_url`. They have 15 minutes to complete the full flow in the Etherfuse-hosted UI:

      1. Identity verification (name, address, phone, email)
      2. Document upload (government ID + selfie)
      3. Bank account registration (CLABE)
      4. Agreement signing (electronic signature, terms, customer agreement)

      <Info>In **sandbox**, fake data is accepted — no real personal information required.</Info>
    </Expandable>
  </Step>

  <Step title="Monitor Status">
    <Expandable title="Details">
      Track onboarding progress via `kyc_updated` webhooks or by polling [GET /ramp/customer/\{id}/kyc](/api-reference/kyc/get-kyc-status).

      ```bash theme={null}
      GET /ramp/customer/{customer_uuid}/kyc
      ```

      In **sandbox**, the customer is auto-approved when the customer agreement is signed. In **production**, Etherfuse admins review the submitted documents before approving. See [Checking KYC status](#checking-kyc-status) for response details and status values.
    </Expandable>
  </Step>
</Steps>

<Info>
  **Field naming convention:** API responses use `snake_case` (e.g., `presigned_url`), while request bodies use `camelCase` (e.g., `presignedUrl`). This is consistent across all Etherfuse endpoints.
</Info>

### Programmatic

Programmatic onboarding lets you create a customer's organization via API and collect identity data in your own UI rather than the Etherfuse-hosted verification page. This is ideal for white-label integrations where the customer never leaves your app.

<Info>
  **Supported blockchains:** The `blockchain` field accepts `solana`, `stellar`, `base`, `polygon`, or `monad`. All examples use Solana, but the flow works identically across chains.
</Info>

<Info>
  **Two types of auth** are used in this flow:

  * The **API steps** (create organization, submit identity, upload documents, register bank) use your **API key** with the `customer_id` in the URL path.
  * The **launch steps** (identity verification when a bank is not compliant, and signing agreements) send the customer into the Etherfuse app with a scoped JWT. These are browser launches, not API calls (no API key).
</Info>

<Steps>
  <Step title="Create Customer Organization — POST /ramp/organization">
    <Expandable title="Details">
      Create a child organization for your customer. You can optionally include wallets and a bank account to set everything up in a single call. See [POST /ramp/organization](/api-reference/organizations/create-child-org) for the full schema.

      <CodeGroup>
        ```bash Sandbox theme={null}
        curl -X POST https://api.sand.etherfuse.com/ramp/organization \
          -H "Authorization: <api_key>" \
          -H "Content-Type: application/json" \
          -d '{
            "id": "<customer_uuid>",
            "displayName": "Ana García",
            "accountType": "personal",
            "userInfo": {
              "email": "ana@example.com",
              "displayName": "Ana García"
            },
            "wallets": [
              { "publicKey": "<wallet_public_key>", "blockchain": "solana" }
            ]
          }'
        ```

        ```bash Production theme={null}
        curl -X POST https://api.etherfuse.com/ramp/organization \
          -H "Authorization: <api_key>" \
          -H "Content-Type: application/json" \
          -d '{
            "id": "<customer_uuid>",
            "displayName": "Ana García",
            "accountType": "personal",
            "userInfo": {
              "email": "ana@example.com",
              "displayName": "Ana García"
            },
            "wallets": [
              { "publicKey": "<wallet_public_key>", "blockchain": "solana" }
            ]
          }'
        ```
      </CodeGroup>

      **Response:**

      ```json theme={null}
      {
        "organizationId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
        "displayName": "Ana García",
        "accountType": "personal",
        "wallets": [
          { "id": "c3d4e5f6-a7b8-9012-cdef-123456789012", "publicKey": "9Qx7r...", "blockchain": "solana" }
        ],
        "bankAccount": null
      }
      ```

      <Info>
        **`userInfo` is recommended** and **will eventually be required**. When provided, Etherfuse pre-creates the user record so the customer's eventual sign-in attaches to the right user, and we can email them on status changes (KYC approved/rejected, bank account verified, etc.).
      </Info>

      <Tip>
        **Partner fees:** To set a default partner fee for this child org, add `"partnerFeeDefaultBps": 100` (0–500 bps) to the request body. This fee layers on top of platform fees for all quotes under this org. See [Fees](/fees).
      </Tip>
    </Expandable>
  </Step>

  <Step title="Submit Identity Data — POST /ramp/customer/{customer_uuid}/kyc">
    <Expandable title="Details">
      Submit the customer's identity information via your API key. KYC attaches to the customer org — no wallet key required. See [POST /ramp/customer/\{id}/kyc](/api-reference/kyc/submit-kyc) for the full schema.

      <CodeGroup>
        ```bash Sandbox theme={null}
        curl -X POST https://api.sand.etherfuse.com/ramp/customer/<customer_uuid>/kyc \
          -H "Authorization: <api_key>" \
          -H "Content-Type: application/json" \
          -d '{
            "identity": {
              "email": "juan@example.com",
              "phoneNumber": "+521234567890",
              "occupation": "Software Engineer",
              "name": { "givenName": "Juan", "familyName": "Garcia" },
              "dateOfBirth": "1990-05-15",
              "address": {
                "street": "Av. Reforma 123",
                "city": "Mexico City",
                "region": "CDMX",
                "postalCode": "06600",
                "country": "MX"
              },
              "idNumbers": [
                { "value": "GAJU900515HDFRNN09", "type": "CURP" },
                { "value": "GAJU9005156V3", "type": "RFC" }
              ]
            }
          }'
        ```

        ```bash Production theme={null}
        curl -X POST https://api.etherfuse.com/ramp/customer/<customer_uuid>/kyc \
          -H "Authorization: <api_key>" \
          -H "Content-Type: application/json" \
          -d '{
            "identity": {
              "email": "juan@example.com",
              "phoneNumber": "+521234567890",
              "occupation": "Software Engineer",
              "name": { "givenName": "Juan", "familyName": "Garcia" },
              "dateOfBirth": "1990-05-15",
              "address": {
                "street": "Av. Reforma 123",
                "city": "Mexico City",
                "region": "CDMX",
                "postalCode": "06600",
                "country": "MX"
              },
              "idNumbers": [
                { "value": "GAJU900515HDFRNN09", "type": "CURP" },
                { "value": "GAJU9005156V3", "type": "RFC" }
              ]
            }
          }'
        ```
      </CodeGroup>

      <Expandable title="Required fields reference">
        The Submit KYC endpoint requires the following fields on submission and returns a `400` listing any that are missing:

        | Field         | Required         | Path in identity payload                                                |
        | ------------- | ---------------- | ----------------------------------------------------------------------- |
        | First Name    | Yes              | `name.givenName`                                                        |
        | Last Name     | Yes              | `name.familyName`                                                       |
        | Phone Number  | Yes              | `phoneNumber`                                                           |
        | Email         | Yes              | `email`                                                                 |
        | Occupation    | Yes              | `occupation`                                                            |
        | Address       | Yes              | `address` (object: `street`, `city`, `region`, `postalCode`, `country`) |
        | Date of Birth | Yes              | `dateOfBirth`                                                           |
        | CURP          | Mexico: required | `idNumbers` entry with `type: "CURP"`                                   |
        | RFC           | Mexico: required | `idNumbers` entry with `type: "RFC"`                                    |
      </Expandable>

      <Info>
        For Mexican customers (`country: "MX"`), both a `CURP` and an `RFC` are **required**. For all other countries, omit `idNumbers` entirely — any other ID type is rejected.
      </Info>

      <Info>
        **Sandbox skips manual review, not the steps.** Submitting KYC sets the customer to `proposed` — this call does not approve them on its own. In sandbox there is no admin review: the customer auto-approves once all agreements are signed (Step 5), at which point a `kyc_updated` webhook fires and the wallet becomes order-eligible. Steps 3 and 5 are both required in sandbox, the same as in production — sandbox only differs in that approval is automatic rather than manually reviewed.
      </Info>
    </Expandable>
  </Step>

  <Step title="Upload Documents — POST /ramp/customer/{customer_uuid}/kyc/documents">
    <Expandable title="Details">
      Upload government ID and selfie images as base64-encoded data URLs. See [POST /ramp/customer/\{id}/kyc/documents](/api-reference/kyc/upload-kyc-docs) for the full schema.

      **Upload ID Document (front and back):**

      ```json theme={null}
      {
        "documentType": "document",
        "images": [
          { "label": "id_front", "image": "data:image/jpeg;base64,/9j/4AAQ..." },
          { "label": "id_back", "image": "data:image/jpeg;base64,/9j/4AAQ..." }
        ]
      }
      ```

      **Upload Selfie:**

      ```json theme={null}
      {
        "documentType": "selfie",
        "images": [
          { "label": "selfie", "image": "data:image/jpeg;base64,/9j/4AAQ..." }
        ]
      }
      ```

      **Requirements:** JPEG or PNG, max 10MB per image. Labels: `id_front`, `id_back` (if applicable), `selfie`.

      <Info>
        In **sandbox**, you can submit any placeholder image — the content isn't validated — but the upload is still required. The customer is not approved until all agreements are signed (Step 5).
      </Info>
    </Expandable>
  </Step>

  <Step title="Register Bank Account — POST /ramp/customer/{customer_uuid}/bank-account">
    <Expandable title="Details">
      Register the customer's Mexican bank account (CLABE) using your API key. See [POST /ramp/customer/\{id}/bank-account](/api-reference/bank-accounts/create-bank-account-api) for the full schema.

      <Info>
        **Use the sandbox RFC to auto-approve the account.** In sandbox, submit `XEXX010101000` as the `rfc`. This placeholder causes Etherfuse to skip the SPEI provider registration and mark the account `compliant: true` immediately, so the customer can start transacting. Any other RFC in sandbox is sent through to the SPEI provider's sandbox environment and treated as a real registration — it will not auto-approve. The placeholder is rejected in production; use the customer's real RFC there.
      </Info>

      ```json theme={null}
      {
        "account": {
          "transactionId": "<uuid>",
          "firstName": "Juan",
          "paternalLastName": "Garcia",
          "maternalLastName": "Lopez",
          "birthDate": "19900515",
          "birthCountryIsoCode": "MX",
          "curp": "GAJU900515HDFRNN09",
          "rfc": "XEXX010101000",
          "clabe": "012345678901234567"
        }
      }
      ```

      <Expandable title="Bank account field reference">
        | Field                 | Required | Format                                                                                               |
        | --------------------- | -------- | ---------------------------------------------------------------------------------------------------- |
        | `transactionId`       | Required | UUID (you generate)                                                                                  |
        | `firstName`           | Required |                                                                                                      |
        | `paternalLastName`    | Required |                                                                                                      |
        | `maternalLastName`    | Required |                                                                                                      |
        | `birthDate`           | Required | `YYYYMMDD`                                                                                           |
        | `birthCountryIsoCode` | Required | ISO 3166-1 alpha-2 (e.g. `MX`)                                                                       |
        | `curp`                | Required | 18 characters                                                                                        |
        | `rfc`                 | Required | 13 characters                                                                                        |
        | `clabe`               | Required | Exactly 18 digits. CLABEs starting with `646` are not supported — use a CLABE from a different bank. |

        Valid country codes: [GET /lookup/country-codes](/api-reference/lookup/list-country-codes)
      </Expandable>

      <Tip>
        **Alternative: Presigned URL auth** — You can also register bank accounts via `POST /ramp/bank-account` using the presigned URL instead of your API key. This is the path used by the Etherfuse hosted onboarding UI. See [POST /ramp/bank-account](/api-reference/bank-accounts/create-bank-account-presigned) for details.
      </Tip>
    </Expandable>
  </Step>

  <Step title="Launch the customer to verify a non-compliant bank">
    <Expandable title="Details">
      Some bank accounts require additional per-bank compliance when registered — independent of org approval. Check the bank's `compliant` flag after registering it (in the registration response, or via [GET /ramp/bank-accounts](/api-reference/bank-accounts/list-bank-accounts)). If it comes back `compliant: false`, the customer must complete that verification before the bank can be used in orders; if it's `compliant: true`, this step is unnecessary. For Mexican (SPEI) accounts the verification is a liveness face scan, matched against the ID the customer already submitted during KYC.

      Launch the customer into the [Identity verification](/guides/user-launch-flows#identity-verification) flow (`scope: verification`), using the same UUID as the `sub`. Pass the bank in the target — **`target: /idv?bank_account_id=<uuid>`** (required) — so the verification is bound to that bank. Append `&lang=es` to render the flow in Spanish (default is English). When it succeeds the bank flips to `compliant: true` and you receive a [`bank_account_updated`](/guides/verifying-webhooks) webhook, so you can react without polling.

      ```html theme={null}
      <form method="POST" action="https://sandbox.etherfuse.com/auth/launch">
        <input type="hidden" name="grant_type" value="urn:ietf:params:oauth:grant-type:jwt-bearer" />
        <input type="hidden" name="assertion" value="<verification_scoped_jwt>" />
        <input type="hidden" name="target" value="/idv?bank_account_id=<bank_account_id>" />
      </form>
      ```

      <Info>
        In **sandbox**, registering with the placeholder RFC `XEXX010101000` marks the bank `compliant: true` immediately, so you can skip this step. Register with any other RFC to leave it `compliant: false` and exercise the `/idv` flow end to end.
      </Info>

      <Warning>
        The face scan needs camera access. If you embed the launch in an iframe, set `allow="camera *; microphone *"` on the iframe element. The bare `allow="camera"` only grants the iframe's own `src` origin, but the scan SDK uses the camera from a nested frame, so without the `*` the scan cannot start.
      </Warning>
    </Expandable>
  </Step>

  <Step title="Launch the customer to sign agreements">
    <Expandable title="Details">
      The customer signs the three agreements (electronic signature, terms and conditions, and customer agreement) inside the Etherfuse app, in a real authenticated session rather than against a presigned URL. Authenticate them with a launch JWT (using the same UUID you used for `customerId` as the `sub`) and send them into the agreements flow. See [Sign agreements](/guides/user-launch-flows#sign-agreements) for the flow's `scope` and `target`, and [JWT User Authentication](/guides/jwt-authentication) for signing and launching.

      ```html theme={null}
      <form method="POST" action="https://sandbox.etherfuse.com/auth/launch">
        <input type="hidden" name="grant_type" value="urn:ietf:params:oauth:grant-type:jwt-bearer" />
        <input type="hidden" name="assertion" value="<agreements_scoped_jwt>" />
        <input type="hidden" name="target" value="/agreements" />
      </form>
      ```

      The signing page prefills from the identity you submitted in Step 2; the customer accepts each agreement and can fill any identity fields still missing. Pass an optional `return_url` on the launch to send them back to your app when they finish.

      <Info>
        In **sandbox**, the customer is auto-approved once all agreements are signed.
      </Info>
    </Expandable>
  </Step>

  <Step title="Monitor Status">
    <Expandable title="Details">
      Track onboarding progress via `kyc_updated` webhooks or by polling [GET /ramp/customer/\{id}/kyc](/api-reference/kyc/get-kyc-status):

      ```bash theme={null}
      GET /ramp/customer/{customer_uuid}/kyc
      ```

      See [Checking KYC status](#checking-kyc-status) for response details and status values.
    </Expandable>
  </Step>
</Steps>

## Reference

<Expandable title="Checking KYC status">
  Query the current KYC status at any time (works for both approaches) via [GET /ramp/customer/\{id}/kyc](/api-reference/kyc/get-kyc-status).

  <CodeGroup>
    ```bash Sandbox theme={null}
    curl -H "Authorization: <api_key>" \
      https://api.sand.etherfuse.com/ramp/customer/<customer_uuid>/kyc
    ```

    ```bash Production theme={null}
    curl -H "Authorization: <api_key>" \
      https://api.etherfuse.com/ramp/customer/<customer_uuid>/kyc
    ```
  </CodeGroup>

  | Status                     | Description                                          |
  | -------------------------- | ---------------------------------------------------- |
  | `not_started`              | No KYC data submitted                                |
  | `proposed`                 | Data submitted, awaiting admin review                |
  | `approved`                 | KYC approved                                         |
  | `approved_chain_deploying` | Approved, on-chain marking in progress (Solana only) |
  | `rejected`                 | KYC rejected (see `currentRejectionReason`)          |

  ```json theme={null}
  {
    "customerId": "123e4567-e89b-12d3-a456-426614174000",
    "walletPublicKey": "9Qx7r...",
    "status": "proposed",
    "onChainMarked": false,
    "currentRejectionReason": null,
    "selfies": [...],
    "documents": [...],
    "approvedAt": null
  }
  ```
</Expandable>

<Expandable title="Handling rejections">
  If KYC is rejected, the `updateReason` in the webhook (or `currentRejectionReason` in the status response) explains why. To resubmit:

  1. Address the rejection reason (e.g., upload a clearer document)
  2. Submit new data via the same endpoints
  3. The new submission creates a fresh review; old rejected data remains for audit purposes
</Expandable>

<Expandable title="Bank account compliance">
  A bank account must be `compliant: true` before you can create orders. Two paths to compliance:

  1. **Customer self-verifies** — After submitting data via API, redirect the customer to the presigned URL. They complete identity verification (facial scan + ID matching) in the Etherfuse UI, which marks the bank account compliant immediately.
  2. **Etherfuse admin reviews** — Submit everything programmatically and wait for admins to review the uploaded documents. The customer's selfie and government ID must be uploaded before the admin can complete the compliance check.

  While a bank is `compliant: false`, the customer must complete additional verification before it can be used. The form that takes depends on the bank type: Mexican SPEI bank accounts require an identity face scan, which you trigger by launching the customer into the [Identity verification](/guides/user-launch-flows#identity-verification) flow (`scope: verification`, `target: /idv?bank_account_id=<uuid>`). Append `&lang=es` to the target for Spanish. Once it succeeds the bank flips to `compliant: true`. You receive a [`bank_account_updated`](/guides/verifying-webhooks) webhook when this changes, so you can react without polling.

  <Info>In **sandbox**, register the bank account with the sandbox RFC `XEXX010101000` to skip SPEI provider registration and have the account marked `compliant: true` immediately. Any other RFC goes through the SPEI provider's sandbox as a real registration and will not auto-approve. A bank's `compliant` state reflects reality in sandbox too, so you can exercise the `/idv` flow there end-to-end.</Info>
</Expandable>

<Expandable title="Webhooks">
  Register a webhook for `kyc_updated` events to receive status updates. See [POST /ramp/webhook](/api-reference/webhooks/create-webhook) for setup and payload details.

  **Approved:**

  ```json theme={null}
  {
    "kyc_updated": {
      "customerId": "123e4567-e89b-12d3-a456-426614174000",
      "walletPublicKey": "9Qx7r...",
      "approved": true,
      "updateReason": "KYC approved by admin"
    }
  }
  ```

  **Rejected:**

  ```json theme={null}
  {
    "kyc_updated": {
      "customerId": "123e4567-e89b-12d3-a456-426614174000",
      "walletPublicKey": "9Qx7r...",
      "approved": false,
      "updateReason": "ID document is expired. Please submit a valid government-issued ID."
    }
  }
  ```
</Expandable>

<Expandable title="Data isolation">
  Partners can only access KYC data they submitted:

  | Accessor              | Data Visibility                                            |
  | --------------------- | ---------------------------------------------------------- |
  | Partner (via API key) | Only data where `source_organization_id` matches their org |
  | Wallet Owner          | All data for their wallet                                  |
  | Admin                 | All data                                                   |

  This prevents partners from accessing PII submitted by other partners or directly by users.
</Expandable>

<Expandable title="Email suppression">
  When you submit KYC programmatically, Etherfuse suppresses standard KYC notification emails to the user. Your application is responsible for communicating status updates via webhook events.
</Expandable>
