> ## 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`](/v2/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](/v2/guides/kyc-sumsub-shared-token).

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](/v2/guides/onboarding#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 [`kycUpdated`](/v2/api-reference/webhooks/kycupdated) 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 `customerAgreement`, 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.

<Steps>
  <Step title="Submit personal data">
    [`POST /organization/{organizationId}/verification`](/v2/api-reference/kyc/submit-verification-data)
  </Step>

  <Step title="Read what is being asked for">
    [`GET /organization/{organizationId}/kyc?requirements=true`](/v2/api-reference/kyc/get-kyc-status), after the `kycUpdated` webhook.
  </Step>

  <Step title="Upload the documents being asked for">
    [`POST /organization/{organizationId}/verification/documents`](/v2/api-reference/kyc/upload-verification-documents). Include a proof of address here if you hold one.
  </Step>

  <Step title="Submit the questionnaire answers being asked for">
    [`POST /organization/{organizationId}/verification/questionnaire`](/v2/api-reference/kyc/submit-a-verification-questionnaire)
  </Step>

  <Step title="Launch the customer for the rest">
    The liveness selfie, and anything else you did not send. The customer finishes the identity stage in `/idv`.
  </Step>

  <Step title="Send later stages as they appear">
    After each `kycUpdated`, read the requirements list again. Send `proofOfAddress` if it appears and you did not send one, and `customerAgreement` if you are [forwarding the customer's acceptance](#the-customer-agreement).
  </Step>
</Steps>

<Warning>
  **These endpoints return `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 [`kycUpdated`](/v2/api-reference/webhooks/kycupdated) webhook or the [requirements breakdown](/v2/guides/onboarding#the-per-requirement-breakdown).
</Warning>

## 1. Submit personal data

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

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

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

Every field, its format, and whether it is required: [`POST /organization/{organizationId}/verification`](/v2/api-reference/kyc/submit-verification-data).

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

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

Read this when the [`kycUpdated`](/v2/api-reference/webhooks/kycupdated) webhook arrives. It arrives when the customer is routed and the requirement list becomes available. Without a `kycUpdated` subscription, poll this endpoint.

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

```json theme={null}
{
  "organizationId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "status": "inProgress",
  "currentRejectionReason": null,
  "selfies": [],
  "documents": [],
  "approvedAt": null,
  "needsWork": false,
  "userMessage": null,
  "requirements": [
    { "type": "personalData",      "status": "satisfied", "requiresLaunch": false },
    { "type": "identityDocument",  "status": "pending",   "requiresLaunch": false },
    { "type": "occupation",        "status": "pending",   "requiresLaunch": false },
    { "type": "taxDocument",       "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](/v2/guides/onboarding#tracking-verification-status).

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

## 3. Upload documents

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

<CodeGroup>
  ```bash Sandbox theme={null}
  curl -X POST https://api.sand.etherfuse.com/organization/<organization_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/organization/<organization_uuid>/verification/documents \
    -H "Authorization: <api_key>" \
    -F id_type=ID_CARD \
    -F id_front=@ine-front.jpg \
    -F id_back=@ine-back.jpg
  ```
</CodeGroup>

Send `tax_document` here too, where the customer's country requires one. See [Country-specific requirements](/v2/guides/onboarding#country-specific-requirements).

**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 `proofOfAddress` to appear in the requirements list, then send it on its own.

Every part, the accepted document types, and the file limits: [`POST /organization/{organizationId}/verification/documents`](/v2/api-reference/kyc/upload-verification-documents).

## 4. Submit the questionnaire answers (optional)

Questions only. Documents, including a region's tax document, go through [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

<CodeGroup>
  ```bash Sandbox theme={null}
  curl -X POST https://api.sand.etherfuse.com/organization/<organization_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/organization/<organization_uuid>/verification/questionnaire \
    -H "Authorization: <api_key>" \
    -H "Content-Type: application/json" \
    -d '{
      "type": "occupation",
      "jobTitle": "Software Engineer",
      "industry": "4430200"
    }'
  ```
</CodeGroup>

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

**`industry` is a code you have to look up.** [`GET /verification/occupations`](/v2/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 and does not leave the requirement 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 `customerAgreement` 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 `kycUpdated` webhook with `needsWork: true`. Wait for the `kycUpdated` that moves the customer back to `inProgress`, read the list, and send it then.
</Warning>

<CodeGroup>
  ```bash Sandbox theme={null}
  curl -X POST https://api.sand.etherfuse.com/organization/<organization_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/organization/<organization_uuid>/verification/questionnaire \
    -H "Authorization: <api_key>" \
    -H "Content-Type: application/json" \
    -d '{
      "type": "agreement",
      "acceptTerms": true,
      "signatureName": "Ana Ejemplo",
      "signatureDate": "2026-08-24"
    }'
  ```
</CodeGroup>

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

`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`](/v2/guides/user-launch-flows#identity-verification) exactly as in the [hosted flow](/v2/guides/kyc-websdk#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 `customerAgreement` to appear in the requirements list and send it then.

## Error handling

Each endpoint's full response codes are on its API reference page. Errors are [problem details](/v2/errors); the messages below appear in `detail`. Requirement and overall statuses are in [Tracking verification status](/v2/guides/onboarding#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 `kycUpdated` webhook (`notStarted` to `inProgress`), 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, return `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 `kycUpdated` 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](/v2/test-environment).

Mexico's RFC registry check still runs in sandbox. The sandbox registry returns a single test person, so send `"firstName": "LETICIA"` and `"lastName": "HERNANDEZ GONZALEZ"` for a Mexican customer. Any other name is rejected.


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