# Submit a KYC verification step (/docs/api-reference/reseller-verification/submitResellerKycStep)

> Run one step of a client's identity verification. One endpoint handles every step: the step path parameter names the step, and the body carries user_id, region, carrier, and whatever that step needs. The example below is the register step on carrier-1.

> **Reseller accounts only.** [Request access](https://omnidim.io/contact-us?reason=reseller_api&lock=1).

Every step is a `POST` to this operation with the step name in the path. The body carries
`user_id`, `region` and `carrier`, plus that step's own fields. Every response carries the next
step to run:

```json
{
  "success": true,
  "status": "pan_verified",
  "next_step": "upload-pan",
  "message": "PAN verified."
}
```

Run `next_step`, read the next one off that response, and stop when it is `null`.

The steps, the fields each one sends, and the walkthrough for every carrier are on the
[Verify a client](/docs/reseller/your-clients/verification) guide. Read the carrier you are on rather than
hard-coding a sequence.

**POST** `/reseller/kyc/steps/{step}`

Run one step of a client's identity verification. One endpoint handles
every step: the `step` path parameter names the step, and the body
carries `user_id`, `region`, `carrier`, and whatever that step needs.
The example below is the `register` step on `carrier-1`.

Which steps exist, which fields they need, and how each one is
performed are all per carrier. Read them from the requirements
operation for the carrier you are on, follow `next_step`, and branch
on `method`.

```yaml
operationId: submitResellerKycStep
parameters:
  - name: step
    in: path
    required: true
    description: |
      The verification step to run. Not a fixed list: carriers do not run
      the same checks, so take the names from
      `GET /reseller/kyc/requirements` for the carrier you are on.
    schema:
      type: string
requestBody:
  required: true
  content:
    application/json:
      schema:
        type: object
        required:
          - user_id
          - region
          - carrier
        properties:
          user_id:
            type: integer
            description: ID of the client completing verification.
          region:
            type: string
            enum:
              - IN
              - US
            description: Region this verification is for.
          carrier:
            type: string
            description: |
              The carrier to verify on: verification is per carrier, so
              this decides which flow the client walks and which record
              it writes. Always required, and omitting it returns
              `409 carrier_required` naming that region's carriers.
            example: carrier-1
          name:
            type: string
            description: Customer's full name. Required for `register`.
            example: Demo User
          email:
            type: string
            format: email
            description: Customer's email address. Required for `register`.
            example: demo@example.com
          phone:
            type: string
            description: Customer's phone number including country code. Required for `register`.
            example: '+919876543210'
          mobile_otp:
            type: string
            description: OTP the client received by mobile. Required for `verify-otp`.
          email_otp:
            type: string
            description: OTP the client received by email. Required for `verify-otp`.
          pan:
            type: string
            description: Customer's PAN. Required for `verify-pan`.
          business_type:
            type: string
            description: |
              The customer's business type. Required on the PAN step of
              the India carriers and on the business-details step of the
              US carrier, and the accepted values differ between them.
              Read them from `choices` on the requirements response
              rather than assuming a list.
          aadhaar:
            type: string
            description: |
              Customer's Aadhaar number. Required for `aadhaar-otp`.
              Rate limited to one attempt roughly every 30 seconds.
          otp:
            type: string
            description: |
              OTP the client received for Aadhaar verification.
              Required for `aadhaar-verify`. Rate limited to one
              attempt roughly every 30 seconds.
          gst:
            type: string
            description: Customer's GST number. Required for `verify-gst`.
          state:
            type: string
            description: |
              Required for `register` on carriers that register at
              district level, and for the US carrier's business address
              step (where it is the two-letter state code).
            example: Gujarat
          district:
            type: string
            description: |
              Customer's district. Required for `register` on carriers
              that register at district level, and validated against
              the state, so a district that does not belong to it is
              rejected with nothing written.
            example: Surat
          pincode:
            type: string
            description: >-
              Customer's postal code. Required for `register` on carriers that register at district
              level.
            example: '395003'
          pan_holder_name:
            type: string
            description: |
              Name exactly as it appears on the PAN. Required for
              `verify-pan` on carriers that check the name against the
              PAN record.
            example: Demo User
          account_type:
            type: string
            description: |
              Optional on `register`, defaults to `individual`. **Fixed
              at registration and not changeable afterwards**, and only a
              business account can verify GST, so send it deliberately.
              Accepted values come from `choices` on the requirements
              response; anything else is refused with `400
              invalid_request` rather than quietly registered as an
              individual.
          business_name:
            type: string
            description: |
              Trading name. Required when `account_type` is `business`
              on the India carriers, and required on the US carrier's
              business-details step.
          business_identity:
            type: string
            description: |
              How the business relates to its end customers. Required on
              the US carrier's business-details step; values come from
              `choices`.
          business_industry:
            type: string
            description: |
              The industry the business operates in. Required on the US
              carrier's business-details step; values come from
              `choices`.
          ein:
            type: string
            description: >-
              The business's tax registration number. Required on the US carrier's business-details
              step.
            example: 12-3456789
          website_url:
            type: string
            description: The business's public website. Required on the US carrier's business-details step.
            example: https://demo.example
          regions_of_operation:
            type: string
            description: |
              Where the business operates. Optional on the US carrier's
              business-details step, defaults to USA and Canada; values
              come from `choices`.
          street:
            type: string
            description: Street address. Required on the US carrier's business-address step.
            example: 1 Demo Street
          city:
            type: string
            description: City. Required on the US carrier's business-address step.
            example: Springfield
          postal_code:
            type: string
            description: Postal code. Required on the US carrier's business-address step.
            example: '62701'
          country:
            type: string
            description: >-
              Two-letter country code. Optional on the US carrier's business-address step, defaults
              to US.
            example: US
          first_name:
            type: string
            description: >-
              The authorized representative's first name. Required on the US carrier's
              representative step.
            example: Demo
          last_name:
            type: string
            description: >-
              The authorized representative's last name. Required on the US carrier's representative
              step.
            example: User
          business_title:
            type: string
            description: >-
              The representative's job title, as free text. Required on the US carrier's
              representative step.
            example: Head of Operations
          job_position:
            type: string
            description: |
              The representative's role, from a fixed list. Required on
              the US carrier's representative step; values come from
              `choices`.
          return_url:
            type: string
            format: uri
            description: |
              Optional on a `redirect` step. The page in your own app
              where your client's browser comes back after verifying.
              Must be `https` with a host, no `#fragment` and no
              credentials, up to 2,000 characters. Anything else is
              refused with `400 invalid_request`. Result parameters such
              as `status` are added to it. Leave it out and your client
              returns to their own dashboard.
            example: https://app.example.com/kyc/done
      example:
        user_id: 1234
        region: IN
        carrier: carrier-1
        name: Demo User
        email: demo@example.com
        phone: '+919876543210'
    multipart/form-data:
      schema:
        type: object
        description: |
          For a step whose `method` is `upload` (for example
          `upload-pan` on `carrier-2-new`). The step decides which
          document it is.
        required:
          - user_id
          - region
          - carrier
          - document
        properties:
          user_id:
            type: integer
            description: ID of the client completing verification.
          region:
            type: string
            enum:
              - IN
              - US
          carrier:
            type: string
            example: carrier-2-new
          document:
            type: string
            format: binary
            description: |
              The document file: JPG, PNG or PDF, up to 10 MB. Its
              contents must match its extension.
responses:
  '200':
    description: |
      Step completed. `preview` is only present in the response
      to the `preview` step, and only carries the fields listed
      below; any field the verification provider does not have
      yet is omitted.
    content:
      application/json:
        schema:
          type: object
          properties:
            success:
              type: boolean
            status:
              type: string
              description: |
                Where this client has reached in verification.
                `not_started` before anything is submitted,
                `completed` once they are verified. Drive your
                integration off `next_step`, not this value.
            next_step:
              type: string
              nullable: true
              description: |
                The step to run next. Chain to it without re-reading
                status. `null` once there is nothing left for you to do.

                Not a fixed list: which steps exist depends on the
                carrier. Follow this field and the step list from
                `/reseller/kyc/requirements`.
            method:
              type: string
              enum:
                - submit
                - otp
                - redirect
                - upload
              description: |
                Echoes how the step that just ran is performed. Present
                on redirect steps.
            redirect_url:
              type: string
              nullable: true
              description: |
                Only on a `redirect` step. Send your customer to this
                link in their own browser, then poll `poll_step`.

                **Single use.** It expires within minutes and must not
                be stored or reused. Call the step again for a new one:
                repeating it is safe and always returns a fresh link.
            reusable:
              type: boolean
              nullable: true
              description: |
                Only on a `redirect` step, and always `false`. Stated
                as a field so a client library can enforce it.
            poll_step:
              type: string
              nullable: true
              description: |
                Only on a `redirect` step. The step to call, repeatedly,
                to find out whether the customer finished.
            message:
              type: string
              description: Human-readable confirmation for the step that just ran.
            preview:
              type: object
              description: Present only in the response to the `preview` step.
              properties:
                client:
                  type: object
                  properties:
                    name:
                      type: string
                    email:
                      type: string
                      format: email
                    mobile:
                      type: string
                    country_code:
                      type: string
                pan:
                  type: object
                  properties:
                    pan:
                      type: string
                    business_type:
                      type: string
                aadhar:
                  type: object
                  properties:
                    name:
                      type: string
                    address:
                      type: string
                gst:
                  type: object
                  properties:
                    gst_num:
                      type: string
                    gstin:
                      type: string
        examples:
          accept:
            summary: accept
            value:
              success: true
              status: completed
              message: Verification completed.
          redirect:
            summary: a redirect step
            value:
              success: true
              status: aadhaar_pending
              next_step: aadhaar-status
              message: >-
                Open the link in your customer's browser to verify Aadhaar, then poll for the
                result.
              method: redirect
              redirect_url: https://provider.example/verify/one-time-link
              reusable: false
              poll_step: aadhaar-status
          preview:
            summary: preview
            value:
              success: true
              status: preview
              message: Review the details, then call accept.
              preview:
                client:
                  name: Demo User
                  email: demo@example.com
                  mobile: '9876543210'
                  country_code: '91'
                pan:
                  pan: ABCDE1234F
                  business_type: proprietorship
                aadhar:
                  name: Demo User
                  address: 123 Example Street, Example City, 560001
                gst:
                  gst_num: 29ABCDE1234F1Z5
                  gstin: 29ABCDE1234F1Z5
  '400':
    description: The request body is missing a field the step requires.
    content:
      application/json:
        schema:
          type: object
          properties:
            error:
              type: string
              description: Machine-readable error code.
            error_description:
              type: string
              description: Human-readable explanation.
            code:
              type: string
              description: |
                Stable, specific reason. Present on the newer endpoints (Voice
                cloning, WhatsApp); older endpoints return only `error`.
            request_id:
              type: string
              description: |
                Correlates this response with the server log. Present alongside
                `code`. Also sent as the `X-Request-Id` header.
        example:
          error: invalid_request
          error_description: 'Missing required fields: pan, business_type.'
  '401':
    description: Missing or invalid API key.
  '403':
    description: |
      A `user_id` was sent by a key that is not a reseller admin, or
      the named client's account is currently unavailable.
    content:
      application/json:
        schema:
          type: object
          properties:
            error:
              type: string
              description: Machine-readable error code.
            error_description:
              type: string
              description: Human-readable explanation.
            code:
              type: string
              description: |
                Stable, specific reason. Present on the newer endpoints (Voice
                cloning, WhatsApp); older endpoints return only `error`.
            request_id:
              type: string
              description: |
                Correlates this response with the server log. Present alongside
                `code`. Also sent as the `X-Request-Id` header.
        example:
          error: forbidden
          error_description: Access denied. Only reseller accounts can use this endpoint.
  '404':
    description: |
      The child user was not found (`not_found`), or verification
      is not available for this region or step (`not_available`).
    content:
      application/json:
        schema:
          type: object
          properties:
            error:
              type: string
              description: Machine-readable error code.
            error_description:
              type: string
              description: Human-readable explanation.
            code:
              type: string
              description: |
                Stable, specific reason. Present on the newer endpoints (Voice
                cloning, WhatsApp); older endpoints return only `error`.
            request_id:
              type: string
              description: |
                Correlates this response with the server log. Present alongside
                `code`. Also sent as the `X-Request-Id` header.
        example:
          error: not_found
          error_description: Child user not found.
  '409':
    description: |
      The step was called before its prerequisite step was completed
      (`step_order`), or the region has more than one carrier and the
      request named none (`carrier_required`). A `step_order` refusal
      can carry `next_step`, the step that unblocks it. Run that step,
      then retry. This refusal does not count toward a step's cooldown.
    content:
      application/json:
        schema:
          type: object
          properties:
            error:
              type: string
              description: Machine-readable error code.
            error_description:
              type: string
              description: Human-readable explanation.
            code:
              type: string
              description: |
                Stable, specific reason. Present on the newer endpoints (Voice
                cloning, WhatsApp); older endpoints return only `error`.
            request_id:
              type: string
              description: |
                Correlates this response with the server log. Present alongside
                `code`. Also sent as the `X-Request-Id` header.
        examples:
          step_order:
            summary: step_order
            value:
              error: step_order
              error_description: Complete the contact verification (OTP) step first.
          step_order_upload:
            summary: step_order, a document is missing
            value:
              error: step_order
              error_description: Upload your PAN card document to continue to Aadhaar.
              next_step: upload-pan
          carrier_required:
            summary: carrier_required
            value:
              error: carrier_required
              error_description: >-
                Region IN has more than one carrier and no default. Pass carrier=<name> with one of
                the carriers listed here.
              region: IN
              carriers:
                - carrier: carrier-1
                  label: Carrier 1
                  description: Landline numbers, 80 series.
                  kyc_required: true
                  unavailable: false
                  unavailable_note: null
  '422':
    description: The verification provider rejected the submitted details.
    content:
      application/json:
        schema:
          type: object
          properties:
            error:
              type: string
              description: Machine-readable error code.
            error_description:
              type: string
              description: Human-readable explanation.
            code:
              type: string
              description: |
                Stable, specific reason. Present on the newer endpoints (Voice
                cloning, WhatsApp); older endpoints return only `error`.
            request_id:
              type: string
              description: |
                Correlates this response with the server log. Present alongside
                `code`. Also sent as the `X-Request-Id` header.
        example:
          error: verification_failed
          error_description: OTP verification failed. Please try again.
  '429':
    description: An Aadhaar step was retried before the cooldown elapsed.
    content:
      application/json:
        schema:
          type: object
          properties:
            error:
              type: string
              description: Machine-readable error code.
            error_description:
              type: string
              description: Human-readable explanation.
            code:
              type: string
              description: |
                Stable, specific reason. Present on the newer endpoints (Voice
                cloning, WhatsApp); older endpoints return only `error`.
            request_id:
              type: string
              description: |
                Correlates this response with the server log. Present alongside
                `code`. Also sent as the `X-Request-Id` header.
        example:
          error: too_fast
          error_description: Please wait a moment before retrying this step.
  '500':
    description: Unexpected server error.
    content:
      application/json:
        schema:
          type: object
          properties:
            error:
              type: string
              description: Machine-readable error code.
            error_description:
              type: string
              description: Human-readable explanation.
            code:
              type: string
              description: |
                Stable, specific reason. Present on the newer endpoints (Voice
                cloning, WhatsApp); older endpoints return only `error`.
            request_id:
              type: string
              description: |
                Correlates this response with the server log. Present alongside
                `code`. Also sent as the `X-Request-Id` header.
            ref:
              type: string
              description: Reference to quote to support.
        example:
          error: server_error
          error_description: >-
            Something went wrong on our side. Please try again shortly, or contact support with
            reference a1b2c3d4e5f6.
          ref: a1b2c3d4e5f6
  '503':
    description: |
      The verification service is unavailable. Nothing is wrong with the
      client's details: retry the same step after a few minutes.
    content:
      application/json:
        schema:
          type: object
          properties:
            error:
              type: string
              description: Machine-readable error code.
            error_description:
              type: string
              description: Human-readable explanation.
            code:
              type: string
              description: |
                Stable, specific reason. Present on the newer endpoints (Voice
                cloning, WhatsApp); older endpoints return only `error`.
            request_id:
              type: string
              description: |
                Correlates this response with the server log. Present alongside
                `code`. Also sent as the `X-Request-Id` header.
        example:
          error: service_unavailable
          error_description: The verification service is unavailable right now. Please try again in a few minutes.
```

**Python SDK**

```python
from omnidimension import Client
client = Client(api_key)

user_id, region, carrier = 1234, "IN", "carrier-1"

# Read the first step, then follow next_step. Never hard-code the
# sequence: carriers do not run the same checks.
status = client.reseller.kyc_status(user_id)["json"]
entry = next(e for e in status["regions"] if e["carrier"] == carrier)
next_step = entry["next_step"]

while next_step:
    response = client.reseller.submit_kyc_step(
        next_step, user_id, region, carrier=carrier, **fields_for(next_step)
    )["json"]

    # Branch on method, never on the step's name.
    if response.get("method") == "redirect":
        # Single use, expires in minutes. Never store it.
        send_to_customer(response["redirect_url"])
        next_step = response["poll_step"]
        continue

    next_step = response.get("next_step")
```

**curl**

```bash
curl -X POST "https://backend.omnidim.io/api/v1/reseller/kyc/steps/{step}" \
  -H "Authorization: Bearer $OMNIDIM_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "user_id": 1234,
  "region": "IN",
  "carrier": "carrier-1",
  "name": "Demo User",
  "email": "demo@example.com",
  "phone": "+919876543210"
}'
```