/users/import/validate - Validate CSV file for user import

Add MCP server to your AI tool

Allow AI tools and LLMs to interact with the API documentation portal through MCP.

MCP server URL

https://api.my.nethesis.it/mcp

Standard setup for AI tools providing an mcp.json file

mcp.json
{
  "my.nethesis.it MCP server": {
    "url": "https://api.my.nethesis.it/mcp"
  }
}

Close
POST /users/import/validate

Upload and validate a CSV file for bulk user import. Returns a row-by-row report with a verdict per row (valid / error / warning / ambiguous), separating blocking errors[] from non-blocking warnings[]. Validated data is stored in a temporary session (30 min TTL) keyed by import_id, which must be passed to /users/import/confirm to actually create or update the users.

CSV format

  • Columns (in any order): email, name, phone, company_name, roles.
  • Required: email, name, company_name, roles. phone is optional.
  • company_name is the visible name of the user's organization (matched against distributors/resellers/customers in the caller's hierarchy).
  • phone, when present, must include the leading +CC country prefix (e.g. +39 333 1234567). Bare local numbers without +CC are rejected with invalid_format at validate time. Phone uniqueness against existing users is also checked (already_used).
  • roles is a semicolon-separated list of role names (e.g. Admin;Support); names are resolved against the in-memory role cache.
  • company_name is matched case-insensitively against the caller's hierarchy. If multiple organizations share that name the row is marked ambiguous and the response includes the candidates — the caller picks one in resolutions at confirm time.
  • The first data row is row 2 (the header row is row 1).
  • Standard CSV (RFC 4180) — fields containing commas, quotes or newlines must be double-quoted ("value, with comma"). Spreadsheet tools auto-quote on save.

Row outcomes

status What it means Confirm action
valid All checks passed CREATE
error At least one blocking error in errors[] (required, invalid_format, duplicate_in_csv, not_found, unknown, archived, already_used, …) always skipped
warning email already exists in DB (in warnings[]) UPDATE if override: true, else skipped
ambiguous Organization name matches multiple orgs CREATE with chosen org if resolutions[row] is set, else skipped

Errors and warnings can coexist on the same row; status precedence is error > ambiguous > warning > valid.

Limits — max 10 MB, max 1000 rows. Larger files return 400.

multipart/form-data

Body Required

  • file string(binary) Required

    CSV file to validate

