/resellers/import/validate - Validate CSV for reseller import

POST /resellers/import/validate

Upload and validate a CSV file for bulk reseller import. Returns a row-by-row report with a verdict per row (valid / error / warning), 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 /resellers/import/confirm to actually create or update the resellers.

CSV format

  • Columns (in any order): company_name, description, vat_number, address, city, main_contact, email, phone, language, notes.
  • Required: company_name, vat_number. All other columns are optional.
  • phone, when present, must include the leading +CC country prefix (e.g. +39 02 1234567). Bare local numbers are rejected with invalid_format.
  • language accepts it or en (defaults to it when empty).
  • The first data row is row 2.
  • Standard CSV (RFC 4180) — fields containing commas, quotes or newlines must be double-quoted ("note, 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, too_long, duplicate_in_csv, archived, …) always skipped
warning A reseller with the same vat already exists in the database (in warnings[]) UPDATE if override: true, else skipped

Reseller imports cannot produce ambiguous rows — that status only applies to user imports.

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

multipart/form-data

Body Required

  • file string(binary) Required

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 /resellers/import/validate
curl \
 --request POST 'https://collect.your-domain.com/api/resellers/import/validate' \
 --header "Authorization: Bearer $ACCESS_TOKEN" \
 --header "Content-Type: multipart/form-data" \
 --form "file=@file"
Response examples (200)
{
  "code": 200,
  "message": "resellers 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": {}
}