Confirm and execute a previously validated user import. Reuses the same pipelines as
POST /users (CREATE) and PUT /users/{id} (UPDATE) for each row, so caller permissions
are enforced per-row.
Inputs
import_id— from the/validateresponse. Must still be valid (sessions expire after 30 min).override— global flag. Whentrue, every row with statuswarning(the user already exists, matched by email) is UPDATEd with the CSV values. When omitted/false, warning rows are skipped.resolutions— required for any row with statusambiguousthat you want to import; keyed by row number as a string, value is{ organization_id }picked fromcandidates[].
Behaviour per row status
| status | override false |
override true |
|---|---|---|
valid |
CREATE | CREATE |
error |
skipped (reason: error) |
skipped (reason: error) |
warning |
skipped (reason: warning_not_overridden) |
UPDATE — name, phone, company_name, roles overwritten from CSV (email is the lookup key, never changed) |
ambiguous (no resolution) |
skipped (reason: ambiguous_unresolved) |
same |
ambiguous (resolution provided) |
CREATE with chosen org | same |
RBAC for override
Updating an existing user requires the caller to have permission on the user's current
organization (their hierarchy must include the target user's org). Organization changes
("moves") via override are allowed because the destination org has already been resolved
against the caller's hierarchy at validate time. Per-row failures (status failed) are
emitted when the caller cannot manage the existing user.
Atomicity — non-atomic: failures during creation/update (Logto rate limits, RBAC denials) are reported per-row; rows that succeeded before the failure are kept.
Body
Required
-
Import session ID from the validate response. Sessions expire after 30 min.
-
When
true, all rows with statuswarningare UPDATEd using the CSV values (existing entity is looked up by email/name and overwritten field-by-field). Whenfalseor omitted, warning rows are skipped.Default value is
false. -
Per-row organization choices for
ambiguousrows, keyed by row number as a string (e.g."7"). The chosenorganization_idmust be one of thecandidates[].logto_idvalues returned by/import/validatefor that row. Ambiguous rows without a resolution are skipped.
curl \
--request POST 'https://collect.your-domain.com/api/users/import/confirm' \
--header "Authorization: Bearer $ACCESS_TOKEN" \
--header "Content-Type: application/json" \
--data '{"import_id":"3312b187-a8b0-45f5-b23a-98798b64eb31","override":true,"resolutions":{"7":{"organization_id":"abc123"}}}'
{
"import_id": "3312b187-a8b0-45f5-b23a-98798b64eb31",
"override": true,
"resolutions": {
"7": {
"organization_id": "abc123"
}
}
}
{
"code": 200,
"message": "users imported successfully",
"data": {
"created": 2,
"updated": 1,
"skipped": 2,
"failed": 1,
"results": [
{
"row_number": 2,
"status": "created",
"id": "usr_2k3lf9d8sn"
},
{
"row_number": 3,
"status": "created",
"id": "usr_8skd0w29df"
},
{
"row_number": 4,
"status": "skipped",
"reason": "error"
},
{
"row_number": 5,
"status": "skipped",
"reason": "error"
},
{
"row_number": 6,
"status": "updated",
"id": "usr_existing01"
},
{
"row_number": 7,
"status": "failed",
"error": "logto sync failed: rate limit exceeded"
}
]
}
}
{
"code": 400,
"message": "validation failed",
"data": {
"type": "validation_error",
"errors": [
{
"key": "username",
"message": "required",
"value": "string"
}
]
}
}
{
"code": 401,
"message": "invalid token",
"data": {}
}
{
"code": 403,
"message": "insufficient permissions",
"data": {}
}