Responses

  • 200 application/json

    Validation report

    Hide response attributes Show response attributes object
    • code integer
    • message string
    • data object

      Row-by-row validation report produced by /import/validate. The full set of CSV rows is always returned in rows (good and bad alike) so the frontend can render a preview and let the user decide whether to enable override for warnings or pick resolutions for ambiguous rows. The accompanying counters are pre-computed for UX summaries.

      Hide data attributes Show data attributes object
      • import_id string(uuid)

        Session ID — must be passed verbatim to /import/confirm. Sessions expire after 30 minutes.

      • total_rows integer

        Number of data rows in the CSV (excluding the header row)

      • valid_rows integer
      • error_rows integer
      • warning_rows integer

        Rows where the entity already exists in the DB. They will be UPDATEd if the caller passes override: true at confirm time, otherwise skipped.

      • ambiguous_rows integer

        Rows where the organization name matches multiple organizations and requires disambiguation at confirm time

      • rows array[object]
        Hide rows attributes Show rows attributes object
        • row_number integer

          CSV row number (1-indexed, excluding header — i.e. the first data row is 2)

        • status string

          Validation verdict for the row:

          • valid — passes all checks; will be CREATEd by /import/confirm.
          • error — at least one blocking error (see errors[]). Always skipped at confirm.
          • warning — entity already exists in the DB (see warnings[]). At confirm time it is UPDATEd with the CSV values when override: true is passed, otherwise skipped.
          • ambiguous — only for users: the organization name matches multiple orgs in the caller's hierarchy. Must be resolved at confirm time via resolutions[row_number].organization_id, otherwise skipped.

          Status precedence: error > ambiguous > warning > valid. When a row qualifies for multiple categories (e.g. has both an error and a warning), the higher-precedence one wins; the lower-precedence diagnostics are still reported in their array for context.

          Values are valid, error, warning, or ambiguous.

        • data object

          Parsed CSV row. For users, the backend additionally enriches it with organization_id (resolved Logto ID, empty string if unresolved) and role_ids (array of resolved role IDs).

          Additional properties are allowed.

        • errors array[object]

          Blocking field-level errors. Empty/omitted when none. For ambiguous rows contains exactly one entry with message=ambiguous and populated candidates.

          Hide errors attributes Show errors attributes object

          Field-level diagnostic returned by /import/validate. The same struct is used inside both errors[] (blocking — row will never be imported) and warnings[] (non-blocking — row can be turned into an UPDATE by passing override: true to /import/confirm).

          Conventions:

          • field is the CSV column the diagnostic refers to.
          • message is a stable machine-readable code, kept short and field-agnostic — the field is already in field, the frontend builds the localised string by keying off (field, message) (or just message for a generic fallback).
          • values is an ordered list of message-specific parameters: the frontend substitutes values[i] positionally into the i18n template. The slot order per code is documented in the tables below. Codes whose row says carry no values.
          • candidates is populated only when message=ambiguous.

          Error codes (always blocking)

          Code Where it can occur values slots Meaning
          required any required field Field is empty or whitespace
          too_long any string field [value] Value exceeds the field max length
          invalid_format email, phone, language [value] Value is not a valid email / phone / language code. For phone this also catches a missing leading +CC
          invalid_value enum-like fields [value] Value is not in the allowed set
          duplicate_in_csv email (users), vat_number (orgs) [value, firstRowNumber] Same value appears in an earlier row of the same file (slot 1 is the row number as a string, e.g. "2"). Org company_name is NOT checked for duplicates because the DB does not enforce name uniqueness on any org type
          not_found company_name (users) [value] Value does not match any entity in the caller's hierarchy
          lookup_failed company_name (users) [value] Database error while resolving the value (transient — retry)
          ambiguous company_name (users) [value] Multiple matches for the value; candidates is populated and must be resolved at confirm. Row status is ambiguous
          unknown roles (users) [name1, name2, …] One or more values are not recognised; each unknown entry occupies its own slot
          at_least_one_required roles (users) Field present but parsed to zero entries
          insufficient_privileges roles (users) [value] Caller cannot assign one of the requested roles
          already_used phone (users) [value, otherEmail] Value is already in use by a different entity. For phone, slot 1 is the email of the user that already owns it. Logto enforces uniqueness, so the row would otherwise fail at confirm time
          archived email (users), vat_number (distributors, resellers) [value] The value matches a soft-deleted entity. The row cannot be imported as-is — the admin must restore (and re-run import) or destroy definitively first. Customers carry no DB-level uniqueness so this code is never emitted for them

          Warning codes (non-blocking, override-able)

          Code Where it can occur values slots Meaning
          already_exists email (users), vat_number (distributors, resellers) [value] An active entity with the same key already exists. Row status is warning — pass override: true at confirm to turn this into an UPDATE instead of a CREATE. Customers never emit this code (no DB-level uniqueness on either name or VAT)
          • field string

            Name of the CSV column the diagnostic refers to.

          • message string

            Stable, field-agnostic i18n key (see tables above).

          • values array[string]

            Ordered list of message-specific parameters. Frontend substitutes them positionally into the i18n template using the slot order documented per message above. Omitted when the message takes no parameters (e.g. required).

          • candidates array[object]

            Candidate organizations to choose from. Populated only when message=ambiguous.

            Hide candidates attributes Show candidates attributes object
            • logto_id string

              Organization Logto ID — pass this back as organization_id in resolutions at confirm time

            • name string

              Organization name as stored in the database

            • type string

              Organization type

              Values are distributor, reseller, or customer.

        • warnings array[object]

          Non-blocking diagnostics. Empty/omitted when none. Currently only already_exists is emitted as a warning. Rows that have only warnings (no errors, no ambiguity) get status=warning and can be turned into UPDATEs with override: true.

          Hide warnings attributes Show warnings attributes object

          Field-level diagnostic returned by /import/validate. The same struct is used inside both errors[] (blocking — row will never be imported) and warnings[] (non-blocking — row can be turned into an UPDATE by passing override: true to /import/confirm).

          Conventions:

          • field is the CSV column the diagnostic refers to.
          • message is a stable machine-readable code, kept short and field-agnostic — the field is already in field, the frontend builds the localised string by keying off (field, message) (or just message for a generic fallback).
          • values is an ordered list of message-specific parameters: the frontend substitutes values[i] positionally into the i18n template. The slot order per code is documented in the tables below. Codes whose row says carry no values.
          • candidates is populated only when message=ambiguous.

          Error codes (always blocking)

          Code Where it can occur values slots Meaning
          required any required field Field is empty or whitespace
          too_long any string field [value] Value exceeds the field max length
          invalid_format email, phone, language [value] Value is not a valid email / phone / language code. For phone this also catches a missing leading +CC
          invalid_value enum-like fields [value] Value is not in the allowed set
          duplicate_in_csv email (users), vat_number (orgs) [value, firstRowNumber] Same value appears in an earlier row of the same file (slot 1 is the row number as a string, e.g. "2"). Org company_name is NOT checked for duplicates because the DB does not enforce name uniqueness on any org type
          not_found company_name (users) [value] Value does not match any entity in the caller's hierarchy
          lookup_failed company_name (users) [value] Database error while resolving the value (transient — retry)
          ambiguous company_name (users) [value] Multiple matches for the value; candidates is populated and must be resolved at confirm. Row status is ambiguous
          unknown roles (users) [name1, name2, …] One or more values are not recognised; each unknown entry occupies its own slot
          at_least_one_required roles (users) Field present but parsed to zero entries
          insufficient_privileges roles (users) [value] Caller cannot assign one of the requested roles
          already_used phone (users) [value, otherEmail] Value is already in use by a different entity. For phone, slot 1 is the email of the user that already owns it. Logto enforces uniqueness, so the row would otherwise fail at confirm time
          archived email (users), vat_number (distributors, resellers) [value] The value matches a soft-deleted entity. The row cannot be imported as-is — the admin must restore (and re-run import) or destroy definitively first. Customers carry no DB-level uniqueness so this code is never emitted for them

          Warning codes (non-blocking, override-able)

          Code Where it can occur values slots Meaning
          already_exists email (users), vat_number (distributors, resellers) [value] An active entity with the same key already exists. Row status is warning — pass override: true at confirm to turn this into an UPDATE instead of a CREATE. Customers never emit this code (no DB-level uniqueness on either name or VAT)
          • field string

            Name of the CSV column the diagnostic refers to.

          • message string

            Stable, field-agnostic i18n key (see tables above).

          • values array[string]

            Ordered list of message-specific parameters. Frontend substitutes them positionally into the i18n template using the slot order documented per message above. Omitted when the message takes no parameters (e.g. required).

          • candidates array[object]

            Candidate organizations to choose from. Populated only when message=ambiguous.

            Hide candidates attributes Show candidates attributes object
            • logto_id string

              Organization Logto ID — pass this back as organization_id in resolutions at confirm time

            • name string

              Organization name as stored in the database

            • type string

              Organization type

              Values are distributor, reseller, or customer.

  • 400 application/json

    Bad request - validation error

    Hide response attributes Show response attributes object
    • code integer

      HTTP error code

    • message string

      Error message

    • data object
      Hide data attributes Show data attributes object
      • type string

        Type of error

        Values are validation_error or external_api_error.

      • errors array[object]
        Hide errors attributes Show errors attributes object
        • key string

          Field name that failed validation

        • message string

          Error code or message

        • value string

          Value that failed validation

      • details

        Additional error details

  • 401 application/json

    Unauthorized - invalid or missing token

    Hide response attributes Show response attributes object
    • code integer
    • message string
    • data object | null
  • 403 application/json

    Forbidden - insufficient permissions

    Hide response attributes Show response attributes object
    • code integer
    • message string
    • data object | null
