Import a project from an Excel (.xlsx) workbook (multipart/form-data)
POST/api/projects/import/excel
Import a project from an Excel (.xlsx) workbook (multipart/form-data). PT-737: the create-vs-update choice is EXPLICIT — send mode=create or mode=upsert; it wins over the legacy ID-column heuristic, which applies only when mode is omitted (any row with an ID then makes the whole import an upsert). Send projectId to bind an update to that project: it forces upsert and resolves the workbook's Task Keys strictly within that project (a key from another project is rejected), and such an import may carry only new blank-ID rows, which become new tasks there. The create-mode project name comes from the optional in-workbook "Project" sheet. Permission is enforced in the import tool by mode: a create requires projects.create; an upsert requires tasks.update.any for its keyed (update) rows and tasks.create for its unkeyed (new) rows, on the target project's organisation (a mixed workbook requires both). Returns 201 when a project was created, 200 when an existing project was updated.
Request
Responses
- 200
- 201
- 400
- 401
- 403
- 404
- 413
OK — an existing project was updated (upsert mode). Same body as the 201.
Successful response
Bad Request — malformed multipart body, a missing/invalid file, an unexpected MIME type, or the workbook failed structural / tenant validation (details carries the sheet-, row-, or cell-level errors). Also returned when the import was rejected AFTER its writes, by an effective-state guard checked against the state about to commit rather than against the sheet — a row left more than one level deep, a parent no longer in the bound project (PT-855), a dependency cycle (PT-856), or authored progress on a task a skipped row leaves a container (PT-857). The whole import is rolled back; error names the offending sheet row and details is absent. Every 400 from this endpoint takes this one flat shape — the guards throw a platform ValidationError, which the route catches and flattens rather than letting it serialise as the nested { error: { code, message } } envelope.
Unauthorized — no authenticated user.
Forbidden — the caller lacks the mode-dependent permission (projects.create for a create; tasks.update.any for an upsert's keyed rows and/or tasks.create for its unkeyed rows, on the target project's organisation), the import has no resolvable tenant context, or the x-act-as-org selector named a non-member organisation.
Not Found — the target project could not be resolved while the import was running; the whole import is rolled back.
Payload Too Large — the uploaded file exceeds the 10 MB limit.