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

# User verification (KYC)

> How to send your users' identity data on POST /auth, and how to find out which fields Ripio needs for your country.

Before a user can buy, sell or hold crypto, Ripio needs their identity data (KYC). There's no separate flow to build for it: you send the data in the same [`POST /auth`](/crypto-as-a-service/widget/get-started/authentication) call that opens the widget, and Ripio registers the user and verifies them on its own.

1. **Once, while integrating:** ask which fields your country needs, with `GET /partner/kyc-requirements`.
2. **Every time the widget opens:** send the user's data in `kyc_submission` (and their `email`) on `POST /auth`.
3. **If Ripio rejects the data:** fix the field it names and call `POST /auth` again.

### Which fields to send

The fields depend on your account's country. Your server reads them from the widget API with a **partner token**, server to server.

**1. Request a partner token** with your credentials. Reading the requirements needs no scope.

Endpoint: `https://b2b-crypto-widget-api.sandbox.ripio.com/api/v1/partner/token`

Method: `POST` · Content-Type: `application/json`

```bash theme={null}
curl \
  --location --request POST 'https://b2b-crypto-widget-api.sandbox.ripio.com/api/v1/partner/token' \
  --header 'Content-Type: application/json' \
  --data '{
    "client_id": "<client_id>",
    "client_secret": "<client_secret>"
  }'
```

```json theme={null}
{
  "access_token": "<partner-token>",
  "token_type": "Bearer",
  "expires_in": 900,
  "scope": "partner:read"
}
```

The token lasts 15 minutes. Like `client_secret`, it must never reach the browser.

**2. Get the requirements.**

Endpoint: `https://b2b-crypto-widget-api.sandbox.ripio.com/api/v1/partner/kyc-requirements`

Method: `GET` · Header: `Authorization: Bearer <partner-token>`

```bash theme={null}
curl \
  --location --request GET 'https://b2b-crypto-widget-api.sandbox.ripio.com/api/v1/partner/kyc-requirements' \
  --header 'Authorization: Bearer <partner-token>'
```

Response for an account in Argentina, shortened — the real one lists every field and every option:

```json theme={null}
{
  "kyc_required_fields": [
    {
      "field_name": "country",
      "required": true,
      "type": "CHOICE",
      "choices": [{"value": "AR", "label": "Argentina"}]
    },
    {"field_name": "first_name", "required": true, "type": "TEXT"},
    {
      "field_name": "gender",
      "required": true,
      "type": "CHOICE",
      "choices": [
        {"value": "M", "label": "Masculino"},
        {"value": "F", "label": "Femenino"},
        {"value": "X", "label": "Otro"}
      ]
    },
    {"field_name": "birthday", "required": true, "type": "DATE"},
    {"field_name": "id_number", "required": true, "type": "NUMERIC"},
    {"field_name": "address_flat", "required": false, "type": "TEXT"},
    {
      "field_name": "state",
      "required": true,
      "type": "CHOICE",
      "choices": [
        {"value": "AR.BA", "label": "Buenos Aires"},
        {"value": "AR.CT", "label": "Catamarca"},
        {"value": "AR.DF", "label": "Ciudad de Buenos Aires"}
      ]
    },
    {"field_name": "registered_tax_payer", "required": false, "type": "BOOLEAN"},
    {"field_name": "cuit", "required": true, "type": "TEXT"}
  ]
}
```

| Field | Description |
| - | - |
| `field_name` | The key to use inside `kyc_submission`, exactly as written. |
| `required` | Whether Ripio rejects a submission without it. |
| `type` | The kind of value: `TEXT`, `NUMERIC`, `BOOLEAN`, `DATE` (`YYYY-MM-DD`), `CHOICE` (one of the values in `choices`) or `FILE_UPLOAD` (an image of a document, as a base64-encoded string). |
| `choices` | Only on `CHOICE` fields: the accepted options, each with a `value` and a `label`. |

* **The same for every user.** The answer depends only on your account's country, so there's no need to call it before each `POST /auth`. Read it while you build your integration, or cache it on your side.
* **`country`** is your account's country as an ISO 3166-1 alpha-2 code, such as `AR`.
* **`CHOICE` fields.** Send the option's `value` (for example `"AR.BA"`), never its `label`. The `label` is there for you to show in your own form. Any value outside `choices` is rejected with [`20013`](#when-ripio-rejects-the-data).
* **Not something the widget shows.** The widget never asks the user for these fields: you collect them in your own app, or take them from the data you already have about the user.