POST /users/import/validate
curl \
 --request POST 'https://collect.your-domain.com/api/users/import/validate' \
 --header "Authorization: Bearer $ACCESS_TOKEN" \
 --header "Content-Type: multipart/form-data" \
 --form "file=@file"
Response examples (200)
{
  "code": 200,
  "message": "users import validated",
  "data": {
    "import_id": "3312b187-a8b0-45f5-b23a-98798b64eb31",
    "total_rows": 6,
    "valid_rows": 2,
    "error_rows": 2,
    "warning_rows": 1,
    "ambiguous_rows": 1,
    "rows": [
      {
        "row_number": 2,
        "status": "valid",
        "data": {
          "email": "marco.rossi@nethesis.it",
          "name": "Marco Rossi",
          "phone": "+39 333 1234567",
          "company_name": "Acme Corp",
          "organization_id": "zm45rltjc9rr",
          "roles": "Admin",
          "role_ids": [
            "7jmz7ryag1m254m4428n8"
          ]
        }
      },
      {
        "row_number": 3,
        "status": "valid",
        "data": {
          "email": "support@beta.it",
          "name": "Beta Support",
          "phone": "",
          "company_name": "Beta Solutions",
          "organization_id": "jl6lgn3l6nsi",
          "roles": "Support",
          "role_ids": [
            "77evvdf876ze8oykdk4y9"
          ]
        }
      },
      {
        "row_number": 4,
        "status": "error",
        "data": {
          "email": "not-an-email",
          "name": "Bad Email",
          "phone": "+39 333 0000000",
          "company_name": "Acme Corp",
          "organization_id": "",
          "roles": "Admin",
          "role_ids": [
            "7jmz7ryag1m254m4428n8"
          ]
        },
        "errors": [
          {
            "field": "email",
            "message": "invalid_format",
            "values": [
              "not-an-email"
            ]
          }
        ]
      },
      {
        "row_number": 5,
        "status": "error",
        "data": {
          "email": "test@nethesis.it",
          "name": "Wrong Org",
          "phone": "",
          "company_name": "Organization That Does Not Exist",
          "organization_id": "",
          "roles": "Support",
          "role_ids": [
            "77evvdf876ze8oykdk4y9"
          ]
        },
        "errors": [
          {
            "field": "company_name",
            "message": "not_found",
            "values": [
              "Organization That Does Not Exist"
            ]
          }
        ]
      },
      {
        "row_number": 6,
        "status": "warning",
        "data": {
          "email": "edoardo.spadoni@nethesis.it",
          "name": "Mario Rossi",
          "phone": "",
          "company_name": "Acme Corp",
          "organization_id": "zm45rltjc9rr",
          "roles": "Admin",
          "role_ids": [
            "7jmz7ryag1m254m4428n8"
          ]
        },
        "warnings": [
          {
            "field": "email",
            "message": "already_exists",
            "values": [
              "edoardo.spadoni@nethesis.it"
            ]
          }
        ]
      },
      {
        "row_number": 7,
        "status": "ambiguous",
        "data": {
          "email": "ambig@nethesis.it",
          "name": "Ambiguous Org",
          "phone": "",
          "company_name": "Gamma",
          "organization_id": "",
          "roles": "Support",
          "role_ids": [
            "77evvdf876ze8oykdk4y9"
          ]
        },
        "errors": [
          {
            "field": "company_name",
            "message": "ambiguous",
            "values": [
              "Gamma"
            ],
            "candidates": [
              {
                "logto_id": "abc123",
                "name": "Gamma Tech",
                "type": "distributor"
              },
              {
                "logto_id": "def456",
                "name": "Gamma Group",
                "type": "customer"
              }
            ]
          }
        ]
      }
    ]
  }
}
Response examples (400)
{
  "code": 400,
  "message": "validation failed",
  "data": {
    "type": "validation_error",
    "errors": [
      {
        "key": "username",
        "message": "required",
        "value": "string"
      }
    ]
  }
}
Response examples (401)
{
  "code": 401,
  "message": "invalid token",
  "data": {}
}
Response examples (403)
{
  "code": 403,
  "message": "insufficient permissions",
  "data": {}
}