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

# KYC API

> Collect your customer's identity data and documents in your own UI, push them over the API, and launch the customer only for what is left.

Push identity data and documents you already hold over the API, so the customer isn't asked for them twice. They are still launched into [`/idv`](https://docs.etherfuse.com/guides/user-launch-flows#identity-verification), but arrive with most of the work done.

Normally, the customer must complete the **liveness selfie**, which cannot be submitted directly over the API. If you already verified the customer on your own Sumsub level, import that session and they skip the selfie too: see [Sumsub shared token](https://docs.etherfuse.com/guides/kyc-sumsub-shared-token.md).

The customer normally accepts the **customer agreement** themselves. If your own terms incorporate ours, you can forward their acceptance to us over the API: see [the customer agreement](#the-customer-agreement).

## The order matters

Verification runs in [stages](https://docs.etherfuse.com/guides/onboarding.md#requirements-unlock-in-stages), and you can only send what the current stage asks for.

* **Personal data comes first.** A customer's requirements are assigned from the country you send, and that assignment is asynchronous, so documents and answers sent before it lands are refused with a `409`. The [`kyc_updated`](https://docs.etherfuse.com/guides/verifying-webhooks) webhook fires when it lands; send once [step 2](#2-read-what-is-being-asked-for) returns a `requirements` list.
* **After that, send only what the requirements list shows as `pending`.** A later stage's requirements, such as `customer_agreement`, are not in the list until the customer reaches that stage. If you send one early, it is accepted with a `202` and then dropped.
* **The one exception is a proof of address.** If you hold one, send it with the identity document, before the identity stage is submitted.

**1. Submit personal data**

[`POST /ramp/customer/{customer_id}/verification`](https://docs.etherfuse.com/api-reference/kyc/submit-verification-data)

**2. Read what is being asked for**

[`GET /ramp/customer/{customer_id}/kyc?requirements=true`](https://docs.etherfuse.com/api-reference/kyc/get-kyc-status), after the `kyc_updated` webhook.

**3. Upload the documents being asked for**

[`POST /ramp/customer/{customer_id}/verification/documents`](https://docs.etherfuse.com/api-reference/kyc/upload-verification-documents). Include a proof of address here if you hold one.

**4. Submit the questionnaire answers being asked for**

[`POST /ramp/customer/{customer_id}/verification/questionnaire`](https://docs.etherfuse.com/api-reference/kyc/submit-a-verification-questionnaire)

**5. Launch the customer for the rest**

The liveness selfie, and anything else you did not send. The customer finishes the identity stage in `/idv`.

**6. Send later stages as they appear**

After each `kyc_updated`, read the requirements list again. Send `proof_of_address` if it appears and you did not send one, and `customer_agreement` if you are [forwarding the customer's acceptance](#the-customer-agreement).

> [!WARNING]
>
> **These endpoints answer `202 Accepted`, not `200 OK`.** A `202` means "accepted and being applied", not "done". Never treat it as proof that a requirement is satisfied: learn that from the [`kyc_updated`](https://docs.etherfuse.com/guides/verifying-webhooks) webhook or the [requirements breakdown](https://docs.etherfuse.com/guides/onboarding.md#the-per-requirement-breakdown).

## 1. Submit personal data

Supply `userInfo.email` when [creating the customer](https://docs.etherfuse.com/guides/kyc-websdk.md#the-flow). An email address is required for the verification record and status notifications.

```bash Sandbox theme={null}
curl -X POST https://api.sand.etherfuse.com/ramp/customer/<customer_uuid>/verification \
  -H "Authorization: <api_key>" \
  -H "Content-Type: application/json" \
  -d '{
    "firstName": "Ana",
    "lastName": "Ejemplo",
    "dateOfBirth": "1990-01-01",
    "taxId": "XAXX010101000",
    "additionalInfo": {
      "curp": "XEXX010101HNEXXXA4"
    },
    "country": "MEX",
    "address": {
      "street": "Av. Reforma 100",
      "city": "Ciudad de Mexico",
      "region": "CDMX",
      "postalCode": "06600",
      "country": "MEX"
    }
  }'
```

```bash Production theme={null}
curl -X POST https://api.etherfuse.com/ramp/customer/<customer_uuid>/verification \
  -H "Authorization: <api_key>" \
  -H "Content-Type: application/json" \
  -d '{
    "firstName": "Ana",
    "lastName": "Ejemplo",
    "dateOfBirth": "1990-01-01",
    "taxId": "XAXX010101000",
    "additionalInfo": {
      "curp": "XEXX010101HNEXXXA4"
    },
    "country": "MEX",
    "address": {
      "street": "Av. Reforma 100",
      "city": "Ciudad de Mexico",
      "region": "CDMX",
      "postalCode": "06600",
      "country": "MEX"
    }
  }'
```

```json 202 Accepted theme={null}
{
  "customerId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "status": "in_progress"
}
```

Every field, its format, and whether it is required: [`POST /ramp/customer/{customer_id}/verification`](https://docs.etherfuse.com/api-reference/kyc/submit-verification-data).

For Mexico (`country: "MEX"`), supply CURP as `additionalInfo.curp` and RFC as `taxId`. CURP and RFC are required.

> [!WARNING]
>
> **Send only an address that matches the identity document you upload.** The two are cross-checked. An address, date of birth, or name that contradicts the document holds the customer up for review instead of approving them.

Re-submitting overwrites what you sent before and is safe. It never moves the customer's review status on its own.

## 2. Read what is being asked for

Triggered by the [`kyc_updated`](https://docs.etherfuse.com/guides/verifying-webhooks) webhook rather than a timer, where you can: it fires when the customer is routed and the requirement list becomes available.

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

```json theme={null}
{
  "customerId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "status": "in_progress",
  "requirements": [
    { "type": "personal_data",      "status": "satisfied", "requiresLaunch": false },
    { "type": "identity_document",  "status": "pending",   "requiresLaunch": false },
    { "type": "occupation",         "status": "pending",   "requiresLaunch": false },
    { "type": "selfie",             "status": "pending",   "requiresLaunch": true  }
  ]
}
```

`requiresLaunch: false` is yours to send; `true` can only be done by the customer. When everything outstanding reads `true`, you have nothing left to send **for this stage**. Full vocabulary: [Tracking verification status](https://docs.etherfuse.com/guides/onboarding.md#tracking-verification-status).

The list holds the current stage, plus anything already satisfied. `customer_agreement` and, when needed, `proof_of_address` appear only once the identity stage clears. Read the list again after each `kyc_updated`.

## 3. Upload documents

`multipart/form-data`, with files sent as files. Nothing is base64-wrapped into JSON.

```bash Sandbox theme={null}
curl -X POST https://api.sand.etherfuse.com/ramp/customer/<customer_uuid>/verification/documents \
  -H "Authorization: <api_key>" \
  -F id_type=ID_CARD \
  -F id_front=@ine-front.jpg \
  -F id_back=@ine-back.jpg
```

```bash Production theme={null}
curl -X POST https://api.etherfuse.com/ramp/customer/<customer_uuid>/verification/documents \
  -H "Authorization: <api_key>" \
  -F id_type=ID_CARD \
  -F id_front=@ine-front.jpg \
  -F id_back=@ine-back.jpg
```

**A proof of address can go in the same request**, even though it has its own later stage. We hold it for that stage, and it is used only if the identity document turns out to carry no address. This works only until the identity stage is submitted. After that, wait for `proof_of_address` to appear in the requirements list, then send it on its own.

Every part, the accepted document types, and the file limits: [`POST /ramp/customer/{customer_id}/verification/documents`](https://docs.etherfuse.com/api-reference/kyc/upload-verification-documents).

## 4. Submit the questionnaire answers (optional)

Upload identity and address documents in [step 3](#3-upload-documents).

Answer them here if you already hold the answers. Anything you leave out, the customer answers in `/idv`.

Each submission names the questionnaire it answers in `type`:

| `type` | Answers | Applies to |
| - | - | - |
| `occupation` | `jobTitle`, `industry` | Every customer |
| `agreement` | `acceptTerms`, `signatureName`, `signatureDate` | Customers who accepted under your terms |

### Occupation

```bash Sandbox theme={null}
curl -X POST https://api.sand.etherfuse.com/ramp/customer/<customer_uuid>/verification/questionnaire \
  -H "Authorization: <api_key>" \
  -H "Content-Type: application/json" \
  -d '{
    "type": "occupation",
    "jobTitle": "Software Engineer",
    "industry": "4430200"
  }'
```

```bash Production theme={null}
curl -X POST https://api.etherfuse.com/ramp/customer/<customer_uuid>/verification/questionnaire \
  -H "Authorization: <api_key>" \
  -H "Content-Type: application/json" \
  -d '{
    "type": "occupation",
    "jobTitle": "Software Engineer",
    "industry": "4430200"
  }'
```

> [!NOTE]
>
> **Send `type`.** If it names a questionnaire that doesn't exist, the request is rejected and the error tells you which names are valid.

**`industry` is a code you have to look up.** [`GET /ramp/verification/occupations`](https://docs.etherfuse.com/api-reference/kyc/list-occupation-categories) returns every accepted value as `{ code, label }`. Submit the `code`: the `label` is display copy, can be reworded, and is not accepted. An unrecognized code is refused with a `400` naming it, so a typo fails at the request rather than leaving the requirement quietly outstanding.

```json theme={null}
{
  "occupations": [
    { "code": "1000000", "label": "NOT APPLICABLE" },
    { "code": "4430200", "label": "INFORMATION TECHNOLOGY - IT PROGRAMMING ENGINEERS OR TECHNICIANS" }
  ]
}
```

### The customer agreement

Your customer has to accept the Etherfuse customer agreement before they can transact. By default they accept it themselves in `/idv`. Send `type: "agreement"` only if your own terms incorporate ours and you already collected their acceptance.

> [!WARNING]
>
> **Send the agreement only once `customer_agreement` is in the requirements list.** It has its own stage, after the identity stage and any proof of address. Do not send it in the same burst as the occupation answers: at that point the customer is still on the identity stage. An early agreement is accepted with a `202`, then refused in the background, and you get a `kyc_updated` webhook with `needsWork: true` and `updateReason: "verification questionnaire write failed"`. Wait for the `kyc_updated` that moves the customer back to `in_progress`, read the list, and send it then.

```bash Sandbox theme={null}
curl -X POST https://api.sand.etherfuse.com/ramp/customer/<customer_uuid>/verification/questionnaire \
  -H "Authorization: <api_key>" \
  -H "Content-Type: application/json" \
  -d '{
    "type": "agreement",
    "acceptTerms": true,
    "signatureName": "Ana Ejemplo",
    "signatureDate": "2026-08-24"
  }'
```

```bash Production theme={null}
curl -X POST https://api.etherfuse.com/ramp/customer/<customer_uuid>/verification/questionnaire \
  -H "Authorization: <api_key>" \
  -H "Content-Type: application/json" \
  -d '{
    "type": "agreement",
    "acceptTerms": true,
    "signatureName": "Ana Ejemplo",
    "signatureDate": "2026-08-24"
  }'
```

> [!WARNING]
>
> **`acceptTerms: true` is a legal declaration.** By setting it you declare that:
>
> * The person named in `signatureName` accepted terms that incorporate the Etherfuse customer agreement, and did so themselves on the date you give.
> * They saw the agreement, or terms that include it, before they accepted.
> * They consented to signing electronically, agreed to the terms, stated they are not a U.S. Person, and stated they act in their own name and on their own behalf.
> * You keep a record of that acceptance and can produce it for a regulator.

`acceptTerms` must be `true`. It is your affirmation of the declaration above.

`signatureName` is the name the customer typed to sign, in their own words. It is printed on their agreement in a cursive face, so it appears as the signature on the executed document. Up to 255 characters.

`signatureDate` is when the customer accepted, `YYYY-MM-DD`. It is printed on their agreement as the date of signature. It should be within one day of today's date.

## 5. Launch the customer

Launch into [`/idv`](https://docs.etherfuse.com/guides/user-launch-flows#identity-verification) exactly as in the [hosted flow](https://docs.etherfuse.com/guides/kyc-websdk.md#the-flow). The customer is shown only what's left: the liveness selfie and any questionnaire you did not answer in [step 4](#4-submit-the-questionnaire-answers-optional).

Once they finish the identity stage, the later stages follow in the same session: the proof of address if one is needed and was not already sent, then the customer agreement. If you forward the agreement yourself, watch for `customer_agreement` to appear in the requirements list and send it then.

## Error handling

> [!WARNING]
>
> If your integration cannot handle a verification error, launch the customer into the [hosted verification flow](https://docs.etherfuse.com/guides/kyc-websdk.md#the-flow) at `/idv`. We have seen small changes to Sumsub requirements cause API submissions to be rejected without advance notice. The hosted flow lets customers complete the current verification requirements while you update your API integration.

Each endpoint's full response codes are on its API reference page. Requirement and overall statuses are in [Tracking verification status](https://docs.etherfuse.com/guides/onboarding.md#tracking-verification-status). What follows is what actually bites in this flow.

| What you see | Meaning | What to do |
| - | - | - |
| `409` `Submit this customer's personal data first` | Step 1's `202` returns before the customer has a verification profile, so a document or questionnaire call in that window has no stage to land on. | Wait for the requirements breakdown to appear, normally well under a second, then send. |
| `409` `has not been routed to their country's requirements yet` | Step 1 landed, but the stages that hold documents and questions are assigned from the country you sent, and that assignment is asynchronous. | Wait for the `kyc_updated` webhook (`not_started` to `in_progress`), then re-read step 2 and send. With no webhook registered, read step 2 until the requirements list appears. |
| `202`, but the requirement stays `pending`, or never appears | Steps 1, 3 and 4 validate your request, answer `202`, then apply it in the background, and that background work failed. The usual cause is a document or answer sent before its stage was reached, most often the `agreement` sent together with `occupation`. | A `kyc_updated` webhook arrives with `needsWork: true` and a `userMessage` naming what to send again, and `status` unchanged. Nothing retries on our side, so send it again. For documents send every part in the same request: one side of a two-sided document is refused on its own. That webhook is the only place the message appears, so without one registered, re-read the requirements list and re-send what is still outstanding. |
| `202`, then the customer stalls in review | The personal data contradicts the document it was cross-checked against. | Re-submit personal data matching the document, or upload the document that matches. |
| `409` `already approved` / `already denied` | Concluded verifications can't be written to. | A denied customer resubmits by being launched into `/idv`. |

## Sandbox

The document checks don't run, so document content isn't scrutinised: a placeholder image is accepted where production would reject it. Everything else is identical, including the submission order and the conditional proof-of-address stage. See [Test environment](https://docs.etherfuse.com/test-environment.md).

Mexico's RFC check still runs in sandbox. For a Mexican customer, send `"firstName": "CARLOS"`, `"lastName": "RAMIREZ LOPEZ"` and `"taxId": "HEGL900101AB6"`, the person the sandbox registry returns for that RFC. With any other name, personal data is rejected.

Sandbox reads every identity document as test data with no address, so the flow always asks for a proof of address after the identity stage. Any image passes.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.