---
title: "validateErpImport"
method: POST
path: "/v2/erp/imports/{importId}:validate"
tags: ["erp-imports"]
---

# validateErpImport

`POST /v2/erp/imports/{importId}:validate`

Choose the use case to read this file with, and start the validate phase.
Callable from PENDING, READY and FAILED — so a wrong choice is corrected by calling this again with a different use case, rather than re-uploading. Each call replaces any previous verdict. Any other status returns 409.

## Path parameters

- `importId` string, required

## Request body

- ValidateErpImportRequest
  - `integration_id` string, required
  - `use_case_slug` string, required — An inbound use case's slug, unique within its integration.

## Response `202`

Validation started

- ErpImportJob
  - `import_id` string, required — `imp_{ULID}` — time-ordered, also used as the job's correlation_id.
  - `org_id` string, required
  - `created_by` string
  - `integration_id` string — The integration owning the chosen use case. Absent until the first `:validate`.
  - `use_case_slug` string — The inbound use case whose mapping drives both phases. Absent until the first `:validate`, and MUTABLE — re-validating with a different use case replaces it.
  - `format` 'csv' | 'xlsx', required
  - `status` 'PENDING' | 'VALIDATING' | 'READY' | 'PROCESSING' | 'IMPORTED' | 'FAILED' | 'CANCELLING' | 'CANCELLED', required — PENDING → VALIDATING → READY → PROCESSING → IMPORTED, with FAILED reachable from any working status, and CANCELLING → CANCELLED reachable from VALIDATING or PROCESSING via :abort. Only IMPORTED and CANCELLED are terminal. READY and FAILED both accept a further `:validate`, which is how a wrong use case is corrected without re-uploading the file. IMPORTED means every row was handed to the platform, not that the platform finished — per-row outcomes live in monitoring, filtered by correlation_id. A file that fails validation is FAILED with error.code = VALIDATION_BLOCKED. READY is legitimately idle for as long as the user takes to confirm, so it carries no running work and never goes stale. CANCELLING is transient and cooperative: the abort has been recorded but the worker only notices at its next batch boundary. Rows already published stay published — a stop is not a rollback.
  - `s3_input_ref` S3Reference, required
    - `bucket` string, required
    - `key` string, required
  - `size_bytes` integer — Size of the uploaded file, recorded at registration. Present on every job registered from version 1.17.0 onwards; absent on older rows, which were written before it was captured.
  - `column_count` integer — How many effective columns the file's header yielded — the length of the `preview.columns` returned at registration, kept so a job loaded later can still describe its file. The preview ROWS are deliberately not stored: they are a sample for the person about to choose a mapping, not job state. Present on every job registered from version 1.17.0 onwards.
  - `validation` ErpImportValidation — Validate-phase summary: what the file will create, and whether it may be confirmed. Absent until the validate phase completes. No per-row detail is kept — a rejected file is corrected and imported again.
    - `total_rows` integer, required — Data rows read from the file.
    - `blocking` integer, required — Blocking problems found, counting per-row ones that are not listed in `issues`.
    - `warnings` integer, required
    - `entities` object, required — Distinct entities the file expresses, keyed by entity slug.
    - `issues` ErpImportIssue[] — Whole-file issues, at most one per `code`. Do not expect the length to match blocking + warnings: those also count per-row problems, which are recorded for support but never listed here. Warnings here are what `ack_warnings` on `:execute` acknowledges.
      - `code` 'UNIQUE_ID_COLUMN_MISSING' | 'MAPPED_COLUMN_MISSING' | 'MALFORMED_ROW' | 'INVALID_ENCODING' | 'EMPTY_FILE' | 'TOO_MANY_ROWS', required — Enum of possible issue codes.
      - `severity` 'warning' | 'blocking', required
      - `columns` object[] — The columns this issue is about, at most one entry per column per entity. On UNIQUE_ID_COLUMN_MISSING the file has NONE of these. Do NOT tell the user that adding one of them is enough: a unique id may read several columns through a JSONata expression, and whether it combines them (`A & B`, both needed) or falls back between them (`A ? A : B`, either will do) is not knowable here.
        - `name` string, required — The column name, spelled as the mapping reads it.
        - `entity` string — Slug of the entity this column helps identify. UNIQUE_ID_COLUMN_MISSING only.
      - `row` integer — The offending data row, 1-based as the user counts rows. MALFORMED_ROW only.
  - `progress` ErpImportProgress — How far the currently running phase has got. Written at every batch boundary, so it advances during long runs rather than only at the end. `total_rows` is ABSENT during the validate phase until the file has been read to the end — there is deliberately no counting pass, since that would be a second unbounded read of the whole file. Render an indeterminate indicator while it is missing: dividing by a missing total yields a determinate bar pinned at 0%, which reads as a hung import.
    - `processed_rows` integer, required — Rows fully processed. An exact "the first N rows are done" watermark, not an estimate — it only advances once a batch has been completely handled.
    - `total_rows` integer — Rows in the file. Known only once a phase has read to EOF; the execute phase has it from the start, because validate recorded it first.
  - `error` ErpImportError — Why the import failed — present if and only if status = FAILED. `code` is the translation key; for VALIDATION_BLOCKED the specifics are in `validation`.
    - `code` 'VALIDATION_BLOCKED' | 'FILE_FORMAT_UNSUPPORTED' | 'FILE_UNAVAILABLE' | 'VALIDATE_TIMEOUT' | 'IMPORT_TIMEOUT' | 'USE_CASE_NOT_USABLE' | 'IMPORT_NO_PROGRESS' | 'INTERNAL_ERROR', required — Enum of possible error codes.
    - `message` string, required — One English sentence, derived from `code` so the two always agree. A fallback for a client that has no translation for this code — prefer translating `code`, and never parse this. It deliberately does NOT restate `validation.issues`.
  - `correlation_id` string — Scopes this run in monitoring. Always equal to `import_id`.
  - `activity_id` string
  - `created_at` string, date-time, required
  - `updated_at` string, date-time, required

## Other responses

- `403` — Forbidden - insufficient permissions
- `404` — Resource not found
- `409` — Conflict - the resource is not in a state that allows this operation
- `422` — The use case cannot drive an import
- `500` — Internal Server Error

---

[API](https://skmtc.net/epilot/apis/integration-toolkit-api.md) · [All operations](https://skmtc.net/epilot/apis/integration-toolkit-api/llms.txt) · [OpenAPI document](https://skmtc-service-staging.skmtc.workers.dev/v1/apis/epilot/integration-toolkit-api/revisions/cf02c9310c0f/schema)