### Send the data on `POST /auth`

Add two fields to the [`POST /auth`](/crypto-as-a-service/widget/get-started/authentication#generate-a-session-code) body:

| Field | Type | Description |
| - | - | - |
| `kyc_submission` | *object* | The user's identity data, keyed by the `field_name`s of the [requirements](#which-fields-to-send). Required on every call. |
| `email` | *string* (optional) | The user's email. Required unless your account approves KYC automatically. Ripio rejects the data without it otherwise. |

```bash theme={null}
curl \
  --location --request POST 'https://b2b-crypto-widget-api.sandbox.ripio.com/api/v1/auth' \
  --header 'Content-Type: application/json' \
  --data '{
    "client_id": "<client_id>",
    "client_secret": "<client_secret>",
    "external_ref": "<your-user-id>",
    "email": "ana.diaz@example.com",
    "kyc_submission": {
      "country": "AR",
      "first_name": "Ana",
      "last_name": "Diaz",
      "gender": "F",
      "birthday": "1990-05-17",
      "nationality": "AR",
      "id_number": "12345678",
      "address": "Calle 7",
      "address_number": "1234",
      "postal_code": "B1900",
      "city": "La Plata",
      "state": "AR.BA",
      "phone": "+5492211234567",
      "cuit": "20123456786",
      "personal_activity": "AR.1"
    }
  }'
```

The response is the usual `session_code`. The KYC data never comes back in any response.

### What Ripio does with the data

Ripio decides from the user's status, so you can send the same body every time:

| The user… | What Ripio does | Session |
| - | - | - |
| Is new (an `external_ref` Ripio hasn't seen) | Registers the user and sends their data for verification. | Starts. |
| Already exists and can operate | Ignores `kyc_submission` and `email`. Nothing is updated. | Starts. |
| Already exists but can't operate yet | Sends their data for verification again. | Starts. |

Once the data is in, whether the user can operate right away depends on how your account verifies identities, which Ripio sets up with you:

* **Automatic approval.** The user can operate in the same session.
* **Verification by Ripio.** Verification is asynchronous. The session starts, but the user can't operate until it is approved. The widget shows a screen telling them their account isn't enabled yet and fires `ripio-handoff` with `reason: "not_operable"`, so you can take them to the right place in your app. See [Session lifecycle](/crypto-as-a-service/widget/get-started/session-lifecycle).

<Note>
  A session keeps the status the user had when it started. When their verification is approved, they can operate from the **next** session, the next time you call `POST /auth` and open the widget. An open session doesn't pick up the change.
</Note>

<Warning>
  While a user can't operate, **every `POST /auth` sends their data for verification again**. Send the same complete data each time, and don't call `POST /auth` in a loop to find out whether the verification was approved.
</Warning>

A user that Ripio has blocked also gets a session in which they can't operate. Sending their data again doesn't change that.

### When Ripio rejects the data

If a required field is missing or a value is invalid, `POST /auth` answers `400` with code `20013`, and `detail` names each field to fix:

```json theme={null}
{
  "code": 20013,
  "detail": {
    "kyc_submission": {
      "first_name": [{"message": "This field is required.", "code": "required"}]
    }
  }
}
```

* **No session is started.** Fix the fields in `detail` and call `POST /auth` again with the same `external_ref`. Ripio has already registered the user, so it sends the corrected data for verification.
* **`detail` names fields and rules, never values.** You can log it without logging the user's data.
* **`20013` isn't `20000`.** `20000` means the request itself is malformed — for example, `kyc_submission` is missing or isn't an object — and Ripio never looked at the data. `20013` means the request was well formed and Ripio found the data incomplete.

If Ripio can't process the data for a reason of its own, `POST /auth` answers `502` or `503` with a code of `50000` or above and starts no session. Retry the request.

### Errors on `GET /partner/kyc-requirements`

| Cause | What happens |
| - | - |
| Missing, invalid or expired partner token | `401`. Request a new token. |
| Your account has been deactivated | `403`, code `40009`. |
| Too many requests | `429`, code `40004`. |
| Ripio can't answer the requirements right now | `502` or `503`, code `50000` or above. Retry the request. |

See [Troubleshooting](/crypto-as-a-service/widget/troubleshooting) for the full error contract.


